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 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.
@@ -1,5 +1,5 @@
1
- import { L as SyncLock, P as SQLiteDriver, Q as InjectedModuleFn, Z as InjectedCreateCollection, p as EventSourcedSharedOptions } from "./types-BSdudVLh.mjs";
2
- import { r as ModulesInput, t as EventSourcedDBHandle } from "./create-event-sourced-db-handle-BAos-9pS.mjs";
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
@@ -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;;ADFT;;;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;EH5BK;;;;EGiCR,WAAA,EAAa,KAAA;EH9BF;;;;EGmCX,YAAA;EHlCmB;;;;EGuCnB,iBAAA;EF3CkC;;;AAC7B;AAOT;;EE0CI,OAAA,EAAS,YAAA,CAAa,0BAAA;EFzCqC;;;;EE8C3D,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"}
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-BPsVV1x6.mjs";
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,
@@ -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
- * Splits events into batches of at most `size`, without ever separating events
950
- * that share a `txId`. The server is asked to commit each transaction
951
- * atomically, which it cannot do if we deliver half of one now and half later.
952
- * A single transaction larger than `size` is sent whole rather than split.
953
- */
954
- function batchByTransaction(events, size) {
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
- group.push(events[index]);
964
+ const entry = events[index];
965
+ group.entries.push(entry);
966
+ if (measure) group.bytes += jsonBytes(entry) + 1;
962
967
  index++;
963
968
  }
964
- if (current.length > 0 && current.length + group.length > size) {
965
- batches.push(current);
966
- current = [];
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
- current.push(...group);
969
- if (current.length >= size) {
970
- batches.push(current);
971
- current = [];
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
- if (current.length > 0) batches.push(current);
975
- return batches;
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. Each batch's result is persisted before
979
- * the next is sent, so a failure part-way through keeps the progress already
980
- * made instead of restarting from zero.
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 batch of batchByTransaction(due, batchSize)) {
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 entry of batch) {
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
- let response;
1014
- try {
1015
- response = normalizePushResponse(await push(attempted.map(toOutboundEvent)));
1016
- } catch (err) {
1017
- deadLettered += await backoffBatch(args, attempted, err);
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?.length ?? 0
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 byId = new Map(attempted.map((entry) => [entry.eventId, entry]));
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 = Math.max(1, config.pushBatchSize ?? 100);
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, SyncPushError as c, createEventSourcedDB as i, createHttpTransport as l, resolveModules as n, createEventSourcedLogger as o, createLazySingleton as r, SyncPullError as s, createEventSourcedDBHandle as t, BackendMismatchError as u };
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-BPsVV1x6.mjs.map
2176
+ //# sourceMappingURL=create-event-sourced-db-handle-D23nsV5c.mjs.map