@absolutejs/sync 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,10 +33,11 @@ top-N ordering are maintained incrementally through a composable operator graph
33
33
  > write-behind cache), Tier 2 (Drizzle + Prisma topic adapters, `createLiveQuery`),
34
34
  > and Tier 3 (sync engine: collections, WebSocket diff transport, optimistic
35
35
  > mutations + offline queue, a local-first client cache, declarative row-level
36
- > permissions, live full-text + vector search, scheduled functions, a live
37
- > devtools dashboard, CDC for Postgres/MySQL/SQLite, incremental aggregations +
38
- > joins, and a declarative operator graph) are in place. Everything ships as
39
- > subpaths of this one package.
36
+ > permissions, schema validation + lazy migrations, live full-text + vector
37
+ > search, scheduled functions, a live devtools dashboard, conflict-free
38
+ > collaborative editing (CRDTs), CDC for Postgres/MySQL/SQLite, incremental
39
+ > aggregations + joins, and a declarative operator graph) are in place.
40
+ > Everything ships as subpaths of this one package.
40
41
 
41
42
  ## Install
42
43
 
@@ -403,11 +404,24 @@ mutate({
403
404
  | `defineGraphCollection({ name, query, key, authorize? })` | Run a `query` as a live collection. |
404
405
  | `defineReactiveQuery({ name, run, key })` + `registerReactive` / `registerReader` | Read-set-tracked query: `run(ctx)` reads via `ctx.db` (`all`/`get`/`where`) and re-runs only when the rows/ranges it read change — no `match`, no manual emit. |
405
406
  | `definePermissions({ [table]: { read?, insert?, update?, delete?, write? } })` | Declarative row-level access control. Pass as `createSyncEngine({ permissions })` or `registerPermissions(table, rules)`. Read rules filter every row emitted; write rules gate `actions.insert/update/delete`. |
407
+ | `defineSchema({ [table]: { fields, version?, migrate? } })` + `field` kit | Declarative row schema. Pass as `createSyncEngine({ schemas })` or `registerSchema(table, schema)`. Writes are validated (bad write → `SchemaError`); `migrate` lazily upcasts rows on read (no DB migration needed). |
406
408
  | `defineSearchCollection({ name, table, index, source, key, limit? })` + `registerSearch` | Live search collection: the subscription's `params` are the query (string/vector), the ranked top-K stream back as a normal collection, re-ranked as rows change. Each row carries its score under `_score`. |
407
409
  | `createTextIndex({ key, fields, tokenize?, stopwords?, k1?, b? })` | Incremental BM25 full-text index (keyword search). Implements `SearchIndex`; usable standalone or inside a search collection. |
408
410
  | `createVectorIndex({ key, embedding, metric? })` | Incremental vector index (cosine/dot/euclidean exact k-NN) for semantic search — pairs with `@absolutejs/ai` / `@absolutejs/rag` for RAG retrieval on your own data. |
409
411
  | `defineSchedule({ name, pattern, run })` + `registerSchedule` / `runSchedule` | Scheduled function: `run({ db, actions })` fires on a cron `pattern`; its writes go live through the change feed. Wire triggers with the `scheduled` plugin (or call `runSchedule(name)` on demand). |
410
412
 
413
+ ### `@absolutejs/sync/crdt`
414
+
415
+ Conflict-free replicated data types — pure, **zero-dependency**, and isomorphic (same code client and server). They merge concurrent edits from different tabs/devices without a server round-trip per keystroke and without clobbering: every `merge` is commutative, associative, and idempotent, so replicas converge no matter the order. They ride the existing engine with no engine changes — store the CRDT state as a row field and have a mutation `merge` the incoming state into the stored one.
416
+
417
+ | Export | What it is |
418
+ | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
419
+ | `counter` | PN-counter: `create/value/increment/decrement/merge`. Concurrent increments and decrements across replicas all survive. |
420
+ | `lww` | Last-write-wins register: `create/set/merge`. The latest timestamp wins (replica id breaks ties) — for "just take the newest value" fields. |
421
+ | `createTextCrdt(replica, initial?)` | Collaborative text (an RGA sequence CRDT): `text/insert/delete/setText/merge/state`. Drive it from an input via `setText`; broadcast `state()`; apply remote state via `merge` — concurrent edits merge and converge. |
422
+ | `textOf(state)` / `mergeTextState(a, b)` | Pure helpers for the text state — use them server-side (e.g. a merge-on-write mutation) with no live instance. |
423
+ | `CrdtText<State>` / `TextCrdtAdapter<State>` | The pluggable collaborative-text contract. `rgaText` is the first-party (zero-dep) backend; swap in an adapter from the `sync-adapters` repo (e.g. `@absolutejs/sync-yjs`, which wraps the Yjs staple) behind the same call sites. |
424
+
411
425
  ### `@absolutejs/sync/postgres`
412
426
 
413
427
  | Export | What it is |
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Conflict-free replicated data types (CRDTs) for multiplayer/offline editing —
3
+ * pure, dependency-free, and isomorphic (use the same code client and server).
4
+ *
5
+ * These are *state-based* CRDTs (CvRDTs): every `merge` is commutative,
6
+ * associative, and idempotent, so replicas that exchange state in any order
7
+ * converge to the same value. That fits the sync engine without engine changes:
8
+ * store the CRDT state as a row field, have a mutation `merge` the incoming
9
+ * state into the stored one (concurrent writes combine instead of clobbering),
10
+ * and have each client merge the broadcast state into its local edits.
11
+ */
12
+ /** A counter that survives concurrent increments/decrements across replicas. */
13
+ export type CounterState = {
14
+ increments: Record<string, number>;
15
+ decrements: Record<string, number>;
16
+ };
17
+ export declare const counter: {
18
+ create: () => CounterState;
19
+ /** Current value: total increments minus total decrements. */
20
+ value: (state: CounterState) => number;
21
+ increment: (state: CounterState, replica: string, by?: number) => CounterState;
22
+ decrement: (state: CounterState, replica: string, by?: number) => CounterState;
23
+ /** Merge by taking the max count seen per replica (monotonic). */
24
+ merge: (a: CounterState, b: CounterState) => CounterState;
25
+ };
26
+ /** A single value where the latest write wins (ties broken by replica id). */
27
+ export type LwwState<T> = {
28
+ value: T;
29
+ timestamp: number;
30
+ replica: string;
31
+ };
32
+ export declare const lww: {
33
+ create: <T>(value: T, replica: string, timestamp?: number) => LwwState<T>;
34
+ set: <T>(value: T, replica: string, timestamp?: number) => LwwState<T>;
35
+ /** Keep the entry with the higher timestamp (replica id breaks ties). */
36
+ merge: <T>(a: LwwState<T>, b: LwwState<T>) => LwwState<T>;
37
+ };
38
+ /**
39
+ * The contract a collaborative-text CRDT exposes, independent of the algorithm
40
+ * behind it. Implemented first-party by the RGA below ({@link createTextCrdt})
41
+ * and by third-party backends in the `sync-adapters` repo (e.g.
42
+ * `@absolutejs/sync-yjs`). `State` is whatever that backend persists and
43
+ * broadcasts — JSON ({@link TextState}) for the RGA, a base64 update for Yjs.
44
+ */
45
+ export type CrdtText<State> = {
46
+ /** The current visible text. */
47
+ text: () => string;
48
+ /** Reconcile the local text to `next` (the backend computes the edit). */
49
+ setText: (next: string) => void;
50
+ /** Merge another replica's state in (e.g. a broadcast from the server). */
51
+ merge: (state: State) => void;
52
+ /** The serializable state to persist/broadcast. */
53
+ state: () => State;
54
+ };
55
+ /**
56
+ * A pluggable collaborative-text backend. `create` mints a live doc for a
57
+ * replica; `merge` combines two persisted states server-side (no live instance
58
+ * needed — for the merge-on-write mutation); `empty`/`textOf` are conveniences.
59
+ * Swap the first-party {@link rgaText} for an adapter to get a different engine
60
+ * (e.g. Yjs) behind the exact same call sites.
61
+ */
62
+ export type TextCrdtAdapter<State> = {
63
+ create: (replica: string, initial?: State) => CrdtText<State>;
64
+ merge: (a: State, b: State) => State;
65
+ empty: () => State;
66
+ textOf: (state: State) => string;
67
+ };
68
+ /** One inserted character in the replicated sequence (kept as a tombstone if deleted). */
69
+ export type TextElement = {
70
+ id: string;
71
+ replica: string;
72
+ clock: number;
73
+ /** Id of the element this was inserted after (`null` = start of document). */
74
+ after: string | null;
75
+ value: string;
76
+ deleted: boolean;
77
+ };
78
+ /** Serializable state of a {@link TextCrdt} — safe to store as a row field. */
79
+ export type TextState = {
80
+ elements: TextElement[];
81
+ };
82
+ /** The visible string of a text-CRDT state. Pure — use it server-side too. */
83
+ export declare const textOf: (state: TextState) => string;
84
+ /** Merge two text-CRDT states (commutative/idempotent). Pure — for server mutations. */
85
+ export declare const mergeTextState: (a: TextState, b: TextState) => TextState;
86
+ /** The RGA text CRDT — {@link CrdtText} plus direct positional edits. */
87
+ export type TextCrdt = CrdtText<TextState> & {
88
+ /** Insert `value` at visible `index`. */
89
+ insert: (index: number, value: string) => void;
90
+ /** Tombstone `count` visible characters from `index`. */
91
+ delete: (index: number, count: number) => void;
92
+ };
93
+ /**
94
+ * A collaborative text buffer backed by an RGA sequence CRDT. Concurrent inserts
95
+ * and deletes from different replicas merge without conflict and converge. Drive
96
+ * it from an input via {@link TextCrdt.setText}; persist/broadcast
97
+ * {@link TextCrdt.state}; apply remote state via {@link TextCrdt.merge}.
98
+ */
99
+ export declare const createTextCrdt: (replica: string, initial?: TextState) => TextCrdt;
100
+ /**
101
+ * The first-party collaborative-text backend (the RGA above) packaged as a
102
+ * {@link TextCrdtAdapter}. Zero dependencies. Use it directly, or swap in an
103
+ * adapter from `sync-adapters` (e.g. `@absolutejs/sync-yjs`) for the same shape.
104
+ */
105
+ export declare const rgaText: TextCrdtAdapter<TextState>;
@@ -0,0 +1,256 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __name = (target, name) => {
5
+ Object.defineProperty(target, "name", {
6
+ value: name,
7
+ enumerable: false,
8
+ configurable: true
9
+ });
10
+ return target;
11
+ };
12
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
13
+ var __typeError = (msg) => {
14
+ throw TypeError(msg);
15
+ };
16
+ var __defNormalProp = (obj, key, value) => (key in obj) ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
17
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
18
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
19
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
20
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
21
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
22
+ var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
23
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
24
+ var __expectFn = (fn) => fn !== undefined && typeof fn !== "function" ? __typeError("Function expected") : fn;
25
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({
26
+ kind: __decoratorStrings[kind],
27
+ name,
28
+ metadata,
29
+ addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null))
30
+ });
31
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
32
+ var __runInitializers = (array, flags, self, value) => {
33
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length;i < n; i++)
34
+ flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
35
+ return value;
36
+ };
37
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
38
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
39
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
40
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
41
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : {
42
+ get [name]() {
43
+ return __privateGet(this, extra);
44
+ },
45
+ set [name](x) {
46
+ __privateSet(this, extra, x);
47
+ }
48
+ }, name));
49
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
50
+ for (var i = decorators.length - 1;i >= 0; i--) {
51
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
52
+ if (k) {
53
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => (name in x) };
54
+ if (k ^ 3)
55
+ access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
56
+ if (k > 2)
57
+ access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
58
+ }
59
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? undefined : { get: desc.get, set: desc.set } : target, ctx);
60
+ done._ = 1;
61
+ if (k ^ 4 || it === undefined)
62
+ __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
63
+ else if (typeof it !== "object" || it === null)
64
+ __typeError("Object expected");
65
+ else
66
+ __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
67
+ }
68
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
69
+ };
70
+
71
+ // src/crdt/index.ts
72
+ var sumValues = (counts) => Object.values(counts).reduce((total, value) => total + value, 0);
73
+ var mergeMax = (a, b) => {
74
+ const merged = { ...a };
75
+ for (const [replica, value] of Object.entries(b)) {
76
+ merged[replica] = Math.max(merged[replica] ?? 0, value);
77
+ }
78
+ return merged;
79
+ };
80
+ var counter = {
81
+ create: () => ({ increments: {}, decrements: {} }),
82
+ value: (state) => sumValues(state.increments) - sumValues(state.decrements),
83
+ increment: (state, replica, by = 1) => ({
84
+ increments: {
85
+ ...state.increments,
86
+ [replica]: (state.increments[replica] ?? 0) + by
87
+ },
88
+ decrements: state.decrements
89
+ }),
90
+ decrement: (state, replica, by = 1) => ({
91
+ increments: state.increments,
92
+ decrements: {
93
+ ...state.decrements,
94
+ [replica]: (state.decrements[replica] ?? 0) + by
95
+ }
96
+ }),
97
+ merge: (a, b) => ({
98
+ increments: mergeMax(a.increments, b.increments),
99
+ decrements: mergeMax(a.decrements, b.decrements)
100
+ })
101
+ };
102
+ var lww = {
103
+ create: (value, replica, timestamp = Date.now()) => ({ value, timestamp, replica }),
104
+ set: (value, replica, timestamp = Date.now()) => ({
105
+ value,
106
+ timestamp,
107
+ replica
108
+ }),
109
+ merge: (a, b) => {
110
+ if (b.timestamp > a.timestamp) {
111
+ return b;
112
+ }
113
+ if (b.timestamp < a.timestamp) {
114
+ return a;
115
+ }
116
+ return b.replica > a.replica ? b : a;
117
+ }
118
+ };
119
+ var compare = (a, b) => {
120
+ if (a.clock !== b.clock) {
121
+ return b.clock - a.clock;
122
+ }
123
+ if (a.replica === b.replica) {
124
+ return 0;
125
+ }
126
+ return a.replica > b.replica ? -1 : 1;
127
+ };
128
+ var linearize = (elements) => {
129
+ const children = new Map;
130
+ for (const element of elements) {
131
+ const list = children.get(element.after);
132
+ if (list === undefined) {
133
+ children.set(element.after, [element]);
134
+ } else {
135
+ list.push(element);
136
+ }
137
+ }
138
+ for (const list of children.values()) {
139
+ list.sort(compare);
140
+ }
141
+ const ordered = [];
142
+ const stack = [...children.get(null) ?? []].reverse();
143
+ while (stack.length > 0) {
144
+ const element = stack.pop();
145
+ ordered.push(element);
146
+ const kids = children.get(element.id);
147
+ if (kids !== undefined) {
148
+ for (let index = kids.length - 1;index >= 0; index -= 1) {
149
+ stack.push(kids[index]);
150
+ }
151
+ }
152
+ }
153
+ return ordered;
154
+ };
155
+ var textOf = (state) => linearize(state.elements).filter((element) => !element.deleted).map((element) => element.value).join("");
156
+ var mergeTextState = (a, b) => {
157
+ const byId = new Map;
158
+ for (const element of [...a.elements, ...b.elements]) {
159
+ const existing = byId.get(element.id);
160
+ byId.set(element.id, existing === undefined ? element : { ...existing, deleted: existing.deleted || element.deleted });
161
+ }
162
+ return { elements: [...byId.values()] };
163
+ };
164
+ var createTextCrdt = (replica, initial) => {
165
+ const elements = new Map;
166
+ let clock = 0;
167
+ if (initial !== undefined) {
168
+ for (const element of initial.elements) {
169
+ elements.set(element.id, element);
170
+ clock = Math.max(clock, element.clock);
171
+ }
172
+ }
173
+ const visible = () => linearize([...elements.values()]).filter((element) => !element.deleted);
174
+ const insert = (index, value) => {
175
+ const seen = visible();
176
+ let after = index <= 0 ? null : seen[index - 1]?.id ?? null;
177
+ for (const char of [...value]) {
178
+ clock += 1;
179
+ const element = {
180
+ id: `${replica}:${clock}`,
181
+ replica,
182
+ clock,
183
+ after,
184
+ value: char,
185
+ deleted: false
186
+ };
187
+ elements.set(element.id, element);
188
+ after = element.id;
189
+ }
190
+ };
191
+ const remove = (index, count) => {
192
+ const seen = visible();
193
+ for (let offset = 0;offset < count; offset += 1) {
194
+ const target = seen[index + offset];
195
+ if (target !== undefined) {
196
+ elements.set(target.id, { ...target, deleted: true });
197
+ }
198
+ }
199
+ };
200
+ return {
201
+ text: () => textOf({ elements: [...elements.values()] }),
202
+ insert,
203
+ delete: remove,
204
+ merge: (state) => {
205
+ for (const element of state.elements) {
206
+ const existing = elements.get(element.id);
207
+ elements.set(element.id, existing === undefined ? element : {
208
+ ...existing,
209
+ deleted: existing.deleted || element.deleted
210
+ });
211
+ clock = Math.max(clock, element.clock);
212
+ }
213
+ },
214
+ setText: (next) => {
215
+ const current = textOf({ elements: [...elements.values()] });
216
+ if (current === next) {
217
+ return;
218
+ }
219
+ let prefix = 0;
220
+ const maxPrefix = Math.min(current.length, next.length);
221
+ while (prefix < maxPrefix && current[prefix] === next[prefix]) {
222
+ prefix += 1;
223
+ }
224
+ let suffix = 0;
225
+ while (suffix < maxPrefix - prefix && current[current.length - 1 - suffix] === next[next.length - 1 - suffix]) {
226
+ suffix += 1;
227
+ }
228
+ const removed = current.length - prefix - suffix;
229
+ if (removed > 0) {
230
+ remove(prefix, removed);
231
+ }
232
+ const inserted = next.slice(prefix, next.length - suffix);
233
+ if (inserted.length > 0) {
234
+ insert(prefix, inserted);
235
+ }
236
+ },
237
+ state: () => ({ elements: [...elements.values()] })
238
+ };
239
+ };
240
+ var rgaText = {
241
+ create: createTextCrdt,
242
+ empty: () => ({ elements: [] }),
243
+ merge: mergeTextState,
244
+ textOf
245
+ };
246
+ export {
247
+ textOf,
248
+ rgaText,
249
+ mergeTextState,
250
+ lww,
251
+ createTextCrdt,
252
+ counter
253
+ };
254
+
255
+ //# debugId=21FF50AD5A6B235464756E2164756E21
256
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/crdt/index.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Conflict-free replicated data types (CRDTs) for multiplayer/offline editing —\n * pure, dependency-free, and isomorphic (use the same code client and server).\n *\n * These are *state-based* CRDTs (CvRDTs): every `merge` is commutative,\n * associative, and idempotent, so replicas that exchange state in any order\n * converge to the same value. That fits the sync engine without engine changes:\n * store the CRDT state as a row field, have a mutation `merge` the incoming\n * state into the stored one (concurrent writes combine instead of clobbering),\n * and have each client merge the broadcast state into its local edits.\n */\n\nconst sumValues = (counts: Record<string, number>) =>\n\tObject.values(counts).reduce((total, value) => total + value, 0);\n\nconst mergeMax = (\n\ta: Record<string, number>,\n\tb: Record<string, number>\n): Record<string, number> => {\n\tconst merged: Record<string, number> = { ...a };\n\tfor (const [replica, value] of Object.entries(b)) {\n\t\tmerged[replica] = Math.max(merged[replica] ?? 0, value);\n\t}\n\treturn merged;\n};\n\n/* ─── PN-counter ─── */\n\n/** A counter that survives concurrent increments/decrements across replicas. */\nexport type CounterState = {\n\tincrements: Record<string, number>;\n\tdecrements: Record<string, number>;\n};\n\nexport const counter = {\n\tcreate: (): CounterState => ({ increments: {}, decrements: {} }),\n\t/** Current value: total increments minus total decrements. */\n\tvalue: (state: CounterState) =>\n\t\tsumValues(state.increments) - sumValues(state.decrements),\n\tincrement: (\n\t\tstate: CounterState,\n\t\treplica: string,\n\t\tby = 1\n\t): CounterState => ({\n\t\tincrements: {\n\t\t\t...state.increments,\n\t\t\t[replica]: (state.increments[replica] ?? 0) + by\n\t\t},\n\t\tdecrements: state.decrements\n\t}),\n\tdecrement: (\n\t\tstate: CounterState,\n\t\treplica: string,\n\t\tby = 1\n\t): CounterState => ({\n\t\tincrements: state.increments,\n\t\tdecrements: {\n\t\t\t...state.decrements,\n\t\t\t[replica]: (state.decrements[replica] ?? 0) + by\n\t\t}\n\t}),\n\t/** Merge by taking the max count seen per replica (monotonic). */\n\tmerge: (a: CounterState, b: CounterState): CounterState => ({\n\t\tincrements: mergeMax(a.increments, b.increments),\n\t\tdecrements: mergeMax(a.decrements, b.decrements)\n\t})\n};\n\n/* ─── LWW register ─── */\n\n/** A single value where the latest write wins (ties broken by replica id). */\nexport type LwwState<T> = { value: T; timestamp: number; replica: string };\n\nexport const lww = {\n\tcreate: <T>(\n\t\tvalue: T,\n\t\treplica: string,\n\t\ttimestamp = Date.now()\n\t): LwwState<T> => ({ value, timestamp, replica }),\n\tset: <T>(\n\t\tvalue: T,\n\t\treplica: string,\n\t\ttimestamp = Date.now()\n\t): LwwState<T> => ({\n\t\tvalue,\n\t\ttimestamp,\n\t\treplica\n\t}),\n\t/** Keep the entry with the higher timestamp (replica id breaks ties). */\n\tmerge: <T>(a: LwwState<T>, b: LwwState<T>): LwwState<T> => {\n\t\tif (b.timestamp > a.timestamp) {\n\t\t\treturn b;\n\t\t}\n\t\tif (b.timestamp < a.timestamp) {\n\t\t\treturn a;\n\t\t}\n\t\treturn b.replica > a.replica ? b : a;\n\t}\n};\n\n/* ─── Collaborative text ─── */\n\n/**\n * The contract a collaborative-text CRDT exposes, independent of the algorithm\n * behind it. Implemented first-party by the RGA below ({@link createTextCrdt})\n * and by third-party backends in the `sync-adapters` repo (e.g.\n * `@absolutejs/sync-yjs`). `State` is whatever that backend persists and\n * broadcasts — JSON ({@link TextState}) for the RGA, a base64 update for Yjs.\n */\nexport type CrdtText<State> = {\n\t/** The current visible text. */\n\ttext: () => string;\n\t/** Reconcile the local text to `next` (the backend computes the edit). */\n\tsetText: (next: string) => void;\n\t/** Merge another replica's state in (e.g. a broadcast from the server). */\n\tmerge: (state: State) => void;\n\t/** The serializable state to persist/broadcast. */\n\tstate: () => State;\n};\n\n/**\n * A pluggable collaborative-text backend. `create` mints a live doc for a\n * replica; `merge` combines two persisted states server-side (no live instance\n * needed — for the merge-on-write mutation); `empty`/`textOf` are conveniences.\n * Swap the first-party {@link rgaText} for an adapter to get a different engine\n * (e.g. Yjs) behind the exact same call sites.\n */\nexport type TextCrdtAdapter<State> = {\n\tcreate: (replica: string, initial?: State) => CrdtText<State>;\n\tmerge: (a: State, b: State) => State;\n\tempty: () => State;\n\ttextOf: (state: State) => string;\n};\n\n/* ─── Collaborative text (RGA) — the first-party backend ─── */\n\n/** One inserted character in the replicated sequence (kept as a tombstone if deleted). */\nexport type TextElement = {\n\tid: string;\n\treplica: string;\n\tclock: number;\n\t/** Id of the element this was inserted after (`null` = start of document). */\n\tafter: string | null;\n\tvalue: string;\n\tdeleted: boolean;\n};\n\n/** Serializable state of a {@link TextCrdt} — safe to store as a row field. */\nexport type TextState = { elements: TextElement[] };\n\n// Sibling order (same `after`): higher clock first, then higher replica id.\nconst compare = (a: TextElement, b: TextElement) => {\n\tif (a.clock !== b.clock) {\n\t\treturn b.clock - a.clock;\n\t}\n\tif (a.replica === b.replica) {\n\t\treturn 0;\n\t}\n\treturn a.replica > b.replica ? -1 : 1;\n};\n\n/** Flatten the sequence into document order (an iterative RGA pre-order walk). */\nconst linearize = (elements: TextElement[]): TextElement[] => {\n\tconst children = new Map<string | null, TextElement[]>();\n\tfor (const element of elements) {\n\t\tconst list = children.get(element.after);\n\t\tif (list === undefined) {\n\t\t\tchildren.set(element.after, [element]);\n\t\t} else {\n\t\t\tlist.push(element);\n\t\t}\n\t}\n\tfor (const list of children.values()) {\n\t\tlist.sort(compare);\n\t}\n\tconst ordered: TextElement[] = [];\n\tconst stack = [...(children.get(null) ?? [])].reverse();\n\twhile (stack.length > 0) {\n\t\tconst element = stack.pop()!;\n\t\tordered.push(element);\n\t\tconst kids = children.get(element.id);\n\t\tif (kids !== undefined) {\n\t\t\tfor (let index = kids.length - 1; index >= 0; index -= 1) {\n\t\t\t\tstack.push(kids[index]!);\n\t\t\t}\n\t\t}\n\t}\n\treturn ordered;\n};\n\n/** The visible string of a text-CRDT state. Pure — use it server-side too. */\nexport const textOf = (state: TextState): string =>\n\tlinearize(state.elements)\n\t\t.filter((element) => !element.deleted)\n\t\t.map((element) => element.value)\n\t\t.join('');\n\n/** Merge two text-CRDT states (commutative/idempotent). Pure — for server mutations. */\nexport const mergeTextState = (a: TextState, b: TextState): TextState => {\n\tconst byId = new Map<string, TextElement>();\n\tfor (const element of [...a.elements, ...b.elements]) {\n\t\tconst existing = byId.get(element.id);\n\t\tbyId.set(\n\t\t\telement.id,\n\t\t\texisting === undefined\n\t\t\t\t? element\n\t\t\t\t: { ...existing, deleted: existing.deleted || element.deleted }\n\t\t);\n\t}\n\treturn { elements: [...byId.values()] };\n};\n\n/** The RGA text CRDT — {@link CrdtText} plus direct positional edits. */\nexport type TextCrdt = CrdtText<TextState> & {\n\t/** Insert `value` at visible `index`. */\n\tinsert: (index: number, value: string) => void;\n\t/** Tombstone `count` visible characters from `index`. */\n\tdelete: (index: number, count: number) => void;\n};\n\n/**\n * A collaborative text buffer backed by an RGA sequence CRDT. Concurrent inserts\n * and deletes from different replicas merge without conflict and converge. Drive\n * it from an input via {@link TextCrdt.setText}; persist/broadcast\n * {@link TextCrdt.state}; apply remote state via {@link TextCrdt.merge}.\n */\nexport const createTextCrdt = (\n\treplica: string,\n\tinitial?: TextState\n): TextCrdt => {\n\tconst elements = new Map<string, TextElement>();\n\tlet clock = 0;\n\tif (initial !== undefined) {\n\t\tfor (const element of initial.elements) {\n\t\t\telements.set(element.id, element);\n\t\t\tclock = Math.max(clock, element.clock);\n\t\t}\n\t}\n\n\tconst visible = () =>\n\t\tlinearize([...elements.values()]).filter((element) => !element.deleted);\n\n\tconst insert = (index: number, value: string) => {\n\t\tconst seen = visible();\n\t\tlet after = index <= 0 ? null : (seen[index - 1]?.id ?? null);\n\t\tfor (const char of [...value]) {\n\t\t\tclock += 1;\n\t\t\tconst element: TextElement = {\n\t\t\t\tid: `${replica}:${clock}`,\n\t\t\t\treplica,\n\t\t\t\tclock,\n\t\t\t\tafter,\n\t\t\t\tvalue: char,\n\t\t\t\tdeleted: false\n\t\t\t};\n\t\t\telements.set(element.id, element);\n\t\t\tafter = element.id;\n\t\t}\n\t};\n\n\tconst remove = (index: number, count: number) => {\n\t\tconst seen = visible();\n\t\tfor (let offset = 0; offset < count; offset += 1) {\n\t\t\tconst target = seen[index + offset];\n\t\t\tif (target !== undefined) {\n\t\t\t\telements.set(target.id, { ...target, deleted: true });\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\ttext: () => textOf({ elements: [...elements.values()] }),\n\t\tinsert,\n\t\tdelete: remove,\n\t\tmerge: (state) => {\n\t\t\tfor (const element of state.elements) {\n\t\t\t\tconst existing = elements.get(element.id);\n\t\t\t\telements.set(\n\t\t\t\t\telement.id,\n\t\t\t\t\texisting === undefined\n\t\t\t\t\t\t? element\n\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t...existing,\n\t\t\t\t\t\t\t\tdeleted: existing.deleted || element.deleted\n\t\t\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\tclock = Math.max(clock, element.clock);\n\t\t\t}\n\t\t},\n\t\t// Reconcile to `next` by editing only the changed middle: keep the common\n\t\t// prefix/suffix, delete the old middle, insert the new — so two clients\n\t\t// typing in different places merge instead of overwriting.\n\t\tsetText: (next) => {\n\t\t\tconst current = textOf({ elements: [...elements.values()] });\n\t\t\tif (current === next) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet prefix = 0;\n\t\t\tconst maxPrefix = Math.min(current.length, next.length);\n\t\t\twhile (prefix < maxPrefix && current[prefix] === next[prefix]) {\n\t\t\t\tprefix += 1;\n\t\t\t}\n\t\t\tlet suffix = 0;\n\t\t\twhile (\n\t\t\t\tsuffix < maxPrefix - prefix &&\n\t\t\t\tcurrent[current.length - 1 - suffix] ===\n\t\t\t\t\tnext[next.length - 1 - suffix]\n\t\t\t) {\n\t\t\t\tsuffix += 1;\n\t\t\t}\n\t\t\tconst removed = current.length - prefix - suffix;\n\t\t\tif (removed > 0) {\n\t\t\t\tremove(prefix, removed);\n\t\t\t}\n\t\t\tconst inserted = next.slice(prefix, next.length - suffix);\n\t\t\tif (inserted.length > 0) {\n\t\t\t\tinsert(prefix, inserted);\n\t\t\t}\n\t\t},\n\t\tstate: () => ({ elements: [...elements.values()] })\n\t};\n};\n\n/**\n * The first-party collaborative-text backend (the RGA above) packaged as a\n * {@link TextCrdtAdapter}. Zero dependencies. Use it directly, or swap in an\n * adapter from `sync-adapters` (e.g. `@absolutejs/sync-yjs`) for the same shape.\n */\nexport const rgaText: TextCrdtAdapter<TextState> = {\n\tcreate: createTextCrdt,\n\tempty: () => ({ elements: [] }),\n\tmerge: mergeTextState,\n\ttextOf\n};\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "21FF50AD5A6B235464756E2164756E21",
9
+ "names": []
10
+ }
@@ -42,8 +42,10 @@ export { defineSchedule } from './schedule';
42
42
  export type { ScheduleContext, ScheduleDefinition } from './schedule';
43
43
  export { defineMutation } from './mutation';
44
44
  export type { MutationActions, MutationDefinition, MutationHandler, TableWriter, TransactionRunner } from './mutation';
45
- export { createSyncEngine, UnauthorizedError } from './syncEngine';
45
+ export { createSyncEngine, SchemaError, UnauthorizedError } from './syncEngine';
46
46
  export type { SubscribeArgs, Subscription, SyncEngine } from './syncEngine';
47
+ export { defineSchema, field } from './schema';
48
+ export type { FieldValidator, SchemaDefinition, TableSchema } from './schema';
47
49
  export type { CollectionInspection, CollectionKind, EngineActivity, EngineInspection } from './devtools';
48
50
  export { hydrateRoute, mutateRoute } from './routes';
49
51
  export type { SyncRouteContext } from './routes';
@@ -1046,6 +1046,13 @@ class UnauthorizedError extends Error {
1046
1046
  this.name = "UnauthorizedError";
1047
1047
  }
1048
1048
  }
1049
+
1050
+ class SchemaError extends Error {
1051
+ constructor(table, fieldName) {
1052
+ super(`Schema violation on "${table}": invalid field "${fieldName}"`);
1053
+ this.name = "SchemaError";
1054
+ }
1055
+ }
1049
1056
  var defaultKey = (row) => row.id;
1050
1057
  var shallowEqual4 = (a, b) => {
1051
1058
  if (a === b) {
@@ -1082,6 +1089,30 @@ var createSyncEngine = (options = {}) => {
1082
1089
  const rules = permissions.get(table);
1083
1090
  return rules?.[op] ?? rules?.write;
1084
1091
  };
1092
+ const schemas = new Map;
1093
+ for (const [table, schema] of Object.entries(options.schemas ?? {})) {
1094
+ schemas.set(table, schema);
1095
+ }
1096
+ const validateWrite = (table, op, row) => {
1097
+ const schema = schemas.get(table);
1098
+ if (schema === undefined || typeof row !== "object" || row === null) {
1099
+ return;
1100
+ }
1101
+ const record = row;
1102
+ for (const [fieldName, validate] of Object.entries(schema.fields)) {
1103
+ const present = fieldName in record;
1104
+ if (op === "update" && !present) {
1105
+ continue;
1106
+ }
1107
+ if (!validate(record[fieldName])) {
1108
+ throw new SchemaError(table, fieldName);
1109
+ }
1110
+ }
1111
+ };
1112
+ const migrateRow = (table, row) => {
1113
+ const migrate = schemas.get(table)?.migrate;
1114
+ return migrate ? migrate(row) : row;
1115
+ };
1085
1116
  const reactiveSubs = new Set;
1086
1117
  const searchSubs = new Set;
1087
1118
  const searchIndexes = new Map;
@@ -1222,7 +1253,7 @@ var createSyncEngine = (options = {}) => {
1222
1253
  return {
1223
1254
  all: async (table) => {
1224
1255
  readTables.add(table);
1225
- const rows = [...await readerFor(table).all(ctx)];
1256
+ const rows = [...await readerFor(table).all(ctx)].map((row) => migrateRow(table, row));
1226
1257
  const rule = ruleFor(table);
1227
1258
  return rule ? rows.filter((row) => rule(ctx, row)) : rows;
1228
1259
  },
@@ -1236,7 +1267,8 @@ var createSyncEngine = (options = {}) => {
1236
1267
  } else {
1237
1268
  readTables.add(table);
1238
1269
  }
1239
- const row = await reader.get(key, ctx);
1270
+ const raw = await reader.get(key, ctx);
1271
+ const row = raw === undefined ? undefined : migrateRow(table, raw);
1240
1272
  const rule = ruleFor(table);
1241
1273
  return rule && row !== undefined && !rule(ctx, row) ? undefined : row;
1242
1274
  },
@@ -1244,7 +1276,7 @@ var createSyncEngine = (options = {}) => {
1244
1276
  const reader = readerFor(table);
1245
1277
  const rule = ruleFor(table);
1246
1278
  const effective = rule ? (row) => predicate(row) && rule(ctx, row) : predicate;
1247
- const matched = [...await reader.all(ctx)].filter(effective);
1279
+ const matched = [...await reader.all(ctx)].map((row) => migrateRow(table, row)).filter(effective);
1248
1280
  if (reader.key !== undefined) {
1249
1281
  const key = reader.key;
1250
1282
  rangeDeps.push({
@@ -1299,6 +1331,7 @@ var createSyncEngine = (options = {}) => {
1299
1331
  return Promise.resolve();
1300
1332
  },
1301
1333
  insert: async (table, data) => {
1334
+ validateWrite(table, "insert", data);
1302
1335
  if (enforce) {
1303
1336
  await authorizeWrite(table, "insert", data, ctx);
1304
1337
  }
@@ -1307,6 +1340,7 @@ var createSyncEngine = (options = {}) => {
1307
1340
  return row;
1308
1341
  },
1309
1342
  update: async (table, data) => {
1343
+ validateWrite(table, "update", data);
1310
1344
  if (enforce) {
1311
1345
  await authorizeWrite(table, "update", data, ctx);
1312
1346
  }
@@ -1725,8 +1759,13 @@ var createSyncEngine = (options = {}) => {
1725
1759
  const key = definition.key ?? defaultKey;
1726
1760
  const match = definition.match;
1727
1761
  const tables = definition.tables ?? [collection];
1728
- const readRule = tables.length === 1 ? readRuleFor(tables[0]) : undefined;
1729
- const rehydrate = readRule ? async () => [...await definition.hydrate(params, ctx)].filter((row) => readRule(ctx, row)) : async () => definition.hydrate(params, ctx);
1762
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1763
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1764
+ const rehydrate = async () => {
1765
+ const raw = [...await definition.hydrate(params, ctx)];
1766
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1767
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1768
+ };
1730
1769
  const incremental = match !== undefined && tables.length === 1;
1731
1770
  const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
1732
1771
  const view = createMaterializedView({
@@ -1774,9 +1813,11 @@ var createSyncEngine = (options = {}) => {
1774
1813
  throw new UnauthorizedError(`hydrate collection "${collection}"`);
1775
1814
  }
1776
1815
  }
1777
- const rows = [...await definition.hydrate(params, ctx)];
1816
+ const raw = [...await definition.hydrate(params, ctx)];
1778
1817
  const tables = definition.tables ?? [collection];
1779
- const readRule = tables.length === 1 ? readRuleFor(tables[0]) : undefined;
1818
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1819
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1820
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1780
1821
  return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1781
1822
  },
1782
1823
  applyChange: (table, change) => applyChange(table, change),
@@ -1824,6 +1865,10 @@ var createSyncEngine = (options = {}) => {
1824
1865
  registerPermissions: (table, rules) => {
1825
1866
  permissions.set(table, rules);
1826
1867
  },
1868
+ registerSchema: (table, schema) => {
1869
+ schemas.set(table, schema);
1870
+ },
1871
+ migrate: (table, row) => migrateRow(table, row),
1827
1872
  runMutation: async (name, args, ctx) => {
1828
1873
  const mutation = mutations.get(name);
1829
1874
  if (mutation === undefined) {
@@ -1927,6 +1972,18 @@ var createSyncEngine = (options = {}) => {
1927
1972
  }
1928
1973
  };
1929
1974
  };
1975
+ // src/engine/schema.ts
1976
+ var defineSchema = (schemas) => schemas;
1977
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
1978
+ var field = {
1979
+ string: (value) => typeof value === "string",
1980
+ number: isFiniteNumber,
1981
+ boolean: (value) => typeof value === "boolean",
1982
+ any: () => true,
1983
+ optional: (inner) => (value) => value === undefined || inner(value),
1984
+ array: (inner) => (value) => Array.isArray(value) && value.every(inner),
1985
+ enum: (...values) => (value) => values.includes(value)
1986
+ };
1930
1987
  // src/engine/routes.ts
1931
1988
  var emptyContext = () => ({});
1932
1989
  var hydrateRoute = (engine, collection, resolveContext = emptyContext) => {
@@ -2174,7 +2231,9 @@ export {
2174
2231
  hydrateRoute,
2175
2232
  fromRowChange,
2176
2233
  filterOp,
2234
+ field,
2177
2235
  defineSearchCollection,
2236
+ defineSchema,
2178
2237
  defineSchedule,
2179
2238
  defineReactiveQuery,
2180
2239
  definePermissions,
@@ -2195,8 +2254,9 @@ export {
2195
2254
  chain,
2196
2255
  aggregateOp,
2197
2256
  UnauthorizedError,
2257
+ SchemaError,
2198
2258
  SEARCH_SCORE_FIELD
2199
2259
  };
2200
2260
 
2201
- //# debugId=B0B398375CBCC30B64756E2164756E21
2261
+ //# debugId=B040890280D3C67864756E2164756E21
2202
2262
  //# sourceMappingURL=index.js.map