@graview/core 0.1.2 → 0.1.4
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/dist/app.d.ts +9 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js.map +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/cli/check/policy.d.ts.map +1 -1
- package/dist/cli/check/policy.js +40 -0
- package/dist/cli/check/policy.js.map +1 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/document/canonical.d.ts +2 -1
- package/dist/document/canonical.d.ts.map +1 -1
- package/dist/document/canonical.js +2 -1
- package/dist/document/canonical.js.map +1 -1
- package/dist/document/compile.d.ts.map +1 -1
- package/dist/document/compile.js +22 -2
- package/dist/document/compile.js.map +1 -1
- package/dist/document/diff.d.ts.map +1 -1
- package/dist/document/diff.js +8 -1
- package/dist/document/diff.js.map +1 -1
- package/dist/document/edit.d.ts +7 -1
- package/dist/document/edit.d.ts.map +1 -1
- package/dist/document/edit.js +58 -2
- package/dist/document/edit.js.map +1 -1
- package/dist/document/index.d.ts +1 -1
- package/dist/document/index.d.ts.map +1 -1
- package/dist/document/migrate.d.ts.map +1 -1
- package/dist/document/migrate.js +3 -2
- package/dist/document/migrate.js.map +1 -1
- package/dist/document/patch.d.ts.map +1 -1
- package/dist/document/patch.js +2 -5
- package/dist/document/patch.js.map +1 -1
- package/dist/document/schema.d.ts +12 -10
- package/dist/document/schema.d.ts.map +1 -1
- package/dist/document/schema.js +2 -0
- package/dist/document/schema.js.map +1 -1
- package/dist/document/to-document.d.ts.map +1 -1
- package/dist/document/to-document.js +5 -1
- package/dist/document/to-document.js.map +1 -1
- package/dist/graph/graph.d.ts +9 -0
- package/dist/graph/graph.d.ts.map +1 -1
- package/dist/graph/graph.js +18 -4
- package/dist/graph/graph.js.map +1 -1
- package/dist/index.d.ts +11 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +35 -5
- package/dist/integrity.js.map +1 -1
- package/dist/invariants/types.d.ts +1 -1
- package/dist/invariants/types.d.ts.map +1 -1
- package/dist/labels.js +1 -1
- package/dist/labels.js.map +1 -1
- package/dist/mutations/define-mutation.d.ts +22 -2
- package/dist/mutations/define-mutation.d.ts.map +1 -1
- package/dist/mutations/define-mutation.js +21 -8
- package/dist/mutations/define-mutation.js.map +1 -1
- package/dist/mutations/node-ref.d.ts +5 -2
- package/dist/mutations/node-ref.d.ts.map +1 -1
- package/dist/mutations/node-ref.js +28 -7
- package/dist/mutations/node-ref.js.map +1 -1
- package/dist/mutations/types.d.ts +2 -2
- package/dist/mutations/types.d.ts.map +1 -1
- package/dist/ops/log.d.ts +22 -0
- package/dist/ops/log.d.ts.map +1 -1
- package/dist/ops/log.js +14 -0
- package/dist/ops/log.js.map +1 -1
- package/dist/ops/served.d.ts +80 -0
- package/dist/ops/served.d.ts.map +1 -0
- package/dist/ops/served.js +316 -0
- package/dist/ops/served.js.map +1 -0
- package/dist/ops/types.d.ts +8 -0
- package/dist/ops/types.d.ts.map +1 -1
- package/dist/ops/undo.d.ts +6 -0
- package/dist/ops/undo.d.ts.map +1 -1
- package/dist/ops/undo.js +3 -0
- package/dist/ops/undo.js.map +1 -1
- package/dist/ops/withheld.d.ts +63 -8
- package/dist/ops/withheld.d.ts.map +1 -1
- package/dist/ops/withheld.js +198 -22
- package/dist/ops/withheld.js.map +1 -1
- package/dist/ops/writers.d.ts +29 -0
- package/dist/ops/writers.d.ts.map +1 -0
- package/dist/ops/writers.js +98 -0
- package/dist/ops/writers.js.map +1 -0
- package/dist/permissions/sight.d.ts +33 -4
- package/dist/permissions/sight.d.ts.map +1 -1
- package/dist/permissions/sight.js +106 -22
- package/dist/permissions/sight.js.map +1 -1
- package/dist/presence.d.ts +48 -1
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +51 -2
- package/dist/presence.js.map +1 -1
- package/dist/refusal.d.ts +38 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +44 -0
- package/dist/refusal.js.map +1 -0
- package/dist/schema/json-schema.d.ts.map +1 -1
- package/dist/schema/json-schema.js +27 -1
- package/dist/schema/json-schema.js.map +1 -1
- package/dist/schema/schema.d.ts +19 -4
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/types.d.ts +11 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/seen.d.ts +60 -1
- package/dist/seen.d.ts.map +1 -1
- package/dist/seen.js +467 -29
- package/dist/seen.js.map +1 -1
- package/dist/store.d.ts +151 -10
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +228 -40
- package/dist/store.js.map +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/validate-graph.d.ts +2 -1
- package/dist/validate-graph.d.ts.map +1 -1
- package/dist/validate-graph.js +8 -1
- package/dist/validate-graph.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readingOf, seenBy, seesId } from "./seen.js";
|
|
2
|
-
import { Graph, GraphError } from "./graph/graph.js";
|
|
1
|
+
import { hidesFrom, readingOf, seatLens, seenBy, seesId } from "./seen.js";
|
|
2
|
+
import { Graph, GraphError, MissingRecordError } from "./graph/graph.js";
|
|
3
3
|
import { resolveModules } from "./modules.js";
|
|
4
4
|
import { diffSnapshots, EMPTY_DIFF, isEmptyDiff } from "./graph/diff.js";
|
|
5
5
|
import { invert, normalise, writesOf } from "./graph/primitives.js";
|
|
@@ -7,17 +7,20 @@ import { LabelIndex, refusalFor } from "./labels.js";
|
|
|
7
7
|
import { nodeRefArgs } from "./mutations/node-ref.js";
|
|
8
8
|
import { verifyFold } from "./integrity.js";
|
|
9
9
|
import { evaluate } from "./invariants/engine.js";
|
|
10
|
-
import { compileMutation } from "./mutations/define-mutation.js";
|
|
10
|
+
import { compileMutation, READ_BEFORE_REFUSING } from "./mutations/define-mutation.js";
|
|
11
11
|
import { deriveMutations, derivedVia } from "./mutations/derive-edits.js";
|
|
12
12
|
import { OperationLog } from "./ops/log.js";
|
|
13
13
|
import { actingAs, isSystem, permits, permittedMutations } from "./permissions/policy.js";
|
|
14
|
-
import { redact } from "./ops/withheld.js";
|
|
14
|
+
import { namesUnseen, redact } from "./ops/withheld.js";
|
|
15
15
|
import { nounOf } from "./schema/define-node.js";
|
|
16
|
+
import { creatorsBefore } from "./permissions/sight.js";
|
|
16
17
|
import { PermissionDeniedError } from "./permissions/types.js";
|
|
17
18
|
import { checkUndo, UndoBlockedError, undoPrimitives } from "./ops/undo.js";
|
|
18
19
|
import { validateGraph } from "./validate-graph.js";
|
|
19
20
|
import { tellTheWatchItsNames, tellTheWatchOfAStore, tellTheWatchOfAnAuthor, tellTheWatchOfARefusal } from "./watched.js";
|
|
20
21
|
const HUMAN = { kind: "human" };
|
|
22
|
+
/** The host itself, which sees what it keeps: the principal a question is asked as when no seat is named. */
|
|
23
|
+
const SYSTEM = { kind: "system" };
|
|
21
24
|
/** Who turns a module off or on: the workspace itself, `system · modules` (FR-12). */
|
|
22
25
|
export const MODULES_AUTHOR = { kind: "system", id: "modules", name: "Modules" };
|
|
23
26
|
/**
|
|
@@ -58,6 +61,19 @@ export class ReceiveError extends Error {
|
|
|
58
61
|
this.name = "ReceiveError";
|
|
59
62
|
}
|
|
60
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* A CALL NAMES AN ACT THE APP DOES NOT HAVE. Its message lists every act
|
|
66
|
+
* the store has, for the developer reading it; `mutation` is the name the
|
|
67
|
+
* call gave, so a surface that tells a person can say only that (FR-46).
|
|
68
|
+
*/
|
|
69
|
+
export class UnknownMutationError extends GraphError {
|
|
70
|
+
mutation;
|
|
71
|
+
constructor(mutation, registered) {
|
|
72
|
+
super(`Unknown mutation "${mutation}"`, `Registered: ${registered.join(", ") || "(none)"}`);
|
|
73
|
+
this.mutation = mutation;
|
|
74
|
+
this.name = "UnknownMutationError";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
61
77
|
/**
|
|
62
78
|
* A tag nobody else's store is using, for this store's batch ids. Drawn
|
|
63
79
|
* from the platform's random source where there is one, which every page
|
|
@@ -100,7 +116,16 @@ export class Store {
|
|
|
100
116
|
now;
|
|
101
117
|
validate;
|
|
102
118
|
mintBatch;
|
|
119
|
+
/**
|
|
120
|
+
* THE TAG THIS STORE MINTS ITS OWN BATCHES UNDER — `batch:<tag>:<n>` —
|
|
121
|
+
* when it mints them its default way; undefined when `batchIds` says
|
|
122
|
+
* otherwise. A server that takes batch ids from clients refuses this tag
|
|
123
|
+
* from one, so nothing the store lands later joins a client's batch.
|
|
124
|
+
*/
|
|
125
|
+
batchTag;
|
|
103
126
|
counter = 0;
|
|
127
|
+
/** The default op id generator's count. */
|
|
128
|
+
opCount = 0;
|
|
104
129
|
/** Above zero while a rebase applies its pending calls again: they are told as one change at the end. */
|
|
105
130
|
quiet = 0;
|
|
106
131
|
listeners = new Set();
|
|
@@ -116,11 +141,11 @@ export class Store {
|
|
|
116
141
|
}
|
|
117
142
|
this.declaredModules = options.modules;
|
|
118
143
|
this.startingModules = options.enabledModules;
|
|
119
|
-
|
|
120
|
-
this.nextId = options.ids ?? (() => `op${++n}`);
|
|
144
|
+
this.nextId = options.ids ?? (() => `op${++this.opCount}`);
|
|
121
145
|
this.now = options.now ?? (() => new Date().toISOString());
|
|
122
146
|
this.validate = options.validate ?? true;
|
|
123
147
|
const tag = storeTag();
|
|
148
|
+
this.batchTag = options.batchIds ? undefined : tag;
|
|
124
149
|
this.mintBatch = options.batchIds ?? ((kind) => `${kind}:${tag}:${++this.counter}`);
|
|
125
150
|
for (const mutation of options.mutations ?? []) {
|
|
126
151
|
if (this.mutations.has(mutation.name)) {
|
|
@@ -174,18 +199,22 @@ export class Store {
|
|
|
174
199
|
* Only the DEFAULT generators are wound forward; an app that supplies
|
|
175
200
|
* its own `ids` owns their uniqueness.
|
|
176
201
|
*/
|
|
202
|
+
this.countPastTheLog();
|
|
203
|
+
tellTheWatchOfAStore(this);
|
|
204
|
+
}
|
|
205
|
+
/** Winds the default op and batch counters past every id the log holds, and past the horizon (FR-23). */
|
|
206
|
+
countPastTheLog() {
|
|
177
207
|
const trailing = (value) => {
|
|
178
208
|
const digits = /(\d+)$/.exec(value);
|
|
179
209
|
return digits ? Number(digits[1]) : 0;
|
|
180
210
|
};
|
|
181
211
|
for (const op of this.log.all()) {
|
|
182
212
|
this.counter = Math.max(this.counter, trailing(op.batch));
|
|
183
|
-
|
|
213
|
+
this.opCount = Math.max(this.opCount, trailing(op.id));
|
|
184
214
|
}
|
|
185
215
|
// Behind an undo horizon (FR-23) are ids the log no longer holds: count on past them all.
|
|
186
216
|
if (this.log.horizon > 0)
|
|
187
|
-
|
|
188
|
-
tellTheWatchOfAStore(this);
|
|
217
|
+
this.opCount = Math.max(this.opCount, this.log.length);
|
|
189
218
|
}
|
|
190
219
|
/**
|
|
191
220
|
* What the enabled modules work out to; every surface reads this one answer.
|
|
@@ -266,7 +295,7 @@ export class Store {
|
|
|
266
295
|
mutation(name) {
|
|
267
296
|
const found = this.mutations.get(name);
|
|
268
297
|
if (!found) {
|
|
269
|
-
throw new
|
|
298
|
+
throw new UnknownMutationError(name, [...this.mutations.keys()]);
|
|
270
299
|
}
|
|
271
300
|
// A refusal is a result: the mutation exists, and this workspace has its
|
|
272
301
|
// module off — which is a different sentence from "unknown".
|
|
@@ -323,10 +352,10 @@ export class Store {
|
|
|
323
352
|
* repair as the starter seat, which may only add, and the press met
|
|
324
353
|
* "starter may not take-off here" (W-110).
|
|
325
354
|
*/
|
|
326
|
-
const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call
|
|
355
|
+
const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call, principal);
|
|
327
356
|
if (narrowed)
|
|
328
357
|
return { ok: false, refusal: narrowed };
|
|
329
|
-
return permits(this.policy, principal, call.name, this.subjectKindOf(call), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
|
|
358
|
+
return permits(this.policy, principal, call.name, this.subjectKindOf(call, principal), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
|
|
330
359
|
}
|
|
331
360
|
/** How the policy's refusals name an act and a kind: by title and noun, never by id. */
|
|
332
361
|
get words() {
|
|
@@ -362,10 +391,13 @@ export class Store {
|
|
|
362
391
|
* the core owns — a rename that takes any kind — must not reach into a
|
|
363
392
|
* district the workspace does not have. The sentence names the act.
|
|
364
393
|
*/
|
|
365
|
-
namesTurnedOff(call) {
|
|
394
|
+
namesTurnedOff(call, principal = SYSTEM) {
|
|
366
395
|
const off = this.modules.disabledKinds;
|
|
367
396
|
if (off.size === 0)
|
|
368
397
|
return undefined;
|
|
398
|
+
// A seat the module's records are kept from is not told they are there: naming one is naming nothing (FR-55).
|
|
399
|
+
if (!isSystem(principal))
|
|
400
|
+
return undefined;
|
|
369
401
|
const mutation = this.mutations.get(call.name);
|
|
370
402
|
if (!mutation)
|
|
371
403
|
return undefined;
|
|
@@ -384,16 +416,53 @@ export class Store {
|
|
|
384
416
|
};
|
|
385
417
|
}
|
|
386
418
|
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
419
|
+
* AN ACT'S SENTENCE IS WORDED FROM ITS AUTHOR'S VIEW (FR-55). `describe`
|
|
420
|
+
* reads the graph to name what the act touched ("Point at Venue"); read
|
|
421
|
+
* from the whole store, it named a record the author may not see by its
|
|
422
|
+
* label, and so told the author whether a guessed id was real. Read from
|
|
423
|
+
* the store as the author is served it, such a record is named only by
|
|
424
|
+
* what the author wrote, as one that does not exist is. A reader who sees
|
|
425
|
+
* more reads the author's sentence as the author would have. With nothing
|
|
426
|
+
* kept from the author, the store's own graph.
|
|
393
427
|
*/
|
|
394
|
-
|
|
395
|
-
if (
|
|
396
|
-
return
|
|
428
|
+
wordedFor(author) {
|
|
429
|
+
if (author === undefined || !hidesFrom(this, author))
|
|
430
|
+
return {};
|
|
431
|
+
// And a seat that writes a value naming what it may not see has written its own words, though the store held them already.
|
|
432
|
+
const hidden = seesId(this, author);
|
|
433
|
+
return { describeWith: seenBy(this, author).graph, keepUnchanged: (value) => namesUnseen(value, hidden) };
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* AN ACT'S OWN REFUSAL, AS ITS CALLER MAY READ IT (FR-55). An act's body
|
|
437
|
+
* may refuse in its own words, worded from what it read — "Freya Davies
|
|
438
|
+
* is already booked" — and what it read may be a record its caller may
|
|
439
|
+
* not see. Such a refusal is said without its words: the act, and that it
|
|
440
|
+
* could not be done. The store's own refusals already name nothing.
|
|
441
|
+
*/
|
|
442
|
+
refusalWordedFor(error, author, calls) {
|
|
443
|
+
if (!hidesFrom(this, author) || error instanceof PermissionDeniedError || error instanceof MissingRecordError || error === null || typeof error !== "object")
|
|
444
|
+
return error;
|
|
445
|
+
const read = error[READ_BEFORE_REFUSING] ?? [];
|
|
446
|
+
const visible = seesId(this, author);
|
|
447
|
+
if (!read.some((id) => this.graph.has(id) && !visible(id)))
|
|
448
|
+
return error;
|
|
449
|
+
const title = calls.length === 1 ? (this.mutations.get(calls[0].name)?.title ?? calls[0].name) : undefined;
|
|
450
|
+
return new GraphError(title ? `“${title}” could not be done as asked.` : "This change could not be done as asked.");
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* A CALL THAT NAMES A RECORD THAT IS NOT THERE FOR ITS CALLER IS REFUSED
|
|
454
|
+
* AS MISSING (FR-02, FR-55) — one that does not exist, and one the caller
|
|
455
|
+
* may not see, alike: the same `MissingRecordError`, the same sentence,
|
|
456
|
+
* reason `missing`. Ids are minted from labels, so a seat can guess one,
|
|
457
|
+
* and a refusal that told a hidden record from an absent one would tell
|
|
458
|
+
* it which guesses were real. An act on a record is also a way of reading
|
|
459
|
+
* it — its preview, what its rules say after — so a hidden one is never
|
|
460
|
+
* acted on by a seat that may not see it. Asked of each call as it is
|
|
461
|
+
* about to run, so a record an earlier call in the same gesture made is
|
|
462
|
+
* there. The sentence names the act, never the id: it says the same of a
|
|
463
|
+
* hidden record and an absent one, and says back nothing the seat sent.
|
|
464
|
+
*/
|
|
465
|
+
missingFor(call, principal = HUMAN) {
|
|
397
466
|
const mutation = this.mutations.get(call.name);
|
|
398
467
|
if (!mutation)
|
|
399
468
|
return undefined;
|
|
@@ -410,14 +479,15 @@ export class Store {
|
|
|
410
479
|
add(call.args[mutation.subject.arg]);
|
|
411
480
|
for (const ref of nodeRefArgs(mutation.input))
|
|
412
481
|
add(call.args[ref.name]);
|
|
413
|
-
|
|
414
|
-
if (![...named].some((id) => this.graph.has(id) && !visible(id)))
|
|
482
|
+
if (named.size === 0)
|
|
415
483
|
return undefined;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
484
|
+
const visible = seesId(this, principal);
|
|
485
|
+
for (const id of named) {
|
|
486
|
+
if (this.graph.has(id) && visible(id))
|
|
487
|
+
continue;
|
|
488
|
+
return new MissingRecordError(id, `“${mutation.title ?? call.name}” names a record that is not there.`);
|
|
489
|
+
}
|
|
490
|
+
return undefined;
|
|
421
491
|
}
|
|
422
492
|
/**
|
|
423
493
|
* Whether a principal may run ANY act of a module drawn only for those
|
|
@@ -536,7 +606,7 @@ export class Store {
|
|
|
536
606
|
* refuses that case rather than allowing it: the safe reading of "I could
|
|
537
607
|
* not tell what this acts on" is no.
|
|
538
608
|
*/
|
|
539
|
-
subjectKindOf(call) {
|
|
609
|
+
subjectKindOf(call, principal = SYSTEM) {
|
|
540
610
|
const definition = this.mutations.get(call.name);
|
|
541
611
|
const subject = definition?.subject;
|
|
542
612
|
if (!subject)
|
|
@@ -550,6 +620,9 @@ export class Store {
|
|
|
550
620
|
*/
|
|
551
621
|
if (typeof id !== "string")
|
|
552
622
|
return subject.kinds !== "*" && subject.kinds.length === 1 ? subject.kinds[0] : undefined;
|
|
623
|
+
// A record the principal may not see is judged as one that is not there: its kind is not the policy's to read aloud (FR-55).
|
|
624
|
+
if (!isSystem(principal) && !seesId(this, principal)(id))
|
|
625
|
+
return undefined;
|
|
553
626
|
return this.graph.getNode(id)?.kind;
|
|
554
627
|
}
|
|
555
628
|
/** Current violations, evaluated fresh — nothing is cached or stale. */
|
|
@@ -585,9 +658,9 @@ export class Store {
|
|
|
585
658
|
return true;
|
|
586
659
|
}
|
|
587
660
|
}
|
|
588
|
-
preview(call, context) {
|
|
661
|
+
preview(call, context, options = {}) {
|
|
589
662
|
const definition = this.mutation(call.name);
|
|
590
|
-
const compiled = compileMutation(this.graph, definition, call.args);
|
|
663
|
+
const compiled = compileMutation(this.graph, definition, call.args, this.wordedFor(options.author));
|
|
591
664
|
return this.previewPrimitives(compiled.primitives, {
|
|
592
665
|
reads: compiled.reads,
|
|
593
666
|
writes: compiled.writes,
|
|
@@ -686,6 +759,7 @@ export class Store {
|
|
|
686
759
|
const intents = [];
|
|
687
760
|
const before = this.violations();
|
|
688
761
|
const rollback = this.graph.snapshot();
|
|
762
|
+
const kept = this.log.length;
|
|
689
763
|
/*
|
|
690
764
|
* Permission is checked for the WHOLE gesture before any of it runs.
|
|
691
765
|
*
|
|
@@ -715,7 +789,7 @@ export class Store {
|
|
|
715
789
|
* was brought in for, and the narrower of the two wins.
|
|
716
790
|
*/
|
|
717
791
|
for (const call of calls) {
|
|
718
|
-
const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call);
|
|
792
|
+
const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call, author);
|
|
719
793
|
if (refusal) {
|
|
720
794
|
tellTheWatchOfARefusal(refusal, author.id);
|
|
721
795
|
throw new PermissionDeniedError(refusal);
|
|
@@ -725,7 +799,10 @@ export class Store {
|
|
|
725
799
|
try {
|
|
726
800
|
for (const call of calls) {
|
|
727
801
|
const definition = this.mutation(call.name);
|
|
728
|
-
const
|
|
802
|
+
const missing = this.missingFor(call, author);
|
|
803
|
+
if (missing)
|
|
804
|
+
throw missing;
|
|
805
|
+
const compiled = compileMutation(this.graph, definition, call.args, this.wordedFor(author));
|
|
729
806
|
/*
|
|
730
807
|
* AN ACT THAT DID NOTHING DOES NOT GO IN THE HISTORY.
|
|
731
808
|
*
|
|
@@ -763,6 +840,8 @@ export class Store {
|
|
|
763
840
|
inverse: [...compiled.primitives].reverse().map(normalise).map(invert),
|
|
764
841
|
reads: compiled.reads,
|
|
765
842
|
writes: compiled.writes,
|
|
843
|
+
// Which records its sentence read, so a reader who may not see one is not handed the sentence (FR-55).
|
|
844
|
+
...(definition.describe !== undefined && compiled.described && compiled.described.length > 0 ? { described: compiled.described } : {}),
|
|
766
845
|
at: this.now(),
|
|
767
846
|
...(options.via !== undefined ? { via: options.via } : {}),
|
|
768
847
|
};
|
|
@@ -778,13 +857,15 @@ export class Store {
|
|
|
778
857
|
}
|
|
779
858
|
}
|
|
780
859
|
catch (error) {
|
|
781
|
-
|
|
782
|
-
|
|
860
|
+
// A batch refused part way keeps none of it: the graph, and the ops it had logged before the refusal.
|
|
861
|
+
this.putBack(rollback, kept);
|
|
862
|
+
throw this.refusalWordedFor(error, author, calls);
|
|
783
863
|
}
|
|
864
|
+
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
865
|
+
this.admitted(options, ops, allPrimitives, diff, rollback, kept);
|
|
784
866
|
const after = this.violations();
|
|
785
867
|
const beforeKeys = new Set(before.map(violationKey));
|
|
786
868
|
const afterKeys = new Set(after.map(violationKey));
|
|
787
|
-
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
788
869
|
this.notify(diff, ops);
|
|
789
870
|
return {
|
|
790
871
|
batch,
|
|
@@ -1051,7 +1132,7 @@ export class Store {
|
|
|
1051
1132
|
*/
|
|
1052
1133
|
const sighted = (this.policy?.sees?.length ?? 0) > 0 && !isSystem(author);
|
|
1053
1134
|
if (sighted) {
|
|
1054
|
-
const seen = checkUndo(readingOf(redact(this.log.all(),
|
|
1135
|
+
const seen = checkUndo(readingOf(redact(this.log.all(), seatLens(this, author)), () => this.log.epochs()), ids);
|
|
1055
1136
|
if (!seen.ok)
|
|
1056
1137
|
throw new UndoBlockedError(seen);
|
|
1057
1138
|
}
|
|
@@ -1089,6 +1170,7 @@ export class Store {
|
|
|
1089
1170
|
const rollback = this.graph.snapshot();
|
|
1090
1171
|
const before = this.violations();
|
|
1091
1172
|
const ops = [];
|
|
1173
|
+
const kept = this.log.length;
|
|
1092
1174
|
for (const target of [...check.ops].sort((a, b) => b.seq - a.seq)) {
|
|
1093
1175
|
const op = {
|
|
1094
1176
|
id: this.nextId(),
|
|
@@ -1110,10 +1192,11 @@ export class Store {
|
|
|
1110
1192
|
this.log.append(op);
|
|
1111
1193
|
ops.push(op);
|
|
1112
1194
|
}
|
|
1195
|
+
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
1196
|
+
this.admitted(options, ops, ops.flatMap((op) => [...op.primitives]), diff, rollback, kept);
|
|
1113
1197
|
const after = this.violations();
|
|
1114
1198
|
const beforeKeys = new Set(before.map(violationKey));
|
|
1115
1199
|
const afterKeys = new Set(after.map(violationKey));
|
|
1116
|
-
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
1117
1200
|
this.notify(diff, ops);
|
|
1118
1201
|
return {
|
|
1119
1202
|
batch,
|
|
@@ -1128,6 +1211,33 @@ export class Store {
|
|
|
1128
1211
|
violationsAfter: after,
|
|
1129
1212
|
};
|
|
1130
1213
|
}
|
|
1214
|
+
/** A change not kept: the graph as it was, and the log cut back to where it stood. */
|
|
1215
|
+
putBack(rollback, kept) {
|
|
1216
|
+
this.graph.load(rollback);
|
|
1217
|
+
if (this.log.length > kept)
|
|
1218
|
+
this.log.truncate(kept);
|
|
1219
|
+
}
|
|
1220
|
+
/** Asks the host's `admit`, when there is one, with the change as planned; one it refuses is put back, and its word goes on up. */
|
|
1221
|
+
admitted(options, ops, primitives, diff, rollback, kept) {
|
|
1222
|
+
if (!options.admit || ops.length === 0)
|
|
1223
|
+
return;
|
|
1224
|
+
const size = this.graph.size;
|
|
1225
|
+
try {
|
|
1226
|
+
options.admit({
|
|
1227
|
+
ops,
|
|
1228
|
+
primitives,
|
|
1229
|
+
added: { nodes: diff.addedNodes.length, edges: diff.addedEdges.length },
|
|
1230
|
+
removed: { nodes: diff.removedNodes.length, edges: diff.removedEdges.length },
|
|
1231
|
+
changed: { nodes: diff.changedNodes.length },
|
|
1232
|
+
nodesAfter: size.nodes,
|
|
1233
|
+
edgesAfter: size.edges,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
catch (error) {
|
|
1237
|
+
this.putBack(rollback, kept);
|
|
1238
|
+
throw error;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1131
1241
|
/** Redo is the undo of an undo — no separate stack exists. */
|
|
1132
1242
|
redo(undoBatchId, options = {}) {
|
|
1133
1243
|
return this.undo(undoBatchId, options);
|
|
@@ -1253,6 +1363,78 @@ export class Store {
|
|
|
1253
1363
|
this.notify(diff, ops);
|
|
1254
1364
|
return { diff, confirmed: landed, pending: replayed, refused };
|
|
1255
1365
|
}
|
|
1366
|
+
/**
|
|
1367
|
+
* A CLIENT THAT RESYNCED TAKES THE SERVER'S WHOLE STATE (FR-53).
|
|
1368
|
+
*
|
|
1369
|
+
* `rebase` lands the server's ops on the confirmed prefix this store
|
|
1370
|
+
* holds. When that prefix cannot be trusted — this copy drifted, and an
|
|
1371
|
+
* op of the server's no longer fits it, or the server compacted past
|
|
1372
|
+
* where this client left off, so `/graview/since` cannot bring the ops in
|
|
1373
|
+
* between — the client takes the server's graph and log as they are:
|
|
1374
|
+
*
|
|
1375
|
+
* 1. the ops of the `pending` batches are read off the log as it stands;
|
|
1376
|
+
* 2. the log becomes `log` (with its `epochs` and `horizon`, checked as
|
|
1377
|
+
* `OperationLog.from` checks them; one that is not intact is refused
|
|
1378
|
+
* before anything moves) and the graph becomes `snapshot`, in the
|
|
1379
|
+
* server's order — not put back op by op, as `rebase` puts it;
|
|
1380
|
+
* 3. the `pending` batches are applied again on top, in the order given,
|
|
1381
|
+
* exactly as `rebase` applies them: under the same batch ids, judged
|
|
1382
|
+
* and compiled afresh. One that no longer applies is said in `refused`.
|
|
1383
|
+
*
|
|
1384
|
+
* `drop` names the batches the server has answered or refused: they are
|
|
1385
|
+
* not applied again (nor is any batch left unnamed: the log is the
|
|
1386
|
+
* server's). Subscribers hear ONE change, the net diff, with the ops this
|
|
1387
|
+
* store did not hold before and the re-applied ones.
|
|
1388
|
+
*/
|
|
1389
|
+
adopt(state) {
|
|
1390
|
+
const dropped = new Set(state.drop ?? []);
|
|
1391
|
+
const pending = (state.pending ?? []).filter((batch) => !dropped.has(batch));
|
|
1392
|
+
const held = this.log.all();
|
|
1393
|
+
const before = this.graph.snapshot();
|
|
1394
|
+
const was = { ops: [...held], epochs: [...this.log.epochs()], horizon: this.log.horizon };
|
|
1395
|
+
const known = new Set(held.map((op) => op.id));
|
|
1396
|
+
const tails = new Map(pending.map((batch) => [batch, held.filter((op) => op.batch === batch)]));
|
|
1397
|
+
// A log that is not intact is refused here, before the graph moves.
|
|
1398
|
+
if (state.log.length > 0)
|
|
1399
|
+
this.log.replace(state.log, state.epochs, state.horizon !== undefined ? { horizon: state.horizon } : {});
|
|
1400
|
+
else {
|
|
1401
|
+
const at = state.horizon ?? 0;
|
|
1402
|
+
this.log.replace([], state.epochs ?? [{ seq: at, base: state.snapshot }], { horizon: at });
|
|
1403
|
+
}
|
|
1404
|
+
const replayed = [];
|
|
1405
|
+
const refused = [];
|
|
1406
|
+
this.quiet++;
|
|
1407
|
+
try {
|
|
1408
|
+
this.graph.load(state.snapshot);
|
|
1409
|
+
this.projected = undefined;
|
|
1410
|
+
this.countPastTheLog();
|
|
1411
|
+
for (const batch of pending) {
|
|
1412
|
+
const ops = tails.get(batch) ?? [];
|
|
1413
|
+
if (ops.length === 0)
|
|
1414
|
+
continue;
|
|
1415
|
+
try {
|
|
1416
|
+
replayed.push(...this.replay(batch, ops));
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
refused.push({ batch, error });
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
catch (error) {
|
|
1424
|
+
// A store that could not adopt is the store it was.
|
|
1425
|
+
this.log.replace(was.ops, was.epochs, { horizon: was.horizon });
|
|
1426
|
+
this.graph.load(before);
|
|
1427
|
+
this.projected = undefined;
|
|
1428
|
+
throw error;
|
|
1429
|
+
}
|
|
1430
|
+
finally {
|
|
1431
|
+
this.quiet--;
|
|
1432
|
+
}
|
|
1433
|
+
const adopted = this.log.all().filter((op) => !known.has(op.id) && !replayed.includes(op));
|
|
1434
|
+
const diff = diffSnapshots(before, this.graph.snapshot());
|
|
1435
|
+
this.notify(diff, [...adopted, ...replayed]);
|
|
1436
|
+
return { diff, adopted, pending: replayed, refused };
|
|
1437
|
+
}
|
|
1256
1438
|
/** One pending batch, applied again as it was made: its calls, its undo, or its primitives. */
|
|
1257
1439
|
replay(batch, ops) {
|
|
1258
1440
|
const head = ops[0];
|
|
@@ -1300,6 +1482,9 @@ export class Store {
|
|
|
1300
1482
|
* 1000). It is moved back to the start of the gesture it would split, so
|
|
1301
1483
|
* a batch is never half behind it.
|
|
1302
1484
|
*
|
|
1485
|
+
* The checkpoint keeps who made each record behind it (`creators`), so a
|
|
1486
|
+
* store opened on it knows a record's maker without the archived ops.
|
|
1487
|
+
*
|
|
1303
1488
|
* Nothing moves until `compact` is handed the checkpoint, so a host can
|
|
1304
1489
|
* archive what is behind it first. A store whose graph does not verify
|
|
1305
1490
|
* against its log is not compacted: the checkpoint would vouch for a
|
|
@@ -1327,7 +1512,10 @@ export class Store {
|
|
|
1327
1512
|
const verified = this.verify();
|
|
1328
1513
|
if (!verified.ok)
|
|
1329
1514
|
throw new GraphError(`Cannot compact a store that does not verify: ${verified.reason}`);
|
|
1330
|
-
|
|
1515
|
+
const checkpoint = this.log.checkpointAt(this.schema, seq, { validate: this.validate });
|
|
1516
|
+
// Who made each record behind it, so a store opened on it still knows (an `own` sight reads the log for it).
|
|
1517
|
+
const creators = creatorsBefore(this.log, seq);
|
|
1518
|
+
return Object.keys(creators).length > 0 ? { ...checkpoint, creators } : checkpoint;
|
|
1331
1519
|
}
|
|
1332
1520
|
/**
|
|
1333
1521
|
* MOVES THE UNDO HORIZON TO `checkpoint` (FR-23): the ops and epochs
|