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/angular.json b/angular.json
index 1bcfd546..3a98ddc6 100644
--- a/angular.json
+++ b/angular.json
@@ -688,6 +688,115 @@
}
}
}
+ },
+ "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",
+ {
+ "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"
+ ],
+ "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 +808,4 @@
"enabled": false
}
}
-}
+}
\ No newline at end of file
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/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/.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.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..fa778455
--- /dev/null
+++ b/projects/pglite/src/app/app.logic.ts
@@ -0,0 +1,224 @@
+import {
+ Observable,
+ combineLatest,
+ of
+} from 'rxjs';
+import {
+ switchMap,
+ map,
+ shareReplay,
+ startWith,
+ mergeMap,
+ filter,
+ tap
+} 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 {
+ 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) => {
+ 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)
+ );
+ }
+
+ 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 => streams.length === 0 ? of([]) : 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..4e14f2d5
--- /dev/null
+++ b/projects/pglite/src/app/services/database.service.ts
@@ -0,0 +1,108 @@
+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()');
+
+ /**
+ * 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');
+ 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 00000000..8bedad4e
Binary files /dev/null and b/projects/pglite/src/assets/email-pattern.png differ
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 00000000..997406ad
Binary files /dev/null and b/projects/pglite/src/favicon.ico differ
diff --git a/projects/pglite/src/index.html b/projects/pglite/src/index.html
new file mode 100644
index 00000000..fe861b55
--- /dev/null
+++ b/projects/pglite/src/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ 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": [
+ ]
+}