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,2015 @@
1
+ import { Bytes, generateNonce } from "bytecodec";
2
+ import { generateSignPair } from "zeyra";
3
+ import { v7 as uuidv7 } from "uuid";
4
+ import { CRArray } from "../CRArray/class.js";
5
+ import { CRMap } from "../CRMap/class.js";
6
+ import { CRRecord } from "../CRRecord/class.js";
7
+ import { CRRegister } from "../CRRegister/class.js";
8
+ import { CRSet } from "../CRSet/class.js";
9
+ import { CRText } from "../CRText/class.js";
10
+ import { AclLog } from "./acl.js";
11
+ import { HLC, compareHLC } from "./clock.js";
12
+ import { decodeToken, encodeToken, signToken, verifyToken } from "./crypto.js";
13
+ import { array, map, record, register, set, text, isJsValue, isValueOfType, schemaIdInput, } from "./types.js";
14
+ const TOKEN_TYP = "DACOP";
15
+ function nowSeconds() {
16
+ return Math.floor(Date.now() / 1000);
17
+ }
18
+ function isObject(value) {
19
+ return typeof value === "object" && value !== null;
20
+ }
21
+ function isStringArray(value) {
22
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
23
+ }
24
+ function stableKey(value) {
25
+ if (value === null)
26
+ return "null";
27
+ if (Array.isArray(value))
28
+ return `[${value.map((entry) => stableKey(entry)).join(",")}]`;
29
+ if (typeof value === "object") {
30
+ const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
31
+ const body = entries
32
+ .map(([key, val]) => `${JSON.stringify(key)}:${stableKey(val)}`)
33
+ .join(",");
34
+ return `{${body}}`;
35
+ }
36
+ return JSON.stringify(value);
37
+ }
38
+ function isDagNode(node) {
39
+ if (!isObject(node))
40
+ return false;
41
+ if (typeof node.id !== "string")
42
+ return false;
43
+ if (!Array.isArray(node.after) || !node.after.every((id) => typeof id === "string"))
44
+ return false;
45
+ if (node.deleted !== undefined && typeof node.deleted !== "boolean")
46
+ return false;
47
+ return true;
48
+ }
49
+ function isAclPatch(value) {
50
+ if (!isObject(value))
51
+ return false;
52
+ if (typeof value.id !== "string")
53
+ return false;
54
+ if (typeof value.target !== "string")
55
+ return false;
56
+ if (typeof value.role !== "string")
57
+ return false;
58
+ return true;
59
+ }
60
+ function isAckPatch(value) {
61
+ if (!isObject(value))
62
+ return false;
63
+ if (!isObject(value.seen))
64
+ return false;
65
+ const seen = value.seen;
66
+ return (typeof seen.wallTimeMs === "number" &&
67
+ typeof seen.logical === "number" &&
68
+ typeof seen.clockId === "string");
69
+ }
70
+ function isPatchEnvelope(value) {
71
+ return isObject(value) && Array.isArray(value.nodes);
72
+ }
73
+ function indexMapForNodes(nodes) {
74
+ const map = new Map();
75
+ let aliveIndex = 0;
76
+ for (const node of nodes) {
77
+ map.set(node.id, aliveIndex);
78
+ if (!node.deleted)
79
+ aliveIndex += 1;
80
+ }
81
+ return map;
82
+ }
83
+ function createEmptyField(crdt) {
84
+ switch (crdt.crdt) {
85
+ case "register":
86
+ return new CRRegister();
87
+ case "text":
88
+ return new CRText();
89
+ case "array":
90
+ return new CRArray();
91
+ case "map":
92
+ return new CRMap({ key: crdt.key });
93
+ case "set":
94
+ return new CRSet({ key: crdt.key });
95
+ case "record":
96
+ return new CRRecord();
97
+ }
98
+ }
99
+ function roleNeedsKey(role) {
100
+ return role === "owner" || role === "manager" || role === "editor";
101
+ }
102
+ function parseSignerRole(kid, issuer) {
103
+ if (!kid)
104
+ return null;
105
+ const [kidIssuer, role] = kid.split(":");
106
+ if (kidIssuer !== issuer)
107
+ return null;
108
+ if (role === "owner" || role === "manager" || role === "editor")
109
+ return role;
110
+ return null;
111
+ }
112
+ async function generateRoleKeys() {
113
+ const ownerPair = await generateSignPair();
114
+ const managerPair = await generateSignPair();
115
+ const editorPair = await generateSignPair();
116
+ return {
117
+ owner: { privateKey: ownerPair.signingJwk, publicKey: ownerPair.verificationJwk },
118
+ manager: { privateKey: managerPair.signingJwk, publicKey: managerPair.verificationJwk },
119
+ editor: { privateKey: editorPair.signingJwk, publicKey: editorPair.verificationJwk },
120
+ };
121
+ }
122
+ function toPublicRoleKeys(roleKeys) {
123
+ return {
124
+ owner: roleKeys.owner.publicKey,
125
+ manager: roleKeys.manager.publicKey,
126
+ editor: roleKeys.editor.publicKey,
127
+ };
128
+ }
129
+ export class Dacument {
130
+ static actorId;
131
+ static setActorId(actorId) {
132
+ if (Dacument.actorId)
133
+ return;
134
+ if (!Dacument.isValidActorId(actorId))
135
+ throw new Error("Dacument.setActorId: actorId must be 256-bit base64url");
136
+ Dacument.actorId = actorId;
137
+ }
138
+ static requireActorId() {
139
+ if (!Dacument.actorId)
140
+ throw new Error("Dacument: actorId not set; call Dacument.setActorId()");
141
+ return Dacument.actorId;
142
+ }
143
+ static isValidActorId(actorId) {
144
+ if (typeof actorId !== "string")
145
+ return false;
146
+ try {
147
+ const bytes = Bytes.fromBase64UrlString(actorId);
148
+ return bytes.byteLength === 32 && actorId.length === 43;
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ }
154
+ static schema = (schema) => {
155
+ Dacument.requireActorId();
156
+ return schema;
157
+ };
158
+ static register = register;
159
+ static text = text;
160
+ static array = array;
161
+ static set = set;
162
+ static map = map;
163
+ static record = record;
164
+ static async computeSchemaId(schema) {
165
+ const normalized = schemaIdInput(schema);
166
+ const sortedKeys = Object.keys(normalized).sort();
167
+ const ordered = {};
168
+ for (const key of sortedKeys)
169
+ ordered[key] = normalized[key];
170
+ const json = JSON.stringify(ordered);
171
+ const data = new Uint8Array(Bytes.fromString(json));
172
+ const digest = await crypto.subtle.digest("SHA-256", data);
173
+ return Bytes.toBase64UrlString(new Uint8Array(digest));
174
+ }
175
+ static async create(params) {
176
+ const ownerId = Dacument.requireActorId();
177
+ const docId = params.docId ?? generateNonce();
178
+ const schemaId = await Dacument.computeSchemaId(params.schema);
179
+ const roleKeys = await generateRoleKeys();
180
+ const publicKeys = toPublicRoleKeys(roleKeys);
181
+ const clock = new HLC(ownerId);
182
+ const header = {
183
+ alg: "ES256",
184
+ typ: TOKEN_TYP,
185
+ kid: `${ownerId}:owner`,
186
+ };
187
+ const ops = [];
188
+ const capturePatches = (subscribe, mutate) => {
189
+ const patches = [];
190
+ const stop = subscribe((nodes) => patches.push(...nodes));
191
+ try {
192
+ mutate();
193
+ }
194
+ finally {
195
+ stop();
196
+ }
197
+ return patches;
198
+ };
199
+ const sign = async (payload) => {
200
+ const token = await signToken(roleKeys.owner.privateKey, header, payload);
201
+ ops.push({ token });
202
+ };
203
+ await sign({
204
+ iss: ownerId,
205
+ sub: docId,
206
+ iat: nowSeconds(),
207
+ stamp: clock.next(),
208
+ kind: "acl.set",
209
+ schema: schemaId,
210
+ patch: {
211
+ id: uuidv7(),
212
+ target: ownerId,
213
+ role: "owner",
214
+ },
215
+ });
216
+ for (const [field, schema] of Object.entries(params.schema)) {
217
+ if (schema.crdt === "register") {
218
+ if (schema.initial === undefined)
219
+ continue;
220
+ if (!isValueOfType(schema.initial, schema.jsType))
221
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
222
+ if (schema.regex &&
223
+ typeof schema.initial === "string" &&
224
+ !schema.regex.test(schema.initial))
225
+ throw new Error(`Dacument.create: '${field}' failed regex`);
226
+ await sign({
227
+ iss: ownerId,
228
+ sub: docId,
229
+ iat: nowSeconds(),
230
+ stamp: clock.next(),
231
+ kind: "register.set",
232
+ schema: schemaId,
233
+ field,
234
+ patch: { value: schema.initial },
235
+ });
236
+ continue;
237
+ }
238
+ if (schema.crdt === "text") {
239
+ const initial = schema.initial ?? "";
240
+ if (typeof initial !== "string")
241
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
242
+ if (!initial)
243
+ continue;
244
+ const crdt = new CRText();
245
+ const nodes = capturePatches((listener) => crdt.onChange(listener), () => {
246
+ for (const char of initial)
247
+ crdt.insertAt(crdt.length, char);
248
+ });
249
+ if (nodes.length)
250
+ await sign({
251
+ iss: ownerId,
252
+ sub: docId,
253
+ iat: nowSeconds(),
254
+ stamp: clock.next(),
255
+ kind: "text.patch",
256
+ schema: schemaId,
257
+ field,
258
+ patch: { nodes },
259
+ });
260
+ continue;
261
+ }
262
+ if (schema.crdt === "array") {
263
+ const initial = schema.initial ?? [];
264
+ if (!Array.isArray(initial))
265
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
266
+ if (initial.length === 0)
267
+ continue;
268
+ for (const value of initial) {
269
+ if (!isValueOfType(value, schema.jsType))
270
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
271
+ }
272
+ const crdt = new CRArray();
273
+ const nodes = capturePatches((listener) => crdt.onChange(listener), () => {
274
+ crdt.push(...initial);
275
+ });
276
+ if (nodes.length)
277
+ await sign({
278
+ iss: ownerId,
279
+ sub: docId,
280
+ iat: nowSeconds(),
281
+ stamp: clock.next(),
282
+ kind: "array.patch",
283
+ schema: schemaId,
284
+ field,
285
+ patch: { nodes },
286
+ });
287
+ continue;
288
+ }
289
+ if (schema.crdt === "set") {
290
+ const initial = schema.initial ?? [];
291
+ if (!Array.isArray(initial))
292
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
293
+ if (initial.length === 0)
294
+ continue;
295
+ for (const value of initial) {
296
+ if (!isValueOfType(value, schema.jsType))
297
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
298
+ }
299
+ const crdt = new CRSet({
300
+ key: schema.key,
301
+ });
302
+ const nodes = capturePatches((listener) => crdt.onChange(listener), () => {
303
+ for (const value of initial)
304
+ crdt.add(value);
305
+ });
306
+ if (nodes.length)
307
+ await sign({
308
+ iss: ownerId,
309
+ sub: docId,
310
+ iat: nowSeconds(),
311
+ stamp: clock.next(),
312
+ kind: "set.patch",
313
+ schema: schemaId,
314
+ field,
315
+ patch: { nodes },
316
+ });
317
+ continue;
318
+ }
319
+ if (schema.crdt === "map") {
320
+ const initial = schema.initial ?? [];
321
+ if (!Array.isArray(initial))
322
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
323
+ if (initial.length === 0)
324
+ continue;
325
+ for (const entry of initial) {
326
+ if (!Array.isArray(entry) || entry.length !== 2)
327
+ throw new Error(`Dacument.create: invalid initial entry for '${field}'`);
328
+ const [key, value] = entry;
329
+ if (!isJsValue(key))
330
+ throw new Error(`Dacument.create: map key for '${field}' must be JSON-compatible`);
331
+ if (!isValueOfType(value, schema.jsType))
332
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
333
+ }
334
+ const crdt = new CRMap({
335
+ key: schema.key,
336
+ });
337
+ const nodes = capturePatches((listener) => crdt.onChange(listener), () => {
338
+ for (const [key, value] of initial)
339
+ crdt.set(key, value);
340
+ });
341
+ if (nodes.length)
342
+ await sign({
343
+ iss: ownerId,
344
+ sub: docId,
345
+ iat: nowSeconds(),
346
+ stamp: clock.next(),
347
+ kind: "map.patch",
348
+ schema: schemaId,
349
+ field,
350
+ patch: { nodes },
351
+ });
352
+ continue;
353
+ }
354
+ if (schema.crdt === "record") {
355
+ const initial = schema.initial ?? {};
356
+ if (!isObject(initial) || Array.isArray(initial))
357
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
358
+ const props = Object.keys(initial);
359
+ if (props.length === 0)
360
+ continue;
361
+ for (const prop of props) {
362
+ const value = initial[prop];
363
+ if (!isValueOfType(value, schema.jsType))
364
+ throw new Error(`Dacument.create: invalid initial value for '${field}'`);
365
+ }
366
+ const crdt = new CRRecord();
367
+ const nodes = capturePatches((listener) => crdt.onChange(listener), () => {
368
+ for (const prop of props)
369
+ crdt[prop] = initial[prop];
370
+ });
371
+ if (nodes.length)
372
+ await sign({
373
+ iss: ownerId,
374
+ sub: docId,
375
+ iat: nowSeconds(),
376
+ stamp: clock.next(),
377
+ kind: "record.patch",
378
+ schema: schemaId,
379
+ field,
380
+ patch: { nodes },
381
+ });
382
+ continue;
383
+ }
384
+ }
385
+ const snapshot = {
386
+ docId,
387
+ roleKeys: publicKeys,
388
+ ops,
389
+ };
390
+ return { docId, schemaId, roleKeys, snapshot };
391
+ }
392
+ static async load(params) {
393
+ const actorId = Dacument.requireActorId();
394
+ const schemaId = await Dacument.computeSchemaId(params.schema);
395
+ const doc = new Dacument({
396
+ schema: params.schema,
397
+ schemaId,
398
+ docId: params.snapshot.docId,
399
+ roleKey: params.roleKey,
400
+ roleKeys: params.snapshot.roleKeys,
401
+ });
402
+ await doc.merge(params.snapshot.ops);
403
+ return doc;
404
+ }
405
+ docId;
406
+ actorId;
407
+ schema;
408
+ schemaId;
409
+ fields = new Map();
410
+ aclLog = new AclLog();
411
+ clock;
412
+ roleKey;
413
+ roleKeys;
414
+ opLog = [];
415
+ opTokens = new Set();
416
+ verifiedOps = new Map();
417
+ appliedTokens = new Set();
418
+ currentRole;
419
+ revokedCrdtByField = new Map();
420
+ deleteStampsByField = new Map();
421
+ tombstoneStampsByField = new Map();
422
+ deleteNodeStampsByField = new Map();
423
+ eventListeners = new Map();
424
+ pending = new Set();
425
+ ackByActor = new Map();
426
+ suppressMerge = false;
427
+ ackScheduled = false;
428
+ lastGcBarrier = null;
429
+ snapshotFieldValues() {
430
+ const values = new Map();
431
+ for (const key of this.fields.keys())
432
+ values.set(key, this.fieldValue(key));
433
+ return values;
434
+ }
435
+ acl;
436
+ constructor(params) {
437
+ const actorId = Dacument.requireActorId();
438
+ this.schema = params.schema;
439
+ this.schemaId = params.schemaId;
440
+ this.docId = params.docId;
441
+ this.actorId = actorId;
442
+ this.roleKey = params.roleKey;
443
+ this.roleKeys = params.roleKeys;
444
+ this.clock = new HLC(this.actorId);
445
+ this.assertSchemaKeys();
446
+ for (const [key, schema] of Object.entries(this.schema)) {
447
+ const crdt = createEmptyField(schema);
448
+ this.fields.set(key, { schema, crdt });
449
+ }
450
+ this.acl = {
451
+ setRole: (actorId, role) => this.setRole(actorId, role),
452
+ getRole: (actorId) => this.aclLog.currentRole(actorId),
453
+ knownActors: () => this.aclLog.knownActors(),
454
+ snapshot: () => this.aclLog.snapshot(),
455
+ };
456
+ this.currentRole = this.aclLog.currentRole(this.actorId);
457
+ return new Proxy(this, {
458
+ get: (target, property, receiver) => {
459
+ if (typeof property !== "string")
460
+ return Reflect.get(target, property, receiver);
461
+ if (property in target)
462
+ return Reflect.get(target, property, receiver);
463
+ if (!target.fields.has(property))
464
+ return undefined;
465
+ const field = target.fields.get(property);
466
+ if (field.schema.crdt === "register") {
467
+ const crdt = target.readCrdt(property, field);
468
+ return crdt.get();
469
+ }
470
+ if (!field.view)
471
+ field.view = target.createFieldView(property, field);
472
+ return field.view;
473
+ },
474
+ set: (target, property, value, receiver) => {
475
+ if (typeof property !== "string")
476
+ return Reflect.set(target, property, value, receiver);
477
+ if (property in target)
478
+ return Reflect.set(target, property, value, receiver);
479
+ const field = target.fields.get(property);
480
+ if (!field)
481
+ throw new Error(`Dacument: unknown field '${property}'`);
482
+ if (field.schema.crdt !== "register")
483
+ throw new Error(`Dacument: field '${property}' is read-only`);
484
+ target.setRegisterValue(property, value);
485
+ return true;
486
+ },
487
+ has: (target, property) => {
488
+ if (typeof property !== "string")
489
+ return Reflect.has(target, property);
490
+ if (property in target)
491
+ return true;
492
+ return target.fields.has(property);
493
+ },
494
+ ownKeys: (target) => [...target.fields.keys()],
495
+ getOwnPropertyDescriptor: (target, property) => {
496
+ if (typeof property !== "string")
497
+ return Reflect.getOwnPropertyDescriptor(target, property);
498
+ if (target.fields.has(property))
499
+ return { configurable: true, enumerable: true };
500
+ return Reflect.getOwnPropertyDescriptor(target, property);
501
+ },
502
+ deleteProperty: () => false,
503
+ });
504
+ }
505
+ addEventListener(type, listener) {
506
+ const listeners = this.eventListeners.get(type) ??
507
+ new Set();
508
+ listeners.add(listener);
509
+ this.eventListeners.set(type, listeners);
510
+ }
511
+ removeEventListener(type, listener) {
512
+ const listeners = this.eventListeners.get(type);
513
+ if (!listeners)
514
+ return;
515
+ listeners.delete(listener);
516
+ if (listeners.size === 0)
517
+ this.eventListeners.delete(type);
518
+ }
519
+ async flush() {
520
+ await Promise.all([...this.pending]);
521
+ }
522
+ snapshot() {
523
+ if (this.isRevoked())
524
+ throw new Error("Dacument: revoked actors cannot snapshot");
525
+ return {
526
+ docId: this.docId,
527
+ roleKeys: this.roleKeys,
528
+ ops: this.opLog.slice(),
529
+ };
530
+ }
531
+ async merge(input) {
532
+ const tokens = Array.isArray(input) ? input : [input];
533
+ const decodedOps = [];
534
+ const accepted = [];
535
+ let rejected = 0;
536
+ let sawNewToken = false;
537
+ let diffActor = null;
538
+ let diffStamp = null;
539
+ for (const item of tokens) {
540
+ const token = typeof item === "string" ? item : item.token;
541
+ const decoded = decodeToken(token);
542
+ if (!decoded) {
543
+ rejected++;
544
+ continue;
545
+ }
546
+ const payload = decoded.payload;
547
+ if (!this.isValidPayload(payload)) {
548
+ rejected++;
549
+ continue;
550
+ }
551
+ if (payload.sub !== this.docId || payload.schema !== this.schemaId) {
552
+ rejected++;
553
+ continue;
554
+ }
555
+ const isUnsignedAck = decoded.header.alg === "none" &&
556
+ payload.kind === "ack" &&
557
+ decoded.header.typ === TOKEN_TYP;
558
+ if (decoded.header.alg === "none" && !isUnsignedAck) {
559
+ rejected++;
560
+ continue;
561
+ }
562
+ if (payload.kind === "ack" && decoded.header.alg !== "none") {
563
+ rejected++;
564
+ continue;
565
+ }
566
+ let stored = this.verifiedOps.get(token);
567
+ if (!stored) {
568
+ if (isUnsignedAck) {
569
+ stored = { payload, signerRole: null };
570
+ }
571
+ else {
572
+ const signerRole = parseSignerRole(decoded.header.kid, payload.iss);
573
+ if (!signerRole) {
574
+ rejected++;
575
+ continue;
576
+ }
577
+ const publicKey = this.roleKeys[signerRole];
578
+ const verified = await verifyToken(publicKey, token, TOKEN_TYP);
579
+ if (!verified) {
580
+ rejected++;
581
+ continue;
582
+ }
583
+ stored = { payload, signerRole };
584
+ }
585
+ this.verifiedOps.set(token, stored);
586
+ if (!this.opTokens.has(token)) {
587
+ this.opTokens.add(token);
588
+ this.opLog.push({ token });
589
+ }
590
+ sawNewToken = true;
591
+ if (payload.kind === "acl.set") {
592
+ if (!diffStamp || compareHLC(payload.stamp, diffStamp) > 0) {
593
+ diffStamp = payload.stamp;
594
+ diffActor = payload.iss;
595
+ }
596
+ }
597
+ }
598
+ decodedOps.push({
599
+ token,
600
+ payload: stored.payload,
601
+ signerRole: stored.signerRole,
602
+ });
603
+ }
604
+ const prevRole = this.currentRole;
605
+ let appliedNonAck = false;
606
+ if (sawNewToken) {
607
+ const beforeValues = this.isRevoked() ? undefined : this.snapshotFieldValues();
608
+ const result = this.rebuildFromVerified(new Set(this.appliedTokens), {
609
+ beforeValues,
610
+ diffActor: diffActor ?? this.actorId,
611
+ });
612
+ appliedNonAck = result.appliedNonAck;
613
+ }
614
+ for (const { token } of decodedOps) {
615
+ if (this.appliedTokens.has(token)) {
616
+ accepted.push({ token });
617
+ }
618
+ else {
619
+ rejected++;
620
+ }
621
+ }
622
+ const nextRole = this.currentRole;
623
+ if (nextRole !== prevRole && nextRole === "revoked") {
624
+ const entry = this.aclLog.currentEntry(this.actorId);
625
+ this.emitRevoked(prevRole, entry?.by ?? this.actorId, entry?.stamp ?? this.clock.current);
626
+ }
627
+ if (appliedNonAck)
628
+ this.scheduleAck();
629
+ this.maybeGc();
630
+ return { accepted, rejected };
631
+ }
632
+ rebuildFromVerified(previousApplied, options) {
633
+ const invalidated = new Set(previousApplied);
634
+ let appliedNonAck = false;
635
+ this.aclLog.reset();
636
+ this.ackByActor.clear();
637
+ this.appliedTokens.clear();
638
+ this.deleteStampsByField.clear();
639
+ this.tombstoneStampsByField.clear();
640
+ this.deleteNodeStampsByField.clear();
641
+ this.revokedCrdtByField.clear();
642
+ for (const state of this.fields.values()) {
643
+ state.crdt = createEmptyField(state.schema);
644
+ }
645
+ const ops = [...this.verifiedOps.entries()].map(([token, data]) => ({
646
+ token,
647
+ payload: data.payload,
648
+ signerRole: data.signerRole,
649
+ }));
650
+ ops.sort((left, right) => {
651
+ const cmp = compareHLC(left.payload.stamp, right.payload.stamp);
652
+ if (cmp !== 0)
653
+ return cmp;
654
+ if (left.token === right.token)
655
+ return 0;
656
+ return left.token < right.token ? -1 : 1;
657
+ });
658
+ for (const { token, payload, signerRole } of ops) {
659
+ let allowed = false;
660
+ if (payload.kind === "acl.set") {
661
+ if (!signerRole)
662
+ continue;
663
+ if (this.aclLog.isEmpty() &&
664
+ isAclPatch(payload.patch) &&
665
+ payload.patch.role === "owner" &&
666
+ payload.patch.target === payload.iss &&
667
+ signerRole === "owner") {
668
+ allowed = true;
669
+ }
670
+ else {
671
+ const roleAt = this.aclLog.roleAt(payload.iss, payload.stamp);
672
+ if (roleAt === signerRole &&
673
+ isAclPatch(payload.patch) &&
674
+ this.canWriteAcl(signerRole, payload.patch.role))
675
+ allowed = true;
676
+ }
677
+ }
678
+ else {
679
+ const roleAt = this.aclLog.roleAt(payload.iss, payload.stamp);
680
+ if (payload.kind === "ack") {
681
+ if (roleAt === "revoked")
682
+ continue;
683
+ if (signerRole !== null)
684
+ continue;
685
+ allowed = true;
686
+ }
687
+ else if (signerRole && roleAt === signerRole) {
688
+ if (this.canWriteField(signerRole))
689
+ allowed = true;
690
+ }
691
+ }
692
+ if (!allowed)
693
+ continue;
694
+ const emit = !previousApplied.has(token);
695
+ this.suppressMerge = !emit;
696
+ try {
697
+ const applied = this.applyRemotePayload(payload, signerRole);
698
+ if (!applied)
699
+ continue;
700
+ }
701
+ finally {
702
+ this.suppressMerge = false;
703
+ }
704
+ this.appliedTokens.add(token);
705
+ invalidated.delete(token);
706
+ if (emit && payload.kind !== "ack")
707
+ appliedNonAck = true;
708
+ }
709
+ this.currentRole = this.aclLog.currentRole(this.actorId);
710
+ if (invalidated.size > 0 &&
711
+ options?.beforeValues &&
712
+ options.diffActor &&
713
+ !this.isRevoked()) {
714
+ this.emitInvalidationDiffs(options.beforeValues, options.diffActor);
715
+ }
716
+ return { appliedNonAck };
717
+ }
718
+ ack() {
719
+ const stamp = this.clock.next();
720
+ const role = this.aclLog.roleAt(this.actorId, stamp);
721
+ if (role === "revoked")
722
+ throw new Error("Dacument: revoked actors cannot acknowledge");
723
+ const seen = this.clock.current;
724
+ this.ackByActor.set(this.actorId, seen);
725
+ this.queueLocalOp({
726
+ iss: this.actorId,
727
+ sub: this.docId,
728
+ iat: nowSeconds(),
729
+ stamp,
730
+ kind: "ack",
731
+ schema: this.schemaId,
732
+ patch: { seen },
733
+ }, role);
734
+ }
735
+ scheduleAck() {
736
+ if (this.ackScheduled)
737
+ return;
738
+ if (this.currentRole === "revoked")
739
+ return;
740
+ this.ackScheduled = true;
741
+ queueMicrotask(() => {
742
+ this.ackScheduled = false;
743
+ try {
744
+ this.ack();
745
+ }
746
+ catch (error) {
747
+ this.emitError(error instanceof Error ? error : new Error(String(error)));
748
+ }
749
+ });
750
+ }
751
+ computeGcBarrier() {
752
+ const actors = this.aclLog
753
+ .knownActors()
754
+ .filter((actorId) => this.aclLog.currentRole(actorId) !== "revoked");
755
+ if (actors.length === 0)
756
+ return null;
757
+ let barrier = null;
758
+ for (const actorId of actors) {
759
+ const seen = this.ackByActor.get(actorId);
760
+ if (!seen)
761
+ return null;
762
+ if (!barrier || compareHLC(seen, barrier) < 0)
763
+ barrier = seen;
764
+ }
765
+ return barrier;
766
+ }
767
+ maybeGc() {
768
+ const barrier = this.computeGcBarrier();
769
+ if (!barrier)
770
+ return;
771
+ if (this.lastGcBarrier && compareHLC(barrier, this.lastGcBarrier) <= 0)
772
+ return;
773
+ this.lastGcBarrier = barrier;
774
+ this.compactFields(barrier);
775
+ }
776
+ compactFields(barrier) {
777
+ for (const [field, state] of this.fields.entries()) {
778
+ if (state.schema.crdt === "text" || state.schema.crdt === "array") {
779
+ this.compactListField(field, state, barrier);
780
+ continue;
781
+ }
782
+ if (state.schema.crdt === "set" ||
783
+ state.schema.crdt === "map" ||
784
+ state.schema.crdt === "record") {
785
+ this.compactTombstoneField(field, state, barrier);
786
+ }
787
+ }
788
+ }
789
+ compactListField(field, state, barrier) {
790
+ const deleteMap = this.deleteStampsByField.get(field);
791
+ if (!deleteMap || deleteMap.size === 0)
792
+ return;
793
+ const removable = new Set();
794
+ for (const [nodeId, stamp] of deleteMap.entries()) {
795
+ if (compareHLC(stamp, barrier) <= 0)
796
+ removable.add(nodeId);
797
+ }
798
+ if (removable.size === 0)
799
+ return;
800
+ const crdt = state.crdt;
801
+ const snapshot = crdt.snapshot();
802
+ const filtered = snapshot.filter((node) => !(node.deleted && removable.has(node.id)));
803
+ if (filtered.length === snapshot.length)
804
+ return;
805
+ state.crdt =
806
+ state.schema.crdt === "text"
807
+ ? new CRText(filtered)
808
+ : new CRArray(filtered);
809
+ for (const nodeId of removable)
810
+ deleteMap.delete(nodeId);
811
+ }
812
+ compactTombstoneField(field, state, barrier) {
813
+ const tombstoneMap = this.tombstoneStampsByField.get(field);
814
+ const deleteNodeMap = this.deleteNodeStampsByField.get(field);
815
+ if (!tombstoneMap || tombstoneMap.size === 0 || !deleteNodeMap)
816
+ return;
817
+ const removableTags = new Set();
818
+ for (const [tagId, stamp] of tombstoneMap.entries()) {
819
+ if (compareHLC(stamp, barrier) <= 0)
820
+ removableTags.add(tagId);
821
+ }
822
+ if (removableTags.size === 0)
823
+ return;
824
+ const snapshot = state.crdt.snapshot();
825
+ const filtered = [];
826
+ const remainingDeletes = new Map();
827
+ const remainingTombstones = new Map();
828
+ for (const node of snapshot) {
829
+ if ("op" in node && node.op === "add") {
830
+ if (removableTags.has(node.id))
831
+ continue;
832
+ filtered.push(node);
833
+ continue;
834
+ }
835
+ if ("op" in node && node.op === "set") {
836
+ if (removableTags.has(node.id))
837
+ continue;
838
+ filtered.push(node);
839
+ continue;
840
+ }
841
+ if ("op" in node && (node.op === "rem" || node.op === "del")) {
842
+ const stamp = deleteNodeMap.get(node.id);
843
+ const targets = node.targets;
844
+ const allTargetsRemovable = targets.every((target) => removableTags.has(target));
845
+ if (stamp &&
846
+ compareHLC(stamp, barrier) <= 0 &&
847
+ allTargetsRemovable) {
848
+ continue;
849
+ }
850
+ filtered.push(node);
851
+ if (stamp) {
852
+ remainingDeletes.set(node.id, stamp);
853
+ for (const target of targets) {
854
+ const existing = remainingTombstones.get(target);
855
+ if (!existing || compareHLC(stamp, existing) < 0)
856
+ remainingTombstones.set(target, stamp);
857
+ }
858
+ }
859
+ continue;
860
+ }
861
+ filtered.push(node);
862
+ }
863
+ if (filtered.length === snapshot.length)
864
+ return;
865
+ if (state.schema.crdt === "set") {
866
+ state.crdt = new CRSet({
867
+ snapshot: filtered,
868
+ key: state.schema.key,
869
+ });
870
+ }
871
+ else if (state.schema.crdt === "map") {
872
+ state.crdt = new CRMap({
873
+ snapshot: filtered,
874
+ key: state.schema.key,
875
+ });
876
+ }
877
+ else {
878
+ state.crdt = new CRRecord(filtered);
879
+ }
880
+ this.deleteNodeStampsByField.set(field, remainingDeletes);
881
+ this.tombstoneStampsByField.set(field, remainingTombstones);
882
+ }
883
+ setRegisterValue(field, value) {
884
+ const state = this.fields.get(field);
885
+ if (!state)
886
+ throw new Error(`Dacument: unknown field '${field}'`);
887
+ const schema = state.schema;
888
+ if (schema.crdt !== "register")
889
+ throw new Error(`Dacument: field '${field}' is not a register`);
890
+ if (!isValueOfType(value, schema.jsType))
891
+ throw new Error(`Dacument: invalid value for '${field}'`);
892
+ if (schema.regex && typeof value === "string" && !schema.regex.test(value))
893
+ throw new Error(`Dacument: '${field}' failed regex`);
894
+ const stamp = this.clock.next();
895
+ const role = this.aclLog.roleAt(this.actorId, stamp);
896
+ if (!this.canWriteField(role))
897
+ throw new Error(`Dacument: role '${role}' cannot write '${field}'`);
898
+ this.queueLocalOp({
899
+ iss: this.actorId,
900
+ sub: this.docId,
901
+ iat: nowSeconds(),
902
+ stamp,
903
+ kind: "register.set",
904
+ schema: this.schemaId,
905
+ field,
906
+ patch: { value },
907
+ }, role);
908
+ }
909
+ createFieldView(field, state) {
910
+ switch (state.schema.crdt) {
911
+ case "text":
912
+ return this.createTextView(field, state);
913
+ case "array":
914
+ return this.createArrayView(field, state);
915
+ case "set":
916
+ return this.createSetView(field, state);
917
+ case "map":
918
+ return this.createMapView(field, state);
919
+ case "record":
920
+ return this.createRecordView(field, state);
921
+ default:
922
+ return undefined;
923
+ }
924
+ }
925
+ shadowFor(field, state) {
926
+ const snapshot = state.crdt.snapshot?.();
927
+ const cloned = snapshot ? structuredClone(snapshot) : undefined;
928
+ switch (state.schema.crdt) {
929
+ case "text":
930
+ return new CRText(cloned);
931
+ case "array":
932
+ return new CRArray(cloned);
933
+ case "set":
934
+ return new CRSet({
935
+ snapshot: cloned,
936
+ key: state.schema.key,
937
+ });
938
+ case "map":
939
+ return new CRMap({
940
+ snapshot: cloned,
941
+ key: state.schema.key,
942
+ });
943
+ case "record":
944
+ return new CRRecord(cloned);
945
+ case "register": {
946
+ const reg = new CRRegister();
947
+ if (cloned && Array.isArray(cloned))
948
+ reg.merge(cloned);
949
+ return reg;
950
+ }
951
+ default:
952
+ throw new Error(`Dacument: unknown field '${field}'`);
953
+ }
954
+ }
955
+ isRevoked() {
956
+ return this.currentRole === "revoked";
957
+ }
958
+ readCrdt(field, state) {
959
+ if (!this.isRevoked())
960
+ return state.crdt;
961
+ return this.revokedCrdt(field, state);
962
+ }
963
+ revokedCrdt(field, state) {
964
+ const existing = this.revokedCrdtByField.get(field);
965
+ if (existing)
966
+ return existing;
967
+ const schema = state.schema;
968
+ let crdt;
969
+ switch (schema.crdt) {
970
+ case "register": {
971
+ const reg = new CRRegister();
972
+ if (schema.initial !== undefined)
973
+ reg.set(schema.initial);
974
+ crdt = reg;
975
+ break;
976
+ }
977
+ case "text": {
978
+ const text = new CRText();
979
+ const initial = typeof schema.initial === "string" ? schema.initial : "";
980
+ for (const char of initial)
981
+ text.insertAt(text.length, char);
982
+ crdt = text;
983
+ break;
984
+ }
985
+ case "array": {
986
+ const arr = new CRArray();
987
+ const initial = Array.isArray(schema.initial) ? schema.initial : [];
988
+ if (initial.length)
989
+ arr.push(...initial);
990
+ crdt = arr;
991
+ break;
992
+ }
993
+ case "set": {
994
+ const setCrdt = new CRSet({
995
+ key: schema.key,
996
+ });
997
+ const initial = Array.isArray(schema.initial) ? schema.initial : [];
998
+ for (const value of initial)
999
+ setCrdt.add(value);
1000
+ crdt = setCrdt;
1001
+ break;
1002
+ }
1003
+ case "map": {
1004
+ const mapCrdt = new CRMap({
1005
+ key: schema.key,
1006
+ });
1007
+ const initial = Array.isArray(schema.initial) ? schema.initial : [];
1008
+ for (const entry of initial) {
1009
+ if (!Array.isArray(entry) || entry.length !== 2)
1010
+ continue;
1011
+ const [key, value] = entry;
1012
+ mapCrdt.set(key, value);
1013
+ }
1014
+ crdt = mapCrdt;
1015
+ break;
1016
+ }
1017
+ case "record": {
1018
+ const recordCrdt = new CRRecord();
1019
+ const initial = schema.initial && isObject(schema.initial) && !Array.isArray(schema.initial)
1020
+ ? schema.initial
1021
+ : {};
1022
+ for (const [prop, value] of Object.entries(initial))
1023
+ recordCrdt[prop] = value;
1024
+ crdt = recordCrdt;
1025
+ break;
1026
+ }
1027
+ default:
1028
+ throw new Error(`Dacument: unknown field '${field}'`);
1029
+ }
1030
+ this.revokedCrdtByField.set(field, crdt);
1031
+ return crdt;
1032
+ }
1033
+ stampMapFor(map, field) {
1034
+ const existing = map.get(field);
1035
+ if (existing)
1036
+ return existing;
1037
+ const created = new Map();
1038
+ map.set(field, created);
1039
+ return created;
1040
+ }
1041
+ setMinStamp(map, id, stamp) {
1042
+ const existing = map.get(id);
1043
+ if (!existing || compareHLC(stamp, existing) < 0)
1044
+ map.set(id, stamp);
1045
+ }
1046
+ recordDeletedNode(field, nodeId, stamp) {
1047
+ const map = this.stampMapFor(this.deleteStampsByField, field);
1048
+ this.setMinStamp(map, nodeId, stamp);
1049
+ }
1050
+ recordTombstone(field, tagId, stamp) {
1051
+ const map = this.stampMapFor(this.tombstoneStampsByField, field);
1052
+ this.setMinStamp(map, tagId, stamp);
1053
+ }
1054
+ recordDeleteNodeStamp(field, nodeId, stamp) {
1055
+ const map = this.stampMapFor(this.deleteNodeStampsByField, field);
1056
+ this.setMinStamp(map, nodeId, stamp);
1057
+ }
1058
+ createTextView(field, state) {
1059
+ const doc = this;
1060
+ const readCrdt = () => doc.readCrdt(field, state);
1061
+ return {
1062
+ get length() {
1063
+ return readCrdt().length;
1064
+ },
1065
+ toString() {
1066
+ return readCrdt().toString();
1067
+ },
1068
+ at(index) {
1069
+ return readCrdt().at(index);
1070
+ },
1071
+ insertAt(index, value) {
1072
+ doc.assertValueType(field, value);
1073
+ const stamp = doc.clock.next();
1074
+ const role = doc.aclLog.roleAt(doc.actorId, stamp);
1075
+ doc.assertWritable(field, role);
1076
+ const shadow = doc.shadowFor(field, state);
1077
+ const { patches, result } = doc.capturePatches((listener) => shadow.onChange(listener), () => shadow.insertAt(index, value));
1078
+ if (patches.length === 0)
1079
+ return result;
1080
+ doc.queueLocalOp({
1081
+ iss: doc.actorId,
1082
+ sub: doc.docId,
1083
+ iat: nowSeconds(),
1084
+ stamp,
1085
+ kind: "text.patch",
1086
+ schema: doc.schemaId,
1087
+ field,
1088
+ patch: { nodes: patches },
1089
+ }, role);
1090
+ return result;
1091
+ },
1092
+ deleteAt(index) {
1093
+ const stamp = doc.clock.next();
1094
+ const role = doc.aclLog.roleAt(doc.actorId, stamp);
1095
+ doc.assertWritable(field, role);
1096
+ const shadow = doc.shadowFor(field, state);
1097
+ const { patches, result } = doc.capturePatches((listener) => shadow.onChange(listener), () => shadow.deleteAt(index));
1098
+ if (patches.length === 0)
1099
+ return result;
1100
+ doc.queueLocalOp({
1101
+ iss: doc.actorId,
1102
+ sub: doc.docId,
1103
+ iat: nowSeconds(),
1104
+ stamp,
1105
+ kind: "text.patch",
1106
+ schema: doc.schemaId,
1107
+ field,
1108
+ patch: { nodes: patches },
1109
+ }, role);
1110
+ return result;
1111
+ },
1112
+ [Symbol.iterator]() {
1113
+ return readCrdt().toString()[Symbol.iterator]();
1114
+ },
1115
+ };
1116
+ }
1117
+ createArrayView(field, state) {
1118
+ const doc = this;
1119
+ const readCrdt = () => doc.readCrdt(field, state);
1120
+ return {
1121
+ get length() {
1122
+ return readCrdt().length;
1123
+ },
1124
+ at(index) {
1125
+ return readCrdt().at(index);
1126
+ },
1127
+ slice(start, end) {
1128
+ return readCrdt().slice(start, end);
1129
+ },
1130
+ push(...items) {
1131
+ doc.assertValueArray(field, items);
1132
+ return doc.commitArrayMutation(field, (shadow) => shadow.push(...items));
1133
+ },
1134
+ unshift(...items) {
1135
+ doc.assertValueArray(field, items);
1136
+ return doc.commitArrayMutation(field, (shadow) => shadow.unshift(...items));
1137
+ },
1138
+ pop() {
1139
+ return doc.commitArrayMutation(field, (shadow) => shadow.pop());
1140
+ },
1141
+ shift() {
1142
+ return doc.commitArrayMutation(field, (shadow) => shadow.shift());
1143
+ },
1144
+ setAt(index, value) {
1145
+ doc.assertValueType(field, value);
1146
+ return doc.commitArrayMutation(field, (shadow) => shadow.setAt(index, value));
1147
+ },
1148
+ map(callback, thisArg) {
1149
+ return readCrdt().map(callback, thisArg);
1150
+ },
1151
+ filter(callback, thisArg) {
1152
+ return readCrdt().filter(callback, thisArg);
1153
+ },
1154
+ reduce(callback, initialValue) {
1155
+ return readCrdt().reduce(callback, initialValue);
1156
+ },
1157
+ forEach(callback, thisArg) {
1158
+ return readCrdt().forEach(callback, thisArg);
1159
+ },
1160
+ includes(value) {
1161
+ return readCrdt().includes(value);
1162
+ },
1163
+ indexOf(value) {
1164
+ return readCrdt().indexOf(value);
1165
+ },
1166
+ [Symbol.iterator]() {
1167
+ return readCrdt()[Symbol.iterator]();
1168
+ },
1169
+ };
1170
+ }
1171
+ createSetView(field, state) {
1172
+ const doc = this;
1173
+ const readCrdt = () => doc.readCrdt(field, state);
1174
+ return {
1175
+ get size() {
1176
+ return readCrdt().size;
1177
+ },
1178
+ add(value) {
1179
+ doc.assertValueType(field, value);
1180
+ return doc.commitSetMutation(field, (shadow) => shadow.add(value));
1181
+ },
1182
+ delete(value) {
1183
+ return doc.commitSetMutation(field, (shadow) => shadow.delete(value));
1184
+ },
1185
+ clear() {
1186
+ return doc.commitSetMutation(field, (shadow) => shadow.clear());
1187
+ },
1188
+ has(value) {
1189
+ return readCrdt().has(value);
1190
+ },
1191
+ entries() {
1192
+ return readCrdt().entries();
1193
+ },
1194
+ keys() {
1195
+ return readCrdt().keys();
1196
+ },
1197
+ values() {
1198
+ return readCrdt().values();
1199
+ },
1200
+ forEach(callback, thisArg) {
1201
+ return readCrdt().forEach(callback, thisArg);
1202
+ },
1203
+ [Symbol.iterator]() {
1204
+ return readCrdt()[Symbol.iterator]();
1205
+ },
1206
+ get [Symbol.toStringTag]() {
1207
+ return "CRSet";
1208
+ },
1209
+ };
1210
+ }
1211
+ createMapView(field, state) {
1212
+ const doc = this;
1213
+ const readCrdt = () => doc.readCrdt(field, state);
1214
+ return {
1215
+ get size() {
1216
+ return readCrdt().size;
1217
+ },
1218
+ get(key) {
1219
+ return readCrdt().get(key);
1220
+ },
1221
+ set(key, value) {
1222
+ doc.assertMapKey(field, key);
1223
+ doc.assertValueType(field, value);
1224
+ return doc.commitMapMutation(field, (shadow) => shadow.set(key, value));
1225
+ },
1226
+ has(key) {
1227
+ return readCrdt().has(key);
1228
+ },
1229
+ delete(key) {
1230
+ doc.assertMapKey(field, key);
1231
+ return doc.commitMapMutation(field, (shadow) => shadow.delete(key));
1232
+ },
1233
+ clear() {
1234
+ return doc.commitMapMutation(field, (shadow) => shadow.clear());
1235
+ },
1236
+ entries() {
1237
+ return readCrdt().entries();
1238
+ },
1239
+ keys() {
1240
+ return readCrdt().keys();
1241
+ },
1242
+ values() {
1243
+ return readCrdt().values();
1244
+ },
1245
+ forEach(callback, thisArg) {
1246
+ return readCrdt().forEach(callback, thisArg);
1247
+ },
1248
+ [Symbol.iterator]() {
1249
+ return readCrdt()[Symbol.iterator]();
1250
+ },
1251
+ get [Symbol.toStringTag]() {
1252
+ return "CRMap";
1253
+ },
1254
+ };
1255
+ }
1256
+ createRecordView(field, state) {
1257
+ const doc = this;
1258
+ const readCrdt = () => doc.readCrdt(field, state);
1259
+ return new Proxy({}, {
1260
+ get: (target, prop, receiver) => {
1261
+ if (typeof prop !== "string")
1262
+ return Reflect.get(target, prop, receiver);
1263
+ if (prop in target)
1264
+ return Reflect.get(target, prop, receiver);
1265
+ return readCrdt()[prop];
1266
+ },
1267
+ set: (_target, prop, value) => {
1268
+ if (typeof prop !== "string")
1269
+ return false;
1270
+ doc.assertValueType(field, value);
1271
+ doc.commitRecordMutation(field, (shadow) => {
1272
+ shadow[prop] = value;
1273
+ });
1274
+ return true;
1275
+ },
1276
+ deleteProperty: (_target, prop) => {
1277
+ if (typeof prop !== "string")
1278
+ return false;
1279
+ doc.commitRecordMutation(field, (shadow) => {
1280
+ delete shadow[prop];
1281
+ });
1282
+ return true;
1283
+ },
1284
+ has: (_target, prop) => {
1285
+ if (typeof prop !== "string")
1286
+ return false;
1287
+ return prop in readCrdt();
1288
+ },
1289
+ ownKeys: () => Object.keys(readCrdt()),
1290
+ getOwnPropertyDescriptor: (_target, prop) => {
1291
+ if (typeof prop !== "string")
1292
+ return undefined;
1293
+ if (prop in readCrdt())
1294
+ return { enumerable: true, configurable: true };
1295
+ return undefined;
1296
+ },
1297
+ });
1298
+ }
1299
+ commitArrayMutation(field, mutate) {
1300
+ const state = this.fields.get(field);
1301
+ const stamp = this.clock.next();
1302
+ const role = this.aclLog.roleAt(this.actorId, stamp);
1303
+ this.assertWritable(field, role);
1304
+ const shadow = this.shadowFor(field, state);
1305
+ const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
1306
+ if (patches.length === 0)
1307
+ return result;
1308
+ this.queueLocalOp({
1309
+ iss: this.actorId,
1310
+ sub: this.docId,
1311
+ iat: nowSeconds(),
1312
+ stamp,
1313
+ kind: "array.patch",
1314
+ schema: this.schemaId,
1315
+ field,
1316
+ patch: { nodes: patches },
1317
+ }, role);
1318
+ return result;
1319
+ }
1320
+ commitSetMutation(field, mutate) {
1321
+ const state = this.fields.get(field);
1322
+ const stamp = this.clock.next();
1323
+ const role = this.aclLog.roleAt(this.actorId, stamp);
1324
+ this.assertWritable(field, role);
1325
+ const shadow = this.shadowFor(field, state);
1326
+ const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
1327
+ if (patches.length === 0)
1328
+ return result;
1329
+ this.queueLocalOp({
1330
+ iss: this.actorId,
1331
+ sub: this.docId,
1332
+ iat: nowSeconds(),
1333
+ stamp,
1334
+ kind: "set.patch",
1335
+ schema: this.schemaId,
1336
+ field,
1337
+ patch: { nodes: patches },
1338
+ }, role);
1339
+ return result;
1340
+ }
1341
+ commitMapMutation(field, mutate) {
1342
+ const state = this.fields.get(field);
1343
+ const stamp = this.clock.next();
1344
+ const role = this.aclLog.roleAt(this.actorId, stamp);
1345
+ this.assertWritable(field, role);
1346
+ const shadow = this.shadowFor(field, state);
1347
+ const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
1348
+ if (patches.length === 0)
1349
+ return result;
1350
+ this.queueLocalOp({
1351
+ iss: this.actorId,
1352
+ sub: this.docId,
1353
+ iat: nowSeconds(),
1354
+ stamp,
1355
+ kind: "map.patch",
1356
+ schema: this.schemaId,
1357
+ field,
1358
+ patch: { nodes: patches },
1359
+ }, role);
1360
+ return result;
1361
+ }
1362
+ commitRecordMutation(field, mutate) {
1363
+ const state = this.fields.get(field);
1364
+ const stamp = this.clock.next();
1365
+ const role = this.aclLog.roleAt(this.actorId, stamp);
1366
+ this.assertWritable(field, role);
1367
+ const shadow = this.shadowFor(field, state);
1368
+ const { patches, result } = this.capturePatches((listener) => shadow.onChange(listener), () => mutate(shadow));
1369
+ if (patches.length === 0)
1370
+ return;
1371
+ this.queueLocalOp({
1372
+ iss: this.actorId,
1373
+ sub: this.docId,
1374
+ iat: nowSeconds(),
1375
+ stamp,
1376
+ kind: "record.patch",
1377
+ schema: this.schemaId,
1378
+ field,
1379
+ patch: { nodes: patches },
1380
+ }, role);
1381
+ return result;
1382
+ }
1383
+ capturePatches(subscribe, mutate) {
1384
+ const patches = [];
1385
+ const stop = subscribe((nodes) => patches.push(...nodes));
1386
+ let result;
1387
+ try {
1388
+ result = mutate();
1389
+ }
1390
+ finally {
1391
+ stop();
1392
+ }
1393
+ return { patches, result };
1394
+ }
1395
+ queueLocalOp(payload, role) {
1396
+ if (payload.kind === "ack") {
1397
+ const header = { alg: "none", typ: TOKEN_TYP };
1398
+ const token = encodeToken(header, payload);
1399
+ this.emitEvent("change", { type: "change", ops: [{ token }] });
1400
+ return;
1401
+ }
1402
+ if (!roleNeedsKey(role))
1403
+ throw new Error(`Dacument: role '${role}' cannot sign ops`);
1404
+ if (!this.roleKey)
1405
+ throw new Error("Dacument: missing role private key");
1406
+ const header = { alg: "ES256", typ: TOKEN_TYP, kid: `${payload.iss}:${role}` };
1407
+ const promise = signToken(this.roleKey, header, payload)
1408
+ .then((token) => {
1409
+ const op = { token };
1410
+ this.emitEvent("change", { type: "change", ops: [op] });
1411
+ })
1412
+ .catch((error) => this.emitError(error instanceof Error ? error : new Error(String(error))));
1413
+ this.pending.add(promise);
1414
+ promise.finally(() => this.pending.delete(promise));
1415
+ }
1416
+ applyRemotePayload(payload, signerRole) {
1417
+ this.clock.observe(payload.stamp);
1418
+ if (payload.kind === "ack") {
1419
+ if (!isAckPatch(payload.patch))
1420
+ return false;
1421
+ this.ackByActor.set(payload.iss, payload.patch.seen);
1422
+ return true;
1423
+ }
1424
+ if (!signerRole)
1425
+ return false;
1426
+ if (payload.kind === "acl.set") {
1427
+ return this.applyAclPayload(payload, signerRole);
1428
+ }
1429
+ if (!payload.field)
1430
+ return false;
1431
+ const state = this.fields.get(payload.field);
1432
+ if (!state)
1433
+ return false;
1434
+ switch (payload.kind) {
1435
+ case "register.set":
1436
+ return this.applyRegisterPayload(payload, state);
1437
+ case "text.patch":
1438
+ case "array.patch":
1439
+ case "set.patch":
1440
+ case "map.patch":
1441
+ case "record.patch":
1442
+ return this.applyNodePayload(payload, state);
1443
+ default:
1444
+ return false;
1445
+ }
1446
+ }
1447
+ applyAclPayload(payload, signerRole) {
1448
+ if (!isAclPatch(payload.patch))
1449
+ return false;
1450
+ const patch = payload.patch;
1451
+ if (!this.canWriteAcl(signerRole, patch.role))
1452
+ return false;
1453
+ const assignment = {
1454
+ id: patch.id,
1455
+ actorId: patch.target,
1456
+ role: patch.role,
1457
+ stamp: payload.stamp,
1458
+ by: payload.iss,
1459
+ };
1460
+ const accepted = this.aclLog.merge(assignment);
1461
+ if (accepted.length)
1462
+ return true;
1463
+ return false;
1464
+ }
1465
+ applyRegisterPayload(payload, state) {
1466
+ if (!isObject(payload.patch))
1467
+ return false;
1468
+ if (!("value" in payload.patch))
1469
+ return false;
1470
+ const value = payload.patch.value;
1471
+ const schema = state.schema;
1472
+ if (schema.crdt !== "register")
1473
+ return false;
1474
+ if (!isValueOfType(value, schema.jsType))
1475
+ return false;
1476
+ if (schema.regex && typeof value === "string" && !schema.regex.test(value))
1477
+ return false;
1478
+ const crdt = state.crdt;
1479
+ const before = crdt.get();
1480
+ crdt.set(value, payload.stamp);
1481
+ const after = crdt.get();
1482
+ if (Object.is(before, after))
1483
+ return true;
1484
+ this.emitMerge(payload.iss, payload.field, "set", { value: after });
1485
+ return true;
1486
+ }
1487
+ applyNodePayload(payload, state) {
1488
+ if (!isPatchEnvelope(payload.patch))
1489
+ return false;
1490
+ const nodes = payload.patch.nodes;
1491
+ switch (state.schema.crdt) {
1492
+ case "text":
1493
+ case "array": {
1494
+ const typedNodes = nodes.filter(isDagNode);
1495
+ if (typedNodes.length !== nodes.length)
1496
+ return false;
1497
+ if (!this.validateDagNodeValues(typedNodes, state.schema.jsType))
1498
+ return false;
1499
+ const crdt = state.crdt;
1500
+ const beforeNodes = crdt.snapshot();
1501
+ const beforeIndex = indexMapForNodes(beforeNodes);
1502
+ const changed = crdt.merge(typedNodes);
1503
+ if (changed.length === 0)
1504
+ return true;
1505
+ const afterNodes = crdt.snapshot();
1506
+ const afterIndex = indexMapForNodes(afterNodes);
1507
+ const beforeLength = beforeNodes.filter((node) => !node.deleted).length;
1508
+ for (const node of changed) {
1509
+ if (node.deleted)
1510
+ this.recordDeletedNode(payload.field, node.id, payload.stamp);
1511
+ }
1512
+ this.emitListOps(payload.iss, payload.field, state.schema.crdt, changed, beforeIndex, afterIndex, beforeLength);
1513
+ return true;
1514
+ }
1515
+ case "set":
1516
+ return this.applySetNodes(nodes, state, payload.field, payload.iss, payload.stamp);
1517
+ case "map":
1518
+ return this.applyMapNodes(nodes, state, payload.field, payload.iss, payload.stamp);
1519
+ case "record":
1520
+ return this.applyRecordNodes(nodes, state, payload.field, payload.iss, payload.stamp);
1521
+ default:
1522
+ return false;
1523
+ }
1524
+ }
1525
+ applySetNodes(nodes, state, field, actor, stamp) {
1526
+ const crdt = state.crdt;
1527
+ for (const node of nodes) {
1528
+ if (!isObject(node) || typeof node.op !== "string" || typeof node.id !== "string")
1529
+ return false;
1530
+ if (node.op === "add") {
1531
+ if (!isValueOfType(node.value, state.schema.jsType))
1532
+ return false;
1533
+ if (typeof node.key !== "string")
1534
+ return false;
1535
+ }
1536
+ else if (node.op === "rem") {
1537
+ if (typeof node.key !== "string" || !isStringArray(node.targets))
1538
+ return false;
1539
+ }
1540
+ else {
1541
+ return false;
1542
+ }
1543
+ }
1544
+ const before = [...crdt.values()];
1545
+ const accepted = crdt.merge(nodes);
1546
+ if (accepted.length === 0)
1547
+ return true;
1548
+ for (const node of accepted) {
1549
+ if (node.op !== "rem")
1550
+ continue;
1551
+ this.recordDeleteNodeStamp(field, node.id, stamp);
1552
+ for (const targetTag of node.targets)
1553
+ this.recordTombstone(field, targetTag, stamp);
1554
+ }
1555
+ const after = [...crdt.values()];
1556
+ const { added, removed } = this.diffSet(before, after);
1557
+ for (const value of added)
1558
+ this.emitMerge(actor, field, "add", { value });
1559
+ for (const value of removed)
1560
+ this.emitMerge(actor, field, "delete", { value });
1561
+ return true;
1562
+ }
1563
+ applyMapNodes(nodes, state, field, actor, stamp) {
1564
+ const crdt = state.crdt;
1565
+ for (const node of nodes) {
1566
+ if (!isObject(node) || typeof node.op !== "string" || typeof node.id !== "string")
1567
+ return false;
1568
+ if (node.op === "set") {
1569
+ if (!isValueOfType(node.value, state.schema.jsType))
1570
+ return false;
1571
+ if (!isJsValue(node.key))
1572
+ return false;
1573
+ if (typeof node.keyId !== "string")
1574
+ return false;
1575
+ }
1576
+ else if (node.op === "del") {
1577
+ if (typeof node.keyId !== "string" || !isStringArray(node.targets))
1578
+ return false;
1579
+ }
1580
+ else {
1581
+ return false;
1582
+ }
1583
+ }
1584
+ const before = this.mapValue(crdt);
1585
+ const accepted = crdt.merge(nodes);
1586
+ if (accepted.length === 0)
1587
+ return true;
1588
+ for (const node of accepted) {
1589
+ if (node.op !== "del")
1590
+ continue;
1591
+ this.recordDeleteNodeStamp(field, node.id, stamp);
1592
+ for (const targetTag of node.targets)
1593
+ this.recordTombstone(field, targetTag, stamp);
1594
+ }
1595
+ const after = this.mapValue(crdt);
1596
+ const { set, removed } = this.diffMap(before, after);
1597
+ for (const entry of set)
1598
+ this.emitMerge(actor, field, "set", entry);
1599
+ for (const key of removed)
1600
+ this.emitMerge(actor, field, "delete", { key });
1601
+ return true;
1602
+ }
1603
+ applyRecordNodes(nodes, state, field, actor, stamp) {
1604
+ const crdt = state.crdt;
1605
+ for (const node of nodes) {
1606
+ if (!isObject(node) || typeof node.op !== "string" || typeof node.id !== "string")
1607
+ return false;
1608
+ if (node.op === "set") {
1609
+ if (typeof node.prop !== "string")
1610
+ return false;
1611
+ if (!isValueOfType(node.value, state.schema.jsType))
1612
+ return false;
1613
+ }
1614
+ else if (node.op === "del") {
1615
+ if (typeof node.prop !== "string" || !isStringArray(node.targets))
1616
+ return false;
1617
+ }
1618
+ else {
1619
+ return false;
1620
+ }
1621
+ }
1622
+ const before = this.recordValue(crdt);
1623
+ const accepted = crdt.merge(nodes);
1624
+ if (accepted.length === 0)
1625
+ return true;
1626
+ for (const node of accepted) {
1627
+ if (node.op !== "del")
1628
+ continue;
1629
+ this.recordDeleteNodeStamp(field, node.id, stamp);
1630
+ for (const targetTag of node.targets)
1631
+ this.recordTombstone(field, targetTag, stamp);
1632
+ }
1633
+ const after = this.recordValue(crdt);
1634
+ const { set, removed } = this.diffRecord(before, after);
1635
+ for (const [key, value] of Object.entries(set))
1636
+ this.emitMerge(actor, field, "set", { key, value });
1637
+ for (const key of removed)
1638
+ this.emitMerge(actor, field, "delete", { key });
1639
+ return true;
1640
+ }
1641
+ validateDagNodeValues(nodes, jsType) {
1642
+ for (const node of nodes) {
1643
+ if (!isValueOfType(node.value, jsType))
1644
+ return false;
1645
+ }
1646
+ return true;
1647
+ }
1648
+ emitListOps(actor, field, crdt, changed, beforeIndex, afterIndex, beforeLength) {
1649
+ const deletes = [];
1650
+ if (crdt === "text") {
1651
+ const inserts = [];
1652
+ for (const node of changed) {
1653
+ if (node.deleted) {
1654
+ const index = beforeIndex.get(node.id);
1655
+ if (index === undefined)
1656
+ continue;
1657
+ deletes.push({ type: "delete", index, count: 1 });
1658
+ }
1659
+ else {
1660
+ const index = afterIndex.get(node.id);
1661
+ if (index === undefined)
1662
+ continue;
1663
+ inserts.push({ type: "insert", index, value: String(node.value) });
1664
+ }
1665
+ }
1666
+ deletes.sort((a, b) => b.index - a.index);
1667
+ inserts.sort((a, b) => a.index - b.index);
1668
+ for (const op of deletes)
1669
+ this.emitMerge(actor, field, "deleteAt", { index: op.index });
1670
+ for (const op of inserts)
1671
+ this.emitMerge(actor, field, "insertAt", { index: op.index, value: op.value });
1672
+ return;
1673
+ }
1674
+ const inserts = [];
1675
+ for (const node of changed) {
1676
+ if (node.deleted) {
1677
+ const index = beforeIndex.get(node.id);
1678
+ if (index === undefined)
1679
+ continue;
1680
+ deletes.push({ type: "delete", index, count: 1 });
1681
+ }
1682
+ else {
1683
+ const index = afterIndex.get(node.id);
1684
+ if (index === undefined)
1685
+ continue;
1686
+ inserts.push({ type: "insert", index, value: node.value });
1687
+ }
1688
+ }
1689
+ deletes.sort((a, b) => b.index - a.index);
1690
+ inserts.sort((a, b) => a.index - b.index);
1691
+ for (const op of deletes) {
1692
+ if (op.index === 0) {
1693
+ this.emitMerge(actor, field, "shift", null);
1694
+ continue;
1695
+ }
1696
+ if (op.index === beforeLength - 1) {
1697
+ this.emitMerge(actor, field, "pop", null);
1698
+ continue;
1699
+ }
1700
+ this.emitMerge(actor, field, "deleteAt", { index: op.index });
1701
+ }
1702
+ for (const op of inserts) {
1703
+ if (op.index === 0) {
1704
+ this.emitMerge(actor, field, "unshift", { value: op.value });
1705
+ continue;
1706
+ }
1707
+ if (op.index >= beforeLength) {
1708
+ this.emitMerge(actor, field, "push", { value: op.value });
1709
+ continue;
1710
+ }
1711
+ this.emitMerge(actor, field, "insertAt", { index: op.index, value: op.value });
1712
+ }
1713
+ }
1714
+ diffSet(before, after) {
1715
+ const beforeSet = new Set(before);
1716
+ const afterSet = new Set(after);
1717
+ const added = after.filter((value) => !beforeSet.has(value));
1718
+ const removed = before.filter((value) => !afterSet.has(value));
1719
+ return { added, removed };
1720
+ }
1721
+ diffMap(before, after) {
1722
+ const beforeMap = new Map();
1723
+ for (const [key, value] of before)
1724
+ beforeMap.set(stableKey(key), { key, value });
1725
+ const afterMap = new Map();
1726
+ for (const [key, value] of after)
1727
+ afterMap.set(stableKey(key), { key, value });
1728
+ const set = [];
1729
+ const removed = [];
1730
+ for (const [keyId, entry] of afterMap) {
1731
+ const prev = beforeMap.get(keyId);
1732
+ if (!prev || !Object.is(prev.value, entry.value))
1733
+ set.push(entry);
1734
+ }
1735
+ for (const [keyId, entry] of beforeMap) {
1736
+ if (!afterMap.has(keyId))
1737
+ removed.push(entry.key);
1738
+ }
1739
+ return { set, removed };
1740
+ }
1741
+ diffRecord(before, after) {
1742
+ const set = {};
1743
+ const removed = [];
1744
+ for (const [key, value] of Object.entries(after)) {
1745
+ if (!(key in before) || !Object.is(before[key], value))
1746
+ set[key] = value;
1747
+ }
1748
+ for (const key of Object.keys(before)) {
1749
+ if (!(key in after))
1750
+ removed.push(key);
1751
+ }
1752
+ return { set, removed };
1753
+ }
1754
+ emitInvalidationDiffs(beforeValues, actor) {
1755
+ for (const [field, state] of this.fields.entries()) {
1756
+ const before = beforeValues.get(field);
1757
+ const after = this.fieldValue(field);
1758
+ this.emitFieldDiff(actor, field, state.schema, before, after);
1759
+ }
1760
+ }
1761
+ emitFieldDiff(actor, field, schema, before, after) {
1762
+ switch (schema.crdt) {
1763
+ case "register":
1764
+ if (!Object.is(before, after))
1765
+ this.emitMerge(actor, field, "set", { value: after });
1766
+ return;
1767
+ case "text": {
1768
+ const beforeText = typeof before === "string" ? before : "";
1769
+ const afterText = typeof after === "string" ? after : "";
1770
+ if (beforeText === afterText)
1771
+ return;
1772
+ this.emitTextDiff(actor, field, beforeText, afterText);
1773
+ return;
1774
+ }
1775
+ case "array": {
1776
+ const beforeArr = Array.isArray(before) ? before : [];
1777
+ const afterArr = Array.isArray(after) ? after : [];
1778
+ if (this.arrayEquals(beforeArr, afterArr))
1779
+ return;
1780
+ this.emitArrayDiff(actor, field, beforeArr, afterArr);
1781
+ return;
1782
+ }
1783
+ case "set": {
1784
+ const beforeArr = Array.isArray(before) ? before : [];
1785
+ const afterArr = Array.isArray(after) ? after : [];
1786
+ const { added, removed } = this.diffSet(beforeArr, afterArr);
1787
+ for (const value of added)
1788
+ this.emitMerge(actor, field, "add", { value });
1789
+ for (const value of removed)
1790
+ this.emitMerge(actor, field, "delete", { value });
1791
+ return;
1792
+ }
1793
+ case "map": {
1794
+ const beforeArr = Array.isArray(before) ? before : [];
1795
+ const afterArr = Array.isArray(after) ? after : [];
1796
+ const { set, removed } = this.diffMap(beforeArr, afterArr);
1797
+ for (const entry of set)
1798
+ this.emitMerge(actor, field, "set", entry);
1799
+ for (const key of removed)
1800
+ this.emitMerge(actor, field, "delete", { key });
1801
+ return;
1802
+ }
1803
+ case "record": {
1804
+ const beforeRec = before && isObject(before) && !Array.isArray(before) ? before : {};
1805
+ const afterRec = after && isObject(after) && !Array.isArray(after) ? after : {};
1806
+ const { set, removed } = this.diffRecord(beforeRec, afterRec);
1807
+ for (const [key, value] of Object.entries(set))
1808
+ this.emitMerge(actor, field, "set", { key, value });
1809
+ for (const key of removed)
1810
+ this.emitMerge(actor, field, "delete", { key });
1811
+ }
1812
+ }
1813
+ }
1814
+ emitTextDiff(actor, field, before, after) {
1815
+ const beforeChars = [...before];
1816
+ const afterChars = [...after];
1817
+ const prefix = this.commonPrefix(beforeChars, afterChars);
1818
+ const suffix = this.commonSuffix(beforeChars, afterChars, prefix);
1819
+ const beforeEnd = beforeChars.length - suffix;
1820
+ const afterEnd = afterChars.length - suffix;
1821
+ for (let index = beforeEnd - 1; index >= prefix; index--) {
1822
+ this.emitMerge(actor, field, "deleteAt", { index });
1823
+ }
1824
+ for (let index = prefix; index < afterEnd; index++) {
1825
+ this.emitMerge(actor, field, "insertAt", {
1826
+ index,
1827
+ value: afterChars[index],
1828
+ });
1829
+ }
1830
+ }
1831
+ emitArrayDiff(actor, field, before, after) {
1832
+ const prefix = this.commonPrefix(before, after);
1833
+ const suffix = this.commonSuffix(before, after, prefix);
1834
+ const beforeEnd = before.length - suffix;
1835
+ const afterEnd = after.length - suffix;
1836
+ for (let index = beforeEnd - 1; index >= prefix; index--) {
1837
+ this.emitMerge(actor, field, "deleteAt", { index });
1838
+ }
1839
+ for (let index = prefix; index < afterEnd; index++) {
1840
+ this.emitMerge(actor, field, "insertAt", {
1841
+ index,
1842
+ value: after[index],
1843
+ });
1844
+ }
1845
+ }
1846
+ arrayEquals(left, right) {
1847
+ if (left.length !== right.length)
1848
+ return false;
1849
+ for (let index = 0; index < left.length; index++) {
1850
+ if (!Object.is(left[index], right[index]))
1851
+ return false;
1852
+ }
1853
+ return true;
1854
+ }
1855
+ commonPrefix(left, right) {
1856
+ const max = Math.min(left.length, right.length);
1857
+ let index = 0;
1858
+ while (index < max && Object.is(left[index], right[index]))
1859
+ index++;
1860
+ return index;
1861
+ }
1862
+ commonSuffix(left, right, prefix) {
1863
+ const max = Math.min(left.length, right.length) - prefix;
1864
+ let count = 0;
1865
+ while (count < max &&
1866
+ Object.is(left[left.length - 1 - count], right[right.length - 1 - count])) {
1867
+ count++;
1868
+ }
1869
+ return count;
1870
+ }
1871
+ setRole(actorId, role) {
1872
+ const stamp = this.clock.next();
1873
+ const signerRole = this.aclLog.roleAt(this.actorId, stamp);
1874
+ if (!this.canWriteAcl(signerRole, role))
1875
+ throw new Error(`Dacument: role '${signerRole}' cannot grant '${role}'`);
1876
+ const assignmentId = uuidv7();
1877
+ this.queueLocalOp({
1878
+ iss: this.actorId,
1879
+ sub: this.docId,
1880
+ iat: nowSeconds(),
1881
+ stamp,
1882
+ kind: "acl.set",
1883
+ schema: this.schemaId,
1884
+ patch: {
1885
+ id: assignmentId,
1886
+ target: actorId,
1887
+ role,
1888
+ },
1889
+ }, signerRole);
1890
+ }
1891
+ recordValue(record) {
1892
+ const output = {};
1893
+ for (const key of Object.keys(record))
1894
+ output[key] = record[key];
1895
+ return output;
1896
+ }
1897
+ mapValue(map) {
1898
+ const output = [];
1899
+ for (const [key, value] of map.entries()) {
1900
+ if (!isJsValue(key))
1901
+ throw new Error("Dacument: map key must be JSON-compatible");
1902
+ output.push([key, value]);
1903
+ }
1904
+ return output;
1905
+ }
1906
+ fieldValue(field) {
1907
+ const state = this.fields.get(field);
1908
+ if (!state)
1909
+ return undefined;
1910
+ const crdt = this.readCrdt(field, state);
1911
+ switch (state.schema.crdt) {
1912
+ case "register":
1913
+ return crdt.get();
1914
+ case "text":
1915
+ return crdt.toString();
1916
+ case "array":
1917
+ return [...crdt];
1918
+ case "set":
1919
+ return [...crdt.values()];
1920
+ case "map":
1921
+ return this.mapValue(crdt);
1922
+ case "record":
1923
+ return this.recordValue(crdt);
1924
+ }
1925
+ }
1926
+ emitEvent(type, event) {
1927
+ const listeners = this.eventListeners.get(type);
1928
+ if (!listeners)
1929
+ return;
1930
+ for (const listener of listeners)
1931
+ listener(event);
1932
+ }
1933
+ emitMerge(actor, target, method, data) {
1934
+ if (this.suppressMerge)
1935
+ return;
1936
+ if (this.isRevoked())
1937
+ return;
1938
+ this.emitEvent("merge", { type: "merge", actor, target, method, data });
1939
+ }
1940
+ emitRevoked(previous, by, stamp) {
1941
+ this.emitEvent("revoked", {
1942
+ type: "revoked",
1943
+ actorId: this.actorId,
1944
+ previous,
1945
+ by,
1946
+ stamp,
1947
+ });
1948
+ }
1949
+ emitError(error) {
1950
+ this.emitEvent("error", { type: "error", error });
1951
+ }
1952
+ canWriteField(role) {
1953
+ return role === "owner" || role === "manager" || role === "editor";
1954
+ }
1955
+ canWriteAcl(role, targetRole) {
1956
+ if (role === "owner")
1957
+ return true;
1958
+ if (role === "manager")
1959
+ return targetRole === "editor" || targetRole === "viewer" || targetRole === "revoked";
1960
+ return false;
1961
+ }
1962
+ assertWritable(field, role) {
1963
+ if (!this.canWriteField(role))
1964
+ throw new Error(`Dacument: role '${role}' cannot write '${field}'`);
1965
+ }
1966
+ assertValueType(field, value) {
1967
+ const state = this.fields.get(field);
1968
+ if (!state)
1969
+ throw new Error(`Dacument: unknown field '${field}'`);
1970
+ if (!isValueOfType(value, state.schema.jsType))
1971
+ throw new Error(`Dacument: invalid value for '${field}'`);
1972
+ const regex = state.schema.crdt === "register" ? state.schema.regex : undefined;
1973
+ if (regex && typeof value === "string" && !regex.test(value))
1974
+ throw new Error(`Dacument: '${field}' failed regex`);
1975
+ }
1976
+ assertValueArray(field, values) {
1977
+ for (const value of values)
1978
+ this.assertValueType(field, value);
1979
+ }
1980
+ assertMapKey(field, key) {
1981
+ if (!isJsValue(key))
1982
+ throw new Error(`Dacument: map key for '${field}' must be JSON-compatible`);
1983
+ }
1984
+ isValidPayload(payload) {
1985
+ if (!isObject(payload))
1986
+ return false;
1987
+ if (typeof payload.iss !== "string" || typeof payload.sub !== "string")
1988
+ return false;
1989
+ if (typeof payload.iat !== "number")
1990
+ return false;
1991
+ if (!payload.stamp)
1992
+ return false;
1993
+ const stamp = payload.stamp;
1994
+ if (typeof stamp.wallTimeMs !== "number" ||
1995
+ typeof stamp.logical !== "number" ||
1996
+ typeof stamp.clockId !== "string")
1997
+ return false;
1998
+ if (typeof payload.kind !== "string")
1999
+ return false;
2000
+ if (typeof payload.schema !== "string")
2001
+ return false;
2002
+ return true;
2003
+ }
2004
+ assertSchemaKeys() {
2005
+ const reserved = new Set([
2006
+ ...Object.getOwnPropertyNames(this),
2007
+ ...Object.getOwnPropertyNames(Object.getPrototypeOf(this)),
2008
+ "acl",
2009
+ ]);
2010
+ for (const key of Object.keys(this.schema)) {
2011
+ if (reserved.has(key))
2012
+ throw new Error(`Dacument: schema key '${key}' is reserved`);
2013
+ }
2014
+ }
2015
+ }