From a486668170181f849f721659abf934395810b6e0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 4 Apr 2026 09:44:38 +0000
Subject: [PATCH 1/5] Initial plan
From 15c939ff85ce5fc9da919b73c5497ac6b5bab57d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 4 Apr 2026 09:59:24 +0000
Subject: [PATCH 2/5] feat: add pglite implementation
Agent-Logs-Url: https://github.com/pubkey/client-side-databases/sessions/24850991-bc6e-43e9-8de2-c1b851331a1d
Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com>
---
angular.json | 101 ++++++++-
package.json | 17 +-
projects/pglite/src/app/app.component.html | 1 +
projects/pglite/src/app/app.component.less | 0
projects/pglite/src/app/app.component.ts | 20 ++
projects/pglite/src/app/app.logic.ts | 213 ++++++++++++++++++
projects/pglite/src/app/app.module.ts | 32 +++
.../src/app/services/database.service.ts | 90 ++++++++
projects/pglite/src/assets/email-pattern.png | Bin 0 -> 2310 bytes
.../src/environments/environment.prod.ts | 3 +
.../pglite/src/environments/environment.ts | 7 +
projects/pglite/src/favicon.ico | Bin 0 -> 948 bytes
projects/pglite/src/index.html | 26 +++
projects/pglite/src/main.ts | 12 +
projects/pglite/src/polyfills.ts | 34 +++
projects/pglite/src/styles.less | 2 +
projects/pglite/tsconfig.app.json | 17 ++
17 files changed, 569 insertions(+), 6 deletions(-)
create mode 100644 projects/pglite/src/app/app.component.html
create mode 100644 projects/pglite/src/app/app.component.less
create mode 100644 projects/pglite/src/app/app.component.ts
create mode 100644 projects/pglite/src/app/app.logic.ts
create mode 100644 projects/pglite/src/app/app.module.ts
create mode 100644 projects/pglite/src/app/services/database.service.ts
create mode 100644 projects/pglite/src/assets/email-pattern.png
create mode 100644 projects/pglite/src/environments/environment.prod.ts
create mode 100644 projects/pglite/src/environments/environment.ts
create mode 100644 projects/pglite/src/favicon.ico
create mode 100644 projects/pglite/src/index.html
create mode 100644 projects/pglite/src/main.ts
create mode 100644 projects/pglite/src/polyfills.ts
create mode 100644 projects/pglite/src/styles.less
create mode 100644 projects/pglite/tsconfig.app.json
diff --git a/angular.json b/angular.json
index 1bcfd546..51f1c165 100644
--- a/angular.json
+++ b/angular.json
@@ -688,6 +688,105 @@
}
}
}
+ },
+ "pglite": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:component": {
+ "style": "less"
+ },
+ "@schematics/angular:application": {
+ "strict": true
+ }
+ },
+ "root": "projects/pglite",
+ "sourceRoot": "projects/pglite/src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/pglite",
+ "index": "projects/pglite/src/index.html",
+ "main": "projects/pglite/src/main.ts",
+ "polyfills": "projects/pglite/src/polyfills.ts",
+ "tsConfig": "projects/pglite/tsconfig.app.json",
+ "inlineStyleLanguage": "less",
+ "assets": [
+ "projects/pglite/src/favicon.ico",
+ "projects/pglite/src/assets"
+ ],
+ "styles": [
+ "projects/pglite/src/styles.less"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "5mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "150kb",
+ "maximumError": "150kb"
+ }
+ ],
+ "fileReplacements": [
+ {
+ "replace": "projects/pglite/src/environments/environment.ts",
+ "with": "projects/pglite/src/environments/environment.prod.ts"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "namedChunks": false,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "pglite:build:production"
+ },
+ "development": {
+ "buildTarget": "pglite:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "pglite:build"
+ }
+ },
+ "lint": {
+ "builder": "@angular-eslint/builder:lint",
+ "options": {
+ "lintFilePatterns": [
+ "projects/pglite/**/*.ts",
+ "projects/pglite/**/*.html"
+ ]
+ }
+ }
+ }
}
},
"cli": {
@@ -699,4 +798,4 @@
"enabled": false
}
}
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 2f50409a..0732ff46 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"analyze:watermelondb": "webpack-bundle-analyzer ./dist/watermelondb/stats.json",
"analyze:rxdb-lokijs": "webpack-bundle-analyzer ./dist/rxdb-lokijs/stats.json",
"analyze:rxdb-dexie": "webpack-bundle-analyzer ./dist/rxdb-dexie/stats.json",
+ "analyze:pglite": "webpack-bundle-analyzer ./dist/pglite/stats.json",
"build": "rimraf ./dist && npm-run-all build:*",
"build:aws": "ng build --configuration production --aot --no-progress --project aws --stats-json",
"build:firebase": "ng build --configuration production --aot --no-progress --project firebase --stats-json",
@@ -19,6 +20,7 @@
"build:rxdb-lokijs": "ng build --configuration production --aot --no-progress --project rxdb-lokijs --stats-json",
"build:rxdb-dexie": "ng build --configuration production --aot --no-progress --project rxdb-dexie --stats-json",
"build:watermelondb": "ng build --configuration production --aot --no-progress --project watermelondb --stats-json",
+ "build:pglite": "ng build --configuration production --aot --no-progress --project pglite --stats-json",
"build:template": "ng build --configuration production --aot --no-progress --stats-json",
"lint": "ng lint",
"lint:fix": "ng lint --fix",
@@ -33,12 +35,14 @@
"dev:rxdb-lokijs": "concurrently \"npm run server:rxdb\" \"npm run client:rxdb-lokijs\"",
"dev:rxdb-dexie": "concurrently \"npm run server:rxdb\" \"npm run client:rxdb-dexie\"",
"dev:watermelondb": "concurrently \"npm run server:rxdb\" \"npm run client:watermelondb\"",
+ "dev:pglite": "npm run client:pglite",
"start:aws": "http-server ./dist/aws -p 3000 -c 2592000",
"start:firebase": "concurrently \"npm run server:firebase\" \"sleep 10 && http-server ./dist/firebase -p 3000 -c 2592000\"",
"start:pouchdb": "concurrently \"npm run server:pouchdb\" \"http-server ./dist/pouchdb -p 3000 -c 2592000\" --kill-others --success first",
"start:rxdb-lokijs": "concurrently \"npm run server:rxdb\" \"http-server ./dist/rxdb-lokijs -p 3000 -c 2592000\"",
"start:rxdb-dexie": "concurrently \"npm run server:rxdb\" \"http-server ./dist/rxdb-dexie -p 3000 -c 2592000\"",
"start:watermelondb": "http-server ./dist/watermelondb -p 3000 -c 2592000",
+ "start:pglite": "http-server ./dist/pglite -p 3000 -c 2592000",
"server:firebase": "concurrently \"firebase emulators:start --only firestore\" \"npm run server:firebase:import\"",
"server:firebase:setup": "firebase setup:emulators:firestore",
"server:firebase:import": "ts-node --skip-project ./projects/firebase/src/import-example-data.ts",
@@ -54,6 +58,7 @@
"client:rxdb-lokijs": "ng serve --project rxdb-lokijs --port 3000",
"client:rxdb-dexie": "ng serve --project rxdb-dexie --port 3000",
"client:watermelondb": "ng serve --project watermelondb --port 3000",
+ "client:pglite": "ng serve --project pglite --port 3000",
"test:wait-for-frontend": "ts-node --skip-project ./scripts/wait-for-frontend.ts",
"test": "testcafe chrome:headless --hostname localhost -e test/e2e.test.ts",
"test:aws": "PROJECT_KEY=aws NODE_OPTIONS=--max_old_space_size=4096 concurrently \"npm run start:aws\" \"npm run test:wait-for-frontend && npm run test\" --kill-others --success first",
@@ -61,7 +66,8 @@
"test:pouchdb": "PROJECT_KEY=pouchdb NODE_OPTIONS=--max_old_space_size=4096 concurrently \"npm run start:pouchdb\" \"npm run test:wait-for-frontend && npm run test\" --kill-others --success first",
"test:rxdb-lokijs": "PROJECT_KEY=rxdb-lokijs NODE_OPTIONS=--max_old_space_size=4096 concurrently \"npm run start:rxdb-lokijs\" \"npm run test:wait-for-frontend && npm run test\" --kill-others --success first",
"test:rxdb-dexie": "PROJECT_KEY=rxdb-dexie NODE_OPTIONS=--max_old_space_size=4096 concurrently \"npm run start:rxdb-dexie\" \"npm run test:wait-for-frontend && npm run test\" --kill-others --success first",
- "test:watermelondb": "PROJECT_KEY=watermelondb NODE_OPTIONS=--max_old_space_size=4096 concurrently \"npm run start:watermelondb\" \"npm run test:wait-for-frontend && npm run test\" --kill-others --success first"
+ "test:watermelondb": "PROJECT_KEY=watermelondb NODE_OPTIONS=--max_old_space_size=4096 concurrently \"npm run start:watermelondb\" \"npm run test:wait-for-frontend && npm run test\" --kill-others --success first",
+ "test:pglite": "PROJECT_KEY=pglite NODE_OPTIONS=--max_old_space_size=4096 concurrently \"npm run start:pglite\" \"npm run test:wait-for-frontend && npm run test\" --kill-others --success first"
},
"private": true,
"dependencies": {
@@ -79,6 +85,7 @@
"@aws-amplify/datastore": "3.12.12",
"@aws-amplify/ui-angular": "5.3.2",
"@babel/runtime": "7.29.2",
+ "@electric-sql/pglite": "^0.4.3",
"@nozbe/watermelondb": "0.24.0",
"@types/express": "4.17.25",
"@types/express-serve-static-core": "4.19.8",
@@ -127,15 +134,15 @@
"@angular/language-service": "17.3.12",
"@types/faker": "5.5.9",
"@types/jsonwebtoken": "9.0.10",
+ "@types/lokijs": "1.5.14",
"@types/node": "20.19.39",
+ "@types/pouchdb": "6.4.2",
+ "@types/pouchdb-find": "7.3.3",
+ "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/eslint-plugin-tslint": "7.0.2",
"@typescript-eslint/parser": "7.18.0",
"async-test-util": "2.5.0",
- "@types/lokijs": "1.5.14",
- "@types/pouchdb": "6.4.2",
- "@types/pouchdb-find": "7.3.3",
- "@types/ws": "8.18.1",
"concurrently": "8.2.2",
"eslint": "8.57.1",
"eslint-plugin-import": "2.32.0",
diff --git a/projects/pglite/src/app/app.component.html b/projects/pglite/src/app/app.component.html
new file mode 100644
index 00000000..2f29fc69
--- /dev/null
+++ b/projects/pglite/src/app/app.component.html
@@ -0,0 +1 @@
+
diff --git a/projects/pglite/src/app/app.component.less b/projects/pglite/src/app/app.component.less
new file mode 100644
index 00000000..e69de29b
diff --git a/projects/pglite/src/app/app.component.ts b/projects/pglite/src/app/app.component.ts
new file mode 100644
index 00000000..fda4d050
--- /dev/null
+++ b/projects/pglite/src/app/app.component.ts
@@ -0,0 +1,20 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import {
+ LogicInterface
+} from '../../../../src/app/logic-interface.interface';
+
+import {
+ Logic
+} from './app.logic';
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.less'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class AppComponent {
+ title = 'pglite';
+
+ public logic: LogicInterface = new Logic();
+}
diff --git a/projects/pglite/src/app/app.logic.ts b/projects/pglite/src/app/app.logic.ts
new file mode 100644
index 00000000..2e54e680
--- /dev/null
+++ b/projects/pglite/src/app/app.logic.ts
@@ -0,0 +1,213 @@
+import {
+ Observable,
+ combineLatest
+} from 'rxjs';
+import {
+ switchMap,
+ map,
+ shareReplay,
+ startWith,
+ mergeMap,
+ filter
+} from 'rxjs/operators';
+import {
+ LogicInterface
+} from '../../../../src/app/logic-interface.interface';
+import {
+ Message,
+ UserWithLastMessage,
+ User,
+ AddMessage,
+ UserPair,
+ Search
+} from '../../../../src/shared/types';
+import {
+ DatabaseType,
+ createDatabase
+} from './services/database.service';
+import {
+ doesMessageMapUserPair,
+ sortByNewestFirst
+} from 'src/shared/util-server';
+import { RXJS_SHARE_REPLAY_DEFAULTS } from 'rxdb';
+
+export class Logic implements LogicInterface {
+ private dbPromise: Promise = createDatabase();
+ private db!: DatabaseType;
+
+ constructor() {
+ this.dbPromise.then(db => this.db = db);
+ }
+
+ getUserByName(userName$: Observable): Observable {
+ return userName$.pipe(
+ mergeMap((userName) => this.dbPromise.then(() => userName)),
+ switchMap(userName => {
+ return this.db.users$.pipe(
+ startWith(undefined),
+ map(() => userName)
+ );
+ }),
+ switchMap(async (userName) => {
+ const result = await this.db.db.query(
+ `SELECT id, "createdAt" FROM users WHERE id = $1 LIMIT 1`,
+ [userName]
+ );
+ return result.rows[0] ?? null;
+ }),
+ filter((doc): doc is User => !!doc)
+ );
+ }
+
+ getSearchResults(search$: Observable): Observable {
+ return search$.pipe(
+ switchMap(search => {
+ return this.db.messages$.pipe(
+ startWith(undefined),
+ map(() => search)
+ );
+ }),
+ switchMap(async (search) => {
+ const result = await this.db.db.query(
+ `SELECT id, text, "createdAt", read, sender, reciever
+ FROM messages
+ WHERE text ILIKE $1
+ AND (sender = $2 OR reciever = $2)`,
+ [`%${search.searchTerm}%`, search.ownUser.id]
+ );
+ return { search, messages: result.rows };
+ }),
+ switchMap(async ({ search, messages }) => {
+ return Promise.all(
+ messages.map(async (message) => {
+ const otherUserId = message.sender === search.ownUser.id
+ ? message.reciever
+ : message.sender;
+ const userResult = await this.db.db.query(
+ `SELECT id, "createdAt" FROM users WHERE id = $1 LIMIT 1`,
+ [otherUserId]
+ );
+ return {
+ user: userResult.rows[0],
+ message
+ } as UserWithLastMessage;
+ })
+ );
+ })
+ );
+ }
+
+ getUsersWithLastMessages(ownUser$: Observable): Observable {
+ const usersNotOwn$ = ownUser$.pipe(
+ switchMap(ownUser => {
+ return this.db.users$.pipe(
+ startWith(undefined),
+ map(() => ownUser)
+ );
+ }),
+ switchMap(async (ownUser) => {
+ const result = await this.db.db.query(
+ `SELECT id, "createdAt" FROM users WHERE id != $1`,
+ [ownUser.id]
+ );
+ return result.rows;
+ }),
+ shareReplay(RXJS_SHARE_REPLAY_DEFAULTS)
+ );
+
+ const usersWithLastMessage$: Observable = combineLatest([
+ ownUser$,
+ usersNotOwn$
+ ]).pipe(
+ map(([ownUser, usersNotOwn]) => {
+ return usersNotOwn.map((user) => {
+ return this.getLastMessageOfUserPair({
+ user1: ownUser,
+ user2: user
+ }).pipe(
+ map(message => ({ user, message })),
+ shareReplay(RXJS_SHARE_REPLAY_DEFAULTS)
+ );
+ });
+ }),
+ switchMap(streams => combineLatest(streams)),
+ map(usersWithLastMessage => sortByNewestFirst(usersWithLastMessage as any))
+ );
+
+ return usersWithLastMessage$;
+ }
+
+ private getLastMessageOfUserPair(
+ userPair: UserPair
+ ): Observable {
+ return this.db.messages$.pipe(
+ startWith(undefined),
+ switchMap(async () => {
+ const result = await this.db.db.query(
+ `SELECT id, text, "createdAt", read, sender, reciever
+ FROM messages
+ WHERE (sender = $1 AND reciever = $2)
+ OR (sender = $2 AND reciever = $1)
+ ORDER BY "createdAt" DESC
+ LIMIT 1`,
+ [userPair.user1.id, userPair.user2.id]
+ );
+ return result.rows[0];
+ }),
+ shareReplay(RXJS_SHARE_REPLAY_DEFAULTS)
+ );
+ }
+
+ public getMessagesForUserPair(
+ userPair$: Observable
+ ): Observable {
+ return userPair$.pipe(
+ switchMap(userPair => {
+ return this.db.messages$.pipe(
+ startWith(undefined),
+ map(() => userPair)
+ );
+ }),
+ switchMap(async (userPair) => {
+ const result = await this.db.db.query(
+ `SELECT id, text, "createdAt", read, sender, reciever
+ FROM messages
+ WHERE (sender = $1 AND reciever = $2)
+ OR (sender = $2 AND reciever = $1)
+ ORDER BY "createdAt" ASC`,
+ [userPair.user1.id, userPair.user2.id]
+ );
+ return result.rows;
+ })
+ );
+ }
+
+ async addMessage(message: AddMessage): Promise {
+ await this.dbPromise;
+ const m = message.message;
+ await this.db.db.query(
+ `INSERT INTO messages (id, text, "createdAt", read, sender, reciever)
+ VALUES ($1, $2, $3, $4, $5, $6)
+ ON CONFLICT (id) DO NOTHING`,
+ [m.id, m.text, m.createdAt, m.read, m.sender, m.reciever]
+ );
+ }
+
+ async addUser(user: User): Promise {
+ await this.dbPromise;
+ await this.db.db.query(
+ `INSERT INTO users (id, "createdAt")
+ VALUES ($1, $2)
+ ON CONFLICT (id) DO NOTHING`,
+ [user.id, user.createdAt]
+ );
+ }
+
+ async hasData(): Promise {
+ await this.dbPromise;
+ const result = await this.db.db.query<{ count: string }>(
+ `SELECT COUNT(*) as count FROM users`
+ );
+ return parseInt(result.rows[0]?.count ?? '0', 10) > 0;
+ }
+}
diff --git a/projects/pglite/src/app/app.module.ts b/projects/pglite/src/app/app.module.ts
new file mode 100644
index 00000000..6dda94b2
--- /dev/null
+++ b/projects/pglite/src/app/app.module.ts
@@ -0,0 +1,32 @@
+import { BrowserModule } from '@angular/platform-browser';
+import { NgModule } from '@angular/core';
+
+import { AppComponent } from './app.component';
+import {
+ ChatModule
+} from '../../../../src/app/chat.module';
+
+import {
+ APP_BASE_HREF,
+ LocationStrategy,
+ PathLocationStrategy
+} from '@angular/common';
+
+@NgModule({
+ declarations: [
+ AppComponent
+ ],
+ imports: [
+ BrowserModule,
+ ChatModule
+ ],
+ providers: [
+ { provide: APP_BASE_HREF, useValue: '/' },
+ {
+ provide: LocationStrategy,
+ useClass: PathLocationStrategy
+ }
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
diff --git a/projects/pglite/src/app/services/database.service.ts b/projects/pglite/src/app/services/database.service.ts
new file mode 100644
index 00000000..334a7cac
--- /dev/null
+++ b/projects/pglite/src/app/services/database.service.ts
@@ -0,0 +1,90 @@
+import { PGlite } from '@electric-sql/pglite';
+import { Subject } from 'rxjs';
+import { shareReplay } from 'rxjs/operators';
+import { Observable } from 'rxjs';
+import { RXJS_SHARE_REPLAY_DEFAULTS } from 'rxdb';
+import { logTime } from 'src/shared/util-browser';
+
+export interface DatabaseType {
+ db: PGlite;
+ users$: Observable;
+ messages$: Observable;
+}
+
+/**
+ * Creates the PGlite database with IndexedDB persistence,
+ * initializes tables and change-notification triggers.
+ */
+export async function createDatabase(): Promise {
+ logTime('createDatabase()');
+
+ const db = new PGlite('idb://chat-db');
+ await db.waitReady;
+
+ logTime('create tables');
+ await db.exec(`
+ CREATE TABLE IF NOT EXISTS users (
+ id TEXT PRIMARY KEY,
+ "createdAt" BIGINT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS messages (
+ id TEXT PRIMARY KEY,
+ text TEXT NOT NULL,
+ "createdAt" BIGINT NOT NULL,
+ read BOOLEAN NOT NULL,
+ sender TEXT NOT NULL,
+ reciever TEXT NOT NULL
+ );
+
+ CREATE INDEX IF NOT EXISTS messages_created_at_idx ON messages ("createdAt");
+
+ CREATE OR REPLACE FUNCTION notify_users_change()
+ RETURNS trigger AS $$
+ BEGIN
+ PERFORM pg_notify('users_change', '');
+ RETURN NEW;
+ END;
+ $$ LANGUAGE plpgsql;
+
+ CREATE OR REPLACE FUNCTION notify_messages_change()
+ RETURNS trigger AS $$
+ BEGIN
+ PERFORM pg_notify('messages_change', '');
+ RETURN NEW;
+ END;
+ $$ LANGUAGE plpgsql;
+
+ DROP TRIGGER IF EXISTS users_change_trigger ON users;
+ CREATE TRIGGER users_change_trigger
+ AFTER INSERT OR UPDATE OR DELETE ON users
+ FOR EACH ROW EXECUTE FUNCTION notify_users_change();
+
+ DROP TRIGGER IF EXISTS messages_change_trigger ON messages;
+ CREATE TRIGGER messages_change_trigger
+ AFTER INSERT OR UPDATE OR DELETE ON messages
+ FOR EACH ROW EXECUTE FUNCTION notify_messages_change();
+ `);
+ logTime('create tables DONE');
+
+ const usersSubject = new Subject();
+ const messagesSubject = new Subject();
+
+ await db.listen('users_change', () => {
+ usersSubject.next();
+ });
+ await db.listen('messages_change', () => {
+ messagesSubject.next();
+ });
+
+ logTime('createDatabase() DONE');
+ return {
+ db,
+ users$: usersSubject.asObservable().pipe(
+ shareReplay(RXJS_SHARE_REPLAY_DEFAULTS)
+ ),
+ messages$: messagesSubject.asObservable().pipe(
+ shareReplay(RXJS_SHARE_REPLAY_DEFAULTS)
+ )
+ };
+}
diff --git a/projects/pglite/src/assets/email-pattern.png b/projects/pglite/src/assets/email-pattern.png
new file mode 100644
index 0000000000000000000000000000000000000000..8bedad4e8693659e382be2d047de68460aea8132
GIT binary patch
literal 2310
zcmZvedoM9j6@Vi6;`%vf@3i})s$V&!KG
z6V;MxbJ21I
z)W?3^@8{#{XS^4`_t)C<1`Q3TC%kfly>i`SZm_#Z2gGUGFx-!yKQ(0*L@i@eIkGh4
zT`aN`P!+bbPTz6O5VG`zVoSPsu^v7`-G1SoI`Vxa*KZWE5VFZDn2W>Ytp~H(MVtG4
zzCt$ZFHTkvFPilE=s|FmTaB|C_*SM}+1#K89>%i|@9+NLsfx7qJOPxE9I+GKrMod=
zr)lwoX&xAxlp4`@gw}`l#6A)5i_I2~vIX(KFr8F?ZgE6Jq%gx&N?{MFSx9^)$Z~_O)|$e_dP?Ax
zOI8T6+9{TbSNvV(6_e-4`7Md}wkr=Cj*CfZ^D&1%)%@K9U+|CJc*t4h7>8m%UH)Sx
zc`%)Cu;8k=@J@@PKWwPjP>F!xzTzGSIXRPjsvc33dMXHRjV@~zMeq9E*EE!|uTtlrWEF``wSd2=F(0saDI=QKLe1Nl<6
zfHw>MVI!w$_2N|n@8pog|4#_POsnVq|FqC6SG)Y0%ktr80>*sIt_-8ZlymgJ;JL3Q
zmZij~vR-C6=#p=o`Pd&XfCXKZa@^A*b2j+4zon>*dK12=PES6TFj2VH89>@oa8`R$4WA)qz6Xkg&UNrxs074=k^6lse}A`ZIDe@Y-TS&o
zJT(15ZC=8*9VCSQ*R1J~HR#Ocvzr_SpT26SJaGu1;#U4N;Cz2%i}UHZhRLNeGFY8k
zA7!;nw=>TVj-`1gEH@G78nDpHgWylE!f{Cn7I*O@br7;CM7$iES)o-VNz$>A`2&-g
zgL>^EP(B-yf^;^JIAu8QNt5qQ9KTMOgI*a~D}8djW)Hrj?~jv?K(0aU{N|(I8Ev3~
z^_uw0z{l$C%`~?fMD^F3M>I$Gb~$ygIhhYc0q9Oa$$J_rfbPDgSTO|nk{;6Ds!K3(
zZ`*~69s*ww;0!cXn*1B3R&QO&74V>jGyu7{xR@FVGfC5iL}|L?zaP|WXDIiLik}?Y
ziSHw9@*liyI~doWHCvPY)ETTfw7+9_WX2WqRIZr^vY<(cE*=fMMnQ$Ct1{!FiCj!B
z1*M^m^h(zj^($~7ckrm}Cl+uhMHNOa-pLUodksPO)xjrkyvo;tv{L_cq9bhU`ILum
zDEXZG-4Y+yTgwkyNv{(MWHqK8Wb!ZN!Z`B|S_syHW!X<0Rkr~DYkHd%wV~B~e4S>G
zuG3>gb=EA+)Z=`G<m@n%F*^OhUe*Lf-@
ztX23Jbf+G^?Z_I1^Dle))fgNiH}^?Z1!uL#2`lZZwAe#6^n2I5#r0Nzw(_0&<}K43
zIS9<`Oftz`IsR@%IwnYA9d6y##<>MZ`G0=*mOBvoawGVcGO-2`H!CDo3wXXCG|(_z
z8Our|umLP`WDEIjP+ccXo-g(x!48vmm`UI<=RL0+E73j8q1OLxua7e0c%00b?PhuMr!EYpOS$q1=a_}?XE)&HwFI29D`
zVHNI`lfJ{=6A=BbUMwxh~o1IrO=M-_l{LRR0}Q
z`H=SAis??h*C{MnOH@?t;W3eUxhO4I`VLr7vyrx2{uTP*GqagC=@@ii|A})I+oB@m
zmp%E2xL$cx_f!XE$v!rUIYg-SEace@SQCb4e{M9IY|{@{wy!@yCxpYy{H&u)me9Sd
zk!c-dz8-YE+%NvbOMItO-SUAnGU^*QQNtIC%Noozda^X2l=v_Y%)*7ryiE?w{Y9}K
zyuPpUWIPmC&wiX57;{<-iMeob6tc??;N!oRJ)i4*o#F0hRgCx?2pTiCH=I8`a5l#}
zLjb1JdS7>{IPFnGMX#yS>i~TtjJw3C7kZ_+?3hNceN|tgRV4OR8eU0tD;MOUDApUQ
zVSz#L&%K3&HV0nF?k08GM8GrN
z=|Xl|5VdVeyGen`rGN+4>6xJM=oc_2GY4i8zvH)QxWAN6w9^JzBskJ~ki+yJNVr
I!=$AD0bhY()c^nh
literal 0
HcmV?d00001
diff --git a/projects/pglite/src/environments/environment.prod.ts b/projects/pglite/src/environments/environment.prod.ts
new file mode 100644
index 00000000..3612073b
--- /dev/null
+++ b/projects/pglite/src/environments/environment.prod.ts
@@ -0,0 +1,3 @@
+export const environment = {
+ production: true
+};
diff --git a/projects/pglite/src/environments/environment.ts b/projects/pglite/src/environments/environment.ts
new file mode 100644
index 00000000..5dd10dc0
--- /dev/null
+++ b/projects/pglite/src/environments/environment.ts
@@ -0,0 +1,7 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+ production: false
+};
diff --git a/projects/pglite/src/favicon.ico b/projects/pglite/src/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..997406ad22c29aae95893fb3d666c30258a09537
GIT binary patch
literal 948
zcmV;l155mgP)CBYU7IjCFmI-B}4sMJt3^s9NVg!P0
z6hDQy(L`XWMkB@zOLgN$4KYz;j0zZxq9KKdpZE#5@k0crP^5f9KO};h)ZDQ%ybhht
z%t9#h|nu0K(bJ
ztIkhEr!*UyrZWQ1k2+YkGqDi8Z<|mIN&$kzpKl{cNP=OQzXHz>vn+c)F)zO|Bou>E
z2|-d_=qY#Y+yOu1a}XI?cU}%04)zz%anD(XZC{#~WreV!a$7k2Ug`?&CUEc0EtrkZ
zL49MB)h!_K{H(*l_93D5tO0;BUnvYlo+;yss%n^&qjt6fZOa+}+FDO(~2>G
z2dx@=JZ?DHP^;b7*Y1as5^uphBsh*s*z&MBd?e@I>-9kU>63PjP&^#5YTOb&x^6Cf
z?674rmSHB5Fk!{Gv7rv!?qX#ei_L(XtwVqLX3L}$MI|kJ*w(rhx~tc&L&xP#?cQow
zX_|gx$wMr3pRZIIr_;;O|8fAjd;1`nOeu5K(pCu7>^3E&D2OBBq?sYa(%S?GwG&_0-s%_v$L@R!5H_fc)lOb9ZoOO#p`Nn`KU
z3LTTBtjwo`7(HA6
z7gmO$yTR!5L>Bsg!X8616{JUngg_@&85%>W=mChTR;x4`P=?PJ~oPuy5
zU-L`C@_!34D21{fD~Y8NVnR3t;aqZI3fIhmgmx}$oc-dKDC6Ap$Gy>a!`A*x2L1v0
WcZ@i?LyX}70000
+
+
+
+
+
+ PGlite
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/pglite/src/main.ts b/projects/pglite/src/main.ts
new file mode 100644
index 00000000..c7b673cf
--- /dev/null
+++ b/projects/pglite/src/main.ts
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.error(err));
diff --git a/projects/pglite/src/polyfills.ts b/projects/pglite/src/polyfills.ts
new file mode 100644
index 00000000..3c1fabad
--- /dev/null
+++ b/projects/pglite/src/polyfills.ts
@@ -0,0 +1,34 @@
+import { logPageLoadTime } from '../../../src/shared/util-browser';
+logPageLoadTime();
+
+(window as any).global = window;
+
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js'; // Included with Angular CLI.
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/projects/pglite/src/styles.less b/projects/pglite/src/styles.less
new file mode 100644
index 00000000..2288e329
--- /dev/null
+++ b/projects/pglite/src/styles.less
@@ -0,0 +1,2 @@
+@import '@angular/material/prebuilt-themes/indigo-pink.css';
+@import "../../../src/styles.css";
diff --git a/projects/pglite/tsconfig.app.json b/projects/pglite/tsconfig.app.json
new file mode 100644
index 00000000..c80e4eea
--- /dev/null
+++ b/projects/pglite/tsconfig.app.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts",
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.d.ts"
+ ],
+ "exclude": [
+ ]
+}
From dcd7249a7d32e482cac0f97ecd32dd0fc92be900 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 13:16:28 +0000
Subject: [PATCH 3/5] fix: add pglite eslint config, remove unused import, add
test:pglite to CI
Agent-Logs-Url: https://github.com/pubkey/client-side-databases/sessions/a862d35f-46f2-4322-8f72-969f43042e13
Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com>
---
.github/workflows/main.yml | 5 ++++
projects/pglite/.eslintrc.json | 43 ++++++++++++++++++++++++++++
projects/pglite/src/app/app.logic.ts | 1 -
3 files changed, 48 insertions(+), 1 deletion(-)
create mode 100644 projects/pglite/.eslintrc.json
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 303edabc..44988176 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -128,3 +128,8 @@ jobs:
uses: GabrielBB/xvfb-action@v1
with:
run: npm run test:watermelondb
+
+ - name: test:pglite
+ uses: GabrielBB/xvfb-action@v1
+ with:
+ run: npm run test:pglite
diff --git a/projects/pglite/.eslintrc.json b/projects/pglite/.eslintrc.json
new file mode 100644
index 00000000..1e8a7301
--- /dev/null
+++ b/projects/pglite/.eslintrc.json
@@ -0,0 +1,43 @@
+{
+ "extends": "../../.eslintrc.json",
+ "ignorePatterns": [
+ "!**/*"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts"
+ ],
+ "parserOptions": {
+ "project": [
+ "projects/pglite/tsconfig.app.json"
+ ],
+ "createDefaultProgram": true
+ },
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "app",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "app",
+ "style": "kebab-case"
+ }
+ ]
+ }
+ },
+ {
+ "files": [
+ "*.html"
+ ],
+ "rules": {}
+ }
+ ]
+}
diff --git a/projects/pglite/src/app/app.logic.ts b/projects/pglite/src/app/app.logic.ts
index 2e54e680..9ad1de85 100644
--- a/projects/pglite/src/app/app.logic.ts
+++ b/projects/pglite/src/app/app.logic.ts
@@ -26,7 +26,6 @@ import {
createDatabase
} from './services/database.service';
import {
- doesMessageMapUserPair,
sortByNewestFirst
} from 'src/shared/util-server';
import { RXJS_SHARE_REPLAY_DEFAULTS } from 'rxdb';
From 7c189f2bbbcb87b98c42d8b8fe7c6d97fc34bacb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Apr 2026 13:44:15 +0000
Subject: [PATCH 4/5] fix: resolve WASM MIME type error by pre-fetching via
HTTP and adding wasm/data assets
Agent-Logs-Url: https://github.com/pubkey/client-side-databases/sessions/2b1e7173-ccdb-4e07-a5c8-571da1c75c29
Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com>
---
angular.json | 12 ++++++++++-
.../src/app/services/database.service.ts | 20 ++++++++++++++++++-
2 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/angular.json b/angular.json
index 51f1c165..3a98ddc6 100644
--- a/angular.json
+++ b/angular.json
@@ -714,7 +714,17 @@
"inlineStyleLanguage": "less",
"assets": [
"projects/pglite/src/favicon.ico",
- "projects/pglite/src/assets"
+ "projects/pglite/src/assets",
+ {
+ "glob": "*.wasm",
+ "input": "node_modules/@electric-sql/pglite/dist/",
+ "output": "./"
+ },
+ {
+ "glob": "pglite.data",
+ "input": "node_modules/@electric-sql/pglite/dist/",
+ "output": "./"
+ }
],
"styles": [
"projects/pglite/src/styles.less"
diff --git a/projects/pglite/src/app/services/database.service.ts b/projects/pglite/src/app/services/database.service.ts
index 334a7cac..4e14f2d5 100644
--- a/projects/pglite/src/app/services/database.service.ts
+++ b/projects/pglite/src/app/services/database.service.ts
@@ -18,7 +18,25 @@ export interface DatabaseType {
export async function createDatabase(): Promise {
logTime('createDatabase()');
- const db = new PGlite('idb://chat-db');
+ /**
+ * Pre-fetch WASM modules and the FS bundle via proper HTTP URLs so that
+ * WebAssembly.compileStreaming receives responses with the correct
+ * "application/wasm" MIME type. Without this, webpack replaces
+ * import.meta.url with hardcoded file:// paths which Chrome serves with
+ * "application/octet-stream", causing a streaming-compile error that is
+ * caught internally but still logged to console.error (failing the tests).
+ */
+ const [pgliteWasmModule, initdbWasmModule, fsBundleBlob] = await Promise.all([
+ WebAssembly.compileStreaming(fetch('/pglite.wasm')),
+ WebAssembly.compileStreaming(fetch('/initdb.wasm')),
+ fetch('/pglite.data').then(r => r.blob())
+ ]);
+
+ const db = new PGlite('idb://chat-db', {
+ pgliteWasmModule,
+ initdbWasmModule,
+ fsBundle: fsBundleBlob
+ });
await db.waitReady;
logTime('create tables');
From 92195c62e57b69fe0d1665fd8829e413b8276bd2 Mon Sep 17 00:00:00 2001
From: pubkey <8926560+pubkey@users.noreply.github.com>
Date: Wed, 22 Apr 2026 14:46:34 +0200
Subject: [PATCH 5/5] ADD stuff
---
measure-metrics.sh | 1 +
projects/pglite/src/app/app.logic.ts | 20 ++++++++++++++++----
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/measure-metrics.sh b/measure-metrics.sh
index 5fe30e0b..df99a2bd 100644
--- a/measure-metrics.sh
+++ b/measure-metrics.sh
@@ -10,5 +10,6 @@ npm run test:pouchdb
npm run test:rxdb-lokijs
npm run test:rxdb-dexie
npm run test:watermelondb
+npm run test:pglite
npm run aggregate-metrics
diff --git a/projects/pglite/src/app/app.logic.ts b/projects/pglite/src/app/app.logic.ts
index 9ad1de85..fa778455 100644
--- a/projects/pglite/src/app/app.logic.ts
+++ b/projects/pglite/src/app/app.logic.ts
@@ -1,6 +1,7 @@
import {
Observable,
- combineLatest
+ combineLatest,
+ of
} from 'rxjs';
import {
switchMap,
@@ -8,7 +9,8 @@ import {
shareReplay,
startWith,
mergeMap,
- filter
+ filter,
+ tap
} from 'rxjs/operators';
import {
LogicInterface
@@ -48,10 +50,20 @@ export class Logic implements LogicInterface {
);
}),
switchMap(async (userName) => {
- const result = await this.db.db.query(
+ let result = await this.db.db.query(
`SELECT id, "createdAt" FROM users WHERE id = $1 LIMIT 1`,
[userName]
);
+ if (result.rows.length === 0) {
+ await this.db.db.query(
+ `INSERT INTO users (id, "createdAt") VALUES ($1, $2)`,
+ [userName, new Date().getTime()]
+ );
+ result = await this.db.db.query(
+ `SELECT id, "createdAt" FROM users WHERE id = $1 LIMIT 1`,
+ [userName]
+ );
+ }
return result.rows[0] ?? null;
}),
filter((doc): doc is User => !!doc)
@@ -129,7 +141,7 @@ export class Logic implements LogicInterface {
);
});
}),
- switchMap(streams => combineLatest(streams)),
+ switchMap(streams => streams.length === 0 ? of([]) : combineLatest(streams)),
map(usersWithLastMessage => sortByNewestFirst(usersWithLastMessage as any))
);