@absolutejs/sync 0.1.0 → 0.3.0

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/README.md CHANGED
@@ -149,7 +149,11 @@ import { syncSocket } from '@absolutejs/sync';
149
149
  import { createSyncEngine, defineMutation } from '@absolutejs/sync/engine';
150
150
  import { prismaCollection } from '@absolutejs/sync/prisma';
151
151
 
152
- const engine = createSyncEngine();
152
+ // `transaction` runs every mutation in your DB's transaction (any ORM), so its
153
+ // writes are ACID and the diff is emitted only after the commit.
154
+ const engine = createSyncEngine({
155
+ transaction: (run) => prisma.$transaction(run)
156
+ });
153
157
 
154
158
  engine.register(
155
159
  prismaCollection({
@@ -160,16 +164,22 @@ engine.register(
160
164
  })
161
165
  );
162
166
 
167
+ // Teach the engine how to persist the table once — now writes auto-emit. The
168
+ // third arg is the transaction handle, so the write joins the mutation's tx.
169
+ engine.registerWriter('orders', {
170
+ insert: (data, ctx, tx) =>
171
+ tx.order.create({ data: { ...data, userId: ctx.userId } }),
172
+ update: (data, _ctx, tx) =>
173
+ tx.order.update({ where: { id: data.id }, data }),
174
+ delete: (row, _ctx, tx) => tx.order.delete({ where: { id: row.id } })
175
+ });
176
+
163
177
  engine.registerMutation(
164
178
  defineMutation({
165
179
  name: 'createOrder',
166
- handler: async (args, ctx, actions) => {
167
- const order = await prisma.order.create({
168
- data: { ...args, userId: ctx.userId }
169
- });
170
- await actions.change('orders', { op: 'insert', row: order });
171
- return order;
172
- }
180
+ // Persists AND goes live in one step — you can't forget to emit, and the
181
+ // diff carries the stored row (db-assigned id). Commits atomically.
182
+ handler: (args, ctx, actions) => actions.insert('orders', args)
173
183
  })
174
184
  );
175
185
 
@@ -211,8 +221,12 @@ await orders.mutate({
211
221
  `ChangeSource` — e.g. `postgresChangeSource` (`/postgres`) over `LISTEN/NOTIFY`,
212
222
  wired with `engine.connectSource(...)` and the trigger SQL from
213
223
  `postgresNotifyTrigger`.
214
- - **Offline.** Pending mutations replay on reconnect; pass `storage`
215
- (e.g. `localStorageMutationStorage`) to also survive a reload.
224
+ - **Offline & local-first.** Pending mutations replay on reconnect; pass `storage`
225
+ (e.g. `localStorageMutationStorage`) to let unconfirmed writes survive a reload.
226
+ Pass `cache` (`localStorageCollectionCache` or `indexedDbCollectionCache`) to
227
+ persist the confirmed rows too — reads are then instant on reload and available
228
+ offline, and the socket resumes from the cached version (a catch-up diff if the
229
+ server's changelog still covers it, a fresh snapshot otherwise).
216
230
  - **Access control is mandatory.** Each collection's `authorize` gates subscribe and
217
231
  its filter scopes rows, so a change to a row a caller can't see never reaches them.
218
232
 
@@ -251,13 +265,17 @@ it, ~3 store round-trips every 20ms ran the voice pipeline far slower than real
251
265
 
252
266
  ### `@absolutejs/sync/client`
253
267
 
254
- | Export | What it is |
255
- | ------------------------------------------------- | --------------------------------------------------------------- |
256
- | `createSyncSubscriber({ topics, onEvent, url? })` | Browser SSE client. |
257
- | `createLiveQuery({ topics, fetcher, ... })` | Hydrate-once, refetch-on-event observable query store. |
258
- | `jsonFetcher(url, init?)` | Default `fetcher`: GET + JSON parse, forwards the abort signal. |
259
- | `createSyncCollection({ url, collection, ... })` | Live diff-driven collection store with optimistic `mutate`. |
260
- | `localStorageMutationStorage(key)` | `localStorage`-backed offline queue for `createSyncCollection`. |
268
+ | Export | What it is |
269
+ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
270
+ | `createSyncSubscriber({ topics, onEvent, url? })` | Browser SSE client. |
271
+ | `createLiveQuery({ topics, fetcher, ... })` | Hydrate-once, refetch-on-event observable query store. |
272
+ | `jsonFetcher(url, init?)` | Default `fetcher`: GET + JSON parse, forwards the abort signal. |
273
+ | `createSyncCollection({ url, collection, ... })` | Live diff-driven collection store with optimistic `mutate`. |
274
+ | `createSyncClient({ url })` | One socket, many collections (`client.collection(...)`). Applies a multi-collection mutation's diffs as one **consistent frame** — no torn cross-collection paint. |
275
+ | `createPresence({ url, room, state })` | Join a presence room: see who's online / typing (`get` + `subscribe`) and publish your own state (`set`). |
276
+ | `localStorageMutationStorage(key)` | `localStorage`-backed offline write queue for `createSyncCollection`. |
277
+ | `localStorageCollectionCache(key)` | `localStorage`-backed local-first read cache: confirmed rows survive a reload, resume from the cached version. |
278
+ | `indexedDbCollectionCache({ key, ... })` | IndexedDB-backed local-first read cache — durable, large-capacity. Same resume semantics, async storage. |
261
279
 
262
280
  ### Framework bindings — `@absolutejs/sync/{react,vue,svelte,angular}`
263
281
 
@@ -291,16 +309,20 @@ mutate({
291
309
 
292
310
  ### `@absolutejs/sync/engine`
293
311
 
294
- | Export | What it is |
295
- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
296
- | `createSyncEngine()` | Registry + view syncer: `register`, `subscribe`, `applyChange`, `connectSource`, `registerMutation`, `runMutation`. |
297
- | `defineCollection({ name, hydrate, key?, match?, authorize?, tables? })` | Define a syncable collection. |
298
- | `defineMutation({ name, handler, authorize? })` | Define a server mutation that emits changes. |
299
- | `createAggregate({ key, groupBy?, value? })` | Incremental count/sum/avg/min/max by group. |
300
- | `createMaterializedView({ key, match, equals? })` | The predicate-matching IVM primitive (`apply`/`reset` → diffs). |
301
- | `createPollingChangeSource({ poll, intervalMs?, startSeq?, onProcessed? })` | DB-agnostic CDC `ChangeSource` that tails a changelog (outbox) table. |
302
- | `query(source).filter().map().join().leftJoin().groupBy().orderBy()` | Declarative incremental query builder (the operator graph). |
303
- | `defineGraphCollection({ name, query, key, authorize? })` | Run a `query` as a live collection. |
312
+ | Export | What it is |
313
+ | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
314
+ | `createSyncEngine()` | Registry + view syncer: `register`, `subscribe`, `applyChange`, `connectSource`, `registerMutation`, `registerWriter`, `runMutation`. |
315
+ | `defineCollection({ name, hydrate, key?, match?, authorize?, tables? })` | Define a syncable collection. |
316
+ | `defineMutation({ name, handler, authorize? })` | Define a server mutation. Its `handler` gets `actions.insert/update/delete` (write through a registered `TableWriter` → persists + emits in one step) plus `actions.change` (escape hatch). Changes commit atomically. |
317
+ | `registerWriter(table, { insert, update, delete })` | Teach the engine how to persist a table (any ORM), so writes auto-emit — you can't write without going live. |
318
+ | `createAggregate({ key, groupBy?, value? })` | Incremental count/sum/avg/min/max by group. |
319
+ | `createMaterializedView({ key, match, equals? })` | The predicate-matching IVM primitive (`apply`/`reset` diffs). |
320
+ | `createPollingChangeSource({ poll, intervalMs?, startSeq?, onProcessed? })` | DB-agnostic CDC `ChangeSource` that tails a changelog (outbox) table. |
321
+ | `engine.connectCluster(bus)` + `createInMemoryClusterBus()` | Horizontal scale: fan changes across server instances over a `ClusterBus` (BYO Redis/Postgres; in-memory bus for dev). |
322
+ | `createPresenceHub()` + `syncSocket({ engine, presence })` | Ephemeral room-scoped presence (online / typing / cursors) over the same socket — not persisted, auto-cleaned on disconnect. |
323
+ | `query(source).filter().map().join().leftJoin().groupBy().orderBy()` | Declarative incremental query builder (the operator graph). |
324
+ | `defineGraphCollection({ name, query, key, authorize? })` | Run a `query` as a live collection. |
325
+ | `defineReactiveQuery({ name, run, key })` + `registerReactive` / `registerReader` | Read-set-tracked query: `run(ctx)` reads via `ctx.db` (`all`/`get`/`where`) and re-runs only when the rows/ranges it read change — no `match`, no manual emit. |
304
326
 
305
327
  ### `@absolutejs/sync/postgres`
306
328
 
@@ -327,15 +349,17 @@ mutate({
327
349
 
328
350
  ### `@absolutejs/sync/drizzle` and `@absolutejs/sync/prisma`
329
351
 
330
- | Export | What it is |
331
- | --------------------------------------------------------------------- | ---------------------------------------------------------- |
332
- | `deriveReadTopics(table\|model, where?, options?)` | Topics a read depends on (`{ topics, rowLevel }`). |
333
- | `publishChange(hub, table\|model, { keys?, op? })` | Publish the table topic + a row topic per key. |
334
- | `publishRows(hub, table\|model, rows, { keyField?/keyColumn?, op? })` | Publish topics for returned/created records. |
335
- | `publishWhere(hub, table\|model, where, { ..., op? })` | Publish topics for an update/delete filter. |
336
- | `tableTopic` / `keyTopic` | The shared topic vocabulary both sides speak. |
337
- | `prismaCollection({ name, where, find, ... })` (prisma) | A sync-engine collection; one `where` → hydrate + matcher. |
338
- | `matchesWhere(where, row)` (prisma) | Evaluate a Prisma `where` against a row (the matcher). |
352
+ | Export | What it is |
353
+ | --------------------------------------------------------------------- | ----------------------------------------------------------- |
354
+ | `deriveReadTopics(table\|model, where?, options?)` | Topics a read depends on (`{ topics, rowLevel }`). |
355
+ | `publishChange(hub, table\|model, { keys?, op? })` | Publish the table topic + a row topic per key. |
356
+ | `publishRows(hub, table\|model, rows, { keyField?/keyColumn?, op? })` | Publish topics for returned/created records. |
357
+ | `publishWhere(hub, table\|model, where, { ..., op? })` | Publish topics for an update/delete filter. |
358
+ | `tableTopic` / `keyTopic` | The shared topic vocabulary both sides speak. |
359
+ | `prismaCollection({ name, where, find, ... })` (prisma) | A sync-engine collection; one `where` → hydrate + matcher. |
360
+ | `matchesWhere(where, row)` (prisma) | Evaluate a Prisma `where` against a row (the matcher). |
361
+ | `drizzleCollection({ name, table, where, find, ... })` (drizzle) | Same one-`where`→hydrate+matcher, for Drizzle. |
362
+ | `matchesDrizzleWhere(table, where, row)` (drizzle) | Evaluate a Drizzle SQL `where` against a row (the matcher). |
339
363
 
340
364
  ## License
341
365
 
@@ -0,0 +1,27 @@
1
+ import type { SQL, Table } from 'drizzle-orm';
2
+ import type { CollectionContext, CollectionDefinition } from '../../engine/collection';
3
+ import type { RowKey } from '../../engine/types';
4
+ export type DrizzleCollectionOptions<T, P = void, Ctx = CollectionContext> = {
5
+ /** Collection name clients subscribe to. */
6
+ name: string;
7
+ /** The Drizzle table this collection reads (drives change routing + key). */
8
+ table: Table;
9
+ /** The query filter, written once — powers both hydrate and the matcher. */
10
+ where: (params: P, ctx: Ctx) => SQL;
11
+ /** Run the read for `where` (your `db.select()...`), returning the rows. */
12
+ find: (where: SQL, params: P, ctx: Ctx) => Promise<Iterable<T>> | Iterable<T>;
13
+ /** Row identity. Defaults to the table's single primary key (else `row.id`). */
14
+ key?: (row: T) => RowKey;
15
+ /** Key column JS property, if not the table's primary key. */
16
+ keyColumn?: string;
17
+ /** Access control; return false (or throw) to deny the subscription. */
18
+ authorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;
19
+ };
20
+ /**
21
+ * A sync-engine collection from one Drizzle query — the Drizzle counterpart to
22
+ * `prismaCollection`. You write the `where` once: it drives the DB read
23
+ * (`hydrate`) AND the incremental `match` (via {@link matchesDrizzleWhere}), so
24
+ * the two can't drift and you never hand-maintain a separate predicate. A filter
25
+ * the matcher can't evaluate falls back to a refetch, never a wrong result.
26
+ */
27
+ export declare const drizzleCollection: <T, P = void, Ctx = CollectionContext>(options: DrizzleCollectionOptions<T, P, Ctx>) => CollectionDefinition<T, P, Ctx>;
@@ -11,6 +11,9 @@
11
11
  * narrowing to a single row when a filter is a simple primary-key equality.
12
12
  */
13
13
  export { keyTopic, tableTopic } from './topics';
14
+ export { drizzleCollection } from './collection';
15
+ export type { DrizzleCollectionOptions } from './collection';
16
+ export { matchesDrizzleWhere, UnsupportedDrizzleFilterError } from './predicate';
14
17
  export { deriveReadTopics } from './read';
15
18
  export type { DeriveReadTopicsOptions, DerivedReadTopics } from './read';
16
19
  export { publishChange, publishRows, publishWhere } from './write';
@@ -83,6 +83,140 @@ var extractRowKeys = (table, rows, keyColumn) => {
83
83
  }
84
84
  return keys;
85
85
  };
86
+ // src/adapters/drizzle/collection.ts
87
+ import { getTableName as getTableName2 } from "drizzle-orm";
88
+
89
+ // src/adapters/drizzle/predicate.ts
90
+ import { Column as Column2, getTableColumns as getTableColumns2, is as is2, Param as Param2, SQL as SQL2 } from "drizzle-orm";
91
+
92
+ class UnsupportedDrizzleFilterError extends Error {
93
+ constructor(detail) {
94
+ super(`Cannot evaluate Drizzle filter "${detail}" incrementally`);
95
+ this.name = "UnsupportedDrizzleFilterError";
96
+ }
97
+ }
98
+ var isDate = (value) => value instanceof Date;
99
+ var equals = (value, operand) => {
100
+ if (operand === null) {
101
+ return value === null || value === undefined;
102
+ }
103
+ if (isDate(value) && isDate(operand)) {
104
+ return value.getTime() === operand.getTime();
105
+ }
106
+ return value === operand;
107
+ };
108
+ var order = (value) => isDate(value) ? value.getTime() : value;
109
+ var compare = (value, operand) => {
110
+ const a = order(value);
111
+ const b = order(operand);
112
+ if (a < b) {
113
+ return -1;
114
+ }
115
+ if (a > b) {
116
+ return 1;
117
+ }
118
+ return 0;
119
+ };
120
+ var comparable = (value) => value !== null && value !== undefined;
121
+ var classify = (chunks) => {
122
+ const cols = [];
123
+ const params = [];
124
+ const arrays = [];
125
+ const sqls = [];
126
+ const ops = [];
127
+ for (const chunk of chunks) {
128
+ if (is2(chunk, SQL2)) {
129
+ sqls.push(chunk);
130
+ } else if (is2(chunk, Column2)) {
131
+ cols.push(chunk);
132
+ } else if (is2(chunk, Param2)) {
133
+ params.push(chunk.value);
134
+ } else if (Array.isArray(chunk)) {
135
+ arrays.push(chunk.map((element) => is2(element, Param2) ? element.value : element));
136
+ } else {
137
+ const raw = chunk.value;
138
+ const text = (Array.isArray(raw) ? raw.join("") : String(raw ?? "")).trim();
139
+ if (text !== "" && text !== "(" && text !== ")") {
140
+ ops.push(text);
141
+ }
142
+ }
143
+ }
144
+ return { arrays, cols, ops, params, sqls };
145
+ };
146
+ var evaluateLeaf = (column, op, params, arrays, row, propFor) => {
147
+ const prop = propFor(column);
148
+ if (prop === undefined) {
149
+ throw new UnsupportedDrizzleFilterError(`column ${column.name}`);
150
+ }
151
+ const value = row[prop];
152
+ const operand = params[0];
153
+ switch (op) {
154
+ case "=":
155
+ return equals(value, operand);
156
+ case "<>":
157
+ return !equals(value, operand);
158
+ case ">":
159
+ return comparable(value) && compare(value, operand) > 0;
160
+ case ">=":
161
+ return comparable(value) && compare(value, operand) >= 0;
162
+ case "<":
163
+ return comparable(value) && compare(value, operand) < 0;
164
+ case "<=":
165
+ return comparable(value) && compare(value, operand) <= 0;
166
+ case "in":
167
+ return (arrays[0] ?? []).some((item) => equals(value, item));
168
+ case "not in":
169
+ return !(arrays[0] ?? []).some((item) => equals(value, item));
170
+ case "is null":
171
+ return value === null || value === undefined;
172
+ case "is not null":
173
+ return value !== null && value !== undefined;
174
+ default:
175
+ throw new UnsupportedDrizzleFilterError(op);
176
+ }
177
+ };
178
+ var evaluateCondition = (node, row, propFor) => {
179
+ const { cols, params, arrays, sqls, ops } = classify(node.queryChunks);
180
+ if (ops.length === 1 && ops[0] === "not" && sqls.length === 1 && cols.length === 0) {
181
+ return !evaluateCondition(sqls[0], row, propFor);
182
+ }
183
+ if (cols.length === 0 && sqls.length === 1 && ops.length === 0) {
184
+ return evaluateCondition(sqls[0], row, propFor);
185
+ }
186
+ if (cols.length === 0 && sqls.length >= 2 && ops.length > 0) {
187
+ const connective = ops[0];
188
+ if ((connective === "and" || connective === "or") && ops.every((op) => op === connective)) {
189
+ const results = sqls.map((sql) => evaluateCondition(sql, row, propFor));
190
+ return connective === "and" ? results.every(Boolean) : results.some(Boolean);
191
+ }
192
+ throw new UnsupportedDrizzleFilterError(ops.join(" "));
193
+ }
194
+ if (cols.length === 1 && sqls.length === 0 && ops.length === 1) {
195
+ return evaluateLeaf(cols[0], ops[0], params, arrays, row, propFor);
196
+ }
197
+ throw new UnsupportedDrizzleFilterError(ops.join(" ") || "unrecognized condition");
198
+ };
199
+ var matchesDrizzleWhere = (table, where, row) => {
200
+ const nameToProp = new Map;
201
+ for (const [prop, column] of Object.entries(getTableColumns2(table))) {
202
+ nameToProp.set(column.name, prop);
203
+ }
204
+ return evaluateCondition(where, row, (column) => nameToProp.get(column.name));
205
+ };
206
+
207
+ // src/adapters/drizzle/collection.ts
208
+ var drizzleCollection = (options) => {
209
+ const keyProp = resolveKeyColumn(options.table, options.keyColumn)?.property;
210
+ const key = options.key ?? ((row) => keyProp !== undefined ? row[keyProp] : row.id);
211
+ return {
212
+ name: options.name,
213
+ tables: [getTableName2(options.table)],
214
+ hydrate: (params, ctx) => options.find(options.where(params, ctx), params, ctx),
215
+ match: (row, params, ctx) => matchesDrizzleWhere(options.table, options.where(params, ctx), row),
216
+ key,
217
+ authorize: options.authorize
218
+ };
219
+ };
86
220
  // src/adapters/drizzle/read.ts
87
221
  var deriveReadTopics = (table, where, options = {}) => {
88
222
  const key = where === undefined ? undefined : extractKeyFromWhere(table, where, options.keyColumn);
@@ -120,9 +254,12 @@ export {
120
254
  publishWhere,
121
255
  publishRows,
122
256
  publishChange,
257
+ matchesDrizzleWhere,
123
258
  keyTopic,
124
- deriveReadTopics
259
+ drizzleCollection,
260
+ deriveReadTopics,
261
+ UnsupportedDrizzleFilterError
125
262
  };
126
263
 
127
- //# debugId=ADD7C375EE239C1664756E2164756E21
264
+ //# debugId=A28CA296A764961764756E2164756E21
128
265
  //# sourceMappingURL=index.js.map
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/adapters/drizzle/topics.ts", "../src/adapters/drizzle/read.ts", "../src/adapters/drizzle/write.ts"],
3
+ "sources": ["../src/adapters/drizzle/topics.ts", "../src/adapters/drizzle/collection.ts", "../src/adapters/drizzle/predicate.ts", "../src/adapters/drizzle/read.ts", "../src/adapters/drizzle/write.ts"],
4
4
  "sourcesContent": [
5
5
  "import {\n\tColumn,\n\tgetTableColumns,\n\tgetTableName,\n\tis,\n\tParam,\n\tSQL\n} from 'drizzle-orm';\nimport type { Table } from 'drizzle-orm';\n\n/**\n * Shared topic vocabulary + key resolution for the Drizzle adapter. Both the\n * read side (derive the topics a query depends on) and the write side (publish\n * the topics a mutation invalidates) build on these so the two always agree.\n */\n\n/** The coarse topic every read/write of `table` touches, e.g. `users`. */\nexport const tableTopic = (table: Table): string => getTableName(table);\n\n/** The row-level topic for one key of `table`, e.g. `users:5`. */\nexport const keyTopic = (table: Table, key: string | number): string =>\n\t`${getTableName(table)}:${key}`;\n\ntype ResolvedKey = {\n\t/** JS property name of the key column on the table / result rows. */\n\tproperty: string;\n\t/** Underlying DB column name, as it appears in a SQL expression. */\n\tcolumn: string;\n};\n\n/**\n * Resolve the column to use as the row key: an explicitly requested column (by\n * JS property name), otherwise the table's sole primary key. Returns `undefined`\n * when no single key column applies (composite or missing primary key).\n */\nexport const resolveKeyColumn = (\n\ttable: Table,\n\tkeyColumn?: string\n): ResolvedKey | undefined => {\n\tconst columns = getTableColumns(table);\n\tif (keyColumn !== undefined) {\n\t\tconst column = columns[keyColumn];\n\t\treturn column === undefined\n\t\t\t? undefined\n\t\t\t: { property: keyColumn, column: column.name };\n\t}\n\tconst primaries = Object.entries(columns).filter(\n\t\t([, column]) => column.primary\n\t);\n\tconst primary = primaries.length === 1 ? primaries[0] : undefined;\n\treturn primary === undefined\n\t\t? undefined\n\t\t: { property: primary[0], column: primary[1].name };\n};\n\n/**\n * Best-effort: pull a single key-column equality value out of a Drizzle `where`\n * expression. Recognises only the simple `eq(keyColumn, scalar)` shape — any\n * nesting (`and`/`or`), extra columns/params, a non-`=` operator, or a\n * non-key/cross-table column yields `undefined`.\n *\n * Reads Drizzle's internal `queryChunks`, which is not a stable public API;\n * every branch degrades to `undefined` (coarser topic) rather than throwing, so\n * a Drizzle version bump can only cost precision, never correctness.\n */\nexport const extractKeyFromWhere = (\n\ttable: Table,\n\twhere: SQL,\n\tkeyColumn?: string\n): string | number | undefined => {\n\tconst resolved = resolveKeyColumn(table, keyColumn);\n\tif (resolved === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst chunks: unknown = (where as { queryChunks?: unknown }).queryChunks;\n\tif (!Array.isArray(chunks)) {\n\t\treturn undefined;\n\t}\n\n\tlet column: Column | undefined;\n\tlet param: Param | undefined;\n\tlet tooComplex = false;\n\tlet operator = '';\n\n\tfor (const chunk of chunks) {\n\t\tif (is(chunk, SQL)) {\n\t\t\t// Nested expression (e.g. and/or) — not a simple equality.\n\t\t\treturn undefined;\n\t\t}\n\t\tif (is(chunk, Column)) {\n\t\t\tif (column !== undefined) {\n\t\t\t\ttooComplex = true;\n\t\t\t}\n\t\t\tcolumn = chunk;\n\t\t} else if (is(chunk, Param)) {\n\t\t\tif (param !== undefined) {\n\t\t\t\ttooComplex = true;\n\t\t\t}\n\t\t\tparam = chunk;\n\t\t} else {\n\t\t\tconst value: unknown = (chunk as { value?: unknown }).value;\n\t\t\tif (Array.isArray(value)) {\n\t\t\t\toperator += value.join('');\n\t\t\t}\n\t\t}\n\t}\n\n\tif (tooComplex || column === undefined || param === undefined) {\n\t\treturn undefined;\n\t}\n\tif (operator.trim() !== '=') {\n\t\treturn undefined;\n\t}\n\tif (column.name !== resolved.column) {\n\t\treturn undefined;\n\t}\n\tif (getTableName(column.table) !== getTableName(table)) {\n\t\treturn undefined;\n\t}\n\n\tconst value: unknown = param.value;\n\treturn typeof value === 'string' || typeof value === 'number'\n\t\t? value\n\t\t: undefined;\n};\n\n/**\n * Read the key value from each row (e.g. the output of a mutation's\n * `.returning()`), using the table's primary-key column or an explicit\n * `keyColumn`. Rows without a string/number key are skipped.\n */\nexport const extractRowKeys = (\n\ttable: Table,\n\trows: ReadonlyArray<Record<string, unknown>>,\n\tkeyColumn?: string\n): (string | number)[] => {\n\tconst resolved = resolveKeyColumn(table, keyColumn);\n\tif (resolved === undefined) {\n\t\treturn [];\n\t}\n\tconst keys: (string | number)[] = [];\n\tfor (const row of rows) {\n\t\tconst value = row[resolved.property];\n\t\tif (typeof value === 'string' || typeof value === 'number') {\n\t\t\tkeys.push(value);\n\t\t}\n\t}\n\treturn keys;\n};\n",
6
+ "import { getTableName } from 'drizzle-orm';\nimport type { SQL, Table } from 'drizzle-orm';\nimport type {\n\tCollectionContext,\n\tCollectionDefinition\n} from '../../engine/collection';\nimport type { RowKey } from '../../engine/types';\nimport { matchesDrizzleWhere } from './predicate';\nimport { resolveKeyColumn } from './topics';\n\nexport type DrizzleCollectionOptions<T, P = void, Ctx = CollectionContext> = {\n\t/** Collection name clients subscribe to. */\n\tname: string;\n\t/** The Drizzle table this collection reads (drives change routing + key). */\n\ttable: Table;\n\t/** The query filter, written once — powers both hydrate and the matcher. */\n\twhere: (params: P, ctx: Ctx) => SQL;\n\t/** Run the read for `where` (your `db.select()...`), returning the rows. */\n\tfind: (\n\t\twhere: SQL,\n\t\tparams: P,\n\t\tctx: Ctx\n\t) => Promise<Iterable<T>> | Iterable<T>;\n\t/** Row identity. Defaults to the table's single primary key (else `row.id`). */\n\tkey?: (row: T) => RowKey;\n\t/** Key column JS property, if not the table's primary key. */\n\tkeyColumn?: string;\n\t/** Access control; return false (or throw) to deny the subscription. */\n\tauthorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;\n};\n\n/**\n * A sync-engine collection from one Drizzle query — the Drizzle counterpart to\n * `prismaCollection`. You write the `where` once: it drives the DB read\n * (`hydrate`) AND the incremental `match` (via {@link matchesDrizzleWhere}), so\n * the two can't drift and you never hand-maintain a separate predicate. A filter\n * the matcher can't evaluate falls back to a refetch, never a wrong result.\n */\nexport const drizzleCollection = <T, P = void, Ctx = CollectionContext>(\n\toptions: DrizzleCollectionOptions<T, P, Ctx>\n): CollectionDefinition<T, P, Ctx> => {\n\tconst keyProp = resolveKeyColumn(\n\t\toptions.table,\n\t\toptions.keyColumn\n\t)?.property;\n\tconst key =\n\t\toptions.key ??\n\t\t((row: T) =>\n\t\t\tkeyProp !== undefined\n\t\t\t\t? (row as Record<string, RowKey>)[keyProp]!\n\t\t\t\t: (row as { id: RowKey }).id);\n\n\treturn {\n\t\tname: options.name,\n\t\ttables: [getTableName(options.table)],\n\t\thydrate: (params, ctx) =>\n\t\t\toptions.find(options.where(params, ctx), params, ctx),\n\t\tmatch: (row, params, ctx) =>\n\t\t\tmatchesDrizzleWhere(\n\t\t\t\toptions.table,\n\t\t\t\toptions.where(params, ctx),\n\t\t\t\trow as Record<string, unknown>\n\t\t\t),\n\t\tkey,\n\t\tauthorize: options.authorize\n\t};\n};\n",
7
+ "import { Column, getTableColumns, is, Param, SQL } from 'drizzle-orm';\nimport type { Table } from 'drizzle-orm';\n\n/**\n * Thrown when a Drizzle `where` uses something the incremental matcher can't\n * evaluate in JS (an unsupported operator, a function, a cross-table column…).\n * The sync engine catches it and degrades that subscription to a refetch, so the\n * result is never wrong — only less efficient. Mirrors the Prisma adapter.\n */\nexport class UnsupportedDrizzleFilterError extends Error {\n\tconstructor(detail: string) {\n\t\tsuper(`Cannot evaluate Drizzle filter \"${detail}\" incrementally`);\n\t\tthis.name = 'UnsupportedDrizzleFilterError';\n\t}\n}\n\nconst isDate = (value: unknown): value is Date => value instanceof Date;\n\nconst equals = (value: unknown, operand: unknown): boolean => {\n\tif (operand === null) {\n\t\treturn value === null || value === undefined;\n\t}\n\tif (isDate(value) && isDate(operand)) {\n\t\treturn value.getTime() === operand.getTime();\n\t}\n\treturn value === operand;\n};\n\nconst order = (value: unknown): number | string =>\n\tisDate(value) ? value.getTime() : (value as number | string);\n\nconst compare = (value: unknown, operand: unknown): number => {\n\tconst a = order(value);\n\tconst b = order(operand);\n\tif (a < b) {\n\t\treturn -1;\n\t}\n\tif (a > b) {\n\t\treturn 1;\n\t}\n\treturn 0;\n};\n\nconst comparable = (value: unknown): boolean =>\n\tvalue !== null && value !== undefined;\n\ntype Classified = {\n\tcols: Column[];\n\tparams: unknown[];\n\tarrays: unknown[][];\n\tsqls: SQL[];\n\tops: string[];\n};\n\n/**\n * Split a condition's `queryChunks` into its parts. Drizzle interleaves columns,\n * bound `Param`s, value arrays (for `in`), nested `SQL` (connectives), and string\n * chunks that carry the operators/parens. Reading `queryChunks` is Drizzle's\n * internal shape, not a stable API — but every unrecognized form throws below\n * (→ refetch), so a version bump can only cost efficiency, never correctness.\n */\nconst classify = (chunks: unknown[]): Classified => {\n\tconst cols: Column[] = [];\n\tconst params: unknown[] = [];\n\tconst arrays: unknown[][] = [];\n\tconst sqls: SQL[] = [];\n\tconst ops: string[] = [];\n\tfor (const chunk of chunks) {\n\t\tif (is(chunk, SQL)) {\n\t\t\tsqls.push(chunk);\n\t\t} else if (is(chunk, Column)) {\n\t\t\tcols.push(chunk);\n\t\t} else if (is(chunk, Param)) {\n\t\t\tparams.push(chunk.value);\n\t\t} else if (Array.isArray(chunk)) {\n\t\t\tarrays.push(\n\t\t\t\tchunk.map((element) =>\n\t\t\t\t\tis(element, Param) ? element.value : element\n\t\t\t\t)\n\t\t\t);\n\t\t} else {\n\t\t\tconst raw = (chunk as { value?: unknown }).value;\n\t\t\tconst text = (\n\t\t\t\tArray.isArray(raw) ? raw.join('') : String(raw ?? '')\n\t\t\t).trim();\n\t\t\tif (text !== '' && text !== '(' && text !== ')') {\n\t\t\t\tops.push(text);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { arrays, cols, ops, params, sqls };\n};\n\nconst evaluateLeaf = (\n\tcolumn: Column,\n\top: string,\n\tparams: unknown[],\n\tarrays: unknown[][],\n\trow: Record<string, unknown>,\n\tpropFor: (column: Column) => string | undefined\n): boolean => {\n\tconst prop = propFor(column);\n\tif (prop === undefined) {\n\t\tthrow new UnsupportedDrizzleFilterError(`column ${column.name}`);\n\t}\n\tconst value = row[prop];\n\tconst operand = params[0];\n\tswitch (op) {\n\t\tcase '=':\n\t\t\treturn equals(value, operand);\n\t\tcase '<>':\n\t\t\treturn !equals(value, operand);\n\t\tcase '>':\n\t\t\treturn comparable(value) && compare(value, operand) > 0;\n\t\tcase '>=':\n\t\t\treturn comparable(value) && compare(value, operand) >= 0;\n\t\tcase '<':\n\t\t\treturn comparable(value) && compare(value, operand) < 0;\n\t\tcase '<=':\n\t\t\treturn comparable(value) && compare(value, operand) <= 0;\n\t\tcase 'in':\n\t\t\treturn (arrays[0] ?? []).some((item) => equals(value, item));\n\t\tcase 'not in':\n\t\t\treturn !(arrays[0] ?? []).some((item) => equals(value, item));\n\t\tcase 'is null':\n\t\t\treturn value === null || value === undefined;\n\t\tcase 'is not null':\n\t\t\treturn value !== null && value !== undefined;\n\t\tdefault:\n\t\t\tthrow new UnsupportedDrizzleFilterError(op);\n\t}\n};\n\nconst evaluateCondition = (\n\tnode: SQL,\n\trow: Record<string, unknown>,\n\tpropFor: (column: Column) => string | undefined\n): boolean => {\n\tconst { cols, params, arrays, sqls, ops } = classify(\n\t\t(node as unknown as { queryChunks: unknown[] }).queryChunks\n\t);\n\n\t// not (cond)\n\tif (\n\t\tops.length === 1 &&\n\t\tops[0] === 'not' &&\n\t\tsqls.length === 1 &&\n\t\tcols.length === 0\n\t) {\n\t\treturn !evaluateCondition(sqls[0]!, row, propFor);\n\t}\n\t// A lone nested condition wrapped in parens — unwrap and recurse.\n\tif (cols.length === 0 && sqls.length === 1 && ops.length === 0) {\n\t\treturn evaluateCondition(sqls[0]!, row, propFor);\n\t}\n\t// and / or over sub-conditions.\n\tif (cols.length === 0 && sqls.length >= 2 && ops.length > 0) {\n\t\tconst connective = ops[0];\n\t\tif (\n\t\t\t(connective === 'and' || connective === 'or') &&\n\t\t\tops.every((op) => op === connective)\n\t\t) {\n\t\t\tconst results = sqls.map((sql) =>\n\t\t\t\tevaluateCondition(sql, row, propFor)\n\t\t\t);\n\t\t\treturn connective === 'and'\n\t\t\t\t? results.every(Boolean)\n\t\t\t\t: results.some(Boolean);\n\t\t}\n\t\tthrow new UnsupportedDrizzleFilterError(ops.join(' '));\n\t}\n\t// Leaf comparison: one column, one operator.\n\tif (cols.length === 1 && sqls.length === 0 && ops.length === 1) {\n\t\treturn evaluateLeaf(cols[0]!, ops[0]!, params, arrays, row, propFor);\n\t}\n\tthrow new UnsupportedDrizzleFilterError(\n\t\tops.join(' ') || 'unrecognized condition'\n\t);\n};\n\n/**\n * Evaluate a Drizzle `where` condition against a plain row in JS — the\n * incremental matcher for {@link drizzleCollection}. Supports\n * `eq`/`ne`/`gt`/`gte`/`lt`/`lte`, `isNull`/`isNotNull`,\n * `inArray`/`notInArray`, and nested `and`/`or`/`not`; anything else throws\n * {@link UnsupportedDrizzleFilterError} (the engine then refetches). Rows are\n * read by JS property name, as Drizzle returns them.\n */\nexport const matchesDrizzleWhere = (\n\ttable: Table,\n\twhere: SQL,\n\trow: Record<string, unknown>\n): boolean => {\n\tconst nameToProp = new Map<string, string>();\n\tfor (const [prop, column] of Object.entries(getTableColumns(table))) {\n\t\tnameToProp.set(column.name, prop);\n\t}\n\n\treturn evaluateCondition(where, row, (column) =>\n\t\tnameToProp.get(column.name)\n\t);\n};\n",
6
8
  "import type { SQL, Table } from 'drizzle-orm';\nimport { extractKeyFromWhere, keyTopic, tableTopic } from './topics';\n\nexport type DeriveReadTopicsOptions = {\n\t/**\n\t * Column (its JS property name on the table) to treat as the row key when\n\t * narrowing to a `table:key` topic. Defaults to the table's single\n\t * primary-key column; composite or absent primary keys disable row-level\n\t * narrowing.\n\t */\n\tkeyColumn?: string;\n};\n\nexport type DerivedReadTopics = {\n\t/** Topics this read depends on — subscribe to all of them. */\n\ttopics: string[];\n\t/**\n\t * `true` when derivation narrowed to a specific row (`table:key`); `false`\n\t * when it fell back to the whole-table topic.\n\t */\n\trowLevel: boolean;\n};\n\n/**\n * Derive the reactive topics a read of `table` (optionally filtered by `where`)\n * depends on. A recognised primary-key equality narrows to a single `table:key`\n * topic; everything else subscribes to the whole-table topic, over-invalidating\n * a little rather than missing an update.\n *\n * @example\n * deriveReadTopics(users); // { topics: ['users'], rowLevel: false }\n * deriveReadTopics(users, eq(users.id, 5)); // { topics: ['users:5'], rowLevel: true }\n * deriveReadTopics(users, gt(users.id, 5)); // { topics: ['users'], rowLevel: false }\n */\nexport const deriveReadTopics = (\n\ttable: Table,\n\twhere?: SQL,\n\toptions: DeriveReadTopicsOptions = {}\n): DerivedReadTopics => {\n\tconst key =\n\t\twhere === undefined\n\t\t\t? undefined\n\t\t\t: extractKeyFromWhere(table, where, options.keyColumn);\n\n\tif (key === undefined) {\n\t\treturn { topics: [tableTopic(table)], rowLevel: false };\n\t}\n\treturn { topics: [keyTopic(table, key)], rowLevel: true };\n};\n",
7
9
  "import type { SQL, Table } from 'drizzle-orm';\nimport type { ReactiveHub } from '../../reactiveHub';\nimport {\n\textractKeyFromWhere,\n\textractRowKeys,\n\tkeyTopic,\n\ttableTopic\n} from './topics';\n\n/**\n * Drizzle write-side topic publishing (Tier 2).\n *\n * The mirror of {@link deriveReadTopics}: after a mutation commits, publish the\n * topics it invalidates so subscribed reads refetch. Every change publishes the\n * **table** topic (so list/table queries refresh) plus a **row** topic per\n * affected key (so row-level queries refresh) — the exact topics the read side\n * subscribes to.\n *\n * These are the \"route mutations through us\" change source from the roadmap:\n * call them right after your durable write. They work on any DB Drizzle supports\n * and never touch DB-specific machinery; out-of-band writes (caught later by CDC\n * adapters) are the only thing they miss.\n */\n\n/** The kind of mutation, forwarded in the change-event payload. */\nexport type ChangeOp = 'insert' | 'update' | 'delete';\n\n/** Payload carried by every change event the write side publishes. */\nexport type ChangePayload = {\n\t/** Name of the table that changed. */\n\ttable: string;\n\t/** Mutation kind, when the caller provided it. */\n\top?: ChangeOp;\n\t/** Affected row keys (empty for a table-wide change). */\n\tkeys: (string | number)[];\n};\n\nexport type PublishChangeOptions = {\n\t/** Row keys that changed; each emits a `table:key` topic. */\n\tkeys?: ReadonlyArray<string | number>;\n\top?: ChangeOp;\n};\n\n/**\n * Publish the reactive topics a change to `table` invalidates: the whole-table\n * topic (always) plus a `table:key` topic per affected row. Call after the\n * durable write commits. Returns the (de-duplicated) topics published.\n */\nexport const publishChange = (\n\thub: Pick<ReactiveHub, 'publish'>,\n\ttable: Table,\n\toptions: PublishChangeOptions = {}\n): string[] => {\n\tconst name = tableTopic(table);\n\tconst keys = options.keys === undefined ? [] : [...new Set(options.keys)];\n\tconst payload: ChangePayload = { table: name, op: options.op, keys };\n\tconst topics = [\n\t\t...new Set([name, ...keys.map((key) => keyTopic(table, key))])\n\t];\n\tfor (const topic of topics) {\n\t\thub.publish(topic, payload);\n\t}\n\treturn topics;\n};\n\nexport type PublishRowsOptions = {\n\t/** Key column (JS property name); defaults to the table's primary key. */\n\tkeyColumn?: string;\n\top?: ChangeOp;\n};\n\n/**\n * Publish change topics for a set of rows — typically the output of a mutation's\n * `.returning()`, which yields real keys including auto-generated ones. Reads\n * each row's primary-key column (or `keyColumn`) to emit `table:key` topics.\n *\n * @example\n * const rows = await db.insert(users).values(input).returning();\n * publishRows(hub, users, rows, { op: 'insert' });\n */\nexport const publishRows = (\n\thub: Pick<ReactiveHub, 'publish'>,\n\ttable: Table,\n\trows: ReadonlyArray<Record<string, unknown>>,\n\toptions: PublishRowsOptions = {}\n): string[] =>\n\tpublishChange(hub, table, {\n\t\tkeys: extractRowKeys(table, rows, options.keyColumn),\n\t\top: options.op\n\t});\n\nexport type PublishWhereOptions = {\n\t/** Key column (JS property name); defaults to the table's primary key. */\n\tkeyColumn?: string;\n\top?: ChangeOp;\n};\n\n/**\n * Publish change topics for an `update`/`delete` identified by a `where` filter.\n * A simple primary-key equality narrows to that row's topic; any other filter\n * publishes just the table topic, so every affected subscriber refetches and\n * re-evaluates.\n *\n * @example\n * await db.update(users).set(patch).where(eq(users.id, id));\n * publishWhere(hub, users, eq(users.id, id), { op: 'update' });\n */\nexport const publishWhere = (\n\thub: Pick<ReactiveHub, 'publish'>,\n\ttable: Table,\n\twhere: SQL,\n\toptions: PublishWhereOptions = {}\n): string[] => {\n\tconst key = extractKeyFromWhere(table, where, options.keyColumn);\n\treturn publishChange(hub, table, {\n\t\tkeys: key === undefined ? [] : [key],\n\t\top: options.op\n\t});\n};\n"
8
10
  ],
9
- "mappings": "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",
10
- "debugId": "ADD7C375EE239C1664756E2164756E21",
11
+ "mappings": "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",
12
+ "debugId": "A28CA296A764961764756E2164756E21",
11
13
  "names": []
12
14
  }
@@ -0,0 +1,20 @@
1
+ import { SQL } from 'drizzle-orm';
2
+ import type { Table } from 'drizzle-orm';
3
+ /**
4
+ * Thrown when a Drizzle `where` uses something the incremental matcher can't
5
+ * evaluate in JS (an unsupported operator, a function, a cross-table column…).
6
+ * The sync engine catches it and degrades that subscription to a refetch, so the
7
+ * result is never wrong — only less efficient. Mirrors the Prisma adapter.
8
+ */
9
+ export declare class UnsupportedDrizzleFilterError extends Error {
10
+ constructor(detail: string);
11
+ }
12
+ /**
13
+ * Evaluate a Drizzle `where` condition against a plain row in JS — the
14
+ * incremental matcher for {@link drizzleCollection}. Supports
15
+ * `eq`/`ne`/`gt`/`gte`/`lt`/`lte`, `isNull`/`isNotNull`,
16
+ * `inArray`/`notInArray`, and nested `and`/`or`/`not`; anything else throws
17
+ * {@link UnsupportedDrizzleFilterError} (the engine then refetches). Rows are
18
+ * read by JS property name, as Drizzle returns them.
19
+ */
20
+ export declare const matchesDrizzleWhere: (table: Table, where: SQL, row: Record<string, unknown>) => boolean;
@@ -81,6 +81,57 @@ var localStorageMutationStorage = (key) => ({
81
81
  globalThis.localStorage?.setItem(key, JSON.stringify(records));
82
82
  }
83
83
  });
84
+ var localStorageCollectionCache = (key) => ({
85
+ load: () => {
86
+ const raw = globalThis.localStorage?.getItem(key);
87
+ return raw ? JSON.parse(raw) : undefined;
88
+ },
89
+ save: (snapshot) => {
90
+ globalThis.localStorage?.setItem(key, JSON.stringify(snapshot));
91
+ },
92
+ clear: () => {
93
+ globalThis.localStorage?.removeItem(key);
94
+ }
95
+ });
96
+ var openIndexedDb = (databaseName, storeName) => new Promise((resolve, reject) => {
97
+ const request = globalThis.indexedDB.open(databaseName, 1);
98
+ request.onupgradeneeded = () => {
99
+ request.result.createObjectStore(storeName);
100
+ };
101
+ request.onsuccess = () => resolve(request.result);
102
+ request.onerror = () => reject(request.error);
103
+ });
104
+ var indexedDbCollectionCache = ({
105
+ key,
106
+ databaseName = "absolutejs-sync",
107
+ storeName = "collections"
108
+ }) => {
109
+ let handle;
110
+ const database = () => {
111
+ handle ??= openIndexedDb(databaseName, storeName);
112
+ return handle;
113
+ };
114
+ const withStore = async (mode, run) => {
115
+ if (globalThis.indexedDB === undefined) {
116
+ return;
117
+ }
118
+ const db = await database();
119
+ return new Promise((resolve, reject) => {
120
+ const request = run(db.transaction(storeName, mode).objectStore(storeName));
121
+ request.onsuccess = () => resolve(request.result);
122
+ request.onerror = () => reject(request.error);
123
+ });
124
+ };
125
+ return {
126
+ load: () => withStore("readonly", (store) => store.get(key)),
127
+ save: async (snapshot) => {
128
+ await withStore("readwrite", (store) => store.put(snapshot, key));
129
+ },
130
+ clear: async () => {
131
+ await withStore("readwrite", (store) => store.delete(key));
132
+ }
133
+ };
134
+ };
84
135
  var SUBSCRIPTION_ID = "s";
85
136
  var createSyncCollection = (options) => {
86
137
  const key = options.key ?? ((row) => row.id);
@@ -129,6 +180,20 @@ var createSyncCollection = (options) => {
129
180
  args: mutation.args
130
181
  })));
131
182
  };
183
+ let cacheScheduled = false;
184
+ const persistCache = () => {
185
+ if (options.cache === undefined || cacheScheduled) {
186
+ return;
187
+ }
188
+ cacheScheduled = true;
189
+ queueMicrotask(() => {
190
+ cacheScheduled = false;
191
+ options.cache?.save({
192
+ rows: [...confirmed.values()],
193
+ version: appliedVersion
194
+ });
195
+ });
196
+ };
132
197
  const settlePending = (mutationId) => {
133
198
  const index = pending.findIndex((mutation2) => mutation2.mutationId === mutationId);
134
199
  if (index === -1) {
@@ -147,6 +212,7 @@ var createSyncCollection = (options) => {
147
212
  if (frame.version !== undefined) {
148
213
  appliedVersion = frame.version;
149
214
  }
215
+ persistCache();
150
216
  recompute({ status: "ready", error: undefined });
151
217
  } else if (frame.type === "diff") {
152
218
  for (const row of frame.removed) {
@@ -161,7 +227,8 @@ var createSyncCollection = (options) => {
161
227
  if (frame.version !== undefined) {
162
228
  appliedVersion = Math.max(appliedVersion, frame.version);
163
229
  }
164
- recompute();
230
+ persistCache();
231
+ recompute({ status: "ready", error: undefined });
165
232
  } else if (frame.type === "error") {
166
233
  setState({ error: frame.message });
167
234
  options.onError?.(frame.message);
@@ -171,7 +238,7 @@ var createSyncCollection = (options) => {
171
238
  recompute();
172
239
  mutation.resolve(frame.result);
173
240
  }
174
- } else {
241
+ } else if (frame.type === "reject") {
175
242
  const mutation = settlePending(frame.mutationId);
176
243
  if (mutation !== undefined) {
177
244
  recompute();
@@ -225,7 +292,6 @@ var createSyncCollection = (options) => {
225
292
  reconnectTimer = setTimeout(connect, delay);
226
293
  };
227
294
  };
228
- connect();
229
295
  const hydratePersisted = async () => {
230
296
  if (options.storage === undefined) {
231
297
  return;
@@ -250,7 +316,35 @@ var createSyncCollection = (options) => {
250
316
  }
251
317
  }
252
318
  };
253
- hydratePersisted();
319
+ const hydrateCache = async () => {
320
+ if (options.cache === undefined) {
321
+ return;
322
+ }
323
+ let snapshot;
324
+ try {
325
+ snapshot = await options.cache.load();
326
+ } catch {
327
+ return;
328
+ }
329
+ if (snapshot === undefined || appliedVersion > 0) {
330
+ return;
331
+ }
332
+ for (const row of snapshot.rows) {
333
+ confirmed.set(key(row), row);
334
+ }
335
+ appliedVersion = snapshot.version;
336
+ recompute();
337
+ };
338
+ if (options.cache === undefined) {
339
+ connect();
340
+ hydratePersisted();
341
+ } else {
342
+ (async () => {
343
+ await hydrateCache();
344
+ await hydratePersisted();
345
+ connect();
346
+ })();
347
+ }
254
348
  return {
255
349
  get: () => state,
256
350
  subscribe: (listener) => {
@@ -343,5 +437,5 @@ export {
343
437
  SyncCollectionService
344
438
  };
345
439
 
346
- //# debugId=87735BE336EAF15664756E2164756E21
440
+ //# debugId=C534ED0FEAC9CC6664756E2164756E21
347
441
  //# sourceMappingURL=index.js.map