@absolutejs/sync 0.9.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 +16 -3
- package/dist/crdt/index.d.ts +105 -0
- package/dist/crdt/index.js +256 -0
- package/dist/crdt/index.js.map +10 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -34,9 +34,10 @@ top-N ordering are maintained incrementally through a composable operator graph
|
|
|
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
36
|
> permissions, schema validation + lazy migrations, live full-text + vector
|
|
37
|
-
> search, scheduled functions, a live devtools dashboard,
|
|
38
|
-
> Postgres/MySQL/SQLite, incremental
|
|
39
|
-
> operator graph) are in place.
|
|
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
|
|
|
@@ -409,6 +410,18 @@ mutate({
|
|
|
409
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. |
|
|
410
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). |
|
|
411
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
|
+
|
|
412
425
|
### `@absolutejs/sync/postgres`
|
|
413
426
|
|
|
414
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Lightweight reactive-push and write-behind-cache primitives for Elysia and the AbsoluteJS ecosystem — kill polling and keep a remote store off your hot path, without adopting a whole sync-engine backend.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"import": "./dist/client/index.js",
|
|
29
29
|
"default": "./dist/client/index.js"
|
|
30
30
|
},
|
|
31
|
+
"./crdt": {
|
|
32
|
+
"types": "./dist/crdt/index.d.ts",
|
|
33
|
+
"import": "./dist/crdt/index.js",
|
|
34
|
+
"default": "./dist/crdt/index.js"
|
|
35
|
+
},
|
|
31
36
|
"./drizzle": {
|
|
32
37
|
"types": "./dist/adapters/drizzle/index.d.ts",
|
|
33
38
|
"import": "./dist/adapters/drizzle/index.js",
|