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.
@@ -0,0 +1,198 @@
1
+ import { v7 as uuidv7 } from "uuid";
2
+ export class CRSet {
3
+ nodes = [];
4
+ seenNodeIds = new Set();
5
+ addTagsByKey = new Map();
6
+ tombstones = new Set();
7
+ aliveKeys = new Set();
8
+ latestValueByKey = new Map();
9
+ listeners = new Set();
10
+ objectKeyByRef = new WeakMap();
11
+ objectKeyCounter = 0;
12
+ symbolKeyByRef = new Map();
13
+ symbolKeyCounter = 0;
14
+ keyFn;
15
+ constructor(options) {
16
+ this.keyFn = options?.key;
17
+ if (options?.snapshot?.length)
18
+ this.merge(options.snapshot);
19
+ }
20
+ // --- public API ---
21
+ onChange(listener) {
22
+ this.listeners.add(listener);
23
+ return () => this.listeners.delete(listener);
24
+ }
25
+ snapshot() {
26
+ return this.nodes.slice();
27
+ }
28
+ merge(input) {
29
+ const nodes = Array.isArray(input) ? input : [input];
30
+ const accepted = [];
31
+ for (const node of nodes) {
32
+ if (this.seenNodeIds.has(node.id))
33
+ continue;
34
+ this.seenNodeIds.add(node.id);
35
+ this.nodes.push(node);
36
+ this.applyNode(node);
37
+ accepted.push(node);
38
+ }
39
+ if (accepted.length)
40
+ this.emit(accepted);
41
+ return accepted;
42
+ }
43
+ // --- Set<T> API ---
44
+ get size() {
45
+ return this.aliveKeys.size;
46
+ }
47
+ add(value) {
48
+ const key = this.keyOf(value);
49
+ const node = { op: "add", id: this.newId(), value, key };
50
+ this.appendAndApply(node);
51
+ return this;
52
+ }
53
+ delete(value) {
54
+ const key = this.keyOf(value);
55
+ const targets = this.currentAddTagsForKey(key);
56
+ if (targets.length === 0)
57
+ return false;
58
+ const node = { op: "rem", id: this.newId(), key, targets };
59
+ this.appendAndApply(node);
60
+ return true;
61
+ }
62
+ clear() {
63
+ const patches = [];
64
+ for (const key of this.aliveKeys) {
65
+ const targets = this.currentAddTagsForKey(key);
66
+ if (targets.length === 0)
67
+ continue;
68
+ patches.push({ op: "rem", id: this.newId(), key, targets });
69
+ }
70
+ if (patches.length === 0)
71
+ return;
72
+ for (const patch of patches) {
73
+ this.seenNodeIds.add(patch.id);
74
+ this.nodes.push(patch);
75
+ this.applyNode(patch);
76
+ }
77
+ this.emit(patches);
78
+ }
79
+ has(value) {
80
+ const key = this.keyOf(value);
81
+ return this.aliveKeys.has(key);
82
+ }
83
+ forEach(callbackfn, thisArg) {
84
+ for (const value of this.values())
85
+ callbackfn.call(thisArg, value, value, this);
86
+ }
87
+ *values() {
88
+ for (const key of this.aliveKeys) {
89
+ if (!this.latestValueByKey.has(key))
90
+ continue;
91
+ yield this.latestValueByKey.get(key);
92
+ }
93
+ }
94
+ *keys() {
95
+ yield* this.values();
96
+ }
97
+ *entries() {
98
+ for (const value of this.values())
99
+ yield [value, value];
100
+ }
101
+ [Symbol.iterator]() {
102
+ return this.values();
103
+ }
104
+ [Symbol.toStringTag] = "CRSet";
105
+ // --- internals ---
106
+ appendAndApply(node) {
107
+ this.seenNodeIds.add(node.id);
108
+ this.nodes.push(node);
109
+ this.applyNode(node);
110
+ this.emit([node]);
111
+ }
112
+ applyNode(node) {
113
+ if (node.op === "add") {
114
+ let tags = this.addTagsByKey.get(node.key);
115
+ if (!tags) {
116
+ tags = new Map();
117
+ this.addTagsByKey.set(node.key, tags);
118
+ }
119
+ tags.set(node.id, node.value);
120
+ this.recomputeAliveForKey(node.key);
121
+ return;
122
+ }
123
+ for (const targetTag of node.targets)
124
+ this.tombstones.add(targetTag);
125
+ this.recomputeAliveForKey(node.key);
126
+ }
127
+ recomputeAliveForKey(key) {
128
+ const tags = this.addTagsByKey.get(key);
129
+ if (!tags || tags.size === 0) {
130
+ this.aliveKeys.delete(key);
131
+ this.latestValueByKey.delete(key);
132
+ return;
133
+ }
134
+ let winnerTag = null;
135
+ let winnerValue;
136
+ for (const [tag, value] of tags) {
137
+ if (this.tombstones.has(tag))
138
+ continue;
139
+ if (!winnerTag || tag > winnerTag) {
140
+ winnerTag = tag;
141
+ winnerValue = value;
142
+ }
143
+ }
144
+ if (winnerTag) {
145
+ this.aliveKeys.add(key);
146
+ this.latestValueByKey.set(key, winnerValue);
147
+ return;
148
+ }
149
+ this.aliveKeys.delete(key);
150
+ this.latestValueByKey.delete(key);
151
+ }
152
+ currentAddTagsForKey(key) {
153
+ const tags = this.addTagsByKey.get(key);
154
+ if (!tags)
155
+ return [];
156
+ return [...tags.keys()];
157
+ }
158
+ emit(patches) {
159
+ for (const listener of this.listeners)
160
+ listener(patches);
161
+ }
162
+ newId() {
163
+ return uuidv7();
164
+ }
165
+ keyOf(value) {
166
+ if (this.keyFn)
167
+ return this.keyFn(value);
168
+ const valueType = typeof value;
169
+ if (valueType === "string")
170
+ return `str:${value}`;
171
+ if (valueType === "number")
172
+ return `num:${Object.is(value, -0) ? "-0" : String(value)}`;
173
+ if (valueType === "bigint")
174
+ return `big:${String(value)}`;
175
+ if (valueType === "boolean")
176
+ return `bool:${value ? "1" : "0"}`;
177
+ if (valueType === "undefined")
178
+ return "undef";
179
+ if (value === null)
180
+ return "null";
181
+ if (valueType === "symbol") {
182
+ const existing = this.symbolKeyByRef.get(value);
183
+ if (existing)
184
+ return existing;
185
+ const created = `sym:${(++this.symbolKeyCounter).toString(36)}`;
186
+ this.symbolKeyByRef.set(value, created);
187
+ return created;
188
+ }
189
+ // objects + functions: stable identity key (matches native Set semantics)
190
+ const objectRef = value;
191
+ const existing = this.objectKeyByRef.get(objectRef);
192
+ if (existing)
193
+ return existing;
194
+ const created = `obj:${(++this.objectKeyCounter).toString(36)}`;
195
+ this.objectKeyByRef.set(objectRef, created);
196
+ return created;
197
+ }
198
+ }
@@ -0,0 +1,19 @@
1
+ import { DAGNode } from "../DAGNode/class.js";
2
+ export declare class CRText<CharT extends string = string> {
3
+ private readonly nodes;
4
+ private readonly nodeById;
5
+ private readonly listeners;
6
+ constructor(snapshot?: readonly DAGNode<CharT>[]);
7
+ get length(): number;
8
+ onChange(listener: (nodes: readonly DAGNode<CharT>[]) => void): () => void;
9
+ snapshot(): DAGNode<CharT>[];
10
+ toString(): string;
11
+ at(index: number): CharT | undefined;
12
+ insertAt(index: number, char: CharT): this;
13
+ deleteAt(index: number): CharT | undefined;
14
+ merge(remoteSnapshot: DAGNode<CharT>[] | DAGNode<CharT>): DAGNode<CharT>[];
15
+ sort(compareFn?: (a: DAGNode<CharT>, b: DAGNode<CharT>) => number): this;
16
+ private alive;
17
+ private afterIdForAliveInsertAt;
18
+ private emit;
19
+ }
@@ -0,0 +1,156 @@
1
+ import { DAGNode } from "../DAGNode/class.js";
2
+ const ROOT = [];
3
+ function afterKey(after) {
4
+ return after.join(",");
5
+ }
6
+ export class CRText {
7
+ nodes = [];
8
+ nodeById = new Map();
9
+ listeners = new Set();
10
+ constructor(snapshot) {
11
+ if (snapshot) {
12
+ for (const node of snapshot) {
13
+ if (this.nodeById.has(node.id))
14
+ continue;
15
+ this.nodes.push(node);
16
+ this.nodeById.set(node.id, node);
17
+ }
18
+ }
19
+ this.sort();
20
+ }
21
+ get length() {
22
+ let count = 0;
23
+ for (const node of this.nodes)
24
+ if (!node.deleted)
25
+ count++;
26
+ return count;
27
+ }
28
+ // --- public API ---
29
+ onChange(listener) {
30
+ this.listeners.add(listener);
31
+ return () => this.listeners.delete(listener);
32
+ }
33
+ snapshot() {
34
+ return this.nodes.slice();
35
+ }
36
+ toString() {
37
+ let output = "";
38
+ for (const node of this.nodes)
39
+ if (!node.deleted)
40
+ output += String(node.value);
41
+ return output;
42
+ }
43
+ at(index) {
44
+ return this.alive().at(index);
45
+ }
46
+ insertAt(index, char) {
47
+ if (!Number.isInteger(index))
48
+ throw new TypeError("CRText.insertAt: index must be an integer");
49
+ if (index < 0)
50
+ throw new RangeError("CRText.insertAt: negative index not supported");
51
+ if (index > this.length)
52
+ throw new RangeError("CRText.insertAt: index out of bounds");
53
+ const after = this.afterIdForAliveInsertAt(index);
54
+ const node = new DAGNode({ value: char, after });
55
+ this.nodes.push(node);
56
+ this.nodeById.set(node.id, node);
57
+ this.sort();
58
+ this.emit([node]);
59
+ return this;
60
+ }
61
+ deleteAt(index) {
62
+ if (!Number.isInteger(index))
63
+ throw new TypeError("CRText.deleteAt: index must be an integer");
64
+ if (index < 0)
65
+ throw new RangeError("CRText.deleteAt: negative index not supported");
66
+ let aliveIndex = 0;
67
+ for (const node of this.nodes) {
68
+ if (node.deleted)
69
+ continue;
70
+ if (aliveIndex === index) {
71
+ node.deleted = true;
72
+ this.emit([node]);
73
+ return node.value;
74
+ }
75
+ aliveIndex++;
76
+ }
77
+ return undefined;
78
+ }
79
+ merge(remoteSnapshot) {
80
+ const snapshot = Array.isArray(remoteSnapshot)
81
+ ? remoteSnapshot
82
+ : [remoteSnapshot];
83
+ const changed = [];
84
+ for (const remote of snapshot) {
85
+ const local = this.nodeById.get(remote.id);
86
+ if (!local) {
87
+ const clone = structuredClone(remote);
88
+ this.nodes.push(clone);
89
+ this.nodeById.set(clone.id, clone);
90
+ changed.push(clone);
91
+ }
92
+ else if (!local.deleted && remote.deleted) {
93
+ local.deleted = true;
94
+ changed.push(local);
95
+ }
96
+ }
97
+ if (changed.length) {
98
+ this.sort();
99
+ this.emit(changed);
100
+ }
101
+ return changed;
102
+ }
103
+ sort(compareFn) {
104
+ if (compareFn) {
105
+ this.nodes.sort(compareFn);
106
+ return this;
107
+ }
108
+ this.nodes.sort((left, right) => {
109
+ const leftIsRoot = left.after.length === 0;
110
+ const rightIsRoot = right.after.length === 0;
111
+ if (leftIsRoot !== rightIsRoot)
112
+ return leftIsRoot ? -1 : 1;
113
+ const leftAfterKey = afterKey(left.after);
114
+ const rightAfterKey = afterKey(right.after);
115
+ if (leftAfterKey !== rightAfterKey)
116
+ return leftAfterKey < rightAfterKey ? -1 : 1;
117
+ if (left.id === right.id)
118
+ return 0;
119
+ if (leftIsRoot)
120
+ return left.id > right.id ? -1 : 1;
121
+ return left.id < right.id ? -1 : 1;
122
+ });
123
+ return this;
124
+ }
125
+ // --- internals ---
126
+ alive() {
127
+ const values = [];
128
+ for (const node of this.nodes)
129
+ if (!node.deleted)
130
+ values.push(node.value);
131
+ return values;
132
+ }
133
+ afterIdForAliveInsertAt(index) {
134
+ if (index === 0)
135
+ return ROOT;
136
+ let aliveIndex = 0;
137
+ let previousAliveId = null;
138
+ for (const node of this.nodes) {
139
+ if (node.deleted)
140
+ continue;
141
+ if (aliveIndex === index)
142
+ break;
143
+ previousAliveId = node.id;
144
+ aliveIndex++;
145
+ }
146
+ if (previousAliveId)
147
+ return [previousAliveId];
148
+ return ROOT;
149
+ }
150
+ emit(nodes) {
151
+ if (nodes.length === 0)
152
+ return;
153
+ for (const listener of this.listeners)
154
+ listener(nodes);
155
+ }
156
+ }
@@ -0,0 +1,13 @@
1
+ export type NodeId = string;
2
+ export declare class DAGNode<ValueType> {
3
+ readonly id: NodeId;
4
+ readonly value: ValueType;
5
+ readonly after: readonly NodeId[];
6
+ deleted: boolean;
7
+ constructor(params: {
8
+ value: ValueType;
9
+ after?: readonly NodeId[];
10
+ deleted?: boolean;
11
+ id?: NodeId;
12
+ });
13
+ }
@@ -0,0 +1,13 @@
1
+ import { v7 as uuidv7 } from "uuid";
2
+ export class DAGNode {
3
+ id;
4
+ value;
5
+ after;
6
+ deleted;
7
+ constructor(params) {
8
+ this.id = params.id ?? uuidv7();
9
+ this.value = params.value;
10
+ this.after = params.after ?? [];
11
+ this.deleted = params.deleted ?? false;
12
+ }
13
+ }
@@ -0,0 +1,16 @@
1
+ import type { AclAssignment, Role } from "./types.js";
2
+ export declare class AclLog {
3
+ private readonly nodes;
4
+ private readonly nodesById;
5
+ private readonly nodesByActor;
6
+ private readonly currentByActor;
7
+ merge(input: AclAssignment[] | AclAssignment): AclAssignment[];
8
+ snapshot(): AclAssignment[];
9
+ reset(): void;
10
+ isEmpty(): boolean;
11
+ roleAt(actorId: string, stamp: AclAssignment["stamp"]): Role;
12
+ currentRole(actorId: string): Role;
13
+ currentEntry(actorId: string): AclAssignment | null;
14
+ knownActors(): string[];
15
+ private insert;
16
+ }
@@ -0,0 +1,84 @@
1
+ import { compareHLC } from "./clock.js";
2
+ function compareAssignment(left, right) {
3
+ const cmp = compareHLC(left.stamp, right.stamp);
4
+ if (cmp !== 0)
5
+ return cmp;
6
+ if (left.id === right.id)
7
+ return 0;
8
+ return left.id < right.id ? -1 : 1;
9
+ }
10
+ export class AclLog {
11
+ nodes = [];
12
+ nodesById = new Set();
13
+ nodesByActor = new Map();
14
+ currentByActor = new Map();
15
+ merge(input) {
16
+ const nodes = Array.isArray(input) ? input : [input];
17
+ const accepted = [];
18
+ for (const node of nodes) {
19
+ if (this.nodesById.has(node.id))
20
+ continue;
21
+ this.nodesById.add(node.id);
22
+ this.nodes.push(node);
23
+ this.insert(node);
24
+ accepted.push(node);
25
+ }
26
+ return accepted;
27
+ }
28
+ snapshot() {
29
+ return this.nodes.slice();
30
+ }
31
+ reset() {
32
+ this.nodes.length = 0;
33
+ this.nodesById.clear();
34
+ this.nodesByActor.clear();
35
+ this.currentByActor.clear();
36
+ }
37
+ isEmpty() {
38
+ return this.nodes.length === 0;
39
+ }
40
+ roleAt(actorId, stamp) {
41
+ const list = this.nodesByActor.get(actorId);
42
+ if (!list || list.length === 0)
43
+ return "revoked";
44
+ for (let index = list.length - 1; index >= 0; index--) {
45
+ const entry = list[index];
46
+ if (compareHLC(entry.stamp, stamp) <= 0)
47
+ return entry.role;
48
+ }
49
+ return "revoked";
50
+ }
51
+ currentRole(actorId) {
52
+ const entry = this.currentByActor.get(actorId);
53
+ return entry ? entry.role : "revoked";
54
+ }
55
+ currentEntry(actorId) {
56
+ return this.currentByActor.get(actorId) ?? null;
57
+ }
58
+ knownActors() {
59
+ return [...this.currentByActor.keys()];
60
+ }
61
+ insert(node) {
62
+ const list = this.nodesByActor.get(node.actorId) ?? [];
63
+ if (list.length === 0) {
64
+ list.push(node);
65
+ this.nodesByActor.set(node.actorId, list);
66
+ this.currentByActor.set(node.actorId, node);
67
+ return;
68
+ }
69
+ let inserted = false;
70
+ for (let index = list.length - 1; index >= 0; index--) {
71
+ if (compareAssignment(list[index], node) <= 0) {
72
+ list.splice(index + 1, 0, node);
73
+ inserted = true;
74
+ break;
75
+ }
76
+ }
77
+ if (!inserted)
78
+ list.unshift(node);
79
+ const current = this.currentByActor.get(node.actorId);
80
+ if (!current || compareAssignment(current, node) < 0) {
81
+ this.currentByActor.set(node.actorId, node);
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,141 @@
1
+ import { type AclAssignment, type DacumentEventMap, type DocFieldAccess, type DocSnapshot, type RoleKeys, type RolePublicKeys, type SchemaDefinition, type SchemaId, type SignedOp, type Role, array, map, record, register, set, text } from "./types.js";
2
+ export declare class Dacument<S extends SchemaDefinition> {
3
+ private static actorId?;
4
+ static setActorId(actorId: string): void;
5
+ private static requireActorId;
6
+ private static isValidActorId;
7
+ static schema: <Schema extends SchemaDefinition>(schema: Schema) => Schema;
8
+ static register: typeof register;
9
+ static text: typeof text;
10
+ static array: typeof array;
11
+ static set: typeof set;
12
+ static map: typeof map;
13
+ static record: typeof record;
14
+ static computeSchemaId(schema: SchemaDefinition): Promise<SchemaId>;
15
+ static create<Schema extends SchemaDefinition>(params: {
16
+ schema: Schema;
17
+ docId?: string;
18
+ }): Promise<{
19
+ docId: string;
20
+ schemaId: SchemaId;
21
+ roleKeys: RoleKeys;
22
+ snapshot: DocSnapshot;
23
+ }>;
24
+ static load<Schema extends SchemaDefinition>(params: {
25
+ schema: Schema;
26
+ roleKey?: JsonWebKey;
27
+ snapshot: DocSnapshot;
28
+ }): Promise<DacumentDoc<Schema>>;
29
+ readonly docId: string;
30
+ readonly actorId: string;
31
+ readonly schema: S;
32
+ readonly schemaId: SchemaId;
33
+ private readonly fields;
34
+ private readonly aclLog;
35
+ private readonly clock;
36
+ private readonly roleKey?;
37
+ private readonly roleKeys;
38
+ private readonly opLog;
39
+ private readonly opTokens;
40
+ private readonly verifiedOps;
41
+ private readonly appliedTokens;
42
+ private currentRole;
43
+ private readonly revokedCrdtByField;
44
+ private readonly deleteStampsByField;
45
+ private readonly tombstoneStampsByField;
46
+ private readonly deleteNodeStampsByField;
47
+ private readonly eventListeners;
48
+ private readonly pending;
49
+ private readonly ackByActor;
50
+ private suppressMerge;
51
+ private ackScheduled;
52
+ private lastGcBarrier;
53
+ private snapshotFieldValues;
54
+ readonly acl: {
55
+ setRole: (actorId: string, role: Role) => void;
56
+ getRole: (actorId: string) => Role;
57
+ knownActors: () => string[];
58
+ snapshot: () => AclAssignment[];
59
+ };
60
+ constructor(params: {
61
+ schema: S;
62
+ schemaId: SchemaId;
63
+ docId: string;
64
+ roleKey?: JsonWebKey;
65
+ roleKeys: RolePublicKeys;
66
+ });
67
+ addEventListener<K extends keyof DacumentEventMap>(type: K, listener: (event: DacumentEventMap[K]) => void): void;
68
+ removeEventListener<K extends keyof DacumentEventMap>(type: K, listener: (event: DacumentEventMap[K]) => void): void;
69
+ flush(): Promise<void>;
70
+ snapshot(): DocSnapshot;
71
+ merge(input: SignedOp | SignedOp[] | string | string[]): Promise<{
72
+ accepted: SignedOp[];
73
+ rejected: number;
74
+ }>;
75
+ private rebuildFromVerified;
76
+ private ack;
77
+ private scheduleAck;
78
+ private computeGcBarrier;
79
+ private maybeGc;
80
+ private compactFields;
81
+ private compactListField;
82
+ private compactTombstoneField;
83
+ private setRegisterValue;
84
+ private createFieldView;
85
+ private shadowFor;
86
+ private isRevoked;
87
+ private readCrdt;
88
+ private revokedCrdt;
89
+ private stampMapFor;
90
+ private setMinStamp;
91
+ private recordDeletedNode;
92
+ private recordTombstone;
93
+ private recordDeleteNodeStamp;
94
+ private createTextView;
95
+ private createArrayView;
96
+ private createSetView;
97
+ private createMapView;
98
+ private createRecordView;
99
+ private commitArrayMutation;
100
+ private commitSetMutation;
101
+ private commitMapMutation;
102
+ private commitRecordMutation;
103
+ private capturePatches;
104
+ private queueLocalOp;
105
+ private applyRemotePayload;
106
+ private applyAclPayload;
107
+ private applyRegisterPayload;
108
+ private applyNodePayload;
109
+ private applySetNodes;
110
+ private applyMapNodes;
111
+ private applyRecordNodes;
112
+ private validateDagNodeValues;
113
+ private emitListOps;
114
+ private diffSet;
115
+ private diffMap;
116
+ private diffRecord;
117
+ private emitInvalidationDiffs;
118
+ private emitFieldDiff;
119
+ private emitTextDiff;
120
+ private emitArrayDiff;
121
+ private arrayEquals;
122
+ private commonPrefix;
123
+ private commonSuffix;
124
+ private setRole;
125
+ private recordValue;
126
+ private mapValue;
127
+ private fieldValue;
128
+ private emitEvent;
129
+ private emitMerge;
130
+ private emitRevoked;
131
+ private emitError;
132
+ private canWriteField;
133
+ private canWriteAcl;
134
+ private assertWritable;
135
+ private assertValueType;
136
+ private assertValueArray;
137
+ private assertMapKey;
138
+ private isValidPayload;
139
+ private assertSchemaKeys;
140
+ }
141
+ export type DacumentDoc<S extends SchemaDefinition> = Dacument<S> & DocFieldAccess<S>;