event-sourced-collection 0.0.10 → 0.0.12

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.
@@ -1,6 +1,36 @@
1
- import { a as BrowserPlatformConfig, c as BrowserWASQLiteDatabase, i as BrowserCoordinatorInstance, n as BrowserEventSourcedModules, o as BrowserPlatformDeps, r as createBrowserEventSourcedDB, s as BrowserPlatformResult, t as BrowserEventSourcedDBConfig } from "./browser-event-sourced-db-YBmcmWDR.mjs";
2
- import { t as EventSourcedDBHandle } from "./create-event-sourced-db-handle-B_uCsLJC.mjs";
1
+ import { $ as InjectedCreateCollection, L as SyncLock, P as SQLiteDriver, et as InjectedModuleFn, p as EventSourcedSharedOptions } from "./types-DrV5f9_t.mjs";
2
+ import { r as ModulesInput, t as EventSourcedDBHandle } from "./create-event-sourced-db-handle-Di5qN4QH.mjs";
3
+ import { PersistedCollectionPersistence } from "@tanstack/db-sqlite-persistence-core";
3
4
 
5
+ //#region src/platforms/browser-wa-sqlite-driver.d.ts
6
+ type BrowserWASQLiteDatabase = {
7
+ execute: <TRow = unknown>(sql: string, params?: ReadonlyArray<unknown>) => Promise<ReadonlyArray<TRow>>;
8
+ close?: () => Promise<void> | void;
9
+ };
10
+ //#endregion
11
+ //#region src/platforms/browser-types.d.ts
12
+ type BrowserCoordinatorInstance = {
13
+ dispose: () => void;
14
+ };
15
+ /**
16
+ * Injected TanStack browser persistence bindings. Parameters are `never` so
17
+ * version-skewed `@tanstack/browser-db-sqlite-persistence` exports stay assignable.
18
+ */
19
+ type BrowserPlatformDeps = {
20
+ openBrowserWASQLiteOPFSDatabase: (options: never) => Promise<BrowserWASQLiteDatabase>;
21
+ createBrowserWASQLitePersistence: (options: never) => PersistedCollectionPersistence;
22
+ BrowserCollectionCoordinator: new (options: never) => BrowserCoordinatorInstance;
23
+ };
24
+ type BrowserPlatformConfig = {
25
+ databaseName: string;
26
+ coordinatorDbName?: string;
27
+ };
28
+ type BrowserPlatformResult = {
29
+ driver: SQLiteDriver;
30
+ persistence: PersistedCollectionPersistence;
31
+ close: () => Promise<void>;
32
+ };
33
+ //#endregion
4
34
  //#region src/platforms/browser.d.ts
5
35
  /**
6
36
  * Opens OPFS SQLite, a cross-tab coordinator, and TanStack persistence.
@@ -31,5 +61,91 @@ import { t as EventSourcedDBHandle } from "./create-event-sourced-db-handle-B_uC
31
61
  */
32
62
  declare function createBrowserPlatform(deps: BrowserPlatformDeps, config: BrowserPlatformConfig): Promise<BrowserPlatformResult>;
33
63
  //#endregion
64
+ //#region src/platforms/browser-event-sourced-db.d.ts
65
+ type CollectionDefConstraint = {
66
+ getKey: (state: never) => string | number;
67
+ };
68
+ type BrowserEventSourcedModules = BrowserPlatformDeps & {
69
+ createCollection: InjectedCreateCollection;
70
+ persistedCollectionOptions: InjectedModuleFn;
71
+ };
72
+ /**
73
+ * Browser entry-point config. Shared sync/lifecycle fields come from
74
+ * {@link EventSourcedSharedOptions} (hover those properties for docs).
75
+ * Deep guide: `docs/usage.md`.
76
+ */
77
+ type BrowserEventSourcedDBConfig<TDefs extends Record<string, CollectionDefConstraint>> = Omit<EventSourcedSharedOptions, "lock" | "lockName"> & {
78
+ /**
79
+ * User collection registry. Object keys become `db.collections.<key>` and
80
+ * the wire `collectionId`. Must not collide with reserved names.
81
+ */
82
+ collections: TDefs;
83
+ /**
84
+ * OPFS SQLite database file name (e.g. `"my-app.sqlite"`). Also used as the
85
+ * default sync lock namespace so only one tab syncs for this DB at a time.
86
+ */
87
+ databaseName: string;
88
+ /**
89
+ * Name for the cross-tab collection coordinator database. Defaults to
90
+ * `databaseName` with a trailing `.sqlite` stripped.
91
+ */
92
+ coordinatorDbName?: string;
93
+ /**
94
+ * Browser OPFS persistence modules from
95
+ * `@tanstack/browser-db-sqlite-persistence` plus `createCollection`.
96
+ * Pass the imported bindings directly, or a function to keep WASM off the
97
+ * critical path.
98
+ */
99
+ modules: ModulesInput<BrowserEventSourcedModules>;
100
+ /**
101
+ * Defaults to a Web Locks–backed lock so only one tab syncs at a time.
102
+ * Pass `null` to opt out and let every tab sync independently.
103
+ */
104
+ lock?: SyncLock | null;
105
+ };
106
+ /**
107
+ * Browser-flavoured event-sourced DB: OPFS SQLite + optional Web Locks sync
108
+ * election, returned as a lazy singleton.
109
+ *
110
+ * @param config - See {@link BrowserEventSourcedDBConfig} for every option.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * import { createBrowserEventSourcedDB } from "event-sourced-collection/browser"
115
+ * import type { CollectionDef, EventSourcedDB } from "event-sourced-collection"
116
+ *
117
+ * type Todo = { id: string; title: string }
118
+ * type Defs = { todos: CollectionDef<Todo, string> }
119
+ *
120
+ * const { ensureDb, db } = createBrowserEventSourcedDB<Defs>({
121
+ * databaseName: "app.sqlite",
122
+ * collections: { todos: { getKey: (todo) => todo.id } },
123
+ * sync: { push: "/api/sync/events", pull: "/api/sync/events" },
124
+ * modules: async () => {
125
+ * const { createCollection } = await import("@tanstack/db")
126
+ * const {
127
+ * BrowserCollectionCoordinator,
128
+ * createBrowserWASQLitePersistence,
129
+ * openBrowserWASQLiteOPFSDatabase,
130
+ * persistedCollectionOptions,
131
+ * } = await import("@tanstack/browser-db-sqlite-persistence")
132
+ * return {
133
+ * createCollection,
134
+ * BrowserCollectionCoordinator,
135
+ * createBrowserWASQLitePersistence,
136
+ * openBrowserWASQLiteOPFSDatabase,
137
+ * persistedCollectionOptions,
138
+ * }
139
+ * },
140
+ * })
141
+ *
142
+ * export async function start() {
143
+ * await ensureDb()
144
+ * }
145
+ * export { db }
146
+ * ```
147
+ */
148
+ declare function createBrowserEventSourcedDB<const TDefs extends Record<string, CollectionDefConstraint>>(config: BrowserEventSourcedDBConfig<TDefs>): EventSourcedDBHandle<TDefs>;
149
+ //#endregion
34
150
  export { type BrowserCoordinatorInstance, type BrowserEventSourcedDBConfig, type EventSourcedDBHandle as BrowserEventSourcedDBHandle, type BrowserEventSourcedModules, type BrowserPlatformConfig, type BrowserPlatformDeps, type BrowserPlatformResult, type BrowserWASQLiteDatabase, createBrowserEventSourcedDB, createBrowserPlatform };
35
151
  //# sourceMappingURL=browser.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.mts","names":[],"sources":["../src/platforms/browser.ts"],"mappings":";;;;;AA2CA;;;;;;;;;;;;;;;;AAGgC;;;;;;;;;;iBAHV,qBAAA,CACpB,IAAA,EAAM,mBAAA,EACN,MAAA,EAAQ,qBAAA,GACP,OAAA,CAAQ,qBAAA"}
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"}
package/dist/browser.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as resolveModules, t as createEventSourcedDBHandle } from "./create-event-sourced-db-handle-D-fHKczr.mjs";
1
+ import { n as resolveModules, t as createEventSourcedDBHandle } from "./create-event-sourced-db-handle-CD8WSjXW.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
@@ -946,56 +947,157 @@ async function deadLetter(deadletter, outbox, entry, reason, message, code, now,
946
947
  });
947
948
  emit("onDeadLetter", record);
948
949
  }
949
- /**
950
- * Splits events into batches of at most `size`, without ever separating events
951
- * that share a `txId`. The server is asked to commit each transaction
952
- * atomically, which it cannot do if we deliver half of one now and half later.
953
- * A single transaction larger than `size` is sent whole rather than split.
954
- */
955
- function batchByTransaction(events, size) {
956
- const batches = [];
957
- 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 = [];
958
957
  for (let index = 0; index < events.length;) {
959
958
  const txId = events[index].txId;
960
- const group = [];
959
+ const group = {
960
+ entries: [],
961
+ bytes: 0
962
+ };
961
963
  while (index < events.length && events[index].txId === txId) {
962
- group.push(events[index]);
964
+ const entry = events[index];
965
+ group.entries.push(entry);
966
+ if (measure) group.bytes += jsonBytes(entry) + 1;
963
967
  index++;
964
968
  }
965
- if (current.length > 0 && current.length + group.length > size) {
966
- batches.push(current);
967
- 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
+ };
968
1049
  }
969
- current.push(...group);
970
- if (current.length >= size) {
971
- batches.push(current);
972
- 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
+ };
973
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
+ };
974
1071
  }
975
- if (current.length > 0) batches.push(current);
976
- 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;
977
1076
  }
978
1077
  /**
979
- * Pushes due outbox events in batches. Each batch's result is persisted before
980
- * the next is sent, so a failure part-way through keeps the progress already
981
- * 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.
982
1081
  */
983
1082
  async function pushOutbox(args) {
984
- const { outbox, push, batchSize, now, log } = args;
1083
+ const { outbox, push, batchSize, maxBytes, now, log } = args;
985
1084
  const due = [...outbox.state.values()].filter((entry) => isDue(entry, now)).sort(byLocalSeq);
986
1085
  log.debug("push outbox", {
987
1086
  dueCount: due.length,
988
- batchSize
1087
+ batchSize,
1088
+ maxBytes
989
1089
  });
990
1090
  if (due.length === 0) return {
991
1091
  pushed: 0,
992
1092
  deadLettered: 0
993
1093
  };
1094
+ const { batches, oversized } = packBatches(groupByTransaction(due, maxBytes !== null), batchSize, maxBytes);
994
1095
  let pushed = 0;
995
1096
  let deadLettered = 0;
996
- 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) {
997
1099
  const attempted = [];
998
- for (const entry of batch) {
1100
+ for (const group of batch.groups) for (const entry of group.entries) {
999
1101
  const attemptCount = (entry.attemptCount ?? 0) + 1;
1000
1102
  await outbox.update(entry.eventId, (draft) => {
1001
1103
  draft.syncStatus = "pending";
@@ -1011,27 +1113,17 @@ async function pushOutbox(args) {
1011
1113
  attemptCount
1012
1114
  });
1013
1115
  }
1014
- let response;
1015
- try {
1016
- response = normalizePushResponse(await push(attempted.map(toOutboundEvent)));
1017
- } catch (err) {
1018
- deadLettered += await backoffBatch(args, attempted, err);
1019
- const error = err instanceof Error ? err : new Error(String(err));
1020
- log.warn("push stopped after transport failure", {
1021
- message: error.message,
1022
- pushed,
1023
- remaining: due.length - pushed
1024
- });
1025
- return {
1026
- pushed,
1027
- deadLettered,
1028
- error
1029
- };
1030
- }
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);
1031
1121
  log.info("push batch confirmed", {
1032
1122
  sent: attempted.length,
1123
+ bytes: batch.bytes,
1033
1124
  confirmed: response.confirmed.length,
1034
- failed: response.failed?.length ?? 0
1125
+ failed: response.failed.length,
1126
+ tooLarge: response.tooLarge.length
1035
1127
  });
1036
1128
  for (const confirmation of response.confirmed) {
1037
1129
  if (!outbox.has(confirmation.eventId)) continue;
@@ -1050,12 +1142,26 @@ async function pushOutbox(args) {
1050
1142
  globalSeq: confirmation.globalSeq
1051
1143
  });
1052
1144
  }
1053
- const byId = new Map(attempted.map((entry) => [entry.eventId, entry]));
1054
- for (const failure of response.failed ?? []) {
1145
+ for (const failure of response.failed) {
1055
1146
  const entry = byId.get(failure.eventId);
1056
1147
  if (!entry) continue;
1057
1148
  deadLettered += await handleFailure(args, entry, failure);
1058
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
+ }
1059
1165
  }
1060
1166
  return {
1061
1167
  pushed,
@@ -1103,6 +1209,41 @@ async function backoffBatch(args, batch, err) {
1103
1209
  return deadLettered;
1104
1210
  }
1105
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
1106
1247
  //#region src/internal/serial-queue.ts
1107
1248
  /**
1108
1249
  * Serializes async work so overlapping callers run one after another instead of
@@ -1283,7 +1424,8 @@ async function createEventSourcedDB(config) {
1283
1424
  const pullOverlap = Math.max(0, config.pullOverlap ?? 0);
1284
1425
  const recordLocalEchoes = config.recordLocalEchoes ?? true;
1285
1426
  const eventSchemaVersion = config.eventSchemaVersion ?? 1;
1286
- const pushBatchSize = Math.max(1, config.pushBatchSize ?? 100);
1427
+ let pushLimits = resolvePushLimits(config);
1428
+ const { pushBatchSize, maxPushBytes } = pushLimits;
1287
1429
  const backendMismatch = config.backendMismatch ?? "resetCursor";
1288
1430
  const conflictDetection = config.conflictDetection ?? false;
1289
1431
  const lockName = `${SYNC_LOCK_PREFIX}:${config.lockName ?? "default"}`;
@@ -1301,6 +1443,7 @@ async function createEventSourcedDB(config) {
1301
1443
  pullOverlap,
1302
1444
  recordLocalEchoes,
1303
1445
  pushBatchSize,
1446
+ maxPushBytes,
1304
1447
  backendMismatch,
1305
1448
  conflictDetection,
1306
1449
  eventSchemaVersion
@@ -1435,7 +1578,8 @@ async function createEventSourcedDB(config) {
1435
1578
  deadletter,
1436
1579
  rowversions,
1437
1580
  push: transport.push,
1438
- batchSize: pushBatchSize,
1581
+ batchSize: pushLimits.pushBatchSize,
1582
+ maxBytes: pushLimits.maxPushBytes,
1439
1583
  retry,
1440
1584
  now: Date.now(),
1441
1585
  emit,
@@ -1734,6 +1878,15 @@ async function createEventSourcedDB(config) {
1734
1878
  subscribeSyncStatus,
1735
1879
  retryDeadLetter,
1736
1880
  discardDeadLetter,
1881
+ getPushLimits: () => pushLimits,
1882
+ setPushLimits(next) {
1883
+ pushLimits = resolvePushLimits({
1884
+ pushBatchSize: next.pushBatchSize ?? pushLimits.pushBatchSize,
1885
+ maxPushBytes: (next.maxPushBytes === void 0 ? pushLimits.maxPushBytes : next.maxPushBytes) ?? void 0
1886
+ });
1887
+ log.info("push limits updated", pushLimits);
1888
+ return pushLimits;
1889
+ },
1737
1890
  pruneSyncedEvents,
1738
1891
  dispose
1739
1892
  };
@@ -2028,6 +2181,6 @@ async function resolveModules(modules) {
2028
2181
  return typeof modules === "function" ? await modules() : modules;
2029
2182
  }
2030
2183
  //#endregion
2031
- 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 };
2184
+ 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 };
2032
2185
 
2033
- //# sourceMappingURL=create-event-sourced-db-handle-D-fHKczr.mjs.map
2186
+ //# sourceMappingURL=create-event-sourced-db-handle-CD8WSjXW.mjs.map