@absolutejs/sync 0.9.0 → 0.11.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.
@@ -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 * The minimal server-side surface the engine needs to auto-merge a CRDT field on\n * write (see `engine.registerCrdt`): combine two states and produce an empty one.\n * Both the first-party {@link rgaText} and `@absolutejs/sync-yjs`'s `yjsText`\n * satisfy it, as does any {@link TextCrdtAdapter}.\n */\nexport type CrdtMergeable<State> = {\n\tempty: () => State;\n\tmerge: (a: State, b: State) => 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> = CrdtMergeable<State> & {\n\tcreate: (replica: string, initial?: State) => CrdtText<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
+ }
@@ -43,7 +43,8 @@ export type { ScheduleContext, ScheduleDefinition } from './schedule';
43
43
  export { defineMutation } from './mutation';
44
44
  export type { MutationActions, MutationDefinition, MutationHandler, TableWriter, TransactionRunner } from './mutation';
45
45
  export { createSyncEngine, SchemaError, UnauthorizedError } from './syncEngine';
46
- export type { SubscribeArgs, Subscription, SyncEngine } from './syncEngine';
46
+ export type { CrdtFields, SubscribeArgs, Subscription, SyncEngine } from './syncEngine';
47
+ export type { CrdtMergeable } from '../crdt';
47
48
  export { defineSchema, field } from './schema';
48
49
  export type { FieldValidator, SchemaDefinition, TableSchema } from './schema';
49
50
  export type { CollectionInspection, CollectionKind, EngineActivity, EngineInspection } from './devtools';
@@ -1093,6 +1093,7 @@ var createSyncEngine = (options = {}) => {
1093
1093
  for (const [table, schema] of Object.entries(options.schemas ?? {})) {
1094
1094
  schemas.set(table, schema);
1095
1095
  }
1096
+ const crdtFields = new Map;
1096
1097
  const validateWrite = (table, op, row) => {
1097
1098
  const schema = schemas.get(table);
1098
1099
  if (schema === undefined || typeof row !== "object" || row === null) {
@@ -1298,6 +1299,14 @@ var createSyncEngine = (options = {}) => {
1298
1299
  }
1299
1300
  return writer;
1300
1301
  };
1302
+ const readExisting = async (table, value, ctx) => {
1303
+ const reader = readers.get(table);
1304
+ if (reader?.get === undefined) {
1305
+ return;
1306
+ }
1307
+ const id = reader.key ? reader.key(value) : value.id;
1308
+ return id === undefined ? undefined : reader.get(id, ctx);
1309
+ };
1301
1310
  const authorizeWrite = async (table, op, value, ctx) => {
1302
1311
  const rule = writeRuleFor(table, op);
1303
1312
  if (rule === undefined) {
@@ -1305,21 +1314,32 @@ var createSyncEngine = (options = {}) => {
1305
1314
  }
1306
1315
  let subject = value;
1307
1316
  if (op !== "insert") {
1308
- const reader = readers.get(table);
1309
- if (reader?.get !== undefined) {
1310
- const id = reader.key ? reader.key(value) : value.id;
1311
- if (id !== undefined) {
1312
- const existing = await reader.get(id, ctx);
1313
- if (existing !== undefined) {
1314
- subject = existing;
1315
- }
1316
- }
1317
+ const existing = await readExisting(table, value, ctx);
1318
+ if (existing !== undefined) {
1319
+ subject = existing;
1317
1320
  }
1318
1321
  }
1319
1322
  if (!rule(ctx, subject)) {
1320
1323
  throw new UnauthorizedError(`${op} on table "${table}"`);
1321
1324
  }
1322
1325
  };
1326
+ const mergeCrdtFields = async (table, op, data, ctx) => {
1327
+ const fields = crdtFields.get(table);
1328
+ if (fields === undefined || data === null || typeof data !== "object") {
1329
+ return data;
1330
+ }
1331
+ const incoming = data;
1332
+ const existing = op === "update" ? await readExisting(table, data, ctx) : undefined;
1333
+ const base = existing !== null && typeof existing === "object" ? existing : undefined;
1334
+ const merged = { ...incoming };
1335
+ for (const [field, adapter] of Object.entries(fields)) {
1336
+ if (incoming[field] === undefined) {
1337
+ continue;
1338
+ }
1339
+ merged[field] = adapter.merge(base?.[field] ?? adapter.empty(), incoming[field]);
1340
+ }
1341
+ return merged;
1342
+ };
1323
1343
  const makeActions = (tx, ctx, enforce) => {
1324
1344
  const buffered = [];
1325
1345
  const actions = {
@@ -1335,7 +1355,8 @@ var createSyncEngine = (options = {}) => {
1335
1355
  if (enforce) {
1336
1356
  await authorizeWrite(table, "insert", data, ctx);
1337
1357
  }
1338
- const row = await writerFor(table).insert(data, ctx, tx);
1358
+ const merged = await mergeCrdtFields(table, "insert", data, ctx);
1359
+ const row = await writerFor(table).insert(merged, ctx, tx);
1339
1360
  buffered.push({ table, change: { op: "insert", row } });
1340
1361
  return row;
1341
1362
  },
@@ -1344,7 +1365,8 @@ var createSyncEngine = (options = {}) => {
1344
1365
  if (enforce) {
1345
1366
  await authorizeWrite(table, "update", data, ctx);
1346
1367
  }
1347
- const row = await writerFor(table).update(data, ctx, tx);
1368
+ const merged = await mergeCrdtFields(table, "update", data, ctx);
1369
+ const row = await writerFor(table).update(merged, ctx, tx);
1348
1370
  buffered.push({ table, change: { op: "update", row } });
1349
1371
  return row;
1350
1372
  },
@@ -1868,6 +1890,17 @@ var createSyncEngine = (options = {}) => {
1868
1890
  registerSchema: (table, schema) => {
1869
1891
  schemas.set(table, schema);
1870
1892
  },
1893
+ registerCrdt: (table, fields) => {
1894
+ crdtFields.set(table, fields);
1895
+ const name = `${table}:merge`;
1896
+ mutations.set(name, {
1897
+ handler: async (args, ctx, actions) => {
1898
+ const existing = await readExisting(table, args, ctx);
1899
+ return existing === undefined ? actions.insert(table, args) : actions.update(table, args);
1900
+ },
1901
+ name
1902
+ });
1903
+ },
1871
1904
  migrate: (table, row) => migrateRow(table, row),
1872
1905
  runMutation: async (name, args, ctx) => {
1873
1906
  const mutation = mutations.get(name);
@@ -2258,5 +2291,5 @@ export {
2258
2291
  SEARCH_SCORE_FIELD
2259
2292
  };
2260
2293
 
2261
- //# debugId=B040890280D3C67864756E2164756E21
2294
+ //# debugId=50FA0D17837EB63364756E2164756E21
2262
2295
  //# sourceMappingURL=index.js.map