event-sourced-collection 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/browser.d.mts +2 -2
- package/dist/browser.d.mts.map +1 -1
- package/dist/browser.mjs +4 -1
- package/dist/browser.mjs.map +1 -1
- package/dist/{create-event-sourced-db-handle-BPsVV1x6.mjs → create-event-sourced-db-handle-D23nsV5c.mjs} +200 -53
- package/dist/create-event-sourced-db-handle-D23nsV5c.mjs.map +1 -0
- package/dist/index-BkKizlCB.d.mts +446 -0
- package/dist/index-BkKizlCB.d.mts.map +1 -0
- package/dist/index.d.mts +3 -328
- package/dist/index.mjs +2 -2
- package/dist/node.d.mts +3 -2
- package/dist/node.d.mts.map +1 -1
- package/dist/node.mjs +4 -1
- package/dist/node.mjs.map +1 -1
- package/dist/react-native.d.mts +58 -3
- package/dist/react-native.d.mts.map +1 -1
- package/dist/react-native.mjs +4 -1
- package/dist/react-native.mjs.map +1 -1
- package/dist/react.d.mts +1 -1
- package/dist/{types-BSdudVLh.d.mts → types-CbOMv5E8.d.mts} +32 -5
- package/dist/{types-BSdudVLh.d.mts.map → types-CbOMv5E8.d.mts.map} +1 -1
- package/package.json +1 -1
- package/dist/create-event-sourced-db-handle-BAos-9pS.d.mts +0 -105
- package/dist/create-event-sourced-db-handle-BAos-9pS.d.mts.map +0 -1
- package/dist/create-event-sourced-db-handle-BPsVV1x6.mjs.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/react-native-event-sourced-db-CTS4b8o9.d.mts +0 -61
- package/dist/react-native-event-sourced-db-CTS4b8o9.d.mts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to `event-sourced-collection` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.0.10] - 2026-09-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `recordLocalEchoes` option (default `true`). Set `false` to advance the pull
|
|
13
|
+
cursor past this device's own events without writing resolved inbox echo rows.
|
|
14
|
+
|
|
15
|
+
## [0.0.9] - 2026-09-08
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `useSyncStatus` / `useEnsureDb` on `event-sourced-collection/react`
|
|
20
|
+
- `useManualSync({ db })` so `syncing` follows `subscribeSyncStatus` (load sync + manual)
|
|
21
|
+
|
|
8
22
|
## [0.0.7] - 2026-08-09
|
|
9
23
|
|
|
10
24
|
Reliability-focused release: dead-lettering, backend identity, retries, conflict detection, multi-tab locking, React helpers, and published examples.
|
package/dist/browser.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { L as SyncLock, P as SQLiteDriver,
|
|
2
|
-
import {
|
|
1
|
+
import { $ as InjectedCreateCollection, L as SyncLock, P as SQLiteDriver, et as InjectedModuleFn, p as EventSourcedSharedOptions } from "./types-CbOMv5E8.mjs";
|
|
2
|
+
import { b as ModulesInput, v as EventSourcedDBHandle } from "./index-BkKizlCB.mjs";
|
|
3
3
|
import { PersistedCollectionPersistence } from "@tanstack/db-sqlite-persistence-core";
|
|
4
4
|
|
|
5
5
|
//#region src/platforms/browser-wa-sqlite-driver.d.ts
|
package/dist/browser.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.mts","names":[],"sources":["../src/platforms/browser-wa-sqlite-driver.ts","../src/platforms/browser-types.ts","../src/platforms/browser.ts","../src/platforms/browser-event-sourced-db.ts"],"mappings":";;;;;KAEY,uBAAA;EACV,OAAA,mBACE,GAAA,UACA,MAAA,GAAS,aAAA,cACN,OAAA,CAAQ,aAAA,CAAc,IAAA;EAC3B,KAAA,SAAc,OAAA;AAAA;;;KCJJ,0BAAA;EACV,OAAO;AAAA
|
|
1
|
+
{"version":3,"file":"browser.d.mts","names":[],"sources":["../src/platforms/browser-wa-sqlite-driver.ts","../src/platforms/browser-types.ts","../src/platforms/browser.ts","../src/platforms/browser-event-sourced-db.ts"],"mappings":";;;;;KAEY,uBAAA;EACV,OAAA,mBACE,GAAA,UACA,MAAA,GAAS,aAAA,cACN,OAAA,CAAQ,aAAA,CAAc,IAAA;EAC3B,KAAA,SAAc,OAAA;AAAA;;;KCJJ,0BAAA;EACV,OAAO;AAAA;ADFT;;;;AAAA,KCSY,mBAAA;EACV,+BAAA,GAAkC,OAAA,YAAmB,OAAA,CAAQ,uBAAA;EAC7D,gCAAA,GAAmC,OAAA,YAAmB,8BAAA;EACtD,4BAAA,OAAmC,OAAA,YAAmB,0BAAA;AAAA;AAAA,KAG5C,qBAAA;EACV,YAAA;EACA,iBAAiB;AAAA;AAAA,KAGP,qBAAA;EACV,MAAA,EAD+B,YAAA;EAE/B,WAAA,EAAa,8BAAA;EACb,KAAA,QAAa,OAAA;AAAA;;;;;ADvBf;;;;;;;;;;;;;;;;;;;;;AAKuB;;;;iBEoCD,qBAAA,CACpB,IAAA,EAAM,mBAAA,EACN,MAAA,EAAQ,qBAAA,GACP,OAAA,CAAQ,qBAAA;;;KC9BN,uBAAA;EACH,MAAA,GAAS,KAAY;AAAA;AAAA,KAGX,0BAAA,GAA6B,mBAAA;EACvC,gBAAA,EAAkB,wBAAA;EAClB,0BAAA,EAA4B,gBAAA;AAAA;;;;;;KAQlB,2BAAA,eAA0C,MAAA,SAAe,uBAAA,KACnE,IAAA,CAAK,yBAAA;EH3BH;;;;EGgCA,WAAA,EAAa,KAAA;EH9BY;;;;EGmCzB,YAAA;;;;AFtCJ;EE2CI,iBAAA;;;AF1CK;AAOT;;;EE0CI,OAAA,EAAS,YAAA,CAAa,0BAAA;EFzC6B;;;;EE8CnD,IAAA,GAAO,QAAA;AAAA;;;;;;;;;;AF5CuE;AAGlF;;;;AAEmB;AAGnB;;;;;;;;;;;;;;;AAGsB;;;;ACkBtB;;;;;;;;iBC8DgB,2BAAA,qBACM,MAAA,SAAe,uBAAA,GACnC,MAAA,EAAQ,2BAAA,CAA4B,KAAA,IAAM,oBAAA,CAAA,KAAA"}
|
package/dist/browser.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as resolveModules, t as createEventSourcedDBHandle } from "./create-event-sourced-db-handle-
|
|
1
|
+
import { n as resolveModules, t as createEventSourcedDBHandle } from "./create-event-sourced-db-handle-D23nsV5c.mjs";
|
|
2
2
|
import { t as createWebLocksSyncLock } from "./web-locks-Bi2f732X.mjs";
|
|
3
3
|
//#region src/platforms/browser-wa-sqlite-driver.ts
|
|
4
4
|
function assertTransactionCallbackHasDriverArg(fn) {
|
|
@@ -203,6 +203,9 @@ function createBrowserEventSourcedDB(config) {
|
|
|
203
203
|
upcastEvent: config.upcastEvent,
|
|
204
204
|
retry: config.retry,
|
|
205
205
|
pushBatchSize: config.pushBatchSize,
|
|
206
|
+
maxPushBytes: config.maxPushBytes,
|
|
207
|
+
syncPreset: config.syncPreset,
|
|
208
|
+
recordLocalEchoes: config.recordLocalEchoes,
|
|
206
209
|
backendMismatch: config.backendMismatch,
|
|
207
210
|
conflictDetection: config.conflictDetection,
|
|
208
211
|
hooks: config.hooks,
|
package/dist/browser.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.mjs","names":[],"sources":["../src/platforms/browser-wa-sqlite-driver.ts","../src/platforms/browser.ts","../src/platforms/browser-event-sourced-db.ts"],"sourcesContent":["import type { SQLiteDriver } from \"../core/types\";\n\nexport type BrowserWASQLiteDatabase = {\n execute: <TRow = unknown>(\n sql: string,\n params?: ReadonlyArray<unknown>,\n ) => Promise<ReadonlyArray<TRow>>;\n close?: () => Promise<void> | void;\n};\n\nfunction assertTransactionCallbackHasDriverArg(\n fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,\n): void {\n if (fn.length > 0) {\n return;\n }\n\n throw new Error(\"SQLiteDriver.transaction callback must accept the transaction driver argument\");\n}\n\nfunction assertDatabaseShape(\n database: BrowserWASQLiteDatabase,\n): asserts database is BrowserWASQLiteDatabase {\n if (typeof database.execute !== \"function\") {\n throw new Error(\"Browser wa-sqlite database handle must provide execute(sql, params?)\");\n }\n}\n\nexport class BrowserWASQLiteDriver implements SQLiteDriver {\n private readonly database: BrowserWASQLiteDatabase;\n private queue: Promise<void> = Promise.resolve();\n private nextSavepointId = 1;\n private closed = false;\n\n constructor(database: BrowserWASQLiteDatabase) {\n assertDatabaseShape(database);\n this.database = database;\n }\n\n async exec(sql: string): Promise<void> {\n await this.enqueue(async () => {\n await this.database.execute(sql);\n });\n }\n\n async query<T>(sql: string, params: ReadonlyArray<unknown> = []): Promise<ReadonlyArray<T>> {\n return this.enqueue(() => this.database.execute<T>(sql, params));\n }\n\n async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {\n await this.enqueue(async () => {\n await this.database.execute(sql, params);\n });\n }\n\n async transaction<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn);\n\n return this.enqueue(async () => {\n await this.database.execute(`BEGIN IMMEDIATE`);\n try {\n const result = await fn(this.createTransactionDriver());\n await this.database.execute(`COMMIT`);\n return result;\n } catch (error) {\n try {\n await this.database.execute(`ROLLBACK`);\n } catch {}\n throw error;\n }\n });\n }\n\n async transactionWithDriver<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T> {\n return this.transaction(fn);\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closed = true;\n\n if (typeof this.database.close === \"function\") {\n await Promise.resolve(this.database.close());\n }\n }\n\n private createTransactionDriver(): SQLiteDriver {\n return {\n exec: (sql) => this.database.execute(sql).then(() => undefined),\n query: <T>(sql: string, params: ReadonlyArray<unknown> = []) =>\n this.database.execute<T>(sql, params),\n run: (sql: string, params: ReadonlyArray<unknown> = []) =>\n this.database.execute(sql, params).then(() => undefined),\n transaction: <T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>) =>\n this.runNestedTransaction(fn),\n transactionWithDriver: <T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>) =>\n this.runNestedTransaction(fn),\n };\n }\n\n private async runNestedTransaction<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn);\n\n const savepointName = `tsdb_sp_${this.nextSavepointId}`;\n this.nextSavepointId++;\n await this.database.execute(`SAVEPOINT ${savepointName}`);\n try {\n const result = await fn(this.createTransactionDriver());\n await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`);\n return result;\n } catch (error) {\n await this.database.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`);\n await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`);\n throw error;\n }\n }\n\n private enqueue<T>(operation: () => Promise<T> | T): Promise<T> {\n const queuedOperation = this.queue.then(operation, operation);\n this.queue = queuedOperation.then(\n () => undefined,\n () => undefined,\n );\n return queuedOperation;\n }\n}\n","import { BrowserWASQLiteDriver } from \"./browser-wa-sqlite-driver\";\nimport type {\n BrowserPlatformConfig,\n BrowserPlatformDeps,\n BrowserPlatformResult,\n} from \"./browser-types\";\n\nexport type {\n BrowserCoordinatorInstance,\n BrowserPlatformConfig,\n BrowserPlatformDeps,\n BrowserPlatformResult,\n} from \"./browser-types\";\n\nexport type { BrowserWASQLiteDatabase } from \"./browser-wa-sqlite-driver\";\n\n/**\n * Opens OPFS SQLite, a cross-tab coordinator, and TanStack persistence.\n * Prefer {@link createBrowserEventSourcedDB}; call this only when you need the\n * raw `persistence` / `close` pair (for example a custom handle).\n *\n * @example\n * ```ts\n * import { createBrowserPlatform } from \"event-sourced-collection/browser\"\n * import {\n * BrowserCollectionCoordinator,\n * createBrowserWASQLitePersistence,\n * openBrowserWASQLiteOPFSDatabase,\n * } from \"@tanstack/browser-db-sqlite-persistence\"\n *\n * const platform = await createBrowserPlatform(\n * {\n * openBrowserWASQLiteOPFSDatabase,\n * createBrowserWASQLitePersistence,\n * BrowserCollectionCoordinator,\n * },\n * { databaseName: \"app.sqlite\" },\n * )\n *\n * // pass platform.persistence into createEventSourcedDB\n * await platform.close()\n * ```\n */\nexport async function createBrowserPlatform(\n deps: BrowserPlatformDeps,\n config: BrowserPlatformConfig,\n): Promise<BrowserPlatformResult> {\n const database = await deps.openBrowserWASQLiteOPFSDatabase({\n databaseName: config.databaseName,\n } as never);\n\n const coordinator = new deps.BrowserCollectionCoordinator({\n dbName: config.coordinatorDbName ?? config.databaseName.replace(/\\.sqlite$/, \"\"),\n } as never);\n\n const persistence = deps.createBrowserWASQLitePersistence({\n database,\n coordinator,\n } as never);\n\n const driver = new BrowserWASQLiteDriver(database);\n\n return {\n driver,\n persistence,\n close: async () => {\n coordinator.dispose();\n await driver.close();\n },\n };\n}\n","import {\n createEventSourcedDBHandle,\n resolveModules,\n type ModulesInput,\n} from \"../core/create-event-sourced-db-handle\";\nimport type {\n CreateCollectionFn,\n InjectedCreateCollection,\n InjectedModuleFn,\n PersistedCollectionOptionsFn,\n} from \"../core/persisted-collection\";\nimport type { EventSourcedSharedOptions, SyncLock } from \"../core/types\";\nimport { createBrowserPlatform } from \"./browser\";\nimport type { BrowserPlatformDeps } from \"./browser-types\";\nimport { createWebLocksSyncLock } from \"./web-locks\";\n\ntype CollectionDefConstraint = {\n getKey: (state: never) => string | number;\n};\n\nexport type BrowserEventSourcedModules = BrowserPlatformDeps & {\n createCollection: InjectedCreateCollection;\n persistedCollectionOptions: InjectedModuleFn;\n};\n\n/**\n * Browser entry-point config. Shared sync/lifecycle fields come from\n * {@link EventSourcedSharedOptions} (hover those properties for docs).\n * Deep guide: `docs/usage.md`.\n */\nexport type BrowserEventSourcedDBConfig<TDefs extends Record<string, CollectionDefConstraint>> =\n Omit<EventSourcedSharedOptions, \"lock\" | \"lockName\"> & {\n /**\n * User collection registry. Object keys become `db.collections.<key>` and\n * the wire `collectionId`. Must not collide with reserved names.\n */\n collections: TDefs;\n /**\n * OPFS SQLite database file name (e.g. `\"my-app.sqlite\"`). Also used as the\n * default sync lock namespace so only one tab syncs for this DB at a time.\n */\n databaseName: string;\n /**\n * Name for the cross-tab collection coordinator database. Defaults to\n * `databaseName` with a trailing `.sqlite` stripped.\n */\n coordinatorDbName?: string;\n /**\n * Browser OPFS persistence modules from\n * `@tanstack/browser-db-sqlite-persistence` plus `createCollection`.\n * Pass the imported bindings directly, or a function to keep WASM off the\n * critical path.\n */\n modules: ModulesInput<BrowserEventSourcedModules>;\n /**\n * Defaults to a Web Locks–backed lock so only one tab syncs at a time.\n * Pass `null` to opt out and let every tab sync independently.\n */\n lock?: SyncLock | null;\n };\n\nexport type { EventSourcedDBHandle as BrowserEventSourcedDBHandle } from \"../core/create-event-sourced-db-handle\";\n\n/**\n * Browser-flavoured event-sourced DB: OPFS SQLite + optional Web Locks sync\n * election, returned as a lazy singleton.\n *\n * @param config - See {@link BrowserEventSourcedDBConfig} for every option.\n *\n * @example\n * ```ts\n * import { createBrowserEventSourcedDB } from \"event-sourced-collection/browser\"\n * import type { CollectionDef, EventSourcedDB } from \"event-sourced-collection\"\n *\n * type Todo = { id: string; title: string }\n * type Defs = { todos: CollectionDef<Todo, string> }\n *\n * const { ensureDb, db } = createBrowserEventSourcedDB<Defs>({\n * databaseName: \"app.sqlite\",\n * collections: { todos: { getKey: (todo) => todo.id } },\n * sync: { push: \"/api/sync/events\", pull: \"/api/sync/events\" },\n * modules: async () => {\n * const { createCollection } = await import(\"@tanstack/db\")\n * const {\n * BrowserCollectionCoordinator,\n * createBrowserWASQLitePersistence,\n * openBrowserWASQLiteOPFSDatabase,\n * persistedCollectionOptions,\n * } = await import(\"@tanstack/browser-db-sqlite-persistence\")\n * return {\n * createCollection,\n * BrowserCollectionCoordinator,\n * createBrowserWASQLitePersistence,\n * openBrowserWASQLiteOPFSDatabase,\n * persistedCollectionOptions,\n * }\n * },\n * })\n *\n * export async function start() {\n * await ensureDb()\n * }\n * export { db }\n * ```\n */\nexport function createBrowserEventSourcedDB<\n const TDefs extends Record<string, CollectionDefConstraint>,\n>(config: BrowserEventSourcedDBConfig<TDefs>) {\n return createEventSourcedDBHandle<TDefs>({\n guard: assertBrowserEnvironment,\n setup: async () => {\n const modules = await resolveModules(config.modules);\n\n const platform = await createBrowserPlatform(\n {\n openBrowserWASQLiteOPFSDatabase: modules.openBrowserWASQLiteOPFSDatabase,\n createBrowserWASQLitePersistence: modules.createBrowserWASQLitePersistence,\n BrowserCollectionCoordinator: modules.BrowserCollectionCoordinator,\n },\n {\n databaseName: config.databaseName,\n coordinatorDbName: config.coordinatorDbName,\n },\n );\n\n return {\n persistence: platform.persistence,\n createCollection: modules.createCollection as CreateCollectionFn,\n persistedCollectionOptions:\n modules.persistedCollectionOptions as PersistedCollectionOptionsFn,\n collections: config.collections,\n sync: config.sync,\n syncEnabled: config.syncEnabled,\n schemaVersion: config.schemaVersion,\n debug: config.debug,\n clientId: config.clientId,\n unknownEventHandling: config.unknownEventHandling,\n pullOverlap: config.pullOverlap,\n eventSchemaVersion: config.eventSchemaVersion,\n upcastEvent: config.upcastEvent,\n retry: config.retry,\n pushBatchSize: config.pushBatchSize,\n backendMismatch: config.backendMismatch,\n conflictDetection: config.conflictDetection,\n hooks: config.hooks,\n lock: config.lock === null ? undefined : (config.lock ?? createWebLocksSyncLock()),\n lockName: config.databaseName,\n close: platform.close,\n };\n },\n });\n}\n\nfunction assertBrowserEnvironment(): void {\n if (typeof window === \"undefined\") {\n throw new Error(\"Event-sourced DB (browser) is only available in a browser environment.\");\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"browser.mjs","names":[],"sources":["../src/platforms/browser-wa-sqlite-driver.ts","../src/platforms/browser.ts","../src/platforms/browser-event-sourced-db.ts"],"sourcesContent":["import type { SQLiteDriver } from \"../core/types\";\n\nexport type BrowserWASQLiteDatabase = {\n execute: <TRow = unknown>(\n sql: string,\n params?: ReadonlyArray<unknown>,\n ) => Promise<ReadonlyArray<TRow>>;\n close?: () => Promise<void> | void;\n};\n\nfunction assertTransactionCallbackHasDriverArg(\n fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,\n): void {\n if (fn.length > 0) {\n return;\n }\n\n throw new Error(\"SQLiteDriver.transaction callback must accept the transaction driver argument\");\n}\n\nfunction assertDatabaseShape(\n database: BrowserWASQLiteDatabase,\n): asserts database is BrowserWASQLiteDatabase {\n if (typeof database.execute !== \"function\") {\n throw new Error(\"Browser wa-sqlite database handle must provide execute(sql, params?)\");\n }\n}\n\nexport class BrowserWASQLiteDriver implements SQLiteDriver {\n private readonly database: BrowserWASQLiteDatabase;\n private queue: Promise<void> = Promise.resolve();\n private nextSavepointId = 1;\n private closed = false;\n\n constructor(database: BrowserWASQLiteDatabase) {\n assertDatabaseShape(database);\n this.database = database;\n }\n\n async exec(sql: string): Promise<void> {\n await this.enqueue(async () => {\n await this.database.execute(sql);\n });\n }\n\n async query<T>(sql: string, params: ReadonlyArray<unknown> = []): Promise<ReadonlyArray<T>> {\n return this.enqueue(() => this.database.execute<T>(sql, params));\n }\n\n async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {\n await this.enqueue(async () => {\n await this.database.execute(sql, params);\n });\n }\n\n async transaction<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn);\n\n return this.enqueue(async () => {\n await this.database.execute(`BEGIN IMMEDIATE`);\n try {\n const result = await fn(this.createTransactionDriver());\n await this.database.execute(`COMMIT`);\n return result;\n } catch (error) {\n try {\n await this.database.execute(`ROLLBACK`);\n } catch {}\n throw error;\n }\n });\n }\n\n async transactionWithDriver<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T> {\n return this.transaction(fn);\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closed = true;\n\n if (typeof this.database.close === \"function\") {\n await Promise.resolve(this.database.close());\n }\n }\n\n private createTransactionDriver(): SQLiteDriver {\n return {\n exec: (sql) => this.database.execute(sql).then(() => undefined),\n query: <T>(sql: string, params: ReadonlyArray<unknown> = []) =>\n this.database.execute<T>(sql, params),\n run: (sql: string, params: ReadonlyArray<unknown> = []) =>\n this.database.execute(sql, params).then(() => undefined),\n transaction: <T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>) =>\n this.runNestedTransaction(fn),\n transactionWithDriver: <T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>) =>\n this.runNestedTransaction(fn),\n };\n }\n\n private async runNestedTransaction<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn);\n\n const savepointName = `tsdb_sp_${this.nextSavepointId}`;\n this.nextSavepointId++;\n await this.database.execute(`SAVEPOINT ${savepointName}`);\n try {\n const result = await fn(this.createTransactionDriver());\n await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`);\n return result;\n } catch (error) {\n await this.database.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`);\n await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`);\n throw error;\n }\n }\n\n private enqueue<T>(operation: () => Promise<T> | T): Promise<T> {\n const queuedOperation = this.queue.then(operation, operation);\n this.queue = queuedOperation.then(\n () => undefined,\n () => undefined,\n );\n return queuedOperation;\n }\n}\n","import { BrowserWASQLiteDriver } from \"./browser-wa-sqlite-driver\";\nimport type {\n BrowserPlatformConfig,\n BrowserPlatformDeps,\n BrowserPlatformResult,\n} from \"./browser-types\";\n\nexport type {\n BrowserCoordinatorInstance,\n BrowserPlatformConfig,\n BrowserPlatformDeps,\n BrowserPlatformResult,\n} from \"./browser-types\";\n\nexport type { BrowserWASQLiteDatabase } from \"./browser-wa-sqlite-driver\";\n\n/**\n * Opens OPFS SQLite, a cross-tab coordinator, and TanStack persistence.\n * Prefer {@link createBrowserEventSourcedDB}; call this only when you need the\n * raw `persistence` / `close` pair (for example a custom handle).\n *\n * @example\n * ```ts\n * import { createBrowserPlatform } from \"event-sourced-collection/browser\"\n * import {\n * BrowserCollectionCoordinator,\n * createBrowserWASQLitePersistence,\n * openBrowserWASQLiteOPFSDatabase,\n * } from \"@tanstack/browser-db-sqlite-persistence\"\n *\n * const platform = await createBrowserPlatform(\n * {\n * openBrowserWASQLiteOPFSDatabase,\n * createBrowserWASQLitePersistence,\n * BrowserCollectionCoordinator,\n * },\n * { databaseName: \"app.sqlite\" },\n * )\n *\n * // pass platform.persistence into createEventSourcedDB\n * await platform.close()\n * ```\n */\nexport async function createBrowserPlatform(\n deps: BrowserPlatformDeps,\n config: BrowserPlatformConfig,\n): Promise<BrowserPlatformResult> {\n const database = await deps.openBrowserWASQLiteOPFSDatabase({\n databaseName: config.databaseName,\n } as never);\n\n const coordinator = new deps.BrowserCollectionCoordinator({\n dbName: config.coordinatorDbName ?? config.databaseName.replace(/\\.sqlite$/, \"\"),\n } as never);\n\n const persistence = deps.createBrowserWASQLitePersistence({\n database,\n coordinator,\n } as never);\n\n const driver = new BrowserWASQLiteDriver(database);\n\n return {\n driver,\n persistence,\n close: async () => {\n coordinator.dispose();\n await driver.close();\n },\n };\n}\n","import {\n createEventSourcedDBHandle,\n resolveModules,\n type ModulesInput,\n} from \"../core/create-event-sourced-db-handle\";\nimport type {\n CreateCollectionFn,\n InjectedCreateCollection,\n InjectedModuleFn,\n PersistedCollectionOptionsFn,\n} from \"../core/persisted-collection\";\nimport type { EventSourcedSharedOptions, SyncLock } from \"../core/types\";\nimport { createBrowserPlatform } from \"./browser\";\nimport type { BrowserPlatformDeps } from \"./browser-types\";\nimport { createWebLocksSyncLock } from \"./web-locks\";\n\ntype CollectionDefConstraint = {\n getKey: (state: never) => string | number;\n};\n\nexport type BrowserEventSourcedModules = BrowserPlatformDeps & {\n createCollection: InjectedCreateCollection;\n persistedCollectionOptions: InjectedModuleFn;\n};\n\n/**\n * Browser entry-point config. Shared sync/lifecycle fields come from\n * {@link EventSourcedSharedOptions} (hover those properties for docs).\n * Deep guide: `docs/usage.md`.\n */\nexport type BrowserEventSourcedDBConfig<TDefs extends Record<string, CollectionDefConstraint>> =\n Omit<EventSourcedSharedOptions, \"lock\" | \"lockName\"> & {\n /**\n * User collection registry. Object keys become `db.collections.<key>` and\n * the wire `collectionId`. Must not collide with reserved names.\n */\n collections: TDefs;\n /**\n * OPFS SQLite database file name (e.g. `\"my-app.sqlite\"`). Also used as the\n * default sync lock namespace so only one tab syncs for this DB at a time.\n */\n databaseName: string;\n /**\n * Name for the cross-tab collection coordinator database. Defaults to\n * `databaseName` with a trailing `.sqlite` stripped.\n */\n coordinatorDbName?: string;\n /**\n * Browser OPFS persistence modules from\n * `@tanstack/browser-db-sqlite-persistence` plus `createCollection`.\n * Pass the imported bindings directly, or a function to keep WASM off the\n * critical path.\n */\n modules: ModulesInput<BrowserEventSourcedModules>;\n /**\n * Defaults to a Web Locks–backed lock so only one tab syncs at a time.\n * Pass `null` to opt out and let every tab sync independently.\n */\n lock?: SyncLock | null;\n };\n\nexport type { EventSourcedDBHandle as BrowserEventSourcedDBHandle } from \"../core/create-event-sourced-db-handle\";\n\n/**\n * Browser-flavoured event-sourced DB: OPFS SQLite + optional Web Locks sync\n * election, returned as a lazy singleton.\n *\n * @param config - See {@link BrowserEventSourcedDBConfig} for every option.\n *\n * @example\n * ```ts\n * import { createBrowserEventSourcedDB } from \"event-sourced-collection/browser\"\n * import type { CollectionDef, EventSourcedDB } from \"event-sourced-collection\"\n *\n * type Todo = { id: string; title: string }\n * type Defs = { todos: CollectionDef<Todo, string> }\n *\n * const { ensureDb, db } = createBrowserEventSourcedDB<Defs>({\n * databaseName: \"app.sqlite\",\n * collections: { todos: { getKey: (todo) => todo.id } },\n * sync: { push: \"/api/sync/events\", pull: \"/api/sync/events\" },\n * modules: async () => {\n * const { createCollection } = await import(\"@tanstack/db\")\n * const {\n * BrowserCollectionCoordinator,\n * createBrowserWASQLitePersistence,\n * openBrowserWASQLiteOPFSDatabase,\n * persistedCollectionOptions,\n * } = await import(\"@tanstack/browser-db-sqlite-persistence\")\n * return {\n * createCollection,\n * BrowserCollectionCoordinator,\n * createBrowserWASQLitePersistence,\n * openBrowserWASQLiteOPFSDatabase,\n * persistedCollectionOptions,\n * }\n * },\n * })\n *\n * export async function start() {\n * await ensureDb()\n * }\n * export { db }\n * ```\n */\nexport function createBrowserEventSourcedDB<\n const TDefs extends Record<string, CollectionDefConstraint>,\n>(config: BrowserEventSourcedDBConfig<TDefs>) {\n return createEventSourcedDBHandle<TDefs>({\n guard: assertBrowserEnvironment,\n setup: async () => {\n const modules = await resolveModules(config.modules);\n\n const platform = await createBrowserPlatform(\n {\n openBrowserWASQLiteOPFSDatabase: modules.openBrowserWASQLiteOPFSDatabase,\n createBrowserWASQLitePersistence: modules.createBrowserWASQLitePersistence,\n BrowserCollectionCoordinator: modules.BrowserCollectionCoordinator,\n },\n {\n databaseName: config.databaseName,\n coordinatorDbName: config.coordinatorDbName,\n },\n );\n\n return {\n persistence: platform.persistence,\n createCollection: modules.createCollection as CreateCollectionFn,\n persistedCollectionOptions:\n modules.persistedCollectionOptions as PersistedCollectionOptionsFn,\n collections: config.collections,\n sync: config.sync,\n syncEnabled: config.syncEnabled,\n schemaVersion: config.schemaVersion,\n debug: config.debug,\n clientId: config.clientId,\n unknownEventHandling: config.unknownEventHandling,\n pullOverlap: config.pullOverlap,\n eventSchemaVersion: config.eventSchemaVersion,\n upcastEvent: config.upcastEvent,\n retry: config.retry,\n pushBatchSize: config.pushBatchSize,\n maxPushBytes: config.maxPushBytes,\n syncPreset: config.syncPreset,\n recordLocalEchoes: config.recordLocalEchoes,\n backendMismatch: config.backendMismatch,\n conflictDetection: config.conflictDetection,\n hooks: config.hooks,\n lock: config.lock === null ? undefined : (config.lock ?? createWebLocksSyncLock()),\n lockName: config.databaseName,\n close: platform.close,\n };\n },\n });\n}\n\nfunction assertBrowserEnvironment(): void {\n if (typeof window === \"undefined\") {\n throw new Error(\"Event-sourced DB (browser) is only available in a browser environment.\");\n }\n}\n"],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@ const RESERVED_IDS = /* @__PURE__ */ new Set([
|
|
|
14
14
|
SYNCMETA_ID,
|
|
15
15
|
ROWVERSIONS_ID
|
|
16
16
|
]);
|
|
17
|
+
const PAYLOAD_TOO_LARGE_ERROR_CODE = "PAYLOAD_TOO_LARGE";
|
|
17
18
|
const SYNC_LOCK_PREFIX = "event-sourced-sync";
|
|
18
19
|
//#endregion
|
|
19
20
|
//#region src/internal/hooks.ts
|
|
@@ -480,7 +481,7 @@ var BackendMismatchError = class extends Error {
|
|
|
480
481
|
}
|
|
481
482
|
};
|
|
482
483
|
async function pullInbox(args) {
|
|
483
|
-
const { outbox, inbox, syncmeta, pull, clientId, pullOverlap, context } = args;
|
|
484
|
+
const { outbox, inbox, syncmeta, pull, clientId, pullOverlap, recordLocalEchoes, context } = args;
|
|
484
485
|
const { log } = context;
|
|
485
486
|
let pulled = 0;
|
|
486
487
|
let skipped = 0;
|
|
@@ -513,10 +514,11 @@ async function pullInbox(args) {
|
|
|
513
514
|
let halted = false;
|
|
514
515
|
for (const event of sorted) {
|
|
515
516
|
if (isLocalOrigin(event, outbox, clientId)) {
|
|
516
|
-
await markInboxEventResolved(inbox, event);
|
|
517
|
+
if (recordLocalEchoes) await markInboxEventResolved(inbox, event);
|
|
517
518
|
log.debug("pull skipped: event originated locally", {
|
|
518
519
|
eventId: event.eventId,
|
|
519
|
-
globalSeq: event.globalSeq
|
|
520
|
+
globalSeq: event.globalSeq,
|
|
521
|
+
recorded: recordLocalEchoes
|
|
520
522
|
});
|
|
521
523
|
continue;
|
|
522
524
|
}
|
|
@@ -945,56 +947,157 @@ async function deadLetter(deadletter, outbox, entry, reason, message, code, now,
|
|
|
945
947
|
});
|
|
946
948
|
emit("onDeadLetter", record);
|
|
947
949
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const batches = [];
|
|
956
|
-
let current = [];
|
|
950
|
+
const utf8 = new TextEncoder();
|
|
951
|
+
function jsonBytes(entry) {
|
|
952
|
+
return utf8.encode(JSON.stringify(toOutboundEvent(entry))).length;
|
|
953
|
+
}
|
|
954
|
+
/** Events are ordered by localSeq, so one transaction's events are adjacent. */
|
|
955
|
+
function groupByTransaction(events, measure) {
|
|
956
|
+
const groups = [];
|
|
957
957
|
for (let index = 0; index < events.length;) {
|
|
958
958
|
const txId = events[index].txId;
|
|
959
|
-
const group =
|
|
959
|
+
const group = {
|
|
960
|
+
entries: [],
|
|
961
|
+
bytes: 0
|
|
962
|
+
};
|
|
960
963
|
while (index < events.length && events[index].txId === txId) {
|
|
961
|
-
|
|
964
|
+
const entry = events[index];
|
|
965
|
+
group.entries.push(entry);
|
|
966
|
+
if (measure) group.bytes += jsonBytes(entry) + 1;
|
|
962
967
|
index++;
|
|
963
968
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
969
|
+
groups.push(group);
|
|
970
|
+
}
|
|
971
|
+
return groups;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Packs transaction groups into batches of at most `size` events and
|
|
975
|
+
* `maxBytes` of JSON, never separating events that share a `txId`: the server
|
|
976
|
+
* commits each transaction atomically, which it cannot do if we deliver half
|
|
977
|
+
* of one now and half later. A transaction with more than `size` events is
|
|
978
|
+
* sent whole; one larger than `maxBytes` is returned in `oversized` instead.
|
|
979
|
+
*/
|
|
980
|
+
function packBatches(groups, size, maxBytes) {
|
|
981
|
+
const batches = [];
|
|
982
|
+
const oversized = [];
|
|
983
|
+
let current = {
|
|
984
|
+
groups: [],
|
|
985
|
+
count: 0,
|
|
986
|
+
bytes: 1
|
|
987
|
+
};
|
|
988
|
+
const close = () => {
|
|
989
|
+
if (current.count > 0) batches.push(current);
|
|
990
|
+
current = {
|
|
991
|
+
groups: [],
|
|
992
|
+
count: 0,
|
|
993
|
+
bytes: 1
|
|
994
|
+
};
|
|
995
|
+
};
|
|
996
|
+
for (const group of groups) {
|
|
997
|
+
if (maxBytes !== null && group.bytes + 1 > maxBytes) {
|
|
998
|
+
oversized.push(group);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const overCount = current.count + group.entries.length > size;
|
|
1002
|
+
const overBytes = maxBytes !== null && current.bytes + group.bytes > maxBytes;
|
|
1003
|
+
if (current.count > 0 && (overCount || overBytes)) close();
|
|
1004
|
+
current.groups.push(group);
|
|
1005
|
+
current.count += group.entries.length;
|
|
1006
|
+
current.bytes += group.bytes;
|
|
1007
|
+
if (current.count >= size) close();
|
|
1008
|
+
}
|
|
1009
|
+
close();
|
|
1010
|
+
return {
|
|
1011
|
+
batches,
|
|
1012
|
+
oversized
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function isPayloadTooLarge(err) {
|
|
1016
|
+
return typeof err === "object" && err !== null && "status" in err && err.status === 413;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Posts `groups` as one request. On HTTP 413 the request is halved along
|
|
1020
|
+
* transaction boundaries and each half posted in turn, down to a single
|
|
1021
|
+
* transaction, so one oversized event cannot hold the rest hostage.
|
|
1022
|
+
*/
|
|
1023
|
+
async function postGroups(push, groups, log) {
|
|
1024
|
+
const entries = groups.flatMap((group) => group.entries);
|
|
1025
|
+
try {
|
|
1026
|
+
const response = normalizePushResponse(await push(entries.map(toOutboundEvent)));
|
|
1027
|
+
return {
|
|
1028
|
+
confirmed: [...response.confirmed],
|
|
1029
|
+
failed: [...response.failed ?? []],
|
|
1030
|
+
tooLarge: [],
|
|
1031
|
+
unsent: []
|
|
1032
|
+
};
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
if (!isPayloadTooLarge(err)) return {
|
|
1035
|
+
confirmed: [],
|
|
1036
|
+
failed: [],
|
|
1037
|
+
tooLarge: [],
|
|
1038
|
+
unsent: entries,
|
|
1039
|
+
error: err
|
|
1040
|
+
};
|
|
1041
|
+
if (groups.length === 1) {
|
|
1042
|
+
log.warn("push transaction too large for the server", { events: entries.length });
|
|
1043
|
+
return {
|
|
1044
|
+
confirmed: [],
|
|
1045
|
+
failed: [],
|
|
1046
|
+
tooLarge: entries,
|
|
1047
|
+
unsent: []
|
|
1048
|
+
};
|
|
967
1049
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1050
|
+
const middle = Math.ceil(groups.length / 2);
|
|
1051
|
+
log.info("push batch hit HTTP 413, splitting", {
|
|
1052
|
+
groups: groups.length,
|
|
1053
|
+
events: entries.length
|
|
1054
|
+
});
|
|
1055
|
+
const first = await postGroups(push, groups.slice(0, middle), log);
|
|
1056
|
+
if (first.error !== void 0) {
|
|
1057
|
+
const rest = groups.slice(middle).flatMap((group) => group.entries);
|
|
1058
|
+
return {
|
|
1059
|
+
...first,
|
|
1060
|
+
unsent: [...first.unsent, ...rest]
|
|
1061
|
+
};
|
|
972
1062
|
}
|
|
1063
|
+
const second = await postGroups(push, groups.slice(middle), log);
|
|
1064
|
+
return {
|
|
1065
|
+
confirmed: [...first.confirmed, ...second.confirmed],
|
|
1066
|
+
failed: [...first.failed, ...second.failed],
|
|
1067
|
+
tooLarge: [...first.tooLarge, ...second.tooLarge],
|
|
1068
|
+
unsent: second.unsent,
|
|
1069
|
+
error: second.error
|
|
1070
|
+
};
|
|
973
1071
|
}
|
|
974
|
-
|
|
975
|
-
|
|
1072
|
+
}
|
|
1073
|
+
async function deadLetterTooLarge(args, entries, message) {
|
|
1074
|
+
for (const entry of entries) await deadLetter(args.deadletter, args.outbox, entry, "rejected", message, PAYLOAD_TOO_LARGE_ERROR_CODE, args.now, args.emit, args.log, args.rowversions);
|
|
1075
|
+
return entries.length;
|
|
976
1076
|
}
|
|
977
1077
|
/**
|
|
978
|
-
* Pushes due outbox events in batches
|
|
979
|
-
* the next is sent, so a failure
|
|
980
|
-
* made instead of restarting
|
|
1078
|
+
* Pushes due outbox events in batches limited by event count and JSON bytes.
|
|
1079
|
+
* Each batch's result is persisted before the next is sent, so a failure
|
|
1080
|
+
* part-way through keeps the progress already made instead of restarting.
|
|
981
1081
|
*/
|
|
982
1082
|
async function pushOutbox(args) {
|
|
983
|
-
const { outbox, push, batchSize, now, log } = args;
|
|
1083
|
+
const { outbox, push, batchSize, maxBytes, now, log } = args;
|
|
984
1084
|
const due = [...outbox.state.values()].filter((entry) => isDue(entry, now)).sort(byLocalSeq);
|
|
985
1085
|
log.debug("push outbox", {
|
|
986
1086
|
dueCount: due.length,
|
|
987
|
-
batchSize
|
|
1087
|
+
batchSize,
|
|
1088
|
+
maxBytes
|
|
988
1089
|
});
|
|
989
1090
|
if (due.length === 0) return {
|
|
990
1091
|
pushed: 0,
|
|
991
1092
|
deadLettered: 0
|
|
992
1093
|
};
|
|
1094
|
+
const { batches, oversized } = packBatches(groupByTransaction(due, maxBytes !== null), batchSize, maxBytes);
|
|
993
1095
|
let pushed = 0;
|
|
994
1096
|
let deadLettered = 0;
|
|
995
|
-
for (const
|
|
1097
|
+
for (const group of oversized) deadLettered += await deadLetterTooLarge(args, group.entries, `Transaction is ${group.bytes} bytes, over maxPushBytes (${maxBytes})`);
|
|
1098
|
+
for (const batch of batches) {
|
|
996
1099
|
const attempted = [];
|
|
997
|
-
for (const
|
|
1100
|
+
for (const group of batch.groups) for (const entry of group.entries) {
|
|
998
1101
|
const attemptCount = (entry.attemptCount ?? 0) + 1;
|
|
999
1102
|
await outbox.update(entry.eventId, (draft) => {
|
|
1000
1103
|
draft.syncStatus = "pending";
|
|
@@ -1010,27 +1113,17 @@ async function pushOutbox(args) {
|
|
|
1010
1113
|
attemptCount
|
|
1011
1114
|
});
|
|
1012
1115
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1019
|
-
log.warn("push stopped after transport failure", {
|
|
1020
|
-
message: error.message,
|
|
1021
|
-
pushed,
|
|
1022
|
-
remaining: due.length - pushed
|
|
1023
|
-
});
|
|
1024
|
-
return {
|
|
1025
|
-
pushed,
|
|
1026
|
-
deadLettered,
|
|
1027
|
-
error
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1116
|
+
const byId = new Map(attempted.map((entry) => [entry.eventId, entry]));
|
|
1117
|
+
const response = await postGroups(push, batch.groups.map((group) => ({
|
|
1118
|
+
bytes: group.bytes,
|
|
1119
|
+
entries: group.entries.map((entry) => byId.get(entry.eventId))
|
|
1120
|
+
})), log);
|
|
1030
1121
|
log.info("push batch confirmed", {
|
|
1031
1122
|
sent: attempted.length,
|
|
1123
|
+
bytes: batch.bytes,
|
|
1032
1124
|
confirmed: response.confirmed.length,
|
|
1033
|
-
failed: response.failed
|
|
1125
|
+
failed: response.failed.length,
|
|
1126
|
+
tooLarge: response.tooLarge.length
|
|
1034
1127
|
});
|
|
1035
1128
|
for (const confirmation of response.confirmed) {
|
|
1036
1129
|
if (!outbox.has(confirmation.eventId)) continue;
|
|
@@ -1049,12 +1142,26 @@ async function pushOutbox(args) {
|
|
|
1049
1142
|
globalSeq: confirmation.globalSeq
|
|
1050
1143
|
});
|
|
1051
1144
|
}
|
|
1052
|
-
const
|
|
1053
|
-
for (const failure of response.failed ?? []) {
|
|
1145
|
+
for (const failure of response.failed) {
|
|
1054
1146
|
const entry = byId.get(failure.eventId);
|
|
1055
1147
|
if (!entry) continue;
|
|
1056
1148
|
deadLettered += await handleFailure(args, entry, failure);
|
|
1057
1149
|
}
|
|
1150
|
+
deadLettered += await deadLetterTooLarge(args, response.tooLarge, "Server rejected the transaction as too large (HTTP 413)");
|
|
1151
|
+
if (response.error !== void 0) {
|
|
1152
|
+
deadLettered += await backoffBatch(args, response.unsent, response.error);
|
|
1153
|
+
const error = response.error instanceof Error ? response.error : new Error(String(response.error));
|
|
1154
|
+
log.warn("push stopped after transport failure", {
|
|
1155
|
+
message: error.message,
|
|
1156
|
+
pushed,
|
|
1157
|
+
remaining: due.length - pushed
|
|
1158
|
+
});
|
|
1159
|
+
return {
|
|
1160
|
+
pushed,
|
|
1161
|
+
deadLettered,
|
|
1162
|
+
error
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1058
1165
|
}
|
|
1059
1166
|
return {
|
|
1060
1167
|
pushed,
|
|
@@ -1102,6 +1209,41 @@ async function backoffBatch(args, batch, err) {
|
|
|
1102
1209
|
return deadLettered;
|
|
1103
1210
|
}
|
|
1104
1211
|
//#endregion
|
|
1212
|
+
//#region src/core/sync-presets.ts
|
|
1213
|
+
/**
|
|
1214
|
+
* Push limits per hosting platform. Byte caps leave headroom under the
|
|
1215
|
+
* platform's request body limit (Vercel functions: 4.5 MB; AWS Lambda and
|
|
1216
|
+
* Netlify: 6 MB) for headers and JSON size estimation drift.
|
|
1217
|
+
*
|
|
1218
|
+
* @example
|
|
1219
|
+
* ```ts
|
|
1220
|
+
* createBrowserEventSourcedDB({ ..., syncPreset: "vercel" })
|
|
1221
|
+
* ```
|
|
1222
|
+
*/
|
|
1223
|
+
const SYNC_PUSH_PRESETS = {
|
|
1224
|
+
default: {
|
|
1225
|
+
pushBatchSize: 20,
|
|
1226
|
+
maxPushBytes: null
|
|
1227
|
+
},
|
|
1228
|
+
serverless: {
|
|
1229
|
+
pushBatchSize: 25,
|
|
1230
|
+
maxPushBytes: 4e6
|
|
1231
|
+
},
|
|
1232
|
+
vercel: {
|
|
1233
|
+
pushBatchSize: 10,
|
|
1234
|
+
maxPushBytes: 35e5
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
function resolvePushLimits(config) {
|
|
1238
|
+
const preset = SYNC_PUSH_PRESETS[config.syncPreset ?? "default"];
|
|
1239
|
+
const size = Math.floor(config.pushBatchSize ?? preset.pushBatchSize);
|
|
1240
|
+
const bytes = config.maxPushBytes ?? preset.maxPushBytes;
|
|
1241
|
+
return {
|
|
1242
|
+
pushBatchSize: Math.min(100, Math.max(1, size)),
|
|
1243
|
+
maxPushBytes: bytes === null ? null : Math.max(1, Math.floor(bytes))
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
//#endregion
|
|
1105
1247
|
//#region src/internal/serial-queue.ts
|
|
1106
1248
|
/**
|
|
1107
1249
|
* Serializes async work so overlapping callers run one after another instead of
|
|
@@ -1280,8 +1422,9 @@ async function createEventSourcedDB(config) {
|
|
|
1280
1422
|
const clientId = { value: config.clientId ?? generateEventId() };
|
|
1281
1423
|
const unknownEventHandling = config.unknownEventHandling ?? "skip";
|
|
1282
1424
|
const pullOverlap = Math.max(0, config.pullOverlap ?? 0);
|
|
1425
|
+
const recordLocalEchoes = config.recordLocalEchoes ?? true;
|
|
1283
1426
|
const eventSchemaVersion = config.eventSchemaVersion ?? 1;
|
|
1284
|
-
const pushBatchSize =
|
|
1427
|
+
const { pushBatchSize, maxPushBytes } = resolvePushLimits(config);
|
|
1285
1428
|
const backendMismatch = config.backendMismatch ?? "resetCursor";
|
|
1286
1429
|
const conflictDetection = config.conflictDetection ?? false;
|
|
1287
1430
|
const lockName = `${SYNC_LOCK_PREFIX}:${config.lockName ?? "default"}`;
|
|
@@ -1297,7 +1440,9 @@ async function createEventSourcedDB(config) {
|
|
|
1297
1440
|
syncEnabled,
|
|
1298
1441
|
unknownEventHandling,
|
|
1299
1442
|
pullOverlap,
|
|
1443
|
+
recordLocalEchoes,
|
|
1300
1444
|
pushBatchSize,
|
|
1445
|
+
maxPushBytes,
|
|
1301
1446
|
backendMismatch,
|
|
1302
1447
|
conflictDetection,
|
|
1303
1448
|
eventSchemaVersion
|
|
@@ -1433,6 +1578,7 @@ async function createEventSourcedDB(config) {
|
|
|
1433
1578
|
rowversions,
|
|
1434
1579
|
push: transport.push,
|
|
1435
1580
|
batchSize: pushBatchSize,
|
|
1581
|
+
maxBytes: maxPushBytes,
|
|
1436
1582
|
retry,
|
|
1437
1583
|
now: Date.now(),
|
|
1438
1584
|
emit,
|
|
@@ -1456,6 +1602,7 @@ async function createEventSourcedDB(config) {
|
|
|
1456
1602
|
pull: transport.pull,
|
|
1457
1603
|
clientId: clientId.value,
|
|
1458
1604
|
pullOverlap,
|
|
1605
|
+
recordLocalEchoes,
|
|
1459
1606
|
backendMismatch,
|
|
1460
1607
|
context: replayContext
|
|
1461
1608
|
});
|
|
@@ -2024,6 +2171,6 @@ async function resolveModules(modules) {
|
|
|
2024
2171
|
return typeof modules === "function" ? await modules() : modules;
|
|
2025
2172
|
}
|
|
2026
2173
|
//#endregion
|
|
2027
|
-
export { generateEventId as a,
|
|
2174
|
+
export { generateEventId as a, resolvePushLimits as c, createHttpTransport as d, BackendMismatchError as f, createEventSourcedDB as i, SyncPullError as l, resolveModules as n, createEventSourcedLogger as o, createLazySingleton as r, SYNC_PUSH_PRESETS as s, createEventSourcedDBHandle as t, SyncPushError as u };
|
|
2028
2175
|
|
|
2029
|
-
//# sourceMappingURL=create-event-sourced-db-handle-
|
|
2176
|
+
//# sourceMappingURL=create-event-sourced-db-handle-D23nsV5c.mjs.map
|