@absolutejs/sync 0.0.1 → 0.1.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.
Files changed (68) hide show
  1. package/README.md +264 -24
  2. package/dist/adapters/drizzle/index.d.ts +17 -0
  3. package/dist/adapters/drizzle/index.js +128 -0
  4. package/dist/adapters/drizzle/index.js.map +12 -0
  5. package/dist/adapters/drizzle/read.d.ts +31 -0
  6. package/dist/adapters/drizzle/topics.d.ts +41 -0
  7. package/dist/adapters/drizzle/write.d.ts +69 -0
  8. package/dist/adapters/mysql/index.d.ts +75 -0
  9. package/dist/adapters/mysql/index.js +171 -0
  10. package/dist/adapters/mysql/index.js.map +11 -0
  11. package/dist/adapters/postgres/index.d.ts +53 -0
  12. package/dist/adapters/postgres/index.js +86 -0
  13. package/dist/adapters/postgres/index.js.map +10 -0
  14. package/dist/adapters/prisma/collection.d.ts +39 -0
  15. package/dist/adapters/prisma/index.d.ts +23 -0
  16. package/dist/adapters/prisma/index.js +231 -0
  17. package/dist/adapters/prisma/index.js.map +14 -0
  18. package/dist/adapters/prisma/predicate.d.ts +20 -0
  19. package/dist/adapters/prisma/read.d.ts +28 -0
  20. package/dist/adapters/prisma/topics.d.ts +29 -0
  21. package/dist/adapters/prisma/write.d.ts +65 -0
  22. package/dist/adapters/sqlite/index.d.ts +32 -0
  23. package/dist/adapters/sqlite/index.js +128 -0
  24. package/dist/adapters/sqlite/index.js.map +11 -0
  25. package/dist/angular/index.d.ts +1 -0
  26. package/dist/angular/index.js +347 -0
  27. package/dist/angular/index.js.map +11 -0
  28. package/dist/angular/sync-collection.service.d.ts +20 -0
  29. package/dist/client/index.d.ts +8 -30
  30. package/dist/client/index.js +744 -3
  31. package/dist/client/index.js.map +8 -4
  32. package/dist/client/liveQuery.d.ts +75 -0
  33. package/dist/client/subscriber.d.ts +30 -0
  34. package/dist/client/syncCollection.d.ts +102 -0
  35. package/dist/client/syncStore.d.ts +81 -0
  36. package/dist/engine/aggregate.d.ts +45 -0
  37. package/dist/engine/collection.d.ts +87 -0
  38. package/dist/engine/connection.d.ts +71 -0
  39. package/dist/engine/dataflow.d.ts +109 -0
  40. package/dist/engine/equiJoin.d.ts +51 -0
  41. package/dist/engine/graph.d.ts +85 -0
  42. package/dist/engine/index.d.ts +34 -0
  43. package/dist/engine/index.js +1269 -0
  44. package/dist/engine/index.js.map +20 -0
  45. package/dist/engine/materializedView.d.ts +53 -0
  46. package/dist/engine/mutation.d.ts +30 -0
  47. package/dist/engine/pollingSource.d.ts +42 -0
  48. package/dist/engine/routes.d.ts +40 -0
  49. package/dist/engine/socket.d.ts +64 -0
  50. package/dist/engine/syncEngine.d.ts +100 -0
  51. package/dist/engine/types.d.ts +45 -0
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.js +160 -2
  54. package/dist/index.js.map +7 -5
  55. package/dist/react/index.d.ts +1 -0
  56. package/dist/react/index.js +332 -0
  57. package/dist/react/index.js.map +11 -0
  58. package/dist/react/useSyncCollection.d.ts +16 -0
  59. package/dist/reactiveHub.d.ts +6 -0
  60. package/dist/svelte/createSyncCollectionStore.d.ts +15 -0
  61. package/dist/svelte/index.d.ts +1 -0
  62. package/dist/svelte/index.js +338 -0
  63. package/dist/svelte/index.js.map +11 -0
  64. package/dist/vue/index.d.ts +1 -0
  65. package/dist/vue/index.js +331 -0
  66. package/dist/vue/index.js.map +11 -0
  67. package/dist/vue/useSyncCollection.d.ts +17 -0
  68. package/package.json +102 -6
@@ -0,0 +1,231 @@
1
+ // @bun
2
+ // src/adapters/prisma/topics.ts
3
+ var tableTopic = (model) => model;
4
+ var keyTopic = (model, key) => `${model}:${key}`;
5
+ var isRowKey = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint";
6
+ var extractKeyFromWhere = (where, keyField) => {
7
+ const fields = Object.keys(where);
8
+ if (fields.length !== 1 || fields[0] !== keyField) {
9
+ return;
10
+ }
11
+ const condition = where[keyField];
12
+ if (isRowKey(condition)) {
13
+ return condition;
14
+ }
15
+ if (condition !== null && typeof condition === "object" && !Array.isArray(condition)) {
16
+ const operators = Object.keys(condition);
17
+ if (operators.length === 1 && operators[0] === "equals") {
18
+ const value = condition.equals;
19
+ if (isRowKey(value)) {
20
+ return value;
21
+ }
22
+ }
23
+ }
24
+ return;
25
+ };
26
+ var extractRowKeys = (rows, keyField) => {
27
+ const keys = [];
28
+ for (const row of rows) {
29
+ const value = row[keyField];
30
+ if (isRowKey(value)) {
31
+ keys.push(value);
32
+ }
33
+ }
34
+ return keys;
35
+ };
36
+ // src/adapters/prisma/read.ts
37
+ var deriveReadTopics = (model, where, options = {}) => {
38
+ const keyField = options.keyField ?? "id";
39
+ const key = where === undefined ? undefined : extractKeyFromWhere(where, keyField);
40
+ if (key === undefined) {
41
+ return { topics: [tableTopic(model)], rowLevel: false };
42
+ }
43
+ return { topics: [keyTopic(model, key)], rowLevel: true };
44
+ };
45
+ // src/adapters/prisma/write.ts
46
+ var publishChange = (hub, model, options = {}) => {
47
+ const name = tableTopic(model);
48
+ const keys = options.keys === undefined ? [] : [...new Set(options.keys)];
49
+ const payload = { table: name, op: options.op, keys };
50
+ const topics = [
51
+ ...new Set([name, ...keys.map((key) => keyTopic(model, key))])
52
+ ];
53
+ for (const topic of topics) {
54
+ hub.publish(topic, payload);
55
+ }
56
+ return topics;
57
+ };
58
+ var publishRows = (hub, model, rows, options = {}) => {
59
+ const list = Array.isArray(rows) ? rows : [rows];
60
+ return publishChange(hub, model, {
61
+ keys: extractRowKeys(list, options.keyField ?? "id"),
62
+ op: options.op
63
+ });
64
+ };
65
+ var publishWhere = (hub, model, where, options = {}) => {
66
+ const key = extractKeyFromWhere(where, options.keyField ?? "id");
67
+ return publishChange(hub, model, {
68
+ keys: key === undefined ? [] : [key],
69
+ op: options.op
70
+ });
71
+ };
72
+ // src/adapters/prisma/predicate.ts
73
+ class UnsupportedFilterError extends Error {
74
+ constructor(operator) {
75
+ super(`Cannot evaluate Prisma filter operator "${operator}" incrementally`);
76
+ this.name = "UnsupportedFilterError";
77
+ }
78
+ }
79
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
80
+ var equals = (value, operand) => {
81
+ if (operand === null) {
82
+ return value === null || value === undefined;
83
+ }
84
+ if (value instanceof Date && operand instanceof Date) {
85
+ return value.getTime() === operand.getTime();
86
+ }
87
+ return value === operand;
88
+ };
89
+ var order = (value) => value instanceof Date ? value.getTime() : value;
90
+ var compare = (value, operand) => {
91
+ const a = order(value);
92
+ const b = order(operand);
93
+ if (a < b) {
94
+ return -1;
95
+ }
96
+ if (a > b) {
97
+ return 1;
98
+ }
99
+ return 0;
100
+ };
101
+ var comparable = (value) => value !== null && value !== undefined;
102
+ var FIELD_OPERATORS = new Set([
103
+ "equals",
104
+ "not",
105
+ "in",
106
+ "notIn",
107
+ "lt",
108
+ "lte",
109
+ "gt",
110
+ "gte",
111
+ "contains",
112
+ "startsWith",
113
+ "endsWith"
114
+ ]);
115
+ var matchesField = (value, condition) => {
116
+ if (!isPlainObject(condition)) {
117
+ return equals(value, condition);
118
+ }
119
+ for (const operator of Object.keys(condition)) {
120
+ if (!FIELD_OPERATORS.has(operator)) {
121
+ throw new UnsupportedFilterError(operator);
122
+ }
123
+ }
124
+ for (const [operator, operand] of Object.entries(condition)) {
125
+ switch (operator) {
126
+ case "equals":
127
+ if (!equals(value, operand))
128
+ return false;
129
+ break;
130
+ case "not":
131
+ if (isPlainObject(operand)) {
132
+ if (matchesField(value, operand))
133
+ return false;
134
+ } else if (equals(value, operand)) {
135
+ return false;
136
+ }
137
+ break;
138
+ case "in":
139
+ if (!Array.isArray(operand) || !operand.some((item) => equals(value, item)))
140
+ return false;
141
+ break;
142
+ case "notIn":
143
+ if (Array.isArray(operand) && operand.some((item) => equals(value, item)))
144
+ return false;
145
+ break;
146
+ case "lt":
147
+ if (!comparable(value) || compare(value, operand) >= 0)
148
+ return false;
149
+ break;
150
+ case "lte":
151
+ if (!comparable(value) || compare(value, operand) > 0)
152
+ return false;
153
+ break;
154
+ case "gt":
155
+ if (!comparable(value) || compare(value, operand) <= 0)
156
+ return false;
157
+ break;
158
+ case "gte":
159
+ if (!comparable(value) || compare(value, operand) < 0)
160
+ return false;
161
+ break;
162
+ case "contains":
163
+ if (typeof value !== "string" || !value.includes(String(operand)))
164
+ return false;
165
+ break;
166
+ case "startsWith":
167
+ if (typeof value !== "string" || !value.startsWith(String(operand)))
168
+ return false;
169
+ break;
170
+ case "endsWith":
171
+ if (typeof value !== "string" || !value.endsWith(String(operand)))
172
+ return false;
173
+ break;
174
+ default:
175
+ throw new UnsupportedFilterError(operator);
176
+ }
177
+ }
178
+ return true;
179
+ };
180
+ var toConditions = (value) => {
181
+ if (Array.isArray(value)) {
182
+ return value.filter(isPlainObject);
183
+ }
184
+ return isPlainObject(value) ? [value] : [];
185
+ };
186
+ var matchesWhere = (where, row) => {
187
+ for (const [field, condition] of Object.entries(where)) {
188
+ if (field === "AND") {
189
+ if (!toConditions(condition).every((part) => matchesWhere(part, row)))
190
+ return false;
191
+ continue;
192
+ }
193
+ if (field === "OR") {
194
+ const parts = toConditions(condition);
195
+ if (parts.length > 0 && !parts.some((part) => matchesWhere(part, row)))
196
+ return false;
197
+ continue;
198
+ }
199
+ if (field === "NOT") {
200
+ if (toConditions(condition).some((part) => matchesWhere(part, row)))
201
+ return false;
202
+ continue;
203
+ }
204
+ if (!matchesField(row[field], condition)) {
205
+ return false;
206
+ }
207
+ }
208
+ return true;
209
+ };
210
+ // src/adapters/prisma/collection.ts
211
+ var prismaCollection = (options) => ({
212
+ name: options.name,
213
+ hydrate: (params, ctx) => options.find(options.where(params, ctx), params, ctx),
214
+ match: (row, params, ctx) => matchesWhere(options.where(params, ctx), row),
215
+ key: options.key,
216
+ authorize: options.authorize
217
+ });
218
+ export {
219
+ tableTopic,
220
+ publishWhere,
221
+ publishRows,
222
+ publishChange,
223
+ prismaCollection,
224
+ matchesWhere,
225
+ keyTopic,
226
+ deriveReadTopics,
227
+ UnsupportedFilterError
228
+ };
229
+
230
+ //# debugId=96C8D5DF158A4DF864756E2164756E21
231
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/adapters/prisma/topics.ts", "../src/adapters/prisma/read.ts", "../src/adapters/prisma/write.ts", "../src/adapters/prisma/predicate.ts", "../src/adapters/prisma/collection.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Shared topic vocabulary + key extraction for the Prisma adapter. Operates on\n * Prisma's plain `where` objects and result records — there is no\n * `@prisma/client` import, so it stays generic across every database Prisma\n * supports. Both the read side (derive a query's topics) and the write side\n * (publish a mutation's topics) build on these so the two always agree.\n */\n\n/** A Prisma `where` filter object, e.g. `{ id: 5 }` or `{ id: { gt: 5 } }`. */\nexport type PrismaWhere = Record<string, unknown>;\n\n/** A scalar usable as a row key. */\nexport type RowKey = string | number | bigint;\n\n/** The coarse topic every read/write of `model` touches, e.g. `user`. */\nexport const tableTopic = (model: string): string => model;\n\n/** The row-level topic for one key of `model`, e.g. `user:5`. */\nexport const keyTopic = (model: string, key: RowKey): string =>\n\t`${model}:${key}`;\n\nconst isRowKey = (value: unknown): value is RowKey =>\n\ttypeof value === 'string' ||\n\ttypeof value === 'number' ||\n\ttypeof value === 'bigint';\n\n/**\n * Best-effort: pull a single key-field equality value out of a Prisma `where`.\n * Recognises `{ [keyField]: scalar }` and `{ [keyField]: { equals: scalar } }`\n * only — extra fields, other operators (`gt`/`in`/`not`/…), `AND`/`OR`, or a\n * compound-key object yield `undefined`, so the caller falls back to the table\n * topic (over-invalidating a little rather than missing an update).\n */\nexport const extractKeyFromWhere = (\n\twhere: PrismaWhere,\n\tkeyField: string\n): RowKey | undefined => {\n\tconst fields = Object.keys(where);\n\tif (fields.length !== 1 || fields[0] !== keyField) {\n\t\treturn undefined;\n\t}\n\tconst condition = where[keyField];\n\tif (isRowKey(condition)) {\n\t\treturn condition;\n\t}\n\tif (\n\t\tcondition !== null &&\n\t\ttypeof condition === 'object' &&\n\t\t!Array.isArray(condition)\n\t) {\n\t\tconst operators = Object.keys(condition as Record<string, unknown>);\n\t\tif (operators.length === 1 && operators[0] === 'equals') {\n\t\t\tconst value = (condition as Record<string, unknown>).equals;\n\t\t\tif (isRowKey(value)) {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n};\n\n/**\n * Read the key value from each result record (e.g. the output of a Prisma\n * `create`/`update`/`delete`), using `keyField`. Records without a scalar key\n * are skipped.\n */\nexport const extractRowKeys = (\n\trows: ReadonlyArray<Record<string, unknown>>,\n\tkeyField: string\n): RowKey[] => {\n\tconst keys: RowKey[] = [];\n\tfor (const row of rows) {\n\t\tconst value = row[keyField];\n\t\tif (isRowKey(value)) {\n\t\t\tkeys.push(value);\n\t\t}\n\t}\n\treturn keys;\n};\n",
6
+ "import { extractKeyFromWhere, keyTopic, tableTopic } from './topics';\nimport type { PrismaWhere } from './topics';\n\nexport type DeriveReadTopicsOptions = {\n\t/**\n\t * Primary-key field name to narrow on. Defaults to `id` (Prisma's\n\t * convention); set it for models keyed by another field.\n\t */\n\tkeyField?: 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 (`model:key`); `false`\n\t * when it fell back to the whole-model topic.\n\t */\n\trowLevel: boolean;\n};\n\n/**\n * Derive the reactive topics a read of `model` (optionally filtered by `where`)\n * depends on. A recognised key-field equality narrows to a single `model:key`\n * topic; everything else subscribes to the whole-model topic.\n *\n * @example\n * deriveReadTopics('user'); // { topics: ['user'], rowLevel: false }\n * deriveReadTopics('user', { id: 5 }); // { topics: ['user:5'], rowLevel: true }\n * deriveReadTopics('user', { id: { gt: 5 } }); // { topics: ['user'], rowLevel: false }\n */\nexport const deriveReadTopics = (\n\tmodel: string,\n\twhere?: PrismaWhere,\n\toptions: DeriveReadTopicsOptions = {}\n): DerivedReadTopics => {\n\tconst keyField = options.keyField ?? 'id';\n\tconst key =\n\t\twhere === undefined ? undefined : extractKeyFromWhere(where, keyField);\n\n\tif (key === undefined) {\n\t\treturn { topics: [tableTopic(model)], rowLevel: false };\n\t}\n\treturn { topics: [keyTopic(model, key)], rowLevel: true };\n};\n",
7
+ "import type { ReactiveHub } from '../../reactiveHub';\nimport {\n\textractKeyFromWhere,\n\textractRowKeys,\n\tkeyTopic,\n\ttableTopic\n} from './topics';\nimport type { PrismaWhere, RowKey } from './topics';\n\n/**\n * Prisma 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 * **model** topic (so list queries refresh) plus a **row** topic per affected\n * key (so row-level queries refresh) — the exact topics the read side derives.\n *\n * Use the SAME model identifier you pass on the read side; the topic is the\n * model name verbatim.\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 model 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 model-wide change). */\n\tkeys: RowKey[];\n};\n\nexport type PublishChangeOptions = {\n\t/** Row keys that changed; each emits a `model:key` topic. */\n\tkeys?: ReadonlyArray<RowKey>;\n\top?: ChangeOp;\n};\n\n/**\n * Publish the reactive topics a change to `model` invalidates: the whole-model\n * topic (always) plus a `model:key` topic per affected row. Call after the\n * mutation resolves. Returns the (de-duplicated) topics published.\n */\nexport const publishChange = (\n\thub: Pick<ReactiveHub, 'publish'>,\n\tmodel: string,\n\toptions: PublishChangeOptions = {}\n): string[] => {\n\tconst name = tableTopic(model);\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(model, 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 field (defaults to `id`). */\n\tkeyField?: string;\n\top?: ChangeOp;\n};\n\n/**\n * Publish change topics for the record(s) a mutation returned. Accepts a single\n * record (Prisma `create`/`update`/`delete`) or an array (`findMany` results),\n * reading each record's `keyField` to emit `model:key` topics.\n *\n * @example\n * const user = await prisma.user.create({ data });\n * publishRows(hub, 'user', user, { op: 'insert' });\n */\nexport const publishRows = (\n\thub: Pick<ReactiveHub, 'publish'>,\n\tmodel: string,\n\trows: Record<string, unknown> | ReadonlyArray<Record<string, unknown>>,\n\toptions: PublishRowsOptions = {}\n): string[] => {\n\tconst list = (Array.isArray(rows) ? rows : [rows]) as ReadonlyArray<\n\t\tRecord<string, unknown>\n\t>;\n\treturn publishChange(hub, model, {\n\t\tkeys: extractRowKeys(list, options.keyField ?? 'id'),\n\t\top: options.op\n\t});\n};\n\nexport type PublishWhereOptions = {\n\t/** Key field (defaults to `id`). */\n\tkeyField?: string;\n\top?: ChangeOp;\n};\n\n/**\n * Publish change topics for an `update`/`delete` identified by a `where` filter.\n * A simple key-field equality narrows to that row's topic; any other filter\n * publishes just the model topic, so every affected subscriber refetches.\n *\n * @example\n * await prisma.user.update({ where: { id }, data });\n * publishWhere(hub, 'user', { id }, { op: 'update' });\n */\nexport const publishWhere = (\n\thub: Pick<ReactiveHub, 'publish'>,\n\tmodel: string,\n\twhere: PrismaWhere,\n\toptions: PublishWhereOptions = {}\n): string[] => {\n\tconst key = extractKeyFromWhere(where, options.keyField ?? 'id');\n\treturn publishChange(hub, model, {\n\t\tkeys: key === undefined ? [] : [key],\n\t\top: options.op\n\t});\n};\n",
8
+ "import type { PrismaWhere } from './topics';\n\n/**\n * Thrown when a Prisma `where` uses an operator the incremental matcher can't\n * evaluate in JS. The sync engine catches it and degrades that subscription to\n * a refetch, so it never produces a wrong result — only a less efficient one.\n */\nexport class UnsupportedFilterError extends Error {\n\tconstructor(operator: string) {\n\t\tsuper(\n\t\t\t`Cannot evaluate Prisma filter operator \"${operator}\" incrementally`\n\t\t);\n\t\tthis.name = 'UnsupportedFilterError';\n\t}\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === 'object' &&\n\tvalue !== null &&\n\t!Array.isArray(value) &&\n\t!(value instanceof Date);\n\n/** Prisma equality semantics (a `null` operand means IS NULL). */\nconst equals = (value: unknown, operand: unknown): boolean => {\n\tif (operand === null) {\n\t\treturn value === null || value === undefined;\n\t}\n\tif (value instanceof Date && operand instanceof Date) {\n\t\treturn value.getTime() === operand.getTime();\n\t}\n\treturn value === operand;\n};\n\nconst order = (value: unknown): number | string =>\n\tvalue instanceof Date ? 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\nconst FIELD_OPERATORS = new Set([\n\t'equals',\n\t'not',\n\t'in',\n\t'notIn',\n\t'lt',\n\t'lte',\n\t'gt',\n\t'gte',\n\t'contains',\n\t'startsWith',\n\t'endsWith'\n]);\n\n/** Evaluate a single field's condition (scalar equality or an operator object). */\nconst matchesField = (value: unknown, condition: unknown): boolean => {\n\tif (!isPlainObject(condition)) {\n\t\treturn equals(value, condition);\n\t}\n\t// Validate support up front: an unsupported operator must force a refetch\n\t// even when an earlier operator would short-circuit to false.\n\tfor (const operator of Object.keys(condition)) {\n\t\tif (!FIELD_OPERATORS.has(operator)) {\n\t\t\tthrow new UnsupportedFilterError(operator);\n\t\t}\n\t}\n\tfor (const [operator, operand] of Object.entries(condition)) {\n\t\tswitch (operator) {\n\t\t\tcase 'equals':\n\t\t\t\tif (!equals(value, operand)) return false;\n\t\t\t\tbreak;\n\t\t\tcase 'not':\n\t\t\t\tif (isPlainObject(operand)) {\n\t\t\t\t\tif (matchesField(value, operand)) return false;\n\t\t\t\t} else if (equals(value, operand)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase 'in':\n\t\t\t\tif (\n\t\t\t\t\t!Array.isArray(operand) ||\n\t\t\t\t\t!operand.some((item) => equals(value, item))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'notIn':\n\t\t\t\tif (\n\t\t\t\t\tArray.isArray(operand) &&\n\t\t\t\t\toperand.some((item) => equals(value, item))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'lt':\n\t\t\t\tif (!comparable(value) || compare(value, operand) >= 0)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'lte':\n\t\t\t\tif (!comparable(value) || compare(value, operand) > 0)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'gt':\n\t\t\t\tif (!comparable(value) || compare(value, operand) <= 0)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'gte':\n\t\t\t\tif (!comparable(value) || compare(value, operand) < 0)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'contains':\n\t\t\t\tif (\n\t\t\t\t\ttypeof value !== 'string' ||\n\t\t\t\t\t!value.includes(String(operand))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'startsWith':\n\t\t\t\tif (\n\t\t\t\t\ttypeof value !== 'string' ||\n\t\t\t\t\t!value.startsWith(String(operand))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'endsWith':\n\t\t\t\tif (\n\t\t\t\t\ttypeof value !== 'string' ||\n\t\t\t\t\t!value.endsWith(String(operand))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t// `mode`, relation filters, etc. — bail so the engine refetches.\n\t\t\t\tthrow new UnsupportedFilterError(operator);\n\t\t}\n\t}\n\treturn true;\n};\n\nconst toConditions = (value: unknown): PrismaWhere[] => {\n\tif (Array.isArray(value)) {\n\t\treturn value.filter(isPlainObject);\n\t}\n\treturn isPlainObject(value) ? [value] : [];\n};\n\n/**\n * Evaluate a Prisma `where` object against an in-memory row — the JS mirror of\n * the SQL filter, used for incremental matching. Supports field equality and the\n * `equals/not/in/notIn/lt/lte/gt/gte/contains/startsWith/endsWith` operators\n * plus `AND`/`OR`/`NOT`. Anything else throws {@link UnsupportedFilterError}, so\n * the engine falls back to a refetch.\n *\n * @example\n * matchesWhere({ userId: 5, status: { not: 'archived' } }, row)\n */\nexport const matchesWhere = (\n\twhere: PrismaWhere,\n\trow: Record<string, unknown>\n): boolean => {\n\tfor (const [field, condition] of Object.entries(where)) {\n\t\tif (field === 'AND') {\n\t\t\tif (\n\t\t\t\t!toConditions(condition).every((part) =>\n\t\t\t\t\tmatchesWhere(part, row)\n\t\t\t\t)\n\t\t\t)\n\t\t\t\treturn false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (field === 'OR') {\n\t\t\tconst parts = toConditions(condition);\n\t\t\tif (\n\t\t\t\tparts.length > 0 &&\n\t\t\t\t!parts.some((part) => matchesWhere(part, row))\n\t\t\t)\n\t\t\t\treturn false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (field === 'NOT') {\n\t\t\tif (toConditions(condition).some((part) => matchesWhere(part, row)))\n\t\t\t\treturn false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!matchesField(row[field], condition)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n};\n",
9
+ "import type {\n\tCollectionContext,\n\tCollectionDefinition\n} from '../../engine/collection';\nimport type { RowKey } from '../../engine/types';\nimport { matchesWhere } from './predicate';\nimport type { PrismaWhere } from './topics';\n\nexport type PrismaCollectionOptions<T, P, Ctx> = {\n\t/** Collection name (change-feed key and topic root). */\n\tname: string;\n\t/**\n\t * The query filter, written once. Used both to hydrate from the database\n\t * (passed to `find`) and, mirrored in JS, to match changed rows\n\t * incrementally. Receives the subscription's params and context.\n\t */\n\twhere: (params: P, ctx: Ctx) => PrismaWhere;\n\t/**\n\t * Run the database read for a given `where` — your Prisma call, e.g.\n\t * `(where) => prisma.order.findMany({ where })`.\n\t */\n\tfind: (\n\t\twhere: PrismaWhere,\n\t\tparams: P,\n\t\tctx: Ctx\n\t) => Promise<Iterable<T>> | Iterable<T>;\n\t/** Row identity. Defaults to `row.id`. */\n\tkey?: (row: T) => RowKey;\n\t/** Access control; return false (or throw) to deny. */\n\tauthorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;\n};\n\n/**\n * Build a syncable {@link CollectionDefinition} for Prisma from a single filter:\n * `where` is written once and powers both `hydrate` (via your `find`) and the\n * incremental `match` (via {@link matchesWhere}) — no restating the WHERE.\n *\n * If the filter uses an operator the JS matcher can't evaluate, that change\n * degrades to a refetch (handled by the engine), so the result stays correct.\n *\n * @example\n * prismaCollection({\n * name: 'orders',\n * where: (p) => ({ userId: p.userId, status: 'open' }),\n * find: (where) => prisma.order.findMany({ where }),\n * authorize: (p, ctx) => p.userId === ctx.userId\n * });\n */\nexport const prismaCollection = <T, P = void, Ctx = CollectionContext>(\n\toptions: PrismaCollectionOptions<T, P, Ctx>\n): CollectionDefinition<T, P, Ctx> => ({\n\tname: options.name,\n\thydrate: (params, ctx) =>\n\t\toptions.find(options.where(params, ctx), params, ctx),\n\tmatch: (row, params, ctx) =>\n\t\tmatchesWhere(\n\t\t\toptions.where(params, ctx),\n\t\t\trow as Record<string, unknown>\n\t\t),\n\tkey: options.key,\n\tauthorize: options.authorize\n});\n"
10
+ ],
11
+ "mappings": "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",
12
+ "debugId": "96C8D5DF158A4DF864756E2164756E21",
13
+ "names": []
14
+ }
@@ -0,0 +1,20 @@
1
+ import type { PrismaWhere } from './topics';
2
+ /**
3
+ * Thrown when a Prisma `where` uses an operator the incremental matcher can't
4
+ * evaluate in JS. The sync engine catches it and degrades that subscription to
5
+ * a refetch, so it never produces a wrong result — only a less efficient one.
6
+ */
7
+ export declare class UnsupportedFilterError extends Error {
8
+ constructor(operator: string);
9
+ }
10
+ /**
11
+ * Evaluate a Prisma `where` object against an in-memory row — the JS mirror of
12
+ * the SQL filter, used for incremental matching. Supports field equality and the
13
+ * `equals/not/in/notIn/lt/lte/gt/gte/contains/startsWith/endsWith` operators
14
+ * plus `AND`/`OR`/`NOT`. Anything else throws {@link UnsupportedFilterError}, so
15
+ * the engine falls back to a refetch.
16
+ *
17
+ * @example
18
+ * matchesWhere({ userId: 5, status: { not: 'archived' } }, row)
19
+ */
20
+ export declare const matchesWhere: (where: PrismaWhere, row: Record<string, unknown>) => boolean;
@@ -0,0 +1,28 @@
1
+ import type { PrismaWhere } from './topics';
2
+ export type DeriveReadTopicsOptions = {
3
+ /**
4
+ * Primary-key field name to narrow on. Defaults to `id` (Prisma's
5
+ * convention); set it for models keyed by another field.
6
+ */
7
+ keyField?: string;
8
+ };
9
+ export type DerivedReadTopics = {
10
+ /** Topics this read depends on — subscribe to all of them. */
11
+ topics: string[];
12
+ /**
13
+ * `true` when derivation narrowed to a specific row (`model:key`); `false`
14
+ * when it fell back to the whole-model topic.
15
+ */
16
+ rowLevel: boolean;
17
+ };
18
+ /**
19
+ * Derive the reactive topics a read of `model` (optionally filtered by `where`)
20
+ * depends on. A recognised key-field equality narrows to a single `model:key`
21
+ * topic; everything else subscribes to the whole-model topic.
22
+ *
23
+ * @example
24
+ * deriveReadTopics('user'); // { topics: ['user'], rowLevel: false }
25
+ * deriveReadTopics('user', { id: 5 }); // { topics: ['user:5'], rowLevel: true }
26
+ * deriveReadTopics('user', { id: { gt: 5 } }); // { topics: ['user'], rowLevel: false }
27
+ */
28
+ export declare const deriveReadTopics: (model: string, where?: PrismaWhere, options?: DeriveReadTopicsOptions) => DerivedReadTopics;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared topic vocabulary + key extraction for the Prisma adapter. Operates on
3
+ * Prisma's plain `where` objects and result records — there is no
4
+ * `@prisma/client` import, so it stays generic across every database Prisma
5
+ * supports. Both the read side (derive a query's topics) and the write side
6
+ * (publish a mutation's topics) build on these so the two always agree.
7
+ */
8
+ /** A Prisma `where` filter object, e.g. `{ id: 5 }` or `{ id: { gt: 5 } }`. */
9
+ export type PrismaWhere = Record<string, unknown>;
10
+ /** A scalar usable as a row key. */
11
+ export type RowKey = string | number | bigint;
12
+ /** The coarse topic every read/write of `model` touches, e.g. `user`. */
13
+ export declare const tableTopic: (model: string) => string;
14
+ /** The row-level topic for one key of `model`, e.g. `user:5`. */
15
+ export declare const keyTopic: (model: string, key: RowKey) => string;
16
+ /**
17
+ * Best-effort: pull a single key-field equality value out of a Prisma `where`.
18
+ * Recognises `{ [keyField]: scalar }` and `{ [keyField]: { equals: scalar } }`
19
+ * only — extra fields, other operators (`gt`/`in`/`not`/…), `AND`/`OR`, or a
20
+ * compound-key object yield `undefined`, so the caller falls back to the table
21
+ * topic (over-invalidating a little rather than missing an update).
22
+ */
23
+ export declare const extractKeyFromWhere: (where: PrismaWhere, keyField: string) => RowKey | undefined;
24
+ /**
25
+ * Read the key value from each result record (e.g. the output of a Prisma
26
+ * `create`/`update`/`delete`), using `keyField`. Records without a scalar key
27
+ * are skipped.
28
+ */
29
+ export declare const extractRowKeys: (rows: ReadonlyArray<Record<string, unknown>>, keyField: string) => RowKey[];
@@ -0,0 +1,65 @@
1
+ import type { ReactiveHub } from '../../reactiveHub';
2
+ import type { PrismaWhere, RowKey } from './topics';
3
+ /**
4
+ * Prisma write-side topic publishing (Tier 2).
5
+ *
6
+ * The mirror of {@link deriveReadTopics}: after a mutation commits, publish the
7
+ * topics it invalidates so subscribed reads refetch. Every change publishes the
8
+ * **model** topic (so list queries refresh) plus a **row** topic per affected
9
+ * key (so row-level queries refresh) — the exact topics the read side derives.
10
+ *
11
+ * Use the SAME model identifier you pass on the read side; the topic is the
12
+ * model name verbatim.
13
+ */
14
+ /** The kind of mutation, forwarded in the change-event payload. */
15
+ export type ChangeOp = 'insert' | 'update' | 'delete';
16
+ /** Payload carried by every change event the write side publishes. */
17
+ export type ChangePayload = {
18
+ /** Name of the model that changed. */
19
+ table: string;
20
+ /** Mutation kind, when the caller provided it. */
21
+ op?: ChangeOp;
22
+ /** Affected row keys (empty for a model-wide change). */
23
+ keys: RowKey[];
24
+ };
25
+ export type PublishChangeOptions = {
26
+ /** Row keys that changed; each emits a `model:key` topic. */
27
+ keys?: ReadonlyArray<RowKey>;
28
+ op?: ChangeOp;
29
+ };
30
+ /**
31
+ * Publish the reactive topics a change to `model` invalidates: the whole-model
32
+ * topic (always) plus a `model:key` topic per affected row. Call after the
33
+ * mutation resolves. Returns the (de-duplicated) topics published.
34
+ */
35
+ export declare const publishChange: (hub: Pick<ReactiveHub, "publish">, model: string, options?: PublishChangeOptions) => string[];
36
+ export type PublishRowsOptions = {
37
+ /** Key field (defaults to `id`). */
38
+ keyField?: string;
39
+ op?: ChangeOp;
40
+ };
41
+ /**
42
+ * Publish change topics for the record(s) a mutation returned. Accepts a single
43
+ * record (Prisma `create`/`update`/`delete`) or an array (`findMany` results),
44
+ * reading each record's `keyField` to emit `model:key` topics.
45
+ *
46
+ * @example
47
+ * const user = await prisma.user.create({ data });
48
+ * publishRows(hub, 'user', user, { op: 'insert' });
49
+ */
50
+ export declare const publishRows: (hub: Pick<ReactiveHub, "publish">, model: string, rows: Record<string, unknown> | ReadonlyArray<Record<string, unknown>>, options?: PublishRowsOptions) => string[];
51
+ export type PublishWhereOptions = {
52
+ /** Key field (defaults to `id`). */
53
+ keyField?: string;
54
+ op?: ChangeOp;
55
+ };
56
+ /**
57
+ * Publish change topics for an `update`/`delete` identified by a `where` filter.
58
+ * A simple key-field equality narrows to that row's topic; any other filter
59
+ * publishes just the model topic, so every affected subscriber refetches.
60
+ *
61
+ * @example
62
+ * await prisma.user.update({ where: { id }, data });
63
+ * publishWhere(hub, 'user', { id }, { op: 'update' });
64
+ */
65
+ export declare const publishWhere: (hub: Pick<ReactiveHub, "publish">, model: string, where: PrismaWhere, options?: PublishWhereOptions) => string[];
@@ -0,0 +1,32 @@
1
+ /**
2
+ * SQLite CDC adapter for @absolutejs/sync (Tier 3, M5).
3
+ *
4
+ * SQLite has no `LISTEN/NOTIFY`, and its `update_hook` isn't reachable from the
5
+ * JS runtimes we target, so out-of-band writes are caught with the portable
6
+ * changelog (outbox) pattern: install triggers that append every row change to a
7
+ * changelog table, then tail it with {@link createPollingChangeSource}. Polling a
8
+ * local SQLite table is cheap (same process, no network).
9
+ *
10
+ * Dependency-free — it only generates SQL; bring your own client (`bun:sqlite`,
11
+ * better-sqlite3, …) to run it and to back the poll query.
12
+ */
13
+ export { createPollingChangeSource, parseOutboxRow } from '../../engine/pollingSource';
14
+ export type { OutboxRow, PollingChangeSourceOptions } from '../../engine/pollingSource';
15
+ export type SqliteChangelogOptions = {
16
+ /** Table name → the column names to capture in the change payload. */
17
+ tables: Record<string, string[]>;
18
+ /** Changelog table name. Defaults to `absolute_sync_changelog`. */
19
+ changelogTable?: string;
20
+ /** Trigger name prefix. Defaults to `absolute_sync`. */
21
+ prefix?: string;
22
+ };
23
+ /**
24
+ * Generate the SQL that installs the changelog table and per-table
25
+ * insert/update/delete triggers — run it once (e.g. in a migration). Each
26
+ * trigger appends `{ tbl, op, payload }` (payload built with `json_object` from
27
+ * the listed columns) to the changelog for {@link createPollingChangeSource}.
28
+ *
29
+ * The statements are `;`-separated; run them as a script, or split on `;` if your
30
+ * driver executes one statement per call.
31
+ */
32
+ export declare const sqliteChangelogSchema: (options: SqliteChangelogOptions) => string;
@@ -0,0 +1,128 @@
1
+ // @bun
2
+ // src/engine/pollingSource.ts
3
+ var OP_BY_NAME = {
4
+ insert: "insert",
5
+ INSERT: "insert",
6
+ update: "update",
7
+ UPDATE: "update",
8
+ delete: "delete",
9
+ DELETE: "delete"
10
+ };
11
+ var parseOutboxRow = (row) => {
12
+ if (typeof row.tbl !== "string") {
13
+ return;
14
+ }
15
+ const op = OP_BY_NAME[row.op];
16
+ if (op === undefined) {
17
+ return;
18
+ }
19
+ let payload = row.payload;
20
+ if (typeof payload === "string") {
21
+ try {
22
+ payload = JSON.parse(payload);
23
+ } catch {
24
+ return;
25
+ }
26
+ }
27
+ if (typeof payload !== "object" || payload === null) {
28
+ return;
29
+ }
30
+ return { table: row.tbl, change: { op, row: payload } };
31
+ };
32
+ var createPollingChangeSource = (options) => {
33
+ const intervalMs = options.intervalMs ?? 1000;
34
+ const parse = options.parse ?? parseOutboxRow;
35
+ const onError = options.onError ?? ((error) => {
36
+ console.warn("[sync] polling change source error:", error);
37
+ });
38
+ let cursor = options.startSeq ?? 0;
39
+ let running = false;
40
+ let timer;
41
+ const tick = async (emit) => {
42
+ if (!running) {
43
+ return;
44
+ }
45
+ try {
46
+ const rows = await options.poll(cursor);
47
+ for (const row of rows) {
48
+ const parsed = parse(row);
49
+ if (parsed !== undefined) {
50
+ await emit(parsed.table, parsed.change);
51
+ }
52
+ if (typeof row.seq === "number" && row.seq > cursor) {
53
+ cursor = row.seq;
54
+ }
55
+ }
56
+ if (rows.length > 0) {
57
+ await options.onProcessed?.(cursor);
58
+ }
59
+ } catch (error) {
60
+ onError(error);
61
+ }
62
+ if (running) {
63
+ timer = setTimeout(() => {
64
+ tick(emit);
65
+ }, intervalMs);
66
+ }
67
+ };
68
+ return {
69
+ start: async (emit) => {
70
+ if (running) {
71
+ return;
72
+ }
73
+ running = true;
74
+ await tick(emit);
75
+ },
76
+ stop: () => {
77
+ running = false;
78
+ if (timer !== undefined) {
79
+ clearTimeout(timer);
80
+ timer = undefined;
81
+ }
82
+ }
83
+ };
84
+ };
85
+
86
+ // src/adapters/sqlite/index.ts
87
+ var DEFAULT_CHANGELOG = "absolute_sync_changelog";
88
+ var DEFAULT_PREFIX = "absolute_sync";
89
+ var OPS = ["insert", "update", "delete"];
90
+ var sqliteChangelogSchema = (options) => {
91
+ const changelog = options.changelogTable ?? DEFAULT_CHANGELOG;
92
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
93
+ const createTable = [
94
+ `CREATE TABLE IF NOT EXISTS ${changelog} (`,
95
+ "\tseq INTEGER PRIMARY KEY AUTOINCREMENT,",
96
+ "\ttbl TEXT NOT NULL,",
97
+ "\top TEXT NOT NULL,",
98
+ "\tpayload TEXT NOT NULL,",
99
+ "\tcreated_at TEXT NOT NULL DEFAULT (datetime('now'))",
100
+ ");"
101
+ ].join(`
102
+ `);
103
+ const jsonObject = (columns, ref) => `json_object(${columns.map((column) => `'${column}', ${ref}.${column}`).join(", ")})`;
104
+ const triggers = Object.entries(options.tables).flatMap(([table, columns]) => OPS.map((op) => {
105
+ const ref = op === "delete" ? "OLD" : "NEW";
106
+ const name = `${prefix}_${table}_${op}`;
107
+ return [
108
+ `DROP TRIGGER IF EXISTS ${name};`,
109
+ `CREATE TRIGGER ${name} AFTER ${op.toUpperCase()} ON ${table}`,
110
+ "BEGIN",
111
+ ` INSERT INTO ${changelog} (tbl, op, payload)`,
112
+ ` VALUES ('${table}', '${op}', ${jsonObject(columns, ref)});`,
113
+ "END;"
114
+ ].join(`
115
+ `);
116
+ }));
117
+ return [createTable, ...triggers].join(`
118
+
119
+ `);
120
+ };
121
+ export {
122
+ sqliteChangelogSchema,
123
+ parseOutboxRow,
124
+ createPollingChangeSource
125
+ };
126
+
127
+ //# debugId=3D209B4D7BF1282864756E2164756E21
128
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/engine/pollingSource.ts", "../src/adapters/sqlite/index.ts"],
4
+ "sourcesContent": [
5
+ "import type { ChangeSource, EmitChange, ParsedChange, RowOp } from './types';\n\n/**\n * A database-agnostic CDC {@link ChangeSource} that tails an append-only\n * changelog (outbox) table and emits its rows into the engine.\n *\n * This is the portable way to catch out-of-band writes on databases without a\n * native push channel — MySQL (no `LISTEN/NOTIFY`) and SQLite (no update hook\n * reachable from the JS runtime). Install per-table triggers that append to the\n * changelog (see `@absolutejs/sync/mysql` and `/sqlite`), then poll it here. It\n * is also a fine fallback for Postgres when you'd rather not run `LISTEN`.\n *\n * Driver-agnostic: you supply `poll(sinceSeq)` — a single\n * `SELECT seq, tbl, op, payload FROM <changelog> WHERE seq > ? ORDER BY seq` run\n * with your client — and the adapter tracks the cursor, parses each row, emits,\n * and advances. Delivery is at-least-once across crashes (a row may re-emit if\n * the process dies mid-batch); the engine's per-key last-write-wins makes that\n * safe. Use `onProcessed` to prune the changelog once a watermark is durable.\n */\n\nconst OP_BY_NAME: Record<string, RowOp> = {\n\tinsert: 'insert',\n\tINSERT: 'insert',\n\tupdate: 'update',\n\tUPDATE: 'update',\n\tdelete: 'delete',\n\tDELETE: 'delete'\n};\n\n/** One changelog row, as returned by your `poll` query. */\nexport type OutboxRow = {\n\t/** Monotonic sequence (the cursor advances to the max seen). */\n\tseq: number;\n\t/** Source table the change happened on. */\n\ttbl: string;\n\t/** `insert` | `update` | `delete` (upper- or lower-case). */\n\top: string;\n\t/** The row's captured values — a JSON string or an already-parsed object. */\n\tpayload: unknown;\n};\n\n/**\n * Default changelog-row parser: normalizes `op`, JSON-parses a string `payload`,\n * and returns `{ table, change }`. Returns `undefined` for a malformed row so it\n * is skipped (and its `seq` still advances the cursor) rather than wedging the\n * feed.\n */\nexport const parseOutboxRow = (row: OutboxRow): ParsedChange | undefined => {\n\tif (typeof row.tbl !== 'string') {\n\t\treturn undefined;\n\t}\n\tconst op = OP_BY_NAME[row.op];\n\tif (op === undefined) {\n\t\treturn undefined;\n\t}\n\tlet payload: unknown = row.payload;\n\tif (typeof payload === 'string') {\n\t\ttry {\n\t\t\tpayload = JSON.parse(payload);\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tif (typeof payload !== 'object' || payload === null) {\n\t\treturn undefined;\n\t}\n\treturn { table: row.tbl, change: { op, row: payload } };\n};\n\nexport type PollingChangeSourceOptions = {\n\t/**\n\t * Fetch changelog rows with `seq > sinceSeq`, ordered by `seq` ascending.\n\t * e.g. `(since) => sql\\`SELECT seq, tbl, op, payload FROM absolute_sync_changelog WHERE seq > ${since} ORDER BY seq\\``.\n\t */\n\tpoll: (sinceSeq: number) => Promise<OutboxRow[]> | OutboxRow[];\n\t/** Poll interval in ms. Defaults to 1000. */\n\tintervalMs?: number;\n\t/** Resume cursor (highest `seq` already processed). Defaults to 0. */\n\tstartSeq?: number;\n\t/** Override the row parser (defaults to {@link parseOutboxRow}). */\n\tparse?: (row: OutboxRow) => ParsedChange | undefined;\n\t/** Called after a non-empty batch with the new watermark — prune here. */\n\tonProcessed?: (uptoSeq: number) => void | Promise<void>;\n\t/** Called if a poll throws (the loop keeps running). Defaults to a warning. */\n\tonError?: (error: unknown) => void;\n};\n\n/**\n * Create a polling {@link ChangeSource} over a changelog table. Connect it with\n * `engine.connectSource(...)`; `start` runs an immediate first poll (draining any\n * backlog) and then polls every `intervalMs` until `stop`.\n */\nexport const createPollingChangeSource = (\n\toptions: PollingChangeSourceOptions\n): ChangeSource => {\n\tconst intervalMs = options.intervalMs ?? 1000;\n\tconst parse = options.parse ?? parseOutboxRow;\n\tconst onError =\n\t\toptions.onError ??\n\t\t((error: unknown) => {\n\t\t\tconsole.warn('[sync] polling change source error:', error);\n\t\t});\n\tlet cursor = options.startSeq ?? 0;\n\tlet running = false;\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\n\tconst tick = async (emit: EmitChange): Promise<void> => {\n\t\tif (!running) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst rows = await options.poll(cursor);\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst parsed = parse(row);\n\t\t\t\tif (parsed !== undefined) {\n\t\t\t\t\tawait emit(parsed.table, parsed.change);\n\t\t\t\t}\n\t\t\t\tif (typeof row.seq === 'number' && row.seq > cursor) {\n\t\t\t\t\tcursor = row.seq;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (rows.length > 0) {\n\t\t\t\tawait options.onProcessed?.(cursor);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tonError(error);\n\t\t}\n\t\tif (running) {\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tvoid tick(emit);\n\t\t\t}, intervalMs);\n\t\t}\n\t};\n\n\treturn {\n\t\tstart: async (emit) => {\n\t\t\tif (running) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trunning = true;\n\t\t\tawait tick(emit);\n\t\t},\n\t\tstop: () => {\n\t\t\trunning = false;\n\t\t\tif (timer !== undefined) {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\ttimer = undefined;\n\t\t\t}\n\t\t}\n\t};\n};\n",
6
+ "/**\n * SQLite CDC adapter for @absolutejs/sync (Tier 3, M5).\n *\n * SQLite has no `LISTEN/NOTIFY`, and its `update_hook` isn't reachable from the\n * JS runtimes we target, so out-of-band writes are caught with the portable\n * changelog (outbox) pattern: install triggers that append every row change to a\n * changelog table, then tail it with {@link createPollingChangeSource}. Polling a\n * local SQLite table is cheap (same process, no network).\n *\n * Dependency-free — it only generates SQL; bring your own client (`bun:sqlite`,\n * better-sqlite3, …) to run it and to back the poll query.\n */\n\nexport {\n\tcreatePollingChangeSource,\n\tparseOutboxRow\n} from '../../engine/pollingSource';\nexport type {\n\tOutboxRow,\n\tPollingChangeSourceOptions\n} from '../../engine/pollingSource';\n\nconst DEFAULT_CHANGELOG = 'absolute_sync_changelog';\nconst DEFAULT_PREFIX = 'absolute_sync';\nconst OPS = ['insert', 'update', 'delete'] as const;\n\nexport type SqliteChangelogOptions = {\n\t/** Table name → the column names to capture in the change payload. */\n\ttables: Record<string, string[]>;\n\t/** Changelog table name. Defaults to `absolute_sync_changelog`. */\n\tchangelogTable?: string;\n\t/** Trigger name prefix. Defaults to `absolute_sync`. */\n\tprefix?: string;\n};\n\n/**\n * Generate the SQL that installs the changelog table and per-table\n * insert/update/delete triggers — run it once (e.g. in a migration). Each\n * trigger appends `{ tbl, op, payload }` (payload built with `json_object` from\n * the listed columns) to the changelog for {@link createPollingChangeSource}.\n *\n * The statements are `;`-separated; run them as a script, or split on `;` if your\n * driver executes one statement per call.\n */\nexport const sqliteChangelogSchema = (\n\toptions: SqliteChangelogOptions\n): string => {\n\tconst changelog = options.changelogTable ?? DEFAULT_CHANGELOG;\n\tconst prefix = options.prefix ?? DEFAULT_PREFIX;\n\n\tconst createTable = [\n\t\t`CREATE TABLE IF NOT EXISTS ${changelog} (`,\n\t\t'\\tseq INTEGER PRIMARY KEY AUTOINCREMENT,',\n\t\t'\\ttbl TEXT NOT NULL,',\n\t\t'\\top TEXT NOT NULL,',\n\t\t'\\tpayload TEXT NOT NULL,',\n\t\t\"\\tcreated_at TEXT NOT NULL DEFAULT (datetime('now'))\",\n\t\t');'\n\t].join('\\n');\n\n\tconst jsonObject = (columns: string[], ref: 'NEW' | 'OLD'): string =>\n\t\t`json_object(${columns\n\t\t\t.map((column) => `'${column}', ${ref}.${column}`)\n\t\t\t.join(', ')})`;\n\n\tconst triggers = Object.entries(options.tables).flatMap(\n\t\t([table, columns]) =>\n\t\t\tOPS.map((op) => {\n\t\t\t\tconst ref = op === 'delete' ? 'OLD' : 'NEW';\n\t\t\t\tconst name = `${prefix}_${table}_${op}`;\n\t\t\t\treturn [\n\t\t\t\t\t`DROP TRIGGER IF EXISTS ${name};`,\n\t\t\t\t\t`CREATE TRIGGER ${name} AFTER ${op.toUpperCase()} ON ${table}`,\n\t\t\t\t\t'BEGIN',\n\t\t\t\t\t`\\tINSERT INTO ${changelog} (tbl, op, payload)`,\n\t\t\t\t\t`\\tVALUES ('${table}', '${op}', ${jsonObject(columns, ref)});`,\n\t\t\t\t\t'END;'\n\t\t\t\t].join('\\n');\n\t\t\t})\n\t);\n\n\treturn [createTable, ...triggers].join('\\n\\n');\n};\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "3D209B4D7BF1282864756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1 @@
1
+ export { SyncCollectionService } from './sync-collection.service';