dacument 1.0.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/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/CRArray/class.d.ts +34 -0
- package/dist/CRArray/class.js +284 -0
- package/dist/CRMap/class.d.ts +55 -0
- package/dist/CRMap/class.js +222 -0
- package/dist/CRRecord/class.d.ts +34 -0
- package/dist/CRRecord/class.js +154 -0
- package/dist/CRRegister/class.d.ts +30 -0
- package/dist/CRRegister/class.js +82 -0
- package/dist/CRSet/class.d.ts +52 -0
- package/dist/CRSet/class.js +198 -0
- package/dist/CRText/class.d.ts +19 -0
- package/dist/CRText/class.js +156 -0
- package/dist/DAGNode/class.d.ts +13 -0
- package/dist/DAGNode/class.js +13 -0
- package/dist/Dacument/acl.d.ts +16 -0
- package/dist/Dacument/acl.js +84 -0
- package/dist/Dacument/class.d.ts +141 -0
- package/dist/Dacument/class.js +2015 -0
- package/dist/Dacument/clock.d.ts +16 -0
- package/dist/Dacument/clock.js +38 -0
- package/dist/Dacument/crypto.d.ts +26 -0
- package/dist/Dacument/crypto.js +64 -0
- package/dist/Dacument/types.d.ts +212 -0
- package/dist/Dacument/types.js +79 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/package.json +61 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { v7 as uuidv7 } from "uuid";
|
|
2
|
+
export class CRMap {
|
|
3
|
+
nodes = [];
|
|
4
|
+
seenNodeIds = new Set();
|
|
5
|
+
setTagsByKeyId = new Map();
|
|
6
|
+
tombstones = new Set();
|
|
7
|
+
aliveKeyIds = new Set();
|
|
8
|
+
latestKeyByKeyId = new Map();
|
|
9
|
+
latestValueByKeyId = new Map();
|
|
10
|
+
listeners = new Set();
|
|
11
|
+
keyIdByObjectRef = new WeakMap();
|
|
12
|
+
objectKeyCounter = 0;
|
|
13
|
+
symbolKeyByRef = new Map();
|
|
14
|
+
symbolKeyCounter = 0;
|
|
15
|
+
keyFn;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.keyFn = options?.key;
|
|
18
|
+
if (options?.snapshot?.length)
|
|
19
|
+
this.merge(options.snapshot);
|
|
20
|
+
}
|
|
21
|
+
// --- public API ---
|
|
22
|
+
onChange(listener) {
|
|
23
|
+
this.listeners.add(listener);
|
|
24
|
+
return () => this.listeners.delete(listener);
|
|
25
|
+
}
|
|
26
|
+
snapshot() {
|
|
27
|
+
return this.nodes.slice();
|
|
28
|
+
}
|
|
29
|
+
merge(input) {
|
|
30
|
+
const nodes = Array.isArray(input) ? input : [input];
|
|
31
|
+
const accepted = [];
|
|
32
|
+
for (const node of nodes) {
|
|
33
|
+
if (this.seenNodeIds.has(node.id))
|
|
34
|
+
continue;
|
|
35
|
+
this.seenNodeIds.add(node.id);
|
|
36
|
+
this.nodes.push(node);
|
|
37
|
+
this.applyNode(node);
|
|
38
|
+
accepted.push(node);
|
|
39
|
+
}
|
|
40
|
+
if (accepted.length)
|
|
41
|
+
this.emit(accepted);
|
|
42
|
+
return accepted;
|
|
43
|
+
}
|
|
44
|
+
// --- Map<K, V> API ---
|
|
45
|
+
get size() {
|
|
46
|
+
return this.aliveKeyIds.size;
|
|
47
|
+
}
|
|
48
|
+
clear() {
|
|
49
|
+
const patches = [];
|
|
50
|
+
for (const keyId of this.aliveKeyIds) {
|
|
51
|
+
const targets = this.currentSetTagsForKeyId(keyId);
|
|
52
|
+
if (targets.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
patches.push({ op: "del", id: this.newId(), keyId, targets });
|
|
55
|
+
}
|
|
56
|
+
if (patches.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
for (const patch of patches) {
|
|
59
|
+
this.seenNodeIds.add(patch.id);
|
|
60
|
+
this.nodes.push(patch);
|
|
61
|
+
this.applyNode(patch);
|
|
62
|
+
}
|
|
63
|
+
this.emit(patches);
|
|
64
|
+
}
|
|
65
|
+
delete(key) {
|
|
66
|
+
const keyId = this.keyIdOf(key);
|
|
67
|
+
const targets = this.currentSetTagsForKeyId(keyId);
|
|
68
|
+
if (targets.length === 0)
|
|
69
|
+
return false;
|
|
70
|
+
const node = {
|
|
71
|
+
op: "del",
|
|
72
|
+
id: this.newId(),
|
|
73
|
+
keyId,
|
|
74
|
+
targets,
|
|
75
|
+
};
|
|
76
|
+
this.appendAndApply(node);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
forEach(callbackfn, thisArg) {
|
|
80
|
+
for (const [key, value] of this.entries())
|
|
81
|
+
callbackfn.call(thisArg, value, key, this);
|
|
82
|
+
}
|
|
83
|
+
get(key) {
|
|
84
|
+
const keyId = this.keyIdOf(key);
|
|
85
|
+
if (!this.aliveKeyIds.has(keyId))
|
|
86
|
+
return undefined;
|
|
87
|
+
return this.latestValueByKeyId.get(keyId);
|
|
88
|
+
}
|
|
89
|
+
has(key) {
|
|
90
|
+
const keyId = this.keyIdOf(key);
|
|
91
|
+
return this.aliveKeyIds.has(keyId);
|
|
92
|
+
}
|
|
93
|
+
set(key, value) {
|
|
94
|
+
const keyId = this.keyIdOf(key);
|
|
95
|
+
const node = {
|
|
96
|
+
op: "set",
|
|
97
|
+
id: this.newId(),
|
|
98
|
+
key,
|
|
99
|
+
keyId,
|
|
100
|
+
value,
|
|
101
|
+
};
|
|
102
|
+
this.appendAndApply(node);
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
*entries() {
|
|
106
|
+
for (const keyId of this.aliveKeyIds) {
|
|
107
|
+
if (!this.latestKeyByKeyId.has(keyId) ||
|
|
108
|
+
!this.latestValueByKeyId.has(keyId))
|
|
109
|
+
continue;
|
|
110
|
+
const key = this.latestKeyByKeyId.get(keyId);
|
|
111
|
+
const value = this.latestValueByKeyId.get(keyId);
|
|
112
|
+
yield [key, value];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
*keys() {
|
|
116
|
+
for (const [key] of this.entries())
|
|
117
|
+
yield key;
|
|
118
|
+
}
|
|
119
|
+
*values() {
|
|
120
|
+
for (const [, value] of this.entries())
|
|
121
|
+
yield value;
|
|
122
|
+
}
|
|
123
|
+
[Symbol.iterator]() {
|
|
124
|
+
return this.entries();
|
|
125
|
+
}
|
|
126
|
+
[Symbol.toStringTag] = "CRMap";
|
|
127
|
+
// --- internals ---
|
|
128
|
+
appendAndApply(node) {
|
|
129
|
+
this.seenNodeIds.add(node.id);
|
|
130
|
+
this.nodes.push(node);
|
|
131
|
+
this.applyNode(node);
|
|
132
|
+
this.emit([node]);
|
|
133
|
+
}
|
|
134
|
+
applyNode(node) {
|
|
135
|
+
if (node.op === "set") {
|
|
136
|
+
let tags = this.setTagsByKeyId.get(node.keyId);
|
|
137
|
+
if (!tags) {
|
|
138
|
+
tags = new Map();
|
|
139
|
+
this.setTagsByKeyId.set(node.keyId, tags);
|
|
140
|
+
}
|
|
141
|
+
tags.set(node.id, { key: node.key, value: node.value });
|
|
142
|
+
this.recomputeKeyId(node.keyId);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const targetTag of node.targets)
|
|
146
|
+
this.tombstones.add(targetTag);
|
|
147
|
+
this.recomputeKeyId(node.keyId);
|
|
148
|
+
}
|
|
149
|
+
recomputeKeyId(keyId) {
|
|
150
|
+
const tags = this.setTagsByKeyId.get(keyId);
|
|
151
|
+
if (!tags || tags.size === 0) {
|
|
152
|
+
this.aliveKeyIds.delete(keyId);
|
|
153
|
+
this.latestKeyByKeyId.delete(keyId);
|
|
154
|
+
this.latestValueByKeyId.delete(keyId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let winnerTag = null;
|
|
158
|
+
let winner = null;
|
|
159
|
+
for (const [tag, entry] of tags) {
|
|
160
|
+
if (this.tombstones.has(tag))
|
|
161
|
+
continue;
|
|
162
|
+
if (!winnerTag || tag > winnerTag) {
|
|
163
|
+
winnerTag = tag;
|
|
164
|
+
winner = entry;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (winnerTag && winner) {
|
|
168
|
+
this.aliveKeyIds.add(keyId);
|
|
169
|
+
this.latestKeyByKeyId.set(keyId, winner.key);
|
|
170
|
+
this.latestValueByKeyId.set(keyId, winner.value);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.aliveKeyIds.delete(keyId);
|
|
174
|
+
this.latestKeyByKeyId.delete(keyId);
|
|
175
|
+
this.latestValueByKeyId.delete(keyId);
|
|
176
|
+
}
|
|
177
|
+
currentSetTagsForKeyId(keyId) {
|
|
178
|
+
const tags = this.setTagsByKeyId.get(keyId);
|
|
179
|
+
if (!tags)
|
|
180
|
+
return [];
|
|
181
|
+
return [...tags.keys()];
|
|
182
|
+
}
|
|
183
|
+
emit(patches) {
|
|
184
|
+
for (const listener of this.listeners)
|
|
185
|
+
listener(patches);
|
|
186
|
+
}
|
|
187
|
+
newId() {
|
|
188
|
+
return uuidv7();
|
|
189
|
+
}
|
|
190
|
+
keyIdOf(key) {
|
|
191
|
+
if (this.keyFn)
|
|
192
|
+
return this.keyFn(key);
|
|
193
|
+
const keyType = typeof key;
|
|
194
|
+
if (keyType === "string")
|
|
195
|
+
return `str:${key}`;
|
|
196
|
+
if (keyType === "number")
|
|
197
|
+
return `num:${Object.is(key, -0) ? "-0" : String(key)}`;
|
|
198
|
+
if (keyType === "bigint")
|
|
199
|
+
return `big:${String(key)}`;
|
|
200
|
+
if (keyType === "boolean")
|
|
201
|
+
return `bool:${key ? "1" : "0"}`;
|
|
202
|
+
if (keyType === "undefined")
|
|
203
|
+
return "undef";
|
|
204
|
+
if (key === null)
|
|
205
|
+
return "null";
|
|
206
|
+
if (keyType === "symbol") {
|
|
207
|
+
const existing = this.symbolKeyByRef.get(key);
|
|
208
|
+
if (existing)
|
|
209
|
+
return existing;
|
|
210
|
+
const created = `sym:${(++this.symbolKeyCounter).toString(36)}`;
|
|
211
|
+
this.symbolKeyByRef.set(key, created);
|
|
212
|
+
return created;
|
|
213
|
+
}
|
|
214
|
+
const objectRef = key;
|
|
215
|
+
const existing = this.keyIdByObjectRef.get(objectRef);
|
|
216
|
+
if (existing)
|
|
217
|
+
return existing;
|
|
218
|
+
const created = `obj:${(++this.objectKeyCounter).toString(36)}`;
|
|
219
|
+
this.keyIdByObjectRef.set(objectRef, created);
|
|
220
|
+
return created;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
type CRRecordNode<V> = {
|
|
2
|
+
op: "set";
|
|
3
|
+
id: string;
|
|
4
|
+
prop: string;
|
|
5
|
+
value: V;
|
|
6
|
+
} | {
|
|
7
|
+
op: "del";
|
|
8
|
+
id: string;
|
|
9
|
+
prop: string;
|
|
10
|
+
targets: string[];
|
|
11
|
+
};
|
|
12
|
+
type CRRecordListener<V> = (patches: CRRecordNode<V>[]) => void;
|
|
13
|
+
export declare class CRRecord<V = unknown> {
|
|
14
|
+
private readonly nodes;
|
|
15
|
+
private readonly seenNodeIds;
|
|
16
|
+
private readonly setTagsByProp;
|
|
17
|
+
private readonly tombstones;
|
|
18
|
+
private readonly aliveProps;
|
|
19
|
+
private readonly latestValueByProp;
|
|
20
|
+
private readonly listeners;
|
|
21
|
+
constructor(snapshot?: CRRecordNode<V>[]);
|
|
22
|
+
onChange(listener: CRRecordListener<V>): () => void;
|
|
23
|
+
snapshot(): CRRecordNode<V>[];
|
|
24
|
+
merge(input: CRRecordNode<V>[] | CRRecordNode<V>): CRRecordNode<V>[];
|
|
25
|
+
private get;
|
|
26
|
+
private set;
|
|
27
|
+
private delete;
|
|
28
|
+
private appendAndApply;
|
|
29
|
+
private applyNode;
|
|
30
|
+
private emit;
|
|
31
|
+
private recompute;
|
|
32
|
+
private newId;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { v7 as uuidv7 } from "uuid";
|
|
2
|
+
export class CRRecord {
|
|
3
|
+
nodes = [];
|
|
4
|
+
seenNodeIds = new Set();
|
|
5
|
+
setTagsByProp = new Map();
|
|
6
|
+
tombstones = new Set();
|
|
7
|
+
aliveProps = new Set();
|
|
8
|
+
latestValueByProp = new Map();
|
|
9
|
+
listeners = new Set();
|
|
10
|
+
constructor(snapshot) {
|
|
11
|
+
if (snapshot?.length)
|
|
12
|
+
this.merge(snapshot);
|
|
13
|
+
return new Proxy(this, {
|
|
14
|
+
get: (target, prop, receiver) => {
|
|
15
|
+
if (typeof prop !== "string")
|
|
16
|
+
return Reflect.get(target, prop, receiver);
|
|
17
|
+
if (prop in target)
|
|
18
|
+
return Reflect.get(target, prop, receiver);
|
|
19
|
+
return target.get(prop);
|
|
20
|
+
},
|
|
21
|
+
set: (target, prop, value, receiver) => {
|
|
22
|
+
if (typeof prop !== "string")
|
|
23
|
+
return Reflect.set(target, prop, value, receiver);
|
|
24
|
+
if (prop in target)
|
|
25
|
+
return Reflect.set(target, prop, value, receiver);
|
|
26
|
+
target.set(prop, value);
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
deleteProperty: (target, prop) => {
|
|
30
|
+
if (typeof prop !== "string")
|
|
31
|
+
return Reflect.deleteProperty(target, prop);
|
|
32
|
+
if (prop in target)
|
|
33
|
+
return Reflect.deleteProperty(target, prop);
|
|
34
|
+
return target.delete(prop);
|
|
35
|
+
},
|
|
36
|
+
has: (target, prop) => {
|
|
37
|
+
if (typeof prop !== "string")
|
|
38
|
+
return Reflect.has(target, prop);
|
|
39
|
+
if (prop in target)
|
|
40
|
+
return true;
|
|
41
|
+
return target.aliveProps.has(prop);
|
|
42
|
+
},
|
|
43
|
+
ownKeys: (target) => [...target.aliveProps],
|
|
44
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
45
|
+
if (typeof prop !== "string")
|
|
46
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
47
|
+
if (prop in target)
|
|
48
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
49
|
+
if (!target.aliveProps.has(prop))
|
|
50
|
+
return undefined;
|
|
51
|
+
return { enumerable: true, configurable: true };
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// --- public API ---
|
|
56
|
+
onChange(listener) {
|
|
57
|
+
this.listeners.add(listener);
|
|
58
|
+
return () => this.listeners.delete(listener);
|
|
59
|
+
}
|
|
60
|
+
snapshot() {
|
|
61
|
+
return this.nodes.slice();
|
|
62
|
+
}
|
|
63
|
+
merge(input) {
|
|
64
|
+
const nodes = Array.isArray(input) ? input : [input];
|
|
65
|
+
const accepted = [];
|
|
66
|
+
for (const node of nodes) {
|
|
67
|
+
if (this.seenNodeIds.has(node.id))
|
|
68
|
+
continue;
|
|
69
|
+
this.seenNodeIds.add(node.id);
|
|
70
|
+
this.nodes.push(node);
|
|
71
|
+
this.applyNode(node);
|
|
72
|
+
accepted.push(node);
|
|
73
|
+
}
|
|
74
|
+
if (accepted.length)
|
|
75
|
+
this.emit(accepted);
|
|
76
|
+
return accepted;
|
|
77
|
+
}
|
|
78
|
+
// --- internals ---
|
|
79
|
+
get(prop) {
|
|
80
|
+
if (!this.aliveProps.has(prop))
|
|
81
|
+
return undefined;
|
|
82
|
+
return this.latestValueByProp.get(prop);
|
|
83
|
+
}
|
|
84
|
+
set(prop, value) {
|
|
85
|
+
const node = { op: "set", id: this.newId(), prop, value };
|
|
86
|
+
this.appendAndApply(node);
|
|
87
|
+
}
|
|
88
|
+
delete(prop) {
|
|
89
|
+
const tags = this.setTagsByProp.get(prop);
|
|
90
|
+
if (!tags?.size)
|
|
91
|
+
return false;
|
|
92
|
+
const node = {
|
|
93
|
+
op: "del",
|
|
94
|
+
id: this.newId(),
|
|
95
|
+
prop,
|
|
96
|
+
targets: [...tags.keys()],
|
|
97
|
+
};
|
|
98
|
+
this.appendAndApply(node);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
appendAndApply(node) {
|
|
102
|
+
this.seenNodeIds.add(node.id);
|
|
103
|
+
this.nodes.push(node);
|
|
104
|
+
this.applyNode(node);
|
|
105
|
+
this.emit([node]);
|
|
106
|
+
}
|
|
107
|
+
applyNode(node) {
|
|
108
|
+
if (node.op === "set") {
|
|
109
|
+
let tags = this.setTagsByProp.get(node.prop);
|
|
110
|
+
if (!tags) {
|
|
111
|
+
tags = new Map();
|
|
112
|
+
this.setTagsByProp.set(node.prop, tags);
|
|
113
|
+
}
|
|
114
|
+
tags.set(node.id, node.value);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
for (const t of node.targets)
|
|
118
|
+
this.tombstones.add(t);
|
|
119
|
+
}
|
|
120
|
+
this.recompute(node.prop);
|
|
121
|
+
}
|
|
122
|
+
emit(patches) {
|
|
123
|
+
for (const l of this.listeners)
|
|
124
|
+
l(patches);
|
|
125
|
+
}
|
|
126
|
+
recompute(prop) {
|
|
127
|
+
const tags = this.setTagsByProp.get(prop);
|
|
128
|
+
if (!tags || tags.size === 0) {
|
|
129
|
+
this.aliveProps.delete(prop);
|
|
130
|
+
this.latestValueByProp.delete(prop);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
let winnerTag = null;
|
|
134
|
+
let winnerValue;
|
|
135
|
+
for (const [tag, value] of tags) {
|
|
136
|
+
if (this.tombstones.has(tag))
|
|
137
|
+
continue;
|
|
138
|
+
if (!winnerTag || tag > winnerTag) {
|
|
139
|
+
winnerTag = tag;
|
|
140
|
+
winnerValue = value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (winnerTag) {
|
|
144
|
+
this.aliveProps.add(prop);
|
|
145
|
+
this.latestValueByProp.set(prop, winnerValue);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.aliveProps.delete(prop);
|
|
149
|
+
this.latestValueByProp.delete(prop);
|
|
150
|
+
}
|
|
151
|
+
newId() {
|
|
152
|
+
return uuidv7();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type LWWValue = string | number | boolean;
|
|
2
|
+
type HLCStamp = {
|
|
3
|
+
/** Unix ms */
|
|
4
|
+
readonly wallTimeMs: number;
|
|
5
|
+
/** logical counter for same/older wallTimeMs */
|
|
6
|
+
readonly logical: number;
|
|
7
|
+
/** stable actor/node id for deterministic tie-break */
|
|
8
|
+
readonly clockId: string;
|
|
9
|
+
};
|
|
10
|
+
type CRRegisterNode<TValue extends LWWValue> = {
|
|
11
|
+
readonly value: TValue;
|
|
12
|
+
readonly stamp: HLCStamp;
|
|
13
|
+
};
|
|
14
|
+
type CRRegisterListener<TValue extends LWWValue> = (patches: CRRegisterNode<TValue>[]) => void;
|
|
15
|
+
export declare class CRRegister<TValue extends LWWValue> {
|
|
16
|
+
private last;
|
|
17
|
+
private winner;
|
|
18
|
+
private readonly listeners;
|
|
19
|
+
constructor();
|
|
20
|
+
onChange(listener: CRRegisterListener<TValue>): () => void;
|
|
21
|
+
snapshot(): CRRegisterNode<TValue>[];
|
|
22
|
+
merge(input: CRRegisterNode<TValue>[] | CRRegisterNode<TValue>): CRRegisterNode<TValue>[];
|
|
23
|
+
set(value: TValue, incomingStamp?: HLCStamp): void;
|
|
24
|
+
get(): TValue | null;
|
|
25
|
+
private nextLocalStamp;
|
|
26
|
+
private apply;
|
|
27
|
+
private advanceClock;
|
|
28
|
+
private emit;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { v7 as uuidv7 } from "uuid";
|
|
2
|
+
function compareHLC(left, right) {
|
|
3
|
+
if (left.wallTimeMs !== right.wallTimeMs)
|
|
4
|
+
return left.wallTimeMs - right.wallTimeMs;
|
|
5
|
+
if (left.logical !== right.logical)
|
|
6
|
+
return left.logical - right.logical;
|
|
7
|
+
// final deterministic tie-break
|
|
8
|
+
if (left.clockId === right.clockId)
|
|
9
|
+
return 0;
|
|
10
|
+
return left.clockId < right.clockId ? -1 : 1;
|
|
11
|
+
}
|
|
12
|
+
export class CRRegister {
|
|
13
|
+
last;
|
|
14
|
+
winner = null;
|
|
15
|
+
listeners = new Set();
|
|
16
|
+
constructor() {
|
|
17
|
+
this.last = { wallTimeMs: 0, logical: 0, clockId: uuidv7() };
|
|
18
|
+
}
|
|
19
|
+
// --- public API ---
|
|
20
|
+
onChange(listener) {
|
|
21
|
+
this.listeners.add(listener);
|
|
22
|
+
return () => this.listeners.delete(listener);
|
|
23
|
+
}
|
|
24
|
+
snapshot() {
|
|
25
|
+
return this.winner ? [this.winner] : [];
|
|
26
|
+
}
|
|
27
|
+
merge(input) {
|
|
28
|
+
const nodes = Array.isArray(input) ? input : [input];
|
|
29
|
+
const accepted = [];
|
|
30
|
+
for (const node of nodes) {
|
|
31
|
+
if (this.apply(node))
|
|
32
|
+
accepted.push(node);
|
|
33
|
+
}
|
|
34
|
+
if (accepted.length)
|
|
35
|
+
this.emit(accepted);
|
|
36
|
+
return accepted;
|
|
37
|
+
}
|
|
38
|
+
set(value, incomingStamp) {
|
|
39
|
+
const stamp = incomingStamp ?? this.nextLocalStamp(Date.now());
|
|
40
|
+
const candidate = { value, stamp };
|
|
41
|
+
if (this.apply(candidate))
|
|
42
|
+
this.emit([candidate]);
|
|
43
|
+
}
|
|
44
|
+
get() {
|
|
45
|
+
return this.winner ? this.winner.value : null;
|
|
46
|
+
}
|
|
47
|
+
// --- internals ---
|
|
48
|
+
nextLocalStamp(nowMs) {
|
|
49
|
+
const wallTimeMs = Math.max(nowMs, this.last.wallTimeMs);
|
|
50
|
+
const logical = wallTimeMs === this.last.wallTimeMs ? this.last.logical + 1 : 0;
|
|
51
|
+
const next = { wallTimeMs, logical, clockId: this.last.clockId };
|
|
52
|
+
this.last = next;
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
apply(node) {
|
|
56
|
+
this.advanceClock(node.stamp);
|
|
57
|
+
const current = this.winner;
|
|
58
|
+
if (!current || compareHLC(node.stamp, current.stamp) > 0) {
|
|
59
|
+
this.winner = node;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
advanceClock(stamp) {
|
|
65
|
+
// Keep local HLC monotonic even when receiving remote stamps.
|
|
66
|
+
const mergedWall = Math.max(this.last.wallTimeMs, stamp.wallTimeMs, Date.now());
|
|
67
|
+
const mergedLogical = mergedWall === this.last.wallTimeMs
|
|
68
|
+
? Math.max(this.last.logical, stamp.logical) + 1
|
|
69
|
+
: mergedWall === stamp.wallTimeMs
|
|
70
|
+
? stamp.logical
|
|
71
|
+
: 0;
|
|
72
|
+
this.last = {
|
|
73
|
+
wallTimeMs: mergedWall,
|
|
74
|
+
logical: mergedLogical,
|
|
75
|
+
clockId: this.last.clockId,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
emit(patches) {
|
|
79
|
+
for (const listener of this.listeners)
|
|
80
|
+
listener(patches);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type CRSetNode<T> = {
|
|
2
|
+
op: "add";
|
|
3
|
+
id: string;
|
|
4
|
+
value: T;
|
|
5
|
+
key: string;
|
|
6
|
+
} | {
|
|
7
|
+
op: "rem";
|
|
8
|
+
id: string;
|
|
9
|
+
key: string;
|
|
10
|
+
targets: string[];
|
|
11
|
+
};
|
|
12
|
+
type CRSetListener<T> = (patches: CRSetNode<T>[]) => void;
|
|
13
|
+
export declare class CRSet<T> implements Set<T> {
|
|
14
|
+
private readonly nodes;
|
|
15
|
+
private readonly seenNodeIds;
|
|
16
|
+
private readonly addTagsByKey;
|
|
17
|
+
private readonly tombstones;
|
|
18
|
+
private readonly aliveKeys;
|
|
19
|
+
private readonly latestValueByKey;
|
|
20
|
+
private readonly listeners;
|
|
21
|
+
private readonly objectKeyByRef;
|
|
22
|
+
private objectKeyCounter;
|
|
23
|
+
private readonly symbolKeyByRef;
|
|
24
|
+
private symbolKeyCounter;
|
|
25
|
+
private readonly keyFn;
|
|
26
|
+
constructor(options?: {
|
|
27
|
+
snapshot?: CRSetNode<T>[];
|
|
28
|
+
key?: (value: T) => string;
|
|
29
|
+
});
|
|
30
|
+
onChange(listener: CRSetListener<T>): () => void;
|
|
31
|
+
snapshot(): CRSetNode<T>[];
|
|
32
|
+
merge(input: CRSetNode<T>[] | CRSetNode<T>): CRSetNode<T>[];
|
|
33
|
+
get size(): number;
|
|
34
|
+
add(value: T): this;
|
|
35
|
+
delete(value: T): boolean;
|
|
36
|
+
clear(): void;
|
|
37
|
+
has(value: T): boolean;
|
|
38
|
+
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: unknown): void;
|
|
39
|
+
values(): SetIterator<T>;
|
|
40
|
+
keys(): SetIterator<T>;
|
|
41
|
+
entries(): SetIterator<[T, T]>;
|
|
42
|
+
[Symbol.iterator](): SetIterator<T>;
|
|
43
|
+
readonly [Symbol.toStringTag] = "CRSet";
|
|
44
|
+
private appendAndApply;
|
|
45
|
+
private applyNode;
|
|
46
|
+
private recomputeAliveForKey;
|
|
47
|
+
private currentAddTagsForKey;
|
|
48
|
+
private emit;
|
|
49
|
+
private newId;
|
|
50
|
+
private keyOf;
|
|
51
|
+
}
|
|
52
|
+
export {};
|