@absolutejs/sync 0.13.0 → 0.15.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 +11 -8
- package/dist/angular/index.js +229 -4
- package/dist/angular/index.js.map +9 -6
- package/dist/angular/sync-collection.service.d.ts +2 -0
- package/dist/client/collaborativeText.d.ts +5 -0
- package/dist/client/index.js +225 -4
- package/dist/client/index.js.map +8 -5
- package/dist/crdt/index.d.ts +18 -0
- package/dist/crdt/index.js +228 -4
- package/dist/crdt/index.js.map +7 -4
- package/dist/crdt/list.d.ts +37 -0
- package/dist/crdt/lwwMap.d.ts +24 -0
- package/dist/crdt/orSet.d.ts +26 -0
- package/dist/react/index.js +234 -5
- package/dist/react/index.js.map +9 -6
- package/dist/react/useCollaborativeText.d.ts +2 -0
- package/dist/svelte/createCollaborativeTextStore.d.ts +2 -0
- package/dist/svelte/index.js +227 -4
- package/dist/svelte/index.js.map +9 -6
- package/dist/vue/index.js +228 -5
- package/dist/vue/index.js.map +9 -6
- package/dist/vue/useCollaborativeText.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -418,14 +418,17 @@ mutate({
|
|
|
418
418
|
|
|
419
419
|
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. The declarative path is one line each end: server `engine.registerCrdt(table, { field: rgaText })` (auto-merges that field on write), client `useCollaborativeText({ collection, id, field, url })`. The primitives below are also usable directly.
|
|
420
420
|
|
|
421
|
-
| Export | What it is
|
|
422
|
-
| -------------------------------------------- |
|
|
423
|
-
| `counter` | PN-counter: `create/value/increment/decrement/merge`. Concurrent increments and decrements across replicas all survive.
|
|
424
|
-
| `lww` | Last-write-wins register: `create/set/merge`. The latest timestamp wins (replica id breaks ties) — for "just take the newest value" fields.
|
|
425
|
-
| `
|
|
426
|
-
| `
|
|
427
|
-
| `
|
|
428
|
-
| `
|
|
421
|
+
| Export | What it is |
|
|
422
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
423
|
+
| `counter` | PN-counter: `create/value/increment/decrement/merge`. Concurrent increments and decrements across replicas all survive. |
|
|
424
|
+
| `lww` | Last-write-wins register: `create/set/merge`. The latest timestamp wins (replica id breaks ties) — for "just take the newest value" fields. |
|
|
425
|
+
| `orSet` | Observed-remove set: `create/add/remove/has/values/merge`. Concurrent add/remove resolves **add-wins** (each add gets a unique tag; remove retracts only observed tags) — for collaborative tags/labels/memberships. |
|
|
426
|
+
| `lwwMap` | Last-write-wins map: `create/set/get/delete/has/keys/entries/merge`. Each key is an independent LWW register; `delete` is a tombstone that can lose to a later concurrent `set` — for collaborative key→value records. |
|
|
427
|
+
| `createList(replica, initial?)` | Ordered list CRDT (the RGA over arbitrary items): `list/insert/delete/merge/state/takeDelta` (+ `listOf`/`mergeListState`). Concurrent inserts/deletes at any position merge and converge — for collaborative reorderable lists. |
|
|
428
|
+
| `createTextCrdt(replica, initial?)` | Collaborative text (an RGA sequence CRDT): `text/insert/delete/setText/merge/state` + `takeDelta` + `anchorAt`/`indexOfAnchor`. `takeDelta()` returns just this client's new ops (delta-state) so uploads are O(edit), not O(doc); `anchorAt`/`indexOfAnchor` give a caret a stable element-id anchor for collaborative cursors that survive concurrent edits. Concurrent edits merge and converge. |
|
|
429
|
+
| `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. |
|
|
430
|
+
| `compact(state)` / `tombstoneCount(state)` | Bound state growth: `compact` drops tombstones no live text anchors to (visible text unchanged); `tombstoneCount` is the metric to decide when. Run server-side on the stored state past a threshold; clients adopt the compacted state on the next broadcast. |
|
|
431
|
+
| `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. |
|
|
429
432
|
|
|
430
433
|
### `@absolutejs/sync/postgres`
|
|
431
434
|
|
package/dist/angular/index.js
CHANGED
|
@@ -68,6 +68,203 @@ var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
|
68
68
|
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
// src/crdt/orSet.ts
|
|
72
|
+
var newTag = () => globalThis.crypto.randomUUID();
|
|
73
|
+
var defaultEquals = (a, b) => Object.is(a, b);
|
|
74
|
+
var orSet = {
|
|
75
|
+
create: () => ({ adds: [], removed: [] }),
|
|
76
|
+
add: (state, value, tag = newTag()) => ({
|
|
77
|
+
adds: [...state.adds, { value, tag }],
|
|
78
|
+
removed: state.removed
|
|
79
|
+
}),
|
|
80
|
+
remove: (state, value, equals = defaultEquals) => {
|
|
81
|
+
const tags = state.adds.filter((entry) => equals(entry.value, value)).map((entry) => entry.tag);
|
|
82
|
+
return {
|
|
83
|
+
adds: state.adds,
|
|
84
|
+
removed: [...new Set([...state.removed, ...tags])]
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
has: (state, value, equals = defaultEquals) => {
|
|
88
|
+
const removed = new Set(state.removed);
|
|
89
|
+
return state.adds.some((entry) => equals(entry.value, value) && !removed.has(entry.tag));
|
|
90
|
+
},
|
|
91
|
+
values: (state, equals = defaultEquals) => {
|
|
92
|
+
const removed = new Set(state.removed);
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const entry of state.adds) {
|
|
95
|
+
if (!removed.has(entry.tag) && !out.some((value) => equals(value, entry.value))) {
|
|
96
|
+
out.push(entry.value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
},
|
|
101
|
+
merge: (a, b) => {
|
|
102
|
+
const byTag = new Map;
|
|
103
|
+
for (const entry of [...a.adds, ...b.adds]) {
|
|
104
|
+
byTag.set(entry.tag, entry);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
adds: [...byTag.values()],
|
|
108
|
+
removed: [...new Set([...a.removed, ...b.removed])]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
// src/crdt/lwwMap.ts
|
|
113
|
+
var pick = (a, b) => {
|
|
114
|
+
if (b.timestamp > a.timestamp) {
|
|
115
|
+
return b;
|
|
116
|
+
}
|
|
117
|
+
if (b.timestamp < a.timestamp) {
|
|
118
|
+
return a;
|
|
119
|
+
}
|
|
120
|
+
return b.replica > a.replica ? b : a;
|
|
121
|
+
};
|
|
122
|
+
var lwwMap = {
|
|
123
|
+
create: () => ({}),
|
|
124
|
+
set: (state, key, value, replica, timestamp = Date.now()) => ({
|
|
125
|
+
...state,
|
|
126
|
+
[key]: { value, deleted: false, timestamp, replica }
|
|
127
|
+
}),
|
|
128
|
+
delete: (state, key, replica, timestamp = Date.now()) => ({
|
|
129
|
+
...state,
|
|
130
|
+
[key]: { value: state[key]?.value, deleted: true, timestamp, replica }
|
|
131
|
+
}),
|
|
132
|
+
get: (state, key) => {
|
|
133
|
+
const entry = state[key];
|
|
134
|
+
return entry !== undefined && !entry.deleted ? entry.value : undefined;
|
|
135
|
+
},
|
|
136
|
+
has: (state, key) => {
|
|
137
|
+
const entry = state[key];
|
|
138
|
+
return entry !== undefined && !entry.deleted;
|
|
139
|
+
},
|
|
140
|
+
keys: (state) => Object.keys(state).filter((key) => !state[key]?.deleted),
|
|
141
|
+
entries: (state) => {
|
|
142
|
+
const out = [];
|
|
143
|
+
for (const [key, entry] of Object.entries(state)) {
|
|
144
|
+
if (!entry.deleted && entry.value !== undefined) {
|
|
145
|
+
out.push([key, entry.value]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
},
|
|
150
|
+
merge: (a, b) => {
|
|
151
|
+
const out = { ...a };
|
|
152
|
+
for (const [key, entry] of Object.entries(b)) {
|
|
153
|
+
const existing = out[key];
|
|
154
|
+
out[key] = existing === undefined ? entry : pick(existing, entry);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
// src/crdt/list.ts
|
|
160
|
+
var order = (a, b) => {
|
|
161
|
+
if (a.clock !== b.clock) {
|
|
162
|
+
return b.clock - a.clock;
|
|
163
|
+
}
|
|
164
|
+
if (a.replica === b.replica) {
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
return a.replica > b.replica ? -1 : 1;
|
|
168
|
+
};
|
|
169
|
+
var linearize = (elements) => {
|
|
170
|
+
const present = new Set(elements.map((element) => element.id));
|
|
171
|
+
const children = new Map;
|
|
172
|
+
for (const element of elements) {
|
|
173
|
+
const anchor = element.after !== null && !present.has(element.after) ? null : element.after;
|
|
174
|
+
const list = children.get(anchor);
|
|
175
|
+
if (list === undefined) {
|
|
176
|
+
children.set(anchor, [element]);
|
|
177
|
+
} else {
|
|
178
|
+
list.push(element);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const list of children.values()) {
|
|
182
|
+
list.sort(order);
|
|
183
|
+
}
|
|
184
|
+
const ordered = [];
|
|
185
|
+
const stack = [...children.get(null) ?? []].reverse();
|
|
186
|
+
while (stack.length > 0) {
|
|
187
|
+
const element = stack.pop();
|
|
188
|
+
ordered.push(element);
|
|
189
|
+
const kids = children.get(element.id);
|
|
190
|
+
if (kids !== undefined) {
|
|
191
|
+
for (let index = kids.length - 1;index >= 0; index -= 1) {
|
|
192
|
+
stack.push(kids[index]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return ordered;
|
|
197
|
+
};
|
|
198
|
+
var listOf = (state) => linearize(state.elements).filter((element) => !element.deleted).map((element) => element.value);
|
|
199
|
+
var mergeListState = (a, b) => {
|
|
200
|
+
const byId = new Map;
|
|
201
|
+
for (const element of [...a.elements, ...b.elements]) {
|
|
202
|
+
const existing = byId.get(element.id);
|
|
203
|
+
byId.set(element.id, existing === undefined ? element : { ...existing, deleted: existing.deleted || element.deleted });
|
|
204
|
+
}
|
|
205
|
+
return { elements: [...byId.values()] };
|
|
206
|
+
};
|
|
207
|
+
var createList = (replica, initial) => {
|
|
208
|
+
const elements = new Map;
|
|
209
|
+
const pending = new Map;
|
|
210
|
+
let clock = 0;
|
|
211
|
+
if (initial !== undefined) {
|
|
212
|
+
for (const element of initial.elements) {
|
|
213
|
+
elements.set(element.id, element);
|
|
214
|
+
clock = Math.max(clock, element.clock);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const visible = () => linearize([...elements.values()]).filter((element) => !element.deleted);
|
|
218
|
+
return {
|
|
219
|
+
list: () => listOf({ elements: [...elements.values()] }),
|
|
220
|
+
insert: (index, items) => {
|
|
221
|
+
const seen = visible();
|
|
222
|
+
let after = index <= 0 ? null : seen[index - 1]?.id ?? null;
|
|
223
|
+
for (const value of items) {
|
|
224
|
+
clock += 1;
|
|
225
|
+
const element = {
|
|
226
|
+
id: `${replica}:${clock}`,
|
|
227
|
+
replica,
|
|
228
|
+
clock,
|
|
229
|
+
after,
|
|
230
|
+
value,
|
|
231
|
+
deleted: false
|
|
232
|
+
};
|
|
233
|
+
elements.set(element.id, element);
|
|
234
|
+
pending.set(element.id, element);
|
|
235
|
+
after = element.id;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
delete: (index, count) => {
|
|
239
|
+
const seen = visible();
|
|
240
|
+
for (let offset = 0;offset < count; offset += 1) {
|
|
241
|
+
const target = seen[index + offset];
|
|
242
|
+
if (target !== undefined) {
|
|
243
|
+
const tombstoned = { ...target, deleted: true };
|
|
244
|
+
elements.set(target.id, tombstoned);
|
|
245
|
+
pending.set(target.id, tombstoned);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
merge: (state) => {
|
|
250
|
+
for (const element of state.elements) {
|
|
251
|
+
const existing = elements.get(element.id);
|
|
252
|
+
elements.set(element.id, existing === undefined ? element : {
|
|
253
|
+
...existing,
|
|
254
|
+
deleted: existing.deleted || element.deleted
|
|
255
|
+
});
|
|
256
|
+
clock = Math.max(clock, element.clock);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
state: () => ({ elements: [...elements.values()] }),
|
|
260
|
+
takeDelta: () => {
|
|
261
|
+
const delta = { elements: [...pending.values()] };
|
|
262
|
+
pending.clear();
|
|
263
|
+
return delta;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
71
268
|
// src/crdt/index.ts
|
|
72
269
|
var sumValues = (counts) => Object.values(counts).reduce((total, value) => total + value, 0);
|
|
73
270
|
var mergeMax = (a, b) => {
|
|
@@ -125,7 +322,7 @@ var compare = (a, b) => {
|
|
|
125
322
|
}
|
|
126
323
|
return a.replica > b.replica ? -1 : 1;
|
|
127
324
|
};
|
|
128
|
-
var
|
|
325
|
+
var linearize2 = (elements) => {
|
|
129
326
|
const present = new Set(elements.map((element) => element.id));
|
|
130
327
|
const children = new Map;
|
|
131
328
|
for (const element of elements) {
|
|
@@ -154,7 +351,7 @@ var linearize = (elements) => {
|
|
|
154
351
|
}
|
|
155
352
|
return ordered;
|
|
156
353
|
};
|
|
157
|
-
var textOf = (state) =>
|
|
354
|
+
var textOf = (state) => linearize2(state.elements).filter((element) => !element.deleted).map((element) => element.value).join("");
|
|
158
355
|
var mergeTextState = (a, b) => {
|
|
159
356
|
const byId = new Map;
|
|
160
357
|
for (const element of [...a.elements, ...b.elements]) {
|
|
@@ -195,7 +392,7 @@ var createTextCrdt = (replica, initial) => {
|
|
|
195
392
|
clock = Math.max(clock, element.clock);
|
|
196
393
|
}
|
|
197
394
|
}
|
|
198
|
-
const visible = () =>
|
|
395
|
+
const visible = () => linearize2([...elements.values()]).filter((element) => !element.deleted);
|
|
199
396
|
const insert = (index, value) => {
|
|
200
397
|
const seen = visible();
|
|
201
398
|
let after = index <= 0 ? null : seen[index - 1]?.id ?? null;
|
|
@@ -267,6 +464,28 @@ var createTextCrdt = (replica, initial) => {
|
|
|
267
464
|
const delta = { elements: [...pending.values()] };
|
|
268
465
|
pending.clear();
|
|
269
466
|
return delta;
|
|
467
|
+
},
|
|
468
|
+
anchorAt: (index) => {
|
|
469
|
+
if (index <= 0) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
const seen = visible();
|
|
473
|
+
return seen[Math.min(index, seen.length) - 1]?.id ?? null;
|
|
474
|
+
},
|
|
475
|
+
indexOfAnchor: (anchor) => {
|
|
476
|
+
if (anchor === null) {
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
let visibleCount = 0;
|
|
480
|
+
for (const element of linearize2([...elements.values()])) {
|
|
481
|
+
if (!element.deleted) {
|
|
482
|
+
visibleCount += 1;
|
|
483
|
+
}
|
|
484
|
+
if (element.id === anchor) {
|
|
485
|
+
return visibleCount;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return visibleCount;
|
|
270
489
|
}
|
|
271
490
|
};
|
|
272
491
|
};
|
|
@@ -651,6 +870,8 @@ var createCollaborativeText = (options) => {
|
|
|
651
870
|
name: mutation
|
|
652
871
|
});
|
|
653
872
|
},
|
|
873
|
+
anchorAt: (index) => crdt.anchorAt?.(index) ?? null,
|
|
874
|
+
indexOfAnchor: (anchor) => crdt.indexOfAnchor?.(anchor) ?? 0,
|
|
654
875
|
close() {
|
|
655
876
|
unsubscribe();
|
|
656
877
|
collection.close();
|
|
@@ -705,7 +926,11 @@ class SyncCollectionService {
|
|
|
705
926
|
});
|
|
706
927
|
}
|
|
707
928
|
const setText = (next) => controller?.setText(next);
|
|
929
|
+
const anchorAt = (index) => controller?.anchorAt(index) ?? null;
|
|
930
|
+
const indexOfAnchor = (anchor) => controller?.indexOfAnchor(anchor) ?? 0;
|
|
708
931
|
return {
|
|
932
|
+
anchorAt,
|
|
933
|
+
indexOfAnchor,
|
|
709
934
|
setText,
|
|
710
935
|
status: computed(() => status()),
|
|
711
936
|
text: computed(() => text())
|
|
@@ -730,5 +955,5 @@ export {
|
|
|
730
955
|
SyncCollectionService
|
|
731
956
|
};
|
|
732
957
|
|
|
733
|
-
//# debugId=
|
|
958
|
+
//# debugId=03E6C1D8E1B49A2864756E2164756E21
|
|
734
959
|
//# sourceMappingURL=index.js.map
|