@absolutejs/sync 0.0.1 → 0.2.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 +281 -24
- package/dist/adapters/drizzle/collection.d.ts +27 -0
- package/dist/adapters/drizzle/index.d.ts +20 -0
- package/dist/adapters/drizzle/index.js +265 -0
- package/dist/adapters/drizzle/index.js.map +14 -0
- package/dist/adapters/drizzle/predicate.d.ts +20 -0
- package/dist/adapters/drizzle/read.d.ts +31 -0
- package/dist/adapters/drizzle/topics.d.ts +41 -0
- package/dist/adapters/drizzle/write.d.ts +69 -0
- package/dist/adapters/mysql/index.d.ts +75 -0
- package/dist/adapters/mysql/index.js +171 -0
- package/dist/adapters/mysql/index.js.map +11 -0
- package/dist/adapters/postgres/index.d.ts +53 -0
- package/dist/adapters/postgres/index.js +86 -0
- package/dist/adapters/postgres/index.js.map +10 -0
- package/dist/adapters/prisma/collection.d.ts +39 -0
- package/dist/adapters/prisma/index.d.ts +23 -0
- package/dist/adapters/prisma/index.js +231 -0
- package/dist/adapters/prisma/index.js.map +14 -0
- package/dist/adapters/prisma/predicate.d.ts +20 -0
- package/dist/adapters/prisma/read.d.ts +28 -0
- package/dist/adapters/prisma/topics.d.ts +29 -0
- package/dist/adapters/prisma/write.d.ts +65 -0
- package/dist/adapters/sqlite/index.d.ts +32 -0
- package/dist/adapters/sqlite/index.js +128 -0
- package/dist/adapters/sqlite/index.js.map +11 -0
- package/dist/angular/index.d.ts +1 -0
- package/dist/angular/index.js +347 -0
- package/dist/angular/index.js.map +11 -0
- package/dist/angular/sync-collection.service.d.ts +20 -0
- package/dist/client/index.d.ts +12 -30
- package/dist/client/index.js +1099 -3
- package/dist/client/index.js.map +10 -4
- package/dist/client/liveQuery.d.ts +75 -0
- package/dist/client/presence.d.ts +37 -0
- package/dist/client/subscriber.d.ts +30 -0
- package/dist/client/syncClient.d.ts +53 -0
- package/dist/client/syncCollection.d.ts +102 -0
- package/dist/client/syncStore.d.ts +81 -0
- package/dist/engine/aggregate.d.ts +45 -0
- package/dist/engine/cluster.d.ts +41 -0
- package/dist/engine/collection.d.ts +87 -0
- package/dist/engine/connection.d.ts +103 -0
- package/dist/engine/dataflow.d.ts +109 -0
- package/dist/engine/equiJoin.d.ts +51 -0
- package/dist/engine/graph.d.ts +85 -0
- package/dist/engine/index.d.ts +40 -0
- package/dist/engine/index.js +1774 -0
- package/dist/engine/index.js.map +23 -0
- package/dist/engine/materializedView.d.ts +53 -0
- package/dist/engine/mutation.d.ts +66 -0
- package/dist/engine/pollingSource.d.ts +42 -0
- package/dist/engine/presence.d.ts +46 -0
- package/dist/engine/reactive.d.ts +67 -0
- package/dist/engine/routes.d.ts +40 -0
- package/dist/engine/socket.d.ts +67 -0
- package/dist/engine/syncEngine.d.ts +132 -0
- package/dist/engine/types.d.ts +45 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +327 -3
- package/dist/index.js.map +8 -5
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +332 -0
- package/dist/react/index.js.map +11 -0
- package/dist/react/useSyncCollection.d.ts +16 -0
- package/dist/reactiveHub.d.ts +6 -0
- package/dist/svelte/createSyncCollectionStore.d.ts +15 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +338 -0
- package/dist/svelte/index.js.map +11 -0
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +331 -0
- package/dist/vue/index.js.map +11 -0
- package/dist/vue/useSyncCollection.d.ts +17 -0
- package/package.json +104 -6
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
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
|
+
"sourcesContent": [
|
|
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",
|
|
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",
|
|
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"
|
|
10
|
+
],
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "A28CA296A764961764756E2164756E21",
|
|
13
|
+
"names": []
|
|
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;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SQL, Table } from 'drizzle-orm';
|
|
2
|
+
export type DeriveReadTopicsOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Column (its JS property name on the table) to treat as the row key when
|
|
5
|
+
* narrowing to a `table:key` topic. Defaults to the table's single
|
|
6
|
+
* primary-key column; composite or absent primary keys disable row-level
|
|
7
|
+
* narrowing.
|
|
8
|
+
*/
|
|
9
|
+
keyColumn?: string;
|
|
10
|
+
};
|
|
11
|
+
export type DerivedReadTopics = {
|
|
12
|
+
/** Topics this read depends on — subscribe to all of them. */
|
|
13
|
+
topics: string[];
|
|
14
|
+
/**
|
|
15
|
+
* `true` when derivation narrowed to a specific row (`table:key`); `false`
|
|
16
|
+
* when it fell back to the whole-table topic.
|
|
17
|
+
*/
|
|
18
|
+
rowLevel: boolean;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Derive the reactive topics a read of `table` (optionally filtered by `where`)
|
|
22
|
+
* depends on. A recognised primary-key equality narrows to a single `table:key`
|
|
23
|
+
* topic; everything else subscribes to the whole-table topic, over-invalidating
|
|
24
|
+
* a little rather than missing an update.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* deriveReadTopics(users); // { topics: ['users'], rowLevel: false }
|
|
28
|
+
* deriveReadTopics(users, eq(users.id, 5)); // { topics: ['users:5'], rowLevel: true }
|
|
29
|
+
* deriveReadTopics(users, gt(users.id, 5)); // { topics: ['users'], rowLevel: false }
|
|
30
|
+
*/
|
|
31
|
+
export declare const deriveReadTopics: (table: Table, where?: SQL, options?: DeriveReadTopicsOptions) => DerivedReadTopics;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SQL } from 'drizzle-orm';
|
|
2
|
+
import type { Table } from 'drizzle-orm';
|
|
3
|
+
/**
|
|
4
|
+
* Shared topic vocabulary + key resolution for the Drizzle adapter. Both the
|
|
5
|
+
* read side (derive the topics a query depends on) and the write side (publish
|
|
6
|
+
* the topics a mutation invalidates) build on these so the two always agree.
|
|
7
|
+
*/
|
|
8
|
+
/** The coarse topic every read/write of `table` touches, e.g. `users`. */
|
|
9
|
+
export declare const tableTopic: (table: Table) => string;
|
|
10
|
+
/** The row-level topic for one key of `table`, e.g. `users:5`. */
|
|
11
|
+
export declare const keyTopic: (table: Table, key: string | number) => string;
|
|
12
|
+
type ResolvedKey = {
|
|
13
|
+
/** JS property name of the key column on the table / result rows. */
|
|
14
|
+
property: string;
|
|
15
|
+
/** Underlying DB column name, as it appears in a SQL expression. */
|
|
16
|
+
column: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the column to use as the row key: an explicitly requested column (by
|
|
20
|
+
* JS property name), otherwise the table's sole primary key. Returns `undefined`
|
|
21
|
+
* when no single key column applies (composite or missing primary key).
|
|
22
|
+
*/
|
|
23
|
+
export declare const resolveKeyColumn: (table: Table, keyColumn?: string) => ResolvedKey | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Best-effort: pull a single key-column equality value out of a Drizzle `where`
|
|
26
|
+
* expression. Recognises only the simple `eq(keyColumn, scalar)` shape — any
|
|
27
|
+
* nesting (`and`/`or`), extra columns/params, a non-`=` operator, or a
|
|
28
|
+
* non-key/cross-table column yields `undefined`.
|
|
29
|
+
*
|
|
30
|
+
* Reads Drizzle's internal `queryChunks`, which is not a stable public API;
|
|
31
|
+
* every branch degrades to `undefined` (coarser topic) rather than throwing, so
|
|
32
|
+
* a Drizzle version bump can only cost precision, never correctness.
|
|
33
|
+
*/
|
|
34
|
+
export declare const extractKeyFromWhere: (table: Table, where: SQL, keyColumn?: string) => string | number | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Read the key value from each row (e.g. the output of a mutation's
|
|
37
|
+
* `.returning()`), using the table's primary-key column or an explicit
|
|
38
|
+
* `keyColumn`. Rows without a string/number key are skipped.
|
|
39
|
+
*/
|
|
40
|
+
export declare const extractRowKeys: (table: Table, rows: ReadonlyArray<Record<string, unknown>>, keyColumn?: string) => (string | number)[];
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SQL, Table } from 'drizzle-orm';
|
|
2
|
+
import type { ReactiveHub } from '../../reactiveHub';
|
|
3
|
+
/**
|
|
4
|
+
* Drizzle 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
|
+
* **table** topic (so list/table queries refresh) plus a **row** topic per
|
|
9
|
+
* affected key (so row-level queries refresh) — the exact topics the read side
|
|
10
|
+
* subscribes to.
|
|
11
|
+
*
|
|
12
|
+
* These are the "route mutations through us" change source from the roadmap:
|
|
13
|
+
* call them right after your durable write. They work on any DB Drizzle supports
|
|
14
|
+
* and never touch DB-specific machinery; out-of-band writes (caught later by CDC
|
|
15
|
+
* adapters) are the only thing they miss.
|
|
16
|
+
*/
|
|
17
|
+
/** The kind of mutation, forwarded in the change-event payload. */
|
|
18
|
+
export type ChangeOp = 'insert' | 'update' | 'delete';
|
|
19
|
+
/** Payload carried by every change event the write side publishes. */
|
|
20
|
+
export type ChangePayload = {
|
|
21
|
+
/** Name of the table that changed. */
|
|
22
|
+
table: string;
|
|
23
|
+
/** Mutation kind, when the caller provided it. */
|
|
24
|
+
op?: ChangeOp;
|
|
25
|
+
/** Affected row keys (empty for a table-wide change). */
|
|
26
|
+
keys: (string | number)[];
|
|
27
|
+
};
|
|
28
|
+
export type PublishChangeOptions = {
|
|
29
|
+
/** Row keys that changed; each emits a `table:key` topic. */
|
|
30
|
+
keys?: ReadonlyArray<string | number>;
|
|
31
|
+
op?: ChangeOp;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Publish the reactive topics a change to `table` invalidates: the whole-table
|
|
35
|
+
* topic (always) plus a `table:key` topic per affected row. Call after the
|
|
36
|
+
* durable write commits. Returns the (de-duplicated) topics published.
|
|
37
|
+
*/
|
|
38
|
+
export declare const publishChange: (hub: Pick<ReactiveHub, "publish">, table: Table, options?: PublishChangeOptions) => string[];
|
|
39
|
+
export type PublishRowsOptions = {
|
|
40
|
+
/** Key column (JS property name); defaults to the table's primary key. */
|
|
41
|
+
keyColumn?: string;
|
|
42
|
+
op?: ChangeOp;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Publish change topics for a set of rows — typically the output of a mutation's
|
|
46
|
+
* `.returning()`, which yields real keys including auto-generated ones. Reads
|
|
47
|
+
* each row's primary-key column (or `keyColumn`) to emit `table:key` topics.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const rows = await db.insert(users).values(input).returning();
|
|
51
|
+
* publishRows(hub, users, rows, { op: 'insert' });
|
|
52
|
+
*/
|
|
53
|
+
export declare const publishRows: (hub: Pick<ReactiveHub, "publish">, table: Table, rows: ReadonlyArray<Record<string, unknown>>, options?: PublishRowsOptions) => string[];
|
|
54
|
+
export type PublishWhereOptions = {
|
|
55
|
+
/** Key column (JS property name); defaults to the table's primary key. */
|
|
56
|
+
keyColumn?: string;
|
|
57
|
+
op?: ChangeOp;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Publish change topics for an `update`/`delete` identified by a `where` filter.
|
|
61
|
+
* A simple primary-key equality narrows to that row's topic; any other filter
|
|
62
|
+
* publishes just the table topic, so every affected subscriber refetches and
|
|
63
|
+
* re-evaluates.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* await db.update(users).set(patch).where(eq(users.id, id));
|
|
67
|
+
* publishWhere(hub, users, eq(users.id, id), { op: 'update' });
|
|
68
|
+
*/
|
|
69
|
+
export declare const publishWhere: (hub: Pick<ReactiveHub, "publish">, table: Table, where: SQL, options?: PublishWhereOptions) => string[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ChangeSource, ParsedChange } from '../../engine/types';
|
|
2
|
+
/**
|
|
3
|
+
* MySQL CDC adapter for @absolutejs/sync (Tier 3, M5).
|
|
4
|
+
*
|
|
5
|
+
* MySQL has no `LISTEN/NOTIFY`, so two pluggable strategies catch out-of-band
|
|
6
|
+
* writes — both behind the engine's {@link ChangeSource} seam:
|
|
7
|
+
*
|
|
8
|
+
* - **Changelog + poll (portable).** Install triggers with
|
|
9
|
+
* {@link mysqlChangelogSchema} and tail the changelog with
|
|
10
|
+
* {@link createPollingChangeSource}. Works anywhere, no extra privileges.
|
|
11
|
+
* - **Binlog (higher throughput).** Wire a binlog reader (e.g. zongji) into
|
|
12
|
+
* {@link mysqlBinlogChangeSource}; it normalizes row events into the change
|
|
13
|
+
* feed. Catches writes without the per-write changelog overhead.
|
|
14
|
+
*
|
|
15
|
+
* Dependency-free — you bring the MySQL client / binlog reader.
|
|
16
|
+
*/
|
|
17
|
+
export { createPollingChangeSource, parseOutboxRow } from '../../engine/pollingSource';
|
|
18
|
+
export type { OutboxRow, PollingChangeSourceOptions } from '../../engine/pollingSource';
|
|
19
|
+
export type MysqlChangelogOptions = {
|
|
20
|
+
/** Table name → the column names to capture in the change payload. */
|
|
21
|
+
tables: Record<string, string[]>;
|
|
22
|
+
/** Changelog table name. Defaults to `absolute_sync_changelog`. */
|
|
23
|
+
changelogTable?: string;
|
|
24
|
+
/** Trigger name prefix. Defaults to `absolute_sync`. */
|
|
25
|
+
prefix?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Generate the SQL that installs the changelog table and per-table
|
|
29
|
+
* insert/update/delete triggers — run it once (e.g. in a migration). Each
|
|
30
|
+
* trigger appends `{ tbl, op, payload }` (payload built with `JSON_OBJECT` from
|
|
31
|
+
* the listed columns) to the changelog for {@link createPollingChangeSource}.
|
|
32
|
+
*
|
|
33
|
+
* Each `CREATE TRIGGER` body is a single statement (no `DELIMITER` needed). The
|
|
34
|
+
* statements are `;`-separated; run them as a script, or split on `;` if your
|
|
35
|
+
* driver executes one statement per call.
|
|
36
|
+
*/
|
|
37
|
+
export declare const mysqlChangelogSchema: (options: MysqlChangelogOptions) => string;
|
|
38
|
+
/** A row-level binlog event (the subset {@link normalizeBinlogEvent} needs). */
|
|
39
|
+
export type BinlogRowEvent = {
|
|
40
|
+
/**
|
|
41
|
+
* Event kind — zongji's `writerows` / `updaterows` / `deleterows`
|
|
42
|
+
* (case-insensitive).
|
|
43
|
+
*/
|
|
44
|
+
type: string;
|
|
45
|
+
/** The affected table. */
|
|
46
|
+
table: string;
|
|
47
|
+
/**
|
|
48
|
+
* Affected rows. Insert/delete: each entry is the row object. Update: each
|
|
49
|
+
* entry is `{ before, after }` (zongji's shape).
|
|
50
|
+
*/
|
|
51
|
+
rows: unknown[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Normalize a binlog row event into engine changes — one per affected row. For
|
|
55
|
+
* updates it takes the `after` image; for deletes the row (or its `before`
|
|
56
|
+
* image). Rows that aren't objects are skipped. Pure, so it's easy to test and
|
|
57
|
+
* to swap for a different reader's shape.
|
|
58
|
+
*/
|
|
59
|
+
export declare const normalizeBinlogEvent: (event: BinlogRowEvent) => ParsedChange[];
|
|
60
|
+
export type MysqlBinlogChangeSourceOptions = {
|
|
61
|
+
/**
|
|
62
|
+
* Subscribe to row events from a binlog reader; return a function that stops
|
|
63
|
+
* it. e.g. with zongji:
|
|
64
|
+
* `(onEvent) => { zongji.on('binlog', e => onEvent({ type: e.getEventName(), table: e.tableMap[e.tableId].tableName, rows: e.rows })); zongji.start(...); return () => zongji.stop(); }`.
|
|
65
|
+
*/
|
|
66
|
+
subscribe: (onEvent: (event: BinlogRowEvent) => void) => Promise<() => void | Promise<void>> | (() => void | Promise<void>);
|
|
67
|
+
/** Override the event normalizer (defaults to {@link normalizeBinlogEvent}). */
|
|
68
|
+
normalize?: (event: BinlogRowEvent) => ParsedChange[];
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* A {@link ChangeSource} backed by the MySQL binlog. Each row event is
|
|
72
|
+
* normalized and emitted into the engine. Connect with
|
|
73
|
+
* `engine.connectSource(...)`.
|
|
74
|
+
*/
|
|
75
|
+
export declare const mysqlBinlogChangeSource: (options: MysqlBinlogChangeSourceOptions) => ChangeSource;
|
|
@@ -0,0 +1,171 @@
|
|
|
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/mysql/index.ts
|
|
87
|
+
var DEFAULT_CHANGELOG = "absolute_sync_changelog";
|
|
88
|
+
var DEFAULT_PREFIX = "absolute_sync";
|
|
89
|
+
var OPS = ["insert", "update", "delete"];
|
|
90
|
+
var mysqlChangelogSchema = (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 BIGINT AUTO_INCREMENT PRIMARY KEY,",
|
|
96
|
+
"\ttbl VARCHAR(255) NOT NULL,",
|
|
97
|
+
"\top VARCHAR(16) NOT NULL,",
|
|
98
|
+
"\tpayload JSON NOT NULL,",
|
|
99
|
+
"\tcreated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
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}\` FOR EACH ROW`,
|
|
110
|
+
`INSERT INTO \`${changelog}\` (tbl, op, payload)`,
|
|
111
|
+
`VALUES ('${table}', '${op}', ${jsonObject(columns, ref)});`
|
|
112
|
+
].join(`
|
|
113
|
+
`);
|
|
114
|
+
}));
|
|
115
|
+
return [createTable, ...triggers].join(`
|
|
116
|
+
|
|
117
|
+
`);
|
|
118
|
+
};
|
|
119
|
+
var BINLOG_OP = {
|
|
120
|
+
writerows: "insert",
|
|
121
|
+
updaterows: "update",
|
|
122
|
+
deleterows: "delete"
|
|
123
|
+
};
|
|
124
|
+
var normalizeBinlogEvent = (event) => {
|
|
125
|
+
const op = BINLOG_OP[event.type.toLowerCase()];
|
|
126
|
+
if (op === undefined || typeof event.table !== "string") {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const changes = [];
|
|
130
|
+
for (const entry of event.rows) {
|
|
131
|
+
let row = entry;
|
|
132
|
+
if (entry !== null && typeof entry === "object") {
|
|
133
|
+
if (op === "update" && "after" in entry) {
|
|
134
|
+
row = entry.after;
|
|
135
|
+
} else if (op === "delete" && "before" in entry) {
|
|
136
|
+
row = entry.before;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (typeof row === "object" && row !== null) {
|
|
140
|
+
changes.push({ table: event.table, change: { op, row } });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return changes;
|
|
144
|
+
};
|
|
145
|
+
var mysqlBinlogChangeSource = (options) => {
|
|
146
|
+
const normalize = options.normalize ?? normalizeBinlogEvent;
|
|
147
|
+
let unsubscribe;
|
|
148
|
+
return {
|
|
149
|
+
start: async (emit) => {
|
|
150
|
+
unsubscribe = await options.subscribe((event) => {
|
|
151
|
+
for (const parsed of normalize(event)) {
|
|
152
|
+
emit(parsed.table, parsed.change);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
stop: async () => {
|
|
157
|
+
await unsubscribe?.();
|
|
158
|
+
unsubscribe = undefined;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
export {
|
|
163
|
+
parseOutboxRow,
|
|
164
|
+
normalizeBinlogEvent,
|
|
165
|
+
mysqlChangelogSchema,
|
|
166
|
+
mysqlBinlogChangeSource,
|
|
167
|
+
createPollingChangeSource
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
//# debugId=48EE1E12A50EB47064756E2164756E21
|
|
171
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/engine/pollingSource.ts", "../src/adapters/mysql/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
|
+
"import type { ChangeSource, ParsedChange, RowOp } from '../../engine/types';\n\n/**\n * MySQL CDC adapter for @absolutejs/sync (Tier 3, M5).\n *\n * MySQL has no `LISTEN/NOTIFY`, so two pluggable strategies catch out-of-band\n * writes — both behind the engine's {@link ChangeSource} seam:\n *\n * - **Changelog + poll (portable).** Install triggers with\n * {@link mysqlChangelogSchema} and tail the changelog with\n * {@link createPollingChangeSource}. Works anywhere, no extra privileges.\n * - **Binlog (higher throughput).** Wire a binlog reader (e.g. zongji) into\n * {@link mysqlBinlogChangeSource}; it normalizes row events into the change\n * feed. Catches writes without the per-write changelog overhead.\n *\n * Dependency-free — you bring the MySQL client / binlog reader.\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 MysqlChangelogOptions = {\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 * Each `CREATE TRIGGER` body is a single statement (no `DELIMITER` needed). The\n * statements are `;`-separated; run them as a script, or split on `;` if your\n * driver executes one statement per call.\n */\nexport const mysqlChangelogSchema = (\n\toptions: MysqlChangelogOptions\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 BIGINT AUTO_INCREMENT PRIMARY KEY,',\n\t\t'\\ttbl VARCHAR(255) NOT NULL,',\n\t\t'\\top VARCHAR(16) NOT NULL,',\n\t\t'\\tpayload JSON NOT NULL,',\n\t\t'\\tcreated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP',\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}\\` FOR EACH ROW`,\n\t\t\t\t\t`INSERT INTO \\`${changelog}\\` (tbl, op, payload)`,\n\t\t\t\t\t`VALUES ('${table}', '${op}', ${jsonObject(columns, ref)});`\n\t\t\t\t].join('\\n');\n\t\t\t})\n\t);\n\n\treturn [createTable, ...triggers].join('\\n\\n');\n};\n\nconst BINLOG_OP: Record<string, RowOp> = {\n\twriterows: 'insert',\n\tupdaterows: 'update',\n\tdeleterows: 'delete'\n};\n\n/** A row-level binlog event (the subset {@link normalizeBinlogEvent} needs). */\nexport type BinlogRowEvent = {\n\t/**\n\t * Event kind — zongji's `writerows` / `updaterows` / `deleterows`\n\t * (case-insensitive).\n\t */\n\ttype: string;\n\t/** The affected table. */\n\ttable: string;\n\t/**\n\t * Affected rows. Insert/delete: each entry is the row object. Update: each\n\t * entry is `{ before, after }` (zongji's shape).\n\t */\n\trows: unknown[];\n};\n\n/**\n * Normalize a binlog row event into engine changes — one per affected row. For\n * updates it takes the `after` image; for deletes the row (or its `before`\n * image). Rows that aren't objects are skipped. Pure, so it's easy to test and\n * to swap for a different reader's shape.\n */\nexport const normalizeBinlogEvent = (event: BinlogRowEvent): ParsedChange[] => {\n\tconst op = BINLOG_OP[event.type.toLowerCase()];\n\tif (op === undefined || typeof event.table !== 'string') {\n\t\treturn [];\n\t}\n\tconst changes: ParsedChange[] = [];\n\tfor (const entry of event.rows) {\n\t\tlet row: unknown = entry;\n\t\tif (entry !== null && typeof entry === 'object') {\n\t\t\tif (op === 'update' && 'after' in entry) {\n\t\t\t\trow = (entry as { after: unknown }).after;\n\t\t\t} else if (op === 'delete' && 'before' in entry) {\n\t\t\t\trow = (entry as { before: unknown }).before;\n\t\t\t}\n\t\t}\n\t\tif (typeof row === 'object' && row !== null) {\n\t\t\tchanges.push({ table: event.table, change: { op, row } });\n\t\t}\n\t}\n\treturn changes;\n};\n\nexport type MysqlBinlogChangeSourceOptions = {\n\t/**\n\t * Subscribe to row events from a binlog reader; return a function that stops\n\t * it. e.g. with zongji:\n\t * `(onEvent) => { zongji.on('binlog', e => onEvent({ type: e.getEventName(), table: e.tableMap[e.tableId].tableName, rows: e.rows })); zongji.start(...); return () => zongji.stop(); }`.\n\t */\n\tsubscribe: (\n\t\tonEvent: (event: BinlogRowEvent) => void\n\t) => Promise<() => void | Promise<void>> | (() => void | Promise<void>);\n\t/** Override the event normalizer (defaults to {@link normalizeBinlogEvent}). */\n\tnormalize?: (event: BinlogRowEvent) => ParsedChange[];\n};\n\n/**\n * A {@link ChangeSource} backed by the MySQL binlog. Each row event is\n * normalized and emitted into the engine. Connect with\n * `engine.connectSource(...)`.\n */\nexport const mysqlBinlogChangeSource = (\n\toptions: MysqlBinlogChangeSourceOptions\n): ChangeSource => {\n\tconst normalize = options.normalize ?? normalizeBinlogEvent;\n\tlet unsubscribe: (() => void | Promise<void>) | undefined;\n\n\treturn {\n\t\tstart: async (emit) => {\n\t\t\tunsubscribe = await options.subscribe((event) => {\n\t\t\t\tfor (const parsed of normalize(event)) {\n\t\t\t\t\tvoid emit(parsed.table, parsed.change);\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tstop: async () => {\n\t\t\tawait unsubscribe?.();\n\t\t\tunsubscribe = undefined;\n\t\t}\n\t};\n};\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "48EE1E12A50EB47064756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChangeSource, ParsedChange } from '../../engine/types';
|
|
2
|
+
/** A parsed change ready to feed the engine. */
|
|
3
|
+
export type ParsedNotification = ParsedChange;
|
|
4
|
+
/**
|
|
5
|
+
* Default NOTIFY-payload parser: expects the JSON the trigger from
|
|
6
|
+
* {@link postgresNotifyTrigger} sends — `{ table, op, row }` where `op` is
|
|
7
|
+
* `INSERT`/`UPDATE`/`DELETE`. Returns `undefined` for anything malformed so a
|
|
8
|
+
* bad payload is skipped rather than throwing.
|
|
9
|
+
*/
|
|
10
|
+
export declare const parseNotification: (payload: string) => ParsedNotification | undefined;
|
|
11
|
+
export type PostgresChangeSourceOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* Subscribe to a Postgres NOTIFY channel; return a function that stops
|
|
14
|
+
* listening. The wiring is yours, e.g. with porsager/postgres:
|
|
15
|
+
* `(channel, onNotify) => { const s = await sql.listen(channel, onNotify); return s.unlisten; }`.
|
|
16
|
+
*/
|
|
17
|
+
listen: (channel: string, onNotify: (payload: string) => void) => Promise<() => void | Promise<void>> | (() => void | Promise<void>);
|
|
18
|
+
/** NOTIFY channel; must match the trigger's. Defaults to `absolute_sync`. */
|
|
19
|
+
channel?: string;
|
|
20
|
+
/** Override the payload parser (defaults to {@link parseNotification}). */
|
|
21
|
+
parse?: (payload: string) => ParsedNotification | undefined;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A {@link ChangeSource} backed by Postgres `LISTEN/NOTIFY`. Each notification
|
|
25
|
+
* is parsed to `(table, change)` and emitted into the engine.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const disconnect = await engine.connectSource(
|
|
29
|
+
* postgresChangeSource({
|
|
30
|
+
* listen: async (channel, onNotify) =>
|
|
31
|
+
* (await sql.listen(channel, onNotify)).unlisten
|
|
32
|
+
* })
|
|
33
|
+
* );
|
|
34
|
+
*/
|
|
35
|
+
export declare const postgresChangeSource: (options: PostgresChangeSourceOptions) => ChangeSource;
|
|
36
|
+
export type PostgresNotifyTriggerOptions = {
|
|
37
|
+
/** Tables to emit changes for. */
|
|
38
|
+
tables: string[];
|
|
39
|
+
/** NOTIFY channel; must match the change source's. Defaults to `absolute_sync`. */
|
|
40
|
+
channel?: string;
|
|
41
|
+
/** Trigger function name. Defaults to `absolute_sync_notify`. */
|
|
42
|
+
functionName?: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Generate the SQL that installs a NOTIFY trigger on each table — run it once
|
|
46
|
+
* (e.g. in a migration). On every insert/update/delete it sends
|
|
47
|
+
* `{ table, op, row }` JSON on the channel for {@link postgresChangeSource}.
|
|
48
|
+
*
|
|
49
|
+
* Note: `pg_notify` payloads are capped at 8000 bytes, so very wide rows can be
|
|
50
|
+
* truncated (the parser then skips them). For large rows or high throughput,
|
|
51
|
+
* prefer a logical-replication source behind the same {@link ChangeSource} seam.
|
|
52
|
+
*/
|
|
53
|
+
export declare const postgresNotifyTrigger: (options: PostgresNotifyTriggerOptions) => string;
|