@graview/core 0.1.3 → 0.1.5
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 +14 -2
- package/dist/document/compile.d.ts.map +1 -1
- package/dist/document/compile.js +50 -24
- package/dist/document/compile.js.map +1 -1
- package/dist/document/diff.d.ts.map +1 -1
- package/dist/document/diff.js +3 -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 +46 -2
- package/dist/document/edit.js.map +1 -1
- package/dist/document/graview-template.d.ts +2 -2
- package/dist/document/index.d.ts +2 -2
- package/dist/document/index.d.ts.map +1 -1
- package/dist/document/index.js +1 -1
- package/dist/document/index.js.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 +298 -298
- package/dist/document/schema.d.ts.map +1 -1
- package/dist/document/schema.js +125 -140
- package/dist/document/schema.js.map +1 -1
- package/dist/document/to-document.d.ts.map +1 -1
- package/dist/document/to-document.js +2 -1
- package/dist/document/to-document.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- 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/mutations/define-mutation.d.ts +26 -3
- package/dist/mutations/define-mutation.d.ts.map +1 -1
- package/dist/mutations/define-mutation.js +18 -5
- package/dist/mutations/define-mutation.js.map +1 -1
- package/dist/mutations/derive-edits.js +5 -5
- package/dist/mutations/derive-edits.js.map +1 -1
- package/dist/mutations/form.d.ts.map +1 -1
- package/dist/mutations/form.js +6 -5
- package/dist/mutations/form.js.map +1 -1
- package/dist/mutations/node-ref.d.ts +16 -5
- package/dist/mutations/node-ref.d.ts.map +1 -1
- package/dist/mutations/node-ref.js +52 -16
- 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 +11 -0
- package/dist/ops/log.d.ts.map +1 -1
- 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/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 +15 -2
- package/dist/permissions/sight.d.ts.map +1 -1
- package/dist/permissions/sight.js +34 -5
- package/dist/permissions/sight.js.map +1 -1
- package/dist/presence.d.ts +24 -2
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +26 -1
- package/dist/presence.js.map +1 -1
- package/dist/refusal.d.ts +9 -5
- package/dist/refusal.d.ts.map +1 -1
- package/dist/refusal.js +9 -5
- package/dist/refusal.js.map +1 -1
- package/dist/scaffold/project.d.ts.map +1 -1
- package/dist/scaffold/project.js +5 -0
- package/dist/scaffold/project.js.map +1 -1
- package/dist/schema/json-schema.d.ts +2 -3
- package/dist/schema/json-schema.d.ts.map +1 -1
- package/dist/schema/json-schema.js +30 -10
- 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 +2 -2
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/zod.d.ts +18 -0
- package/dist/schema/zod.d.ts.map +1 -0
- package/dist/schema/zod.js +44 -0
- package/dist/schema/zod.js.map +1 -0
- package/dist/seen.d.ts +54 -1
- package/dist/seen.d.ts.map +1 -1
- package/dist/seen.js +457 -25
- package/dist/seen.js.map +1 -1
- package/dist/store.d.ts +141 -17
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +203 -84
- package/dist/store.js.map +1 -1
- package/dist/testing.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,22 @@ 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";
|
|
21
|
+
/** The batch a preview's ops are under when it was given none: no store mints it. */
|
|
22
|
+
export const PREVIEW_BATCH = "preview";
|
|
20
23
|
const HUMAN = { kind: "human" };
|
|
24
|
+
/** The host itself, which sees what it keeps: the principal a question is asked as when no seat is named. */
|
|
25
|
+
const SYSTEM = { kind: "system" };
|
|
21
26
|
/** Who turns a module off or on: the workspace itself, `system · modules` (FR-12). */
|
|
22
27
|
export const MODULES_AUTHOR = { kind: "system", id: "modules", name: "Modules" };
|
|
23
28
|
/**
|
|
@@ -58,6 +63,19 @@ export class ReceiveError extends Error {
|
|
|
58
63
|
this.name = "ReceiveError";
|
|
59
64
|
}
|
|
60
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* A CALL NAMES AN ACT THE APP DOES NOT HAVE. Its message lists every act
|
|
68
|
+
* the store has, for the developer reading it; `mutation` is the name the
|
|
69
|
+
* call gave, so a surface that tells a person can say only that (FR-46).
|
|
70
|
+
*/
|
|
71
|
+
export class UnknownMutationError extends GraphError {
|
|
72
|
+
mutation;
|
|
73
|
+
constructor(mutation, registered) {
|
|
74
|
+
super(`Unknown mutation "${mutation}"`, `Registered: ${registered.join(", ") || "(none)"}`);
|
|
75
|
+
this.mutation = mutation;
|
|
76
|
+
this.name = "UnknownMutationError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
61
79
|
/**
|
|
62
80
|
* A tag nobody else's store is using, for this store's batch ids. Drawn
|
|
63
81
|
* from the platform's random source where there is one, which every page
|
|
@@ -100,11 +118,20 @@ export class Store {
|
|
|
100
118
|
now;
|
|
101
119
|
validate;
|
|
102
120
|
mintBatch;
|
|
121
|
+
/**
|
|
122
|
+
* THE TAG THIS STORE MINTS ITS OWN BATCHES UNDER — `batch:<tag>:<n>` —
|
|
123
|
+
* when it mints them its default way; undefined when `batchIds` says
|
|
124
|
+
* otherwise. A server that takes batch ids from clients refuses this tag
|
|
125
|
+
* from one, so nothing the store lands later joins a client's batch.
|
|
126
|
+
*/
|
|
127
|
+
batchTag;
|
|
103
128
|
counter = 0;
|
|
104
129
|
/** The default op id generator's count. */
|
|
105
130
|
opCount = 0;
|
|
106
131
|
/** Above zero while a rebase applies its pending calls again: they are told as one change at the end. */
|
|
107
132
|
quiet = 0;
|
|
133
|
+
/** True of a rehearsal (`previewAll`): what it does is told to nobody. */
|
|
134
|
+
rehearsing = false;
|
|
108
135
|
listeners = new Set();
|
|
109
136
|
constructor(options) {
|
|
110
137
|
this.schema = options.schema;
|
|
@@ -122,6 +149,7 @@ export class Store {
|
|
|
122
149
|
this.now = options.now ?? (() => new Date().toISOString());
|
|
123
150
|
this.validate = options.validate ?? true;
|
|
124
151
|
const tag = storeTag();
|
|
152
|
+
this.batchTag = options.batchIds ? undefined : tag;
|
|
125
153
|
this.mintBatch = options.batchIds ?? ((kind) => `${kind}:${tag}:${++this.counter}`);
|
|
126
154
|
for (const mutation of options.mutations ?? []) {
|
|
127
155
|
if (this.mutations.has(mutation.name)) {
|
|
@@ -271,7 +299,7 @@ export class Store {
|
|
|
271
299
|
mutation(name) {
|
|
272
300
|
const found = this.mutations.get(name);
|
|
273
301
|
if (!found) {
|
|
274
|
-
throw new
|
|
302
|
+
throw new UnknownMutationError(name, [...this.mutations.keys()]);
|
|
275
303
|
}
|
|
276
304
|
// A refusal is a result: the mutation exists, and this workspace has its
|
|
277
305
|
// module off — which is a different sentence from "unknown".
|
|
@@ -328,10 +356,10 @@ export class Store {
|
|
|
328
356
|
* repair as the starter seat, which may only add, and the press met
|
|
329
357
|
* "starter may not take-off here" (W-110).
|
|
330
358
|
*/
|
|
331
|
-
const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call
|
|
359
|
+
const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call, principal);
|
|
332
360
|
if (narrowed)
|
|
333
361
|
return { ok: false, refusal: narrowed };
|
|
334
|
-
return permits(this.policy, principal, call.name, this.subjectKindOf(call), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
|
|
362
|
+
return permits(this.policy, principal, call.name, this.subjectKindOf(call, principal), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
|
|
335
363
|
}
|
|
336
364
|
/** How the policy's refusals name an act and a kind: by title and noun, never by id. */
|
|
337
365
|
get words() {
|
|
@@ -367,10 +395,13 @@ export class Store {
|
|
|
367
395
|
* the core owns — a rename that takes any kind — must not reach into a
|
|
368
396
|
* district the workspace does not have. The sentence names the act.
|
|
369
397
|
*/
|
|
370
|
-
namesTurnedOff(call) {
|
|
398
|
+
namesTurnedOff(call, principal = SYSTEM) {
|
|
371
399
|
const off = this.modules.disabledKinds;
|
|
372
400
|
if (off.size === 0)
|
|
373
401
|
return undefined;
|
|
402
|
+
// A seat the module's records are kept from is not told they are there: naming one is naming nothing (FR-55).
|
|
403
|
+
if (!isSystem(principal))
|
|
404
|
+
return undefined;
|
|
374
405
|
const mutation = this.mutations.get(call.name);
|
|
375
406
|
if (!mutation)
|
|
376
407
|
return undefined;
|
|
@@ -389,16 +420,53 @@ export class Store {
|
|
|
389
420
|
};
|
|
390
421
|
}
|
|
391
422
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
423
|
+
* AN ACT'S SENTENCE IS WORDED FROM ITS AUTHOR'S VIEW (FR-55). `describe`
|
|
424
|
+
* reads the graph to name what the act touched ("Point at Venue"); read
|
|
425
|
+
* from the whole store, it named a record the author may not see by its
|
|
426
|
+
* label, and so told the author whether a guessed id was real. Read from
|
|
427
|
+
* the store as the author is served it, such a record is named only by
|
|
428
|
+
* what the author wrote, as one that does not exist is. A reader who sees
|
|
429
|
+
* more reads the author's sentence as the author would have. With nothing
|
|
430
|
+
* kept from the author, the store's own graph.
|
|
398
431
|
*/
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
return
|
|
432
|
+
wordedFor(author) {
|
|
433
|
+
if (author === undefined || !hidesFrom(this, author))
|
|
434
|
+
return {};
|
|
435
|
+
// And a seat that writes a value naming what it may not see has written its own words, though the store held them already.
|
|
436
|
+
const hidden = seesId(this, author);
|
|
437
|
+
return { describeWith: seenBy(this, author).graph, keepUnchanged: (value) => namesUnseen(value, hidden) };
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* AN ACT'S OWN REFUSAL, AS ITS CALLER MAY READ IT (FR-55). An act's body
|
|
441
|
+
* may refuse in its own words, worded from what it read — "Freya Davies
|
|
442
|
+
* is already booked" — and what it read may be a record its caller may
|
|
443
|
+
* not see. Such a refusal is said without its words: the act, and that it
|
|
444
|
+
* could not be done. The store's own refusals already name nothing.
|
|
445
|
+
*/
|
|
446
|
+
refusalWordedFor(error, author, calls) {
|
|
447
|
+
if (!hidesFrom(this, author) || error instanceof PermissionDeniedError || error instanceof MissingRecordError || error === null || typeof error !== "object")
|
|
448
|
+
return error;
|
|
449
|
+
const read = error[READ_BEFORE_REFUSING] ?? [];
|
|
450
|
+
const visible = seesId(this, author);
|
|
451
|
+
if (!read.some((id) => this.graph.has(id) && !visible(id)))
|
|
452
|
+
return error;
|
|
453
|
+
const title = calls.length === 1 ? (this.mutations.get(calls[0].name)?.title ?? calls[0].name) : undefined;
|
|
454
|
+
return new GraphError(title ? `“${title}” could not be done as asked.` : "This change could not be done as asked.");
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* A CALL THAT NAMES A RECORD THAT IS NOT THERE FOR ITS CALLER IS REFUSED
|
|
458
|
+
* AS MISSING (FR-02, FR-55) — one that does not exist, and one the caller
|
|
459
|
+
* may not see, alike: the same `MissingRecordError`, the same sentence,
|
|
460
|
+
* reason `missing`. Ids are minted from labels, so a seat can guess one,
|
|
461
|
+
* and a refusal that told a hidden record from an absent one would tell
|
|
462
|
+
* it which guesses were real. An act on a record is also a way of reading
|
|
463
|
+
* it — its preview, what its rules say after — so a hidden one is never
|
|
464
|
+
* acted on by a seat that may not see it. Asked of each call as it is
|
|
465
|
+
* about to run, so a record an earlier call in the same gesture made is
|
|
466
|
+
* there. The sentence names the act, never the id: it says the same of a
|
|
467
|
+
* hidden record and an absent one, and says back nothing the seat sent.
|
|
468
|
+
*/
|
|
469
|
+
missingFor(call, principal = HUMAN) {
|
|
402
470
|
const mutation = this.mutations.get(call.name);
|
|
403
471
|
if (!mutation)
|
|
404
472
|
return undefined;
|
|
@@ -415,14 +483,15 @@ export class Store {
|
|
|
415
483
|
add(call.args[mutation.subject.arg]);
|
|
416
484
|
for (const ref of nodeRefArgs(mutation.input))
|
|
417
485
|
add(call.args[ref.name]);
|
|
418
|
-
|
|
419
|
-
if (![...named].some((id) => this.graph.has(id) && !visible(id)))
|
|
486
|
+
if (named.size === 0)
|
|
420
487
|
return undefined;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
488
|
+
const visible = seesId(this, principal);
|
|
489
|
+
for (const id of named) {
|
|
490
|
+
if (this.graph.has(id) && visible(id))
|
|
491
|
+
continue;
|
|
492
|
+
return new MissingRecordError(id, `“${mutation.title ?? call.name}” names a record that is not there.`);
|
|
493
|
+
}
|
|
494
|
+
return undefined;
|
|
426
495
|
}
|
|
427
496
|
/**
|
|
428
497
|
* Whether a principal may run ANY act of a module drawn only for those
|
|
@@ -541,7 +610,7 @@ export class Store {
|
|
|
541
610
|
* refuses that case rather than allowing it: the safe reading of "I could
|
|
542
611
|
* not tell what this acts on" is no.
|
|
543
612
|
*/
|
|
544
|
-
subjectKindOf(call) {
|
|
613
|
+
subjectKindOf(call, principal = SYSTEM) {
|
|
545
614
|
const definition = this.mutations.get(call.name);
|
|
546
615
|
const subject = definition?.subject;
|
|
547
616
|
if (!subject)
|
|
@@ -555,6 +624,9 @@ export class Store {
|
|
|
555
624
|
*/
|
|
556
625
|
if (typeof id !== "string")
|
|
557
626
|
return subject.kinds !== "*" && subject.kinds.length === 1 ? subject.kinds[0] : undefined;
|
|
627
|
+
// 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).
|
|
628
|
+
if (!isSystem(principal) && !seesId(this, principal)(id))
|
|
629
|
+
return undefined;
|
|
558
630
|
return this.graph.getNode(id)?.kind;
|
|
559
631
|
}
|
|
560
632
|
/** Current violations, evaluated fresh — nothing is cached or stale. */
|
|
@@ -590,9 +662,9 @@ export class Store {
|
|
|
590
662
|
return true;
|
|
591
663
|
}
|
|
592
664
|
}
|
|
593
|
-
preview(call, context) {
|
|
665
|
+
preview(call, context, options = {}) {
|
|
594
666
|
const definition = this.mutation(call.name);
|
|
595
|
-
const compiled = compileMutation(this.graph, definition, call.args);
|
|
667
|
+
const compiled = compileMutation(this.graph, definition, call.args, this.wordedFor(options.author));
|
|
596
668
|
return this.previewPrimitives(compiled.primitives, {
|
|
597
669
|
reads: compiled.reads,
|
|
598
670
|
writes: compiled.writes,
|
|
@@ -601,51 +673,52 @@ export class Store {
|
|
|
601
673
|
});
|
|
602
674
|
}
|
|
603
675
|
/**
|
|
604
|
-
*
|
|
605
|
-
* (FR-18
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
676
|
+
* WHAT SEVERAL CALLS WOULD DO AS ONE GESTURE, JUDGED AS IT WOULD BE, WITHOUT
|
|
677
|
+
* DOING ANY OF IT (FR-18, FR-56).
|
|
678
|
+
*
|
|
679
|
+
* Not a second path beside `applyAll` but `applyAll`'s own, run on a
|
|
680
|
+
* rehearsal of this store — its graph and its log copied, nobody
|
|
681
|
+
* listening — so it cannot drift from it: it refuses exactly when the
|
|
682
|
+
* apply would, in the same words, for the same author, channel and batch
|
|
683
|
+
* — the policy, the seat's sight (a record the batch made earlier is its
|
|
684
|
+
* maker's for the calls after, read off the rehearsal's log as it goes),
|
|
685
|
+
* a declared agent's `may`, an act's own guard, and whatever `admit`
|
|
686
|
+
* throws, asked with the change as planned. Otherwise it answers what the
|
|
687
|
+
* apply would: the ops as the log would hold them, marked not kept (ids
|
|
688
|
+
* `preview:<n>`, `kept: false`), the net diff, and the problems after,
|
|
689
|
+
* judged in `context`. Nothing is written, logged, told to a subscriber
|
|
690
|
+
* of the store or its graph, or told to a watching harness; no id or
|
|
691
|
+
* batch this store would mint next is spent.
|
|
692
|
+
*
|
|
693
|
+
* Why not `applyAll(calls, { dryRun: true })`: a host wraps `applyAll` —
|
|
694
|
+
* `openRemote` sends every call down the wire — and a flag on it would
|
|
695
|
+
* travel with the call; and an answer that says `kept: false` in its type
|
|
696
|
+
* cannot be mistaken for one that was.
|
|
610
697
|
*/
|
|
611
|
-
previewAll(calls,
|
|
612
|
-
const
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const after = evaluate(trial, this.allInvariants(), {
|
|
634
|
-
...this.invariantOptions,
|
|
635
|
-
...(context === undefined ? {} : { context }),
|
|
698
|
+
previewAll(calls, options = {}) {
|
|
699
|
+
const { context, ...applying } = options;
|
|
700
|
+
const result = this.rehearsal().applying(calls, applying, context);
|
|
701
|
+
return { ...result, kept: false };
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* THIS STORE, TO REHEARSE ON (FR-56): its declaration, policy, modules and
|
|
705
|
+
* clock as they are, its graph and log copied, and nobody listening. Its
|
|
706
|
+
* op ids are `preview:<n>` and its batch `preview`, so nothing this store
|
|
707
|
+
* would mint next is spent; it tells a watching harness nothing.
|
|
708
|
+
*/
|
|
709
|
+
rehearsal() {
|
|
710
|
+
const twin = Object.assign(Object.create(Store.prototype), this);
|
|
711
|
+
let made = 0;
|
|
712
|
+
Object.assign(twin, {
|
|
713
|
+
graph: Graph.from(this.schema, this.graph.snapshot(), { validate: this.validate }),
|
|
714
|
+
log: OperationLog.from(this.log.all(), this.log.epochs(), { horizon: this.log.horizon }),
|
|
715
|
+
listeners: new Set(),
|
|
716
|
+
labels: undefined,
|
|
717
|
+
nextId: () => `${PREVIEW_BATCH}:${++made}`,
|
|
718
|
+
mintBatch: () => PREVIEW_BATCH,
|
|
719
|
+
rehearsing: true,
|
|
636
720
|
});
|
|
637
|
-
|
|
638
|
-
const afterKeys = new Set(after.map(violationKey));
|
|
639
|
-
return {
|
|
640
|
-
diff: diffSnapshots(start, trial.snapshot()),
|
|
641
|
-
primitives,
|
|
642
|
-
reads: [...reads],
|
|
643
|
-
writes: [...writes],
|
|
644
|
-
intent: intents.join("; "),
|
|
645
|
-
introduces: after.filter((v) => !beforeKeys.has(violationKey(v))),
|
|
646
|
-
resolves: before.filter((v) => !afterKeys.has(violationKey(v))),
|
|
647
|
-
violationsAfter: after,
|
|
648
|
-
};
|
|
721
|
+
return twin;
|
|
649
722
|
}
|
|
650
723
|
previewPrimitives(primitives, meta) {
|
|
651
724
|
const before = this.violations(meta.context);
|
|
@@ -681,7 +754,7 @@ export class Store {
|
|
|
681
754
|
* through the store's own path, whatever a host has wrapped `applyAll` in
|
|
682
755
|
* (`openRemote` sends each one down the wire).
|
|
683
756
|
*/
|
|
684
|
-
applying(calls, options) {
|
|
757
|
+
applying(calls, options, context) {
|
|
685
758
|
const batch = options.batch ?? this.mintBatch("batch");
|
|
686
759
|
const author = options.author ?? HUMAN;
|
|
687
760
|
const ops = [];
|
|
@@ -689,8 +762,9 @@ export class Store {
|
|
|
689
762
|
const reads = new Set();
|
|
690
763
|
const writes = new Set();
|
|
691
764
|
const intents = [];
|
|
692
|
-
const before = this.violations();
|
|
765
|
+
const before = this.violations(context);
|
|
693
766
|
const rollback = this.graph.snapshot();
|
|
767
|
+
const kept = this.log.length;
|
|
694
768
|
/*
|
|
695
769
|
* Permission is checked for the WHOLE gesture before any of it runs.
|
|
696
770
|
*
|
|
@@ -704,7 +778,8 @@ export class Store {
|
|
|
704
778
|
for (const call of calls) {
|
|
705
779
|
const verdict = this.permits(call, author);
|
|
706
780
|
if (!verdict.ok) {
|
|
707
|
-
|
|
781
|
+
if (!this.rehearsing)
|
|
782
|
+
tellTheWatchOfARefusal(verdict.refusal, author.id);
|
|
708
783
|
throw new PermissionDeniedError(verdict.refusal);
|
|
709
784
|
}
|
|
710
785
|
}
|
|
@@ -720,17 +795,22 @@ export class Store {
|
|
|
720
795
|
* was brought in for, and the narrower of the two wins.
|
|
721
796
|
*/
|
|
722
797
|
for (const call of calls) {
|
|
723
|
-
const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call);
|
|
798
|
+
const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call, author);
|
|
724
799
|
if (refusal) {
|
|
725
|
-
|
|
800
|
+
if (!this.rehearsing)
|
|
801
|
+
tellTheWatchOfARefusal(refusal, author.id);
|
|
726
802
|
throw new PermissionDeniedError(refusal);
|
|
727
803
|
}
|
|
728
804
|
}
|
|
729
|
-
|
|
805
|
+
if (!this.rehearsing)
|
|
806
|
+
tellTheWatchOfAnAuthor(author.id);
|
|
730
807
|
try {
|
|
731
808
|
for (const call of calls) {
|
|
732
809
|
const definition = this.mutation(call.name);
|
|
733
|
-
const
|
|
810
|
+
const missing = this.missingFor(call, author);
|
|
811
|
+
if (missing)
|
|
812
|
+
throw missing;
|
|
813
|
+
const compiled = compileMutation(this.graph, definition, call.args, this.wordedFor(author));
|
|
734
814
|
/*
|
|
735
815
|
* AN ACT THAT DID NOTHING DOES NOT GO IN THE HISTORY.
|
|
736
816
|
*
|
|
@@ -768,6 +848,8 @@ export class Store {
|
|
|
768
848
|
inverse: [...compiled.primitives].reverse().map(normalise).map(invert),
|
|
769
849
|
reads: compiled.reads,
|
|
770
850
|
writes: compiled.writes,
|
|
851
|
+
// Which records its sentence read, so a reader who may not see one is not handed the sentence (FR-55).
|
|
852
|
+
...(definition.describe !== undefined && compiled.described && compiled.described.length > 0 ? { described: compiled.described } : {}),
|
|
771
853
|
at: this.now(),
|
|
772
854
|
...(options.via !== undefined ? { via: options.via } : {}),
|
|
773
855
|
};
|
|
@@ -783,13 +865,15 @@ export class Store {
|
|
|
783
865
|
}
|
|
784
866
|
}
|
|
785
867
|
catch (error) {
|
|
786
|
-
|
|
787
|
-
|
|
868
|
+
// A batch refused part way keeps none of it: the graph, and the ops it had logged before the refusal.
|
|
869
|
+
this.putBack(rollback, kept);
|
|
870
|
+
throw this.refusalWordedFor(error, author, calls);
|
|
788
871
|
}
|
|
789
|
-
const
|
|
872
|
+
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
873
|
+
this.admitted(options, ops, allPrimitives, diff, rollback, kept);
|
|
874
|
+
const after = this.violations(context);
|
|
790
875
|
const beforeKeys = new Set(before.map(violationKey));
|
|
791
876
|
const afterKeys = new Set(after.map(violationKey));
|
|
792
|
-
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
793
877
|
this.notify(diff, ops);
|
|
794
878
|
return {
|
|
795
879
|
batch,
|
|
@@ -1056,7 +1140,7 @@ export class Store {
|
|
|
1056
1140
|
*/
|
|
1057
1141
|
const sighted = (this.policy?.sees?.length ?? 0) > 0 && !isSystem(author);
|
|
1058
1142
|
if (sighted) {
|
|
1059
|
-
const seen = checkUndo(readingOf(redact(this.log.all(),
|
|
1143
|
+
const seen = checkUndo(readingOf(redact(this.log.all(), seatLens(this, author)), () => this.log.epochs()), ids);
|
|
1060
1144
|
if (!seen.ok)
|
|
1061
1145
|
throw new UndoBlockedError(seen);
|
|
1062
1146
|
}
|
|
@@ -1094,6 +1178,7 @@ export class Store {
|
|
|
1094
1178
|
const rollback = this.graph.snapshot();
|
|
1095
1179
|
const before = this.violations();
|
|
1096
1180
|
const ops = [];
|
|
1181
|
+
const kept = this.log.length;
|
|
1097
1182
|
for (const target of [...check.ops].sort((a, b) => b.seq - a.seq)) {
|
|
1098
1183
|
const op = {
|
|
1099
1184
|
id: this.nextId(),
|
|
@@ -1115,10 +1200,11 @@ export class Store {
|
|
|
1115
1200
|
this.log.append(op);
|
|
1116
1201
|
ops.push(op);
|
|
1117
1202
|
}
|
|
1203
|
+
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
1204
|
+
this.admitted(options, ops, ops.flatMap((op) => [...op.primitives]), diff, rollback, kept);
|
|
1118
1205
|
const after = this.violations();
|
|
1119
1206
|
const beforeKeys = new Set(before.map(violationKey));
|
|
1120
1207
|
const afterKeys = new Set(after.map(violationKey));
|
|
1121
|
-
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
1122
1208
|
this.notify(diff, ops);
|
|
1123
1209
|
return {
|
|
1124
1210
|
batch,
|
|
@@ -1133,6 +1219,33 @@ export class Store {
|
|
|
1133
1219
|
violationsAfter: after,
|
|
1134
1220
|
};
|
|
1135
1221
|
}
|
|
1222
|
+
/** A change not kept: the graph as it was, and the log cut back to where it stood. */
|
|
1223
|
+
putBack(rollback, kept) {
|
|
1224
|
+
this.graph.load(rollback);
|
|
1225
|
+
if (this.log.length > kept)
|
|
1226
|
+
this.log.truncate(kept);
|
|
1227
|
+
}
|
|
1228
|
+
/** 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. */
|
|
1229
|
+
admitted(options, ops, primitives, diff, rollback, kept) {
|
|
1230
|
+
if (!options.admit || ops.length === 0)
|
|
1231
|
+
return;
|
|
1232
|
+
const size = this.graph.size;
|
|
1233
|
+
try {
|
|
1234
|
+
options.admit({
|
|
1235
|
+
ops,
|
|
1236
|
+
primitives,
|
|
1237
|
+
added: { nodes: diff.addedNodes.length, edges: diff.addedEdges.length },
|
|
1238
|
+
removed: { nodes: diff.removedNodes.length, edges: diff.removedEdges.length },
|
|
1239
|
+
changed: { nodes: diff.changedNodes.length },
|
|
1240
|
+
nodesAfter: size.nodes,
|
|
1241
|
+
edgesAfter: size.edges,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
catch (error) {
|
|
1245
|
+
this.putBack(rollback, kept);
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1136
1249
|
/** Redo is the undo of an undo — no separate stack exists. */
|
|
1137
1250
|
redo(undoBatchId, options = {}) {
|
|
1138
1251
|
return this.undo(undoBatchId, options);
|
|
@@ -1377,6 +1490,9 @@ export class Store {
|
|
|
1377
1490
|
* 1000). It is moved back to the start of the gesture it would split, so
|
|
1378
1491
|
* a batch is never half behind it.
|
|
1379
1492
|
*
|
|
1493
|
+
* The checkpoint keeps who made each record behind it (`creators`), so a
|
|
1494
|
+
* store opened on it knows a record's maker without the archived ops.
|
|
1495
|
+
*
|
|
1380
1496
|
* Nothing moves until `compact` is handed the checkpoint, so a host can
|
|
1381
1497
|
* archive what is behind it first. A store whose graph does not verify
|
|
1382
1498
|
* against its log is not compacted: the checkpoint would vouch for a
|
|
@@ -1404,7 +1520,10 @@ export class Store {
|
|
|
1404
1520
|
const verified = this.verify();
|
|
1405
1521
|
if (!verified.ok)
|
|
1406
1522
|
throw new GraphError(`Cannot compact a store that does not verify: ${verified.reason}`);
|
|
1407
|
-
|
|
1523
|
+
const checkpoint = this.log.checkpointAt(this.schema, seq, { validate: this.validate });
|
|
1524
|
+
// Who made each record behind it, so a store opened on it still knows (an `own` sight reads the log for it).
|
|
1525
|
+
const creators = creatorsBefore(this.log, seq);
|
|
1526
|
+
return Object.keys(creators).length > 0 ? { ...checkpoint, creators } : checkpoint;
|
|
1408
1527
|
}
|
|
1409
1528
|
/**
|
|
1410
1529
|
* MOVES THE UNDO HORIZON TO `checkpoint` (FR-23): the ops and epochs
|