@graview/core 0.1.0 → 0.1.3
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/README.md +7 -0
- package/dist/app.d.ts +17 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js.map +1 -1
- package/dist/arrange.d.ts +3 -2
- package/dist/arrange.d.ts.map +1 -1
- package/dist/arrange.js +19 -2
- package/dist/arrange.js.map +1 -1
- package/dist/beginning.d.ts +5 -2
- package/dist/beginning.d.ts.map +1 -1
- package/dist/beginning.js +28 -0
- package/dist/beginning.js.map +1 -1
- package/dist/capabilities.d.ts +32 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +31 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cli/check/index.d.ts +1 -0
- package/dist/cli/check/index.d.ts.map +1 -1
- package/dist/cli/check/index.js +1 -0
- package/dist/cli/check/index.js.map +1 -1
- package/dist/cli/check/lenses.d.ts.map +1 -1
- package/dist/cli/check/lenses.js +25 -0
- package/dist/cli/check/lenses.js.map +1 -1
- package/dist/cli/check/policy.d.ts.map +1 -1
- package/dist/cli/check/policy.js +35 -0
- package/dist/cli/check/policy.js.map +1 -1
- package/dist/cli/check/relations.d.ts.map +1 -1
- package/dist/cli/check/relations.js +31 -0
- package/dist/cli/check/relations.js.map +1 -1
- package/dist/cli/check/views.d.ts +14 -0
- package/dist/cli/check/views.d.ts.map +1 -0
- package/dist/cli/check/views.js +27 -0
- package/dist/cli/check/views.js.map +1 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +2 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/create.d.ts +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +63 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.d.ts +18 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +66 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/conformance/fixtures.d.ts +41 -0
- package/dist/conformance/fixtures.d.ts.map +1 -0
- package/dist/conformance/fixtures.js +950 -0
- package/dist/conformance/fixtures.js.map +1 -0
- package/dist/conformance/index.d.ts +54 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +81 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/document/canonical.d.ts +4 -0
- package/dist/document/canonical.d.ts.map +1 -0
- package/dist/document/canonical.js +29 -0
- package/dist/document/canonical.js.map +1 -0
- package/dist/document/compile.d.ts +43 -0
- package/dist/document/compile.d.ts.map +1 -0
- package/dist/document/compile.js +807 -0
- package/dist/document/compile.js.map +1 -0
- package/dist/document/diff.d.ts +32 -0
- package/dist/document/diff.d.ts.map +1 -0
- package/dist/document/diff.js +148 -0
- package/dist/document/diff.js.map +1 -0
- package/dist/document/edit.d.ts +63 -0
- package/dist/document/edit.d.ts.map +1 -0
- package/dist/document/edit.js +1432 -0
- package/dist/document/edit.js.map +1 -0
- package/dist/document/expr/analyze.d.ts +14 -0
- package/dist/document/expr/analyze.d.ts.map +1 -0
- package/dist/document/expr/analyze.js +52 -0
- package/dist/document/expr/analyze.js.map +1 -0
- package/dist/document/expr/evaluate.d.ts +36 -0
- package/dist/document/expr/evaluate.d.ts.map +1 -0
- package/dist/document/expr/evaluate.js +344 -0
- package/dist/document/expr/evaluate.js.map +1 -0
- package/dist/document/expr/index.d.ts +8 -0
- package/dist/document/expr/index.d.ts.map +1 -0
- package/dist/document/expr/index.js +5 -0
- package/dist/document/expr/index.js.map +1 -0
- package/dist/document/expr/parse.d.ts +50 -0
- package/dist/document/expr/parse.d.ts.map +1 -0
- package/dist/document/expr/parse.js +240 -0
- package/dist/document/expr/parse.js.map +1 -0
- package/dist/document/expr/print.d.ts +4 -0
- package/dist/document/expr/print.d.ts.map +1 -0
- package/dist/document/expr/print.js +81 -0
- package/dist/document/expr/print.js.map +1 -0
- package/dist/document/findings.d.ts +15 -0
- package/dist/document/findings.d.ts.map +1 -0
- package/dist/document/findings.js +30 -0
- package/dist/document/findings.js.map +1 -0
- package/dist/document/graview-template.d.ts +148 -0
- package/dist/document/graview-template.d.ts.map +1 -0
- package/dist/document/graview-template.js +450 -0
- package/dist/document/graview-template.js.map +1 -0
- package/dist/document/index.d.ts +27 -0
- package/dist/document/index.d.ts.map +1 -0
- package/dist/document/index.js +16 -0
- package/dist/document/index.js.map +1 -0
- package/dist/document/migrate.d.ts +41 -0
- package/dist/document/migrate.d.ts.map +1 -0
- package/dist/document/migrate.js +252 -0
- package/dist/document/migrate.js.map +1 -0
- package/dist/document/patch.d.ts +16 -0
- package/dist/document/patch.d.ts.map +1 -0
- package/dist/document/patch.js +79 -0
- package/dist/document/patch.js.map +1 -0
- package/dist/document/rules.d.ts +40 -0
- package/dist/document/rules.d.ts.map +1 -0
- package/dist/document/rules.js +88 -0
- package/dist/document/rules.js.map +1 -0
- package/dist/document/schema.d.ts +461 -0
- package/dist/document/schema.d.ts.map +1 -0
- package/dist/document/schema.js +199 -0
- package/dist/document/schema.js.map +1 -0
- package/dist/document/template.d.ts +28 -0
- package/dist/document/template.d.ts.map +1 -0
- package/dist/document/template.js +117 -0
- package/dist/document/template.js.map +1 -0
- package/dist/document/to-document.d.ts +34 -0
- package/dist/document/to-document.d.ts.map +1 -0
- package/dist/document/to-document.js +186 -0
- package/dist/document/to-document.js.map +1 -0
- package/dist/document/upgrade.d.ts +9 -0
- package/dist/document/upgrade.d.ts.map +1 -0
- package/dist/document/upgrade.js +19 -0
- package/dist/document/upgrade.js.map +1 -0
- package/dist/document/views.d.ts +65 -0
- package/dist/document/views.d.ts.map +1 -0
- package/dist/document/views.js +264 -0
- package/dist/document/views.js.map +1 -0
- package/dist/formats.d.ts +50 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +75 -0
- package/dist/formats.js.map +1 -0
- package/dist/graph/graph.d.ts +61 -3
- package/dist/graph/graph.d.ts.map +1 -1
- package/dist/graph/graph.js +187 -20
- package/dist/graph/graph.js.map +1 -1
- package/dist/index.d.ts +43 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -11
- package/dist/index.js.map +1 -1
- package/dist/integrity.d.ts +46 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +179 -0
- package/dist/integrity.js.map +1 -0
- package/dist/invariants/engine.d.ts +8 -0
- package/dist/invariants/engine.d.ts.map +1 -1
- package/dist/invariants/engine.js +40 -2
- package/dist/invariants/engine.js.map +1 -1
- package/dist/invariants/types.d.ts +19 -0
- package/dist/invariants/types.d.ts.map +1 -1
- package/dist/labels.d.ts +65 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +123 -0
- package/dist/labels.js.map +1 -0
- package/dist/mutations/define-mutation.d.ts.map +1 -1
- package/dist/mutations/define-mutation.js +5 -6
- package/dist/mutations/define-mutation.js.map +1 -1
- package/dist/mutations/derive-edits.d.ts.map +1 -1
- package/dist/mutations/derive-edits.js +20 -1
- package/dist/mutations/derive-edits.js.map +1 -1
- package/dist/mutations/form.d.ts +9 -0
- package/dist/mutations/form.d.ts.map +1 -1
- package/dist/mutations/form.js +20 -0
- package/dist/mutations/form.js.map +1 -1
- package/dist/mutations/node-ref.d.ts +6 -0
- package/dist/mutations/node-ref.d.ts.map +1 -1
- package/dist/mutations/node-ref.js +22 -8
- package/dist/mutations/node-ref.js.map +1 -1
- package/dist/mutations/types.d.ts +8 -0
- package/dist/mutations/types.d.ts.map +1 -1
- package/dist/mutations/words.d.ts +49 -0
- package/dist/mutations/words.d.ts.map +1 -0
- package/dist/mutations/words.js +61 -0
- package/dist/mutations/words.js.map +1 -0
- package/dist/ops/log.d.ts +156 -4
- package/dist/ops/log.d.ts.map +1 -1
- package/dist/ops/log.js +234 -51
- package/dist/ops/log.js.map +1 -1
- package/dist/ops/revisions.d.ts +66 -0
- package/dist/ops/revisions.d.ts.map +1 -0
- package/dist/ops/revisions.js +97 -0
- package/dist/ops/revisions.js.map +1 -0
- package/dist/ops/types.d.ts +49 -1
- package/dist/ops/types.d.ts.map +1 -1
- package/dist/ops/undo.d.ts +29 -2
- package/dist/ops/undo.d.ts.map +1 -1
- package/dist/ops/undo.js +110 -1
- package/dist/ops/undo.js.map +1 -1
- package/dist/ops/withheld.d.ts +37 -0
- package/dist/ops/withheld.d.ts.map +1 -0
- package/dist/ops/withheld.js +84 -0
- package/dist/ops/withheld.js.map +1 -0
- package/dist/permissions/policy.d.ts +15 -0
- package/dist/permissions/policy.d.ts.map +1 -1
- package/dist/permissions/policy.js +28 -1
- package/dist/permissions/policy.js.map +1 -1
- package/dist/permissions/sight.d.ts +66 -0
- package/dist/permissions/sight.d.ts.map +1 -0
- package/dist/permissions/sight.js +131 -0
- package/dist/permissions/sight.js.map +1 -0
- package/dist/permissions/types.d.ts +26 -0
- package/dist/permissions/types.d.ts.map +1 -1
- package/dist/permissions/types.js.map +1 -1
- package/dist/persistence/memory.d.ts.map +1 -1
- package/dist/persistence/memory.js +25 -0
- package/dist/persistence/memory.js.map +1 -1
- package/dist/persistence/sql.d.ts +43 -0
- package/dist/persistence/sql.d.ts.map +1 -0
- package/dist/persistence/sql.js +154 -0
- package/dist/persistence/sql.js.map +1 -0
- package/dist/persistence/sqlite.d.ts +17 -3
- package/dist/persistence/sqlite.d.ts.map +1 -1
- package/dist/persistence/sqlite.js +32 -96
- package/dist/persistence/sqlite.js.map +1 -1
- package/dist/persistence/types.d.ts +19 -0
- package/dist/persistence/types.d.ts.map +1 -1
- package/dist/presence.d.ts +56 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +62 -2
- package/dist/presence.js.map +1 -1
- package/dist/refusal.d.ts +34 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +40 -0
- package/dist/refusal.js.map +1 -0
- package/dist/scaffold/from-template.d.ts +16 -0
- package/dist/scaffold/from-template.d.ts.map +1 -0
- package/dist/scaffold/from-template.js +157 -0
- package/dist/scaffold/from-template.js.map +1 -0
- package/dist/scaffold/index.d.ts +10 -1
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/scaffold/index.js +35 -7
- package/dist/scaffold/index.js.map +1 -1
- package/dist/scaffold/names.d.ts +4 -0
- package/dist/scaffold/names.d.ts.map +1 -1
- package/dist/scaffold/names.js.map +1 -1
- package/dist/scaffold/project.d.ts +1 -1
- package/dist/scaffold/project.d.ts.map +1 -1
- package/dist/scaffold/project.js +44 -13
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/ui.d.ts.map +1 -1
- package/dist/scaffold/ui.js +19 -10
- package/dist/scaffold/ui.js.map +1 -1
- package/dist/schema/define-node.d.ts +20 -0
- package/dist/schema/define-node.d.ts.map +1 -1
- package/dist/schema/define-node.js +27 -2
- package/dist/schema/define-node.js.map +1 -1
- package/dist/schema/path.d.ts +19 -0
- package/dist/schema/path.d.ts.map +1 -0
- package/dist/schema/path.js +34 -0
- package/dist/schema/path.js.map +1 -0
- package/dist/schema/types.d.ts +17 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/seen.d.ts +64 -0
- package/dist/seen.d.ts.map +1 -0
- package/dist/seen.js +200 -0
- package/dist/seen.js.map +1 -0
- package/dist/store.d.ts +378 -8
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +801 -41
- package/dist/store.js.map +1 -1
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -1
- package/dist/testing.js.map +1 -1
- package/dist/theme/palettes.d.ts.map +1 -1
- package/dist/theme/palettes.js +4 -0
- package/dist/theme/palettes.js.map +1 -1
- package/dist/theme/types.d.ts +9 -0
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/types.js +4 -0
- package/dist/theme/types.js.map +1 -1
- package/dist/validate-graph.d.ts +84 -0
- package/dist/validate-graph.d.ts.map +1 -0
- package/dist/validate-graph.js +295 -0
- package/dist/validate-graph.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/views/types.d.ts +8 -0
- package/dist/views/types.d.ts.map +1 -1
- package/dist/views/types.js +5 -0
- package/dist/views/types.js.map +1 -1
- package/dist/watched.d.ts +39 -0
- package/dist/watched.d.ts.map +1 -1
- package/dist/watched.js +94 -1
- package/dist/watched.js.map +1 -1
- package/dist/who.d.ts +24 -1
- package/dist/who.d.ts.map +1 -1
- package/dist/who.js +49 -2
- package/dist/who.js.map +1 -1
- package/package.json +9 -1
package/dist/store.js
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
|
+
import { readingOf, seenBy, seesId } from "./seen.js";
|
|
1
2
|
import { Graph, GraphError } from "./graph/graph.js";
|
|
2
3
|
import { resolveModules } from "./modules.js";
|
|
3
|
-
import { diffSnapshots } from "./graph/diff.js";
|
|
4
|
-
import { invert, normalise } from "./graph/primitives.js";
|
|
4
|
+
import { diffSnapshots, EMPTY_DIFF, isEmptyDiff } from "./graph/diff.js";
|
|
5
|
+
import { invert, normalise, writesOf } from "./graph/primitives.js";
|
|
6
|
+
import { LabelIndex, refusalFor } from "./labels.js";
|
|
7
|
+
import { nodeRefArgs } from "./mutations/node-ref.js";
|
|
8
|
+
import { verifyFold } from "./integrity.js";
|
|
5
9
|
import { evaluate } from "./invariants/engine.js";
|
|
6
10
|
import { compileMutation } from "./mutations/define-mutation.js";
|
|
7
11
|
import { deriveMutations, derivedVia } from "./mutations/derive-edits.js";
|
|
8
12
|
import { OperationLog } from "./ops/log.js";
|
|
9
|
-
import { permits, permittedMutations } from "./permissions/policy.js";
|
|
13
|
+
import { actingAs, isSystem, permits, permittedMutations } from "./permissions/policy.js";
|
|
14
|
+
import { redact } from "./ops/withheld.js";
|
|
10
15
|
import { nounOf } from "./schema/define-node.js";
|
|
11
16
|
import { PermissionDeniedError } from "./permissions/types.js";
|
|
12
|
-
import { checkUndo, undoPrimitives } from "./ops/undo.js";
|
|
17
|
+
import { checkUndo, UndoBlockedError, undoPrimitives } from "./ops/undo.js";
|
|
18
|
+
import { validateGraph } from "./validate-graph.js";
|
|
13
19
|
import { tellTheWatchItsNames, tellTheWatchOfAStore, tellTheWatchOfAnAuthor, tellTheWatchOfARefusal } from "./watched.js";
|
|
14
20
|
const HUMAN = { kind: "human" };
|
|
21
|
+
/** Who turns a module off or on: the workspace itself, `system · modules` (FR-12). */
|
|
22
|
+
export const MODULES_AUTHOR = { kind: "system", id: "modules", name: "Modules" };
|
|
23
|
+
/**
|
|
24
|
+
* WHETHER UNDOING AN OP PUTS BACK EXACTLY WHAT IT TOOK (FR-28).
|
|
25
|
+
*
|
|
26
|
+
* An op that was not an act — a repair from `repairPlan`, a migration, a
|
|
27
|
+
* host's own change through `applyPrimitives` — is undone by putting the
|
|
28
|
+
* records back as they were, even a record that no longer fits: undoing a
|
|
29
|
+
* repair means the misfit returns, and `validateGraph` says so again. The
|
|
30
|
+
* undo of an ACT is itself a change within the declaration, and is held to
|
|
31
|
+
* it like any write: one whose inverse the current declaration refuses is
|
|
32
|
+
* refused, and says why.
|
|
33
|
+
*/
|
|
34
|
+
const putsBack = (op) => op.mutation === null && op.undoes === undefined;
|
|
15
35
|
/** A violation's identity across judgements: the rule, what it is about, and what it says. */
|
|
16
36
|
export function violationKey(v) {
|
|
17
37
|
return `${v.invariant}|${v.subjectId ?? ""}|${v.message}`;
|
|
@@ -24,6 +44,35 @@ export function violationKey(v) {
|
|
|
24
44
|
* agent's work needs no bespoke observability layer — it produces the same
|
|
25
45
|
* diff a human edit does, carrying the same attribution.
|
|
26
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* An op a store could not take: its primitives do not fit the graph (a node
|
|
49
|
+
* it patches is not there, an edge to nowhere, a field the schema refuses).
|
|
50
|
+
* Nothing it was handed has landed — the store is as it was — and `op` says
|
|
51
|
+
* which one, with the graph's own error as `cause`.
|
|
52
|
+
*/
|
|
53
|
+
export class ReceiveError extends Error {
|
|
54
|
+
op;
|
|
55
|
+
constructor(op, cause) {
|
|
56
|
+
super(`Op "${op.id}" (${op.intent}) could not be applied: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
57
|
+
this.op = op;
|
|
58
|
+
this.name = "ReceiveError";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A tag nobody else's store is using, for this store's batch ids. Drawn
|
|
63
|
+
* from the platform's random source where there is one, which every page
|
|
64
|
+
* and Node 22 has.
|
|
65
|
+
*/
|
|
66
|
+
function storeTag() {
|
|
67
|
+
const random = globalThis.crypto;
|
|
68
|
+
const bytes = new Uint8Array(6);
|
|
69
|
+
if (random?.getRandomValues)
|
|
70
|
+
random.getRandomValues(bytes);
|
|
71
|
+
else
|
|
72
|
+
for (let i = 0; i < bytes.length; i++)
|
|
73
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
74
|
+
return [...bytes].map((byte) => byte.toString(36).padStart(2, "0")).join("");
|
|
75
|
+
}
|
|
27
76
|
export class Store {
|
|
28
77
|
schema;
|
|
29
78
|
graph;
|
|
@@ -43,11 +92,19 @@ export class Store {
|
|
|
43
92
|
intelligence;
|
|
44
93
|
/** What each declared agent may do, by provider name. */
|
|
45
94
|
may = new Map();
|
|
46
|
-
|
|
47
|
-
|
|
95
|
+
declaredModules;
|
|
96
|
+
startingModules;
|
|
97
|
+
/** The projection, and the log it was read from: its length and its last op. */
|
|
98
|
+
projected;
|
|
48
99
|
nextId;
|
|
49
100
|
now;
|
|
101
|
+
validate;
|
|
102
|
+
mintBatch;
|
|
50
103
|
counter = 0;
|
|
104
|
+
/** The default op id generator's count. */
|
|
105
|
+
opCount = 0;
|
|
106
|
+
/** Above zero while a rebase applies its pending calls again: they are told as one change at the end. */
|
|
107
|
+
quiet = 0;
|
|
51
108
|
listeners = new Set();
|
|
52
109
|
constructor(options) {
|
|
53
110
|
this.schema = options.schema;
|
|
@@ -59,10 +116,13 @@ export class Store {
|
|
|
59
116
|
if (provider.may)
|
|
60
117
|
this.may.set(provider.name, new Set(provider.may));
|
|
61
118
|
}
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
this.nextId = options.ids ?? (() => `op${++
|
|
65
|
-
this.now = options.now ?? (() => new Date(
|
|
119
|
+
this.declaredModules = options.modules;
|
|
120
|
+
this.startingModules = options.enabledModules;
|
|
121
|
+
this.nextId = options.ids ?? (() => `op${++this.opCount}`);
|
|
122
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
123
|
+
this.validate = options.validate ?? true;
|
|
124
|
+
const tag = storeTag();
|
|
125
|
+
this.mintBatch = options.batchIds ?? ((kind) => `${kind}:${tag}:${++this.counter}`);
|
|
66
126
|
for (const mutation of options.mutations ?? []) {
|
|
67
127
|
if (this.mutations.has(mutation.name)) {
|
|
68
128
|
throw new GraphError(`Duplicate mutation "${mutation.name}"`);
|
|
@@ -81,19 +141,21 @@ export class Store {
|
|
|
81
141
|
this.mutations.set(mutation.name, mutation);
|
|
82
142
|
}
|
|
83
143
|
tellTheWatchItsNames(options.schema, this.mutations.values(), options.policy);
|
|
144
|
+
const horizon = options.horizon !== undefined ? { horizon: options.horizon } : {};
|
|
84
145
|
if (options.log && options.snapshot) {
|
|
85
146
|
// Hydrate: the graph as stored, the history as recorded.
|
|
86
|
-
this.log = OperationLog.from(options.log);
|
|
147
|
+
this.log = OperationLog.from(options.log, options.epochs, horizon);
|
|
87
148
|
this.graph = Graph.from(options.schema, options.snapshot, {
|
|
88
149
|
validate: options.validate ?? true,
|
|
89
150
|
});
|
|
90
151
|
}
|
|
91
152
|
else if (options.log) {
|
|
92
|
-
this.log = OperationLog.from(options.log);
|
|
93
|
-
|
|
153
|
+
this.log = OperationLog.from(options.log, options.epochs, horizon);
|
|
154
|
+
const last = this.log.lastEpoch();
|
|
155
|
+
this.graph = this.log.fold(options.schema, { validate: options.validate ?? true, ...(last ? { from: last } : {}) });
|
|
94
156
|
}
|
|
95
157
|
else {
|
|
96
|
-
this.log =
|
|
158
|
+
this.log = OperationLog.from([], options.epochs ?? (options.snapshot ? [{ seq: options.horizon ?? 0, base: options.snapshot }] : []), horizon);
|
|
97
159
|
this.graph = Graph.from(options.schema, options.snapshot ?? { nodes: [], edges: [] }, {
|
|
98
160
|
validate: options.validate ?? true,
|
|
99
161
|
});
|
|
@@ -113,15 +175,98 @@ export class Store {
|
|
|
113
175
|
* Only the DEFAULT generators are wound forward; an app that supplies
|
|
114
176
|
* its own `ids` owns their uniqueness.
|
|
115
177
|
*/
|
|
178
|
+
this.countPastTheLog();
|
|
179
|
+
tellTheWatchOfAStore(this);
|
|
180
|
+
}
|
|
181
|
+
/** Winds the default op and batch counters past every id the log holds, and past the horizon (FR-23). */
|
|
182
|
+
countPastTheLog() {
|
|
116
183
|
const trailing = (value) => {
|
|
117
184
|
const digits = /(\d+)$/.exec(value);
|
|
118
185
|
return digits ? Number(digits[1]) : 0;
|
|
119
186
|
};
|
|
120
187
|
for (const op of this.log.all()) {
|
|
121
188
|
this.counter = Math.max(this.counter, trailing(op.batch));
|
|
122
|
-
|
|
189
|
+
this.opCount = Math.max(this.opCount, trailing(op.id));
|
|
123
190
|
}
|
|
124
|
-
|
|
191
|
+
// Behind an undo horizon (FR-23) are ids the log no longer holds: count on past them all.
|
|
192
|
+
if (this.log.horizon > 0)
|
|
193
|
+
this.opCount = Math.max(this.opCount, this.log.length);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* What the enabled modules work out to; every surface reads this one answer.
|
|
197
|
+
*
|
|
198
|
+
* READ FROM THE LOG (FR-12): the set the last `system · modules` op says,
|
|
199
|
+
* or the one the store was opened with when no op has said one. Kept
|
|
200
|
+
* between reads and re-read only past what it has read — a log only ever
|
|
201
|
+
* grows at its end, and a rebase that cut it is read again from the start.
|
|
202
|
+
*/
|
|
203
|
+
get modules() {
|
|
204
|
+
const all = this.log.all();
|
|
205
|
+
const last = all.at(-1)?.id;
|
|
206
|
+
const held = this.projected;
|
|
207
|
+
if (held && held.length === all.length && held.last === last)
|
|
208
|
+
return held.projection;
|
|
209
|
+
let said;
|
|
210
|
+
let from = 0;
|
|
211
|
+
if (held && held.length <= all.length && (held.length === 0 || all[held.length - 1]?.id === held.last)) {
|
|
212
|
+
said = held.said;
|
|
213
|
+
from = held.length;
|
|
214
|
+
}
|
|
215
|
+
for (let at = all.length - 1; at >= from; at--) {
|
|
216
|
+
const stated = all[at].enabledModules;
|
|
217
|
+
if (stated !== undefined) {
|
|
218
|
+
said = stated;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const projection = held && said === held.said ? held.projection : resolveModules(this.declaredModules, said ?? this.startingModules);
|
|
223
|
+
this.projected = { projection, length: all.length, last, said };
|
|
224
|
+
return projection;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* TURN MODULES OFF OR ON, AS AN OP (FR-12).
|
|
228
|
+
*
|
|
229
|
+
* The enabled set is a fact about the workspace — usually what it pays
|
|
230
|
+
* for — and changing it is history: one op, authored `system · modules`,
|
|
231
|
+
* saying the set it leaves (`enabledModules`) in a sentence a person can
|
|
232
|
+
* read. It touches no record, so it folds to nothing and takes nothing
|
|
233
|
+
* away: a module off is a horizon, and turning it on again brings every
|
|
234
|
+
* record back as it was. Not undone: the set changes by turning a module
|
|
235
|
+
* the other way. `undefined` is every declared module. Nothing is written
|
|
236
|
+
* when the set is the one the store already has.
|
|
237
|
+
*/
|
|
238
|
+
setEnabledModules(enabled, options = {}) {
|
|
239
|
+
const before = this.modules.enabled;
|
|
240
|
+
const after = resolveModules(this.declaredModules, enabled).enabled;
|
|
241
|
+
const off = [...before].filter((name) => !after.has(name));
|
|
242
|
+
const on = [...after].filter((name) => !before.has(name));
|
|
243
|
+
if (off.length === 0 && on.length === 0)
|
|
244
|
+
return undefined;
|
|
245
|
+
const named = (names) => names
|
|
246
|
+
.map((name) => {
|
|
247
|
+
const kinds = this.declaredModules?.[name]?.kinds ?? [];
|
|
248
|
+
return kinds.length > 0 ? kinds.map((kind) => this.schema.tryDefinition(kind)?.plural ?? kind).join(", ") : name;
|
|
249
|
+
})
|
|
250
|
+
.join(", ");
|
|
251
|
+
const said = [off.length > 0 ? `Turn off ${named(off)}` : "", on.length > 0 ? `turn on ${named(on)}` : ""].filter(Boolean).join("; ");
|
|
252
|
+
const op = {
|
|
253
|
+
id: this.nextId(),
|
|
254
|
+
seq: this.log.length,
|
|
255
|
+
batch: options.batch ?? this.mintBatch("batch"),
|
|
256
|
+
author: MODULES_AUTHOR,
|
|
257
|
+
intent: said.charAt(0).toUpperCase() + said.slice(1),
|
|
258
|
+
mutation: null,
|
|
259
|
+
primitives: [],
|
|
260
|
+
inverse: [],
|
|
261
|
+
reads: [],
|
|
262
|
+
writes: [],
|
|
263
|
+
at: this.now(),
|
|
264
|
+
...(options.via !== undefined ? { via: options.via } : {}),
|
|
265
|
+
enabledModules: [...after].sort(),
|
|
266
|
+
};
|
|
267
|
+
this.log.append(op);
|
|
268
|
+
this.notify(EMPTY_DIFF, [op]);
|
|
269
|
+
return op;
|
|
125
270
|
}
|
|
126
271
|
mutation(name) {
|
|
127
272
|
const found = this.mutations.get(name);
|
|
@@ -183,7 +328,7 @@ export class Store {
|
|
|
183
328
|
* repair as the starter seat, which may only add, and the press met
|
|
184
329
|
* "starter may not take-off here" (W-110).
|
|
185
330
|
*/
|
|
186
|
-
const narrowed = this.refusesAgent(call, principal);
|
|
331
|
+
const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call) ?? this.namesUnseen(call, principal);
|
|
187
332
|
if (narrowed)
|
|
188
333
|
return { ok: false, refusal: narrowed };
|
|
189
334
|
return permits(this.policy, principal, call.name, this.subjectKindOf(call), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
|
|
@@ -216,6 +361,69 @@ export class Store {
|
|
|
216
361
|
wouldNeed: [],
|
|
217
362
|
};
|
|
218
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* A CALL THAT NAMES A RECORD OF A MODULE THAT IS OFF IS REFUSED (FR-12),
|
|
366
|
+
* whoever makes it: the module's own acts are already off, and an act
|
|
367
|
+
* the core owns — a rename that takes any kind — must not reach into a
|
|
368
|
+
* district the workspace does not have. The sentence names the act.
|
|
369
|
+
*/
|
|
370
|
+
namesTurnedOff(call) {
|
|
371
|
+
const off = this.modules.disabledKinds;
|
|
372
|
+
if (off.size === 0)
|
|
373
|
+
return undefined;
|
|
374
|
+
const mutation = this.mutations.get(call.name);
|
|
375
|
+
if (!mutation)
|
|
376
|
+
return undefined;
|
|
377
|
+
const named = [];
|
|
378
|
+
if (mutation.subject)
|
|
379
|
+
named.push(call.args[mutation.subject.arg]);
|
|
380
|
+
for (const ref of nodeRefArgs(mutation.input))
|
|
381
|
+
named.push(call.args[ref.name]);
|
|
382
|
+
const ids = named.flatMap((value) => (typeof value === "string" ? [value] : Array.isArray(value) ? value.filter((inner) => typeof inner === "string") : []));
|
|
383
|
+
if (!ids.some((id) => off.has(this.graph.getNode(id)?.kind)))
|
|
384
|
+
return undefined;
|
|
385
|
+
return {
|
|
386
|
+
mutation: call.name,
|
|
387
|
+
message: `“${mutation.title ?? call.name}” names a record of a module this workspace has turned off.`,
|
|
388
|
+
wouldNeed: [],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* A CALL THAT NAMES A RECORD THE CALLER MAY NOT SEE IS REFUSED (FR-02),
|
|
393
|
+
* before any grant is read: an act on a record is a way of reading it —
|
|
394
|
+
* its preview, its refusal, what its rules say after — and a seat that
|
|
395
|
+
* could act on what it may not see could learn it that way. Only a
|
|
396
|
+
* record that is there is judged; an id that names nothing is the act's
|
|
397
|
+
* own to refuse. The sentence names the act, never the record or its kind.
|
|
398
|
+
*/
|
|
399
|
+
namesUnseen(call, principal) {
|
|
400
|
+
if (!this.policy?.sees?.length || isSystem(principal))
|
|
401
|
+
return undefined;
|
|
402
|
+
const mutation = this.mutations.get(call.name);
|
|
403
|
+
if (!mutation)
|
|
404
|
+
return undefined;
|
|
405
|
+
const named = new Set();
|
|
406
|
+
const add = (value) => {
|
|
407
|
+
if (typeof value === "string")
|
|
408
|
+
named.add(value);
|
|
409
|
+
else if (Array.isArray(value))
|
|
410
|
+
for (const inner of value)
|
|
411
|
+
if (typeof inner === "string")
|
|
412
|
+
named.add(inner);
|
|
413
|
+
};
|
|
414
|
+
if (mutation.subject)
|
|
415
|
+
add(call.args[mutation.subject.arg]);
|
|
416
|
+
for (const ref of nodeRefArgs(mutation.input))
|
|
417
|
+
add(call.args[ref.name]);
|
|
418
|
+
const visible = seesId(this, principal);
|
|
419
|
+
if (![...named].some((id) => this.graph.has(id) && !visible(id)))
|
|
420
|
+
return undefined;
|
|
421
|
+
return {
|
|
422
|
+
mutation: call.name,
|
|
423
|
+
message: `Not permitted: “${mutation.title ?? call.name}” names a record you may not see.`,
|
|
424
|
+
wouldNeed: [],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
219
427
|
/**
|
|
220
428
|
* Whether a principal may run ANY act of a module drawn only for those
|
|
221
429
|
* who administer it. This is the one question the interface asks before
|
|
@@ -236,6 +444,46 @@ export class Store {
|
|
|
236
444
|
return kinds.some((kind) => permits(this.policy, principal, name, kind, this.viaOf(mutation)).ok);
|
|
237
445
|
});
|
|
238
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* The store as one principal may see it: its graph, log, history and
|
|
449
|
+
* problems hold only what the policy's `sees` lets them see, and every
|
|
450
|
+
* act still comes here. With no `sees`, this store itself (see `seen.ts`).
|
|
451
|
+
*/
|
|
452
|
+
seenBy(principal) {
|
|
453
|
+
return seenBy(this, principal);
|
|
454
|
+
}
|
|
455
|
+
/** Whether a principal may see one record (the policy's `sees`). */
|
|
456
|
+
sees(principal, id) {
|
|
457
|
+
return this.graph.has(id) && seesId(this, principal)(id);
|
|
458
|
+
}
|
|
459
|
+
labels;
|
|
460
|
+
/**
|
|
461
|
+
* WHAT A NAME GIVEN FOR A NODE ARGUMENT MEANS, to this principal (FR-33).
|
|
462
|
+
*
|
|
463
|
+
* An id of a record the principal may see is that record, whatever it is
|
|
464
|
+
* called. Otherwise a label, case and accents aside, among the records of
|
|
465
|
+
* the kinds the argument accepts; failing that, the one label the name
|
|
466
|
+
* starts. Only records the principal may see are ever candidates — a
|
|
467
|
+
* name never tells a seat that something it may not see exists. Several
|
|
468
|
+
* matches are refused with every candidate; none says so.
|
|
469
|
+
*/
|
|
470
|
+
resolveRef(arg, given, principal = HUMAN) {
|
|
471
|
+
const sighted = (this.policy?.sees?.length ?? 0) > 0;
|
|
472
|
+
const visible = (id) => !sighted || this.sees(principal, id);
|
|
473
|
+
const named = this.graph.getNode(given);
|
|
474
|
+
this.labels ??= new LabelIndex(this.graph);
|
|
475
|
+
if (named && visible(given))
|
|
476
|
+
return { ok: true, id: given, label: this.labels.labelOf(given) ?? given, by: "id" };
|
|
477
|
+
const found = this.labels.lookup(arg.kinds, given);
|
|
478
|
+
const exact = found.exact.filter(visible);
|
|
479
|
+
const exactly = exact.length > 0;
|
|
480
|
+
const candidates = (exactly ? exact : found.prefix.filter(visible))
|
|
481
|
+
.map((id) => ({ id, kind: this.graph.getNode(id).kind, label: this.labels.labelOf(id) ?? id }))
|
|
482
|
+
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : a.id < b.id ? -1 : 1));
|
|
483
|
+
if (candidates.length === 1)
|
|
484
|
+
return { ok: true, id: candidates[0].id, label: candidates[0].label, by: exactly ? "label" : "prefix" };
|
|
485
|
+
return refusalFor(this.schema, arg, given, candidates);
|
|
486
|
+
}
|
|
239
487
|
/** The kinds of administered modules this principal may not see at all. */
|
|
240
488
|
kindsKeptFrom(principal = HUMAN) {
|
|
241
489
|
const kept = new Set();
|
|
@@ -245,6 +493,22 @@ export class Store {
|
|
|
245
493
|
for (const kind of module.kinds ?? [])
|
|
246
494
|
kept.add(kind);
|
|
247
495
|
}
|
|
496
|
+
/*
|
|
497
|
+
* A KIND THE POLICY SAYS THIS SEAT SEES NONE OF, and may not begin: no
|
|
498
|
+
* district, no tab, no "none yet". Somebody browsing a showroom has no
|
|
499
|
+
* test drives to see and none to book, and a Test drives page that said
|
|
500
|
+
* "none yet — waiting for shoppers" was a page about other people.
|
|
501
|
+
*/
|
|
502
|
+
const sights = this.policy?.sees ?? [];
|
|
503
|
+
if (sights.length > 0 && !isSystem(principal)) {
|
|
504
|
+
const creatable = new Set(this.permittedMutations(principal).flatMap((mutation) => mutation.creates ?? []));
|
|
505
|
+
// Every kind: once a policy says who sees what, a kind no sight names is seen by nobody.
|
|
506
|
+
for (const kind of this.schema.kinds) {
|
|
507
|
+
const seeing = sights.some((sight) => sight.kinds.includes(kind) && (sight.roles === "*" || (principal.roles ?? []).some((role) => sight.roles.includes(role))) && (!sight.own || principal.id !== undefined));
|
|
508
|
+
if (!seeing && !creatable.has(kind))
|
|
509
|
+
kept.add(kind);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
248
512
|
return kept;
|
|
249
513
|
}
|
|
250
514
|
subjectIdOf(call) {
|
|
@@ -266,7 +530,7 @@ export class Store {
|
|
|
266
530
|
const declared = permittedMutations(this.policy, principal, all.filter((mutation) => !mutation.derived));
|
|
267
531
|
// A derived act is offered when the kind's own acts it rides are.
|
|
268
532
|
const derived = all.filter((mutation) => mutation.derived !== undefined &&
|
|
269
|
-
permits(this.policy, principal, mutation.name, mutation.derived.kind, this.viaOf(mutation)).ok);
|
|
533
|
+
permits(this.policy, principal, mutation.name, mutation.derived.kind, this.viaOf(mutation), actingAs(principal).id).ok);
|
|
270
534
|
return [...declared, ...derived];
|
|
271
535
|
}
|
|
272
536
|
/**
|
|
@@ -283,8 +547,14 @@ export class Store {
|
|
|
283
547
|
if (!subject)
|
|
284
548
|
return undefined;
|
|
285
549
|
const id = call.args[subject.arg];
|
|
550
|
+
/*
|
|
551
|
+
* NOT CHOSEN YET — offered from the far end, the subject is still to be
|
|
552
|
+
* asked for. Its kind is the act's to say when it names one: asked as
|
|
553
|
+
* "no kind", a grant drawn by kind ("a reviewer, on talks") refused the
|
|
554
|
+
* reviewer on every topic with "a chair or a reviewer can".
|
|
555
|
+
*/
|
|
286
556
|
if (typeof id !== "string")
|
|
287
|
-
return undefined;
|
|
557
|
+
return subject.kinds !== "*" && subject.kinds.length === 1 ? subject.kinds[0] : undefined;
|
|
288
558
|
return this.graph.getNode(id)?.kind;
|
|
289
559
|
}
|
|
290
560
|
/** Current violations, evaluated fresh — nothing is cached or stale. */
|
|
@@ -330,10 +600,57 @@ export class Store {
|
|
|
330
600
|
context,
|
|
331
601
|
});
|
|
332
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* What several calls would do as one gesture, without doing any of it
|
|
605
|
+
* (FR-18): each compiles on the graph the one before it left, as
|
|
606
|
+
* `applyAll` would run them, on a copy. The diff is the net of them all,
|
|
607
|
+
* and `introduces` and `resolves` judge the end state against today's.
|
|
608
|
+
* A call that cannot compile throws, as it would on applying; nothing is
|
|
609
|
+
* written, logged or heard either way.
|
|
610
|
+
*/
|
|
611
|
+
previewAll(calls, context) {
|
|
612
|
+
const before = this.violations(context);
|
|
613
|
+
const start = this.graph.snapshot();
|
|
614
|
+
const trial = Graph.from(this.schema, start, { validate: this.validate });
|
|
615
|
+
const primitives = [];
|
|
616
|
+
const reads = new Set();
|
|
617
|
+
const writes = new Set();
|
|
618
|
+
const intents = [];
|
|
619
|
+
for (const call of calls) {
|
|
620
|
+
const compiled = compileMutation(trial, this.mutation(call.name), call.args);
|
|
621
|
+
// An act that does nothing leaves no trace when applied, nor here.
|
|
622
|
+
if (compiled.primitives.length === 0)
|
|
623
|
+
continue;
|
|
624
|
+
const recorded = compiled.primitives.map(normalise);
|
|
625
|
+
trial.applyPrimitives(recorded);
|
|
626
|
+
primitives.push(...recorded);
|
|
627
|
+
for (const id of compiled.reads)
|
|
628
|
+
reads.add(id);
|
|
629
|
+
for (const id of compiled.writes)
|
|
630
|
+
writes.add(id);
|
|
631
|
+
intents.push(compiled.intent);
|
|
632
|
+
}
|
|
633
|
+
const after = evaluate(trial, this.allInvariants(), {
|
|
634
|
+
...this.invariantOptions,
|
|
635
|
+
...(context === undefined ? {} : { context }),
|
|
636
|
+
});
|
|
637
|
+
const beforeKeys = new Set(before.map(violationKey));
|
|
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
|
+
};
|
|
649
|
+
}
|
|
333
650
|
previewPrimitives(primitives, meta) {
|
|
334
651
|
const before = this.violations(meta.context);
|
|
335
652
|
const trial = Graph.from(this.schema, this.graph.snapshot());
|
|
336
|
-
const diff = trial.applyPrimitives(primitives);
|
|
653
|
+
const diff = trial.applyPrimitives(primitives, { restoring: meta.restoring === true });
|
|
337
654
|
const after = evaluate(trial, this.allInvariants(), {
|
|
338
655
|
...this.invariantOptions,
|
|
339
656
|
...(meta.context === undefined ? {} : { context: meta.context }),
|
|
@@ -357,7 +674,15 @@ export class Store {
|
|
|
357
674
|
}
|
|
358
675
|
/** Applies several mutations as one gesture — one batch, one undo unit. */
|
|
359
676
|
applyAll(calls, options = {}) {
|
|
360
|
-
|
|
677
|
+
return this.applying(calls, options);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* `applyAll` itself. Kept apart so a rebase applies pending calls again
|
|
681
|
+
* through the store's own path, whatever a host has wrapped `applyAll` in
|
|
682
|
+
* (`openRemote` sends each one down the wire).
|
|
683
|
+
*/
|
|
684
|
+
applying(calls, options) {
|
|
685
|
+
const batch = options.batch ?? this.mintBatch("batch");
|
|
361
686
|
const author = options.author ?? HUMAN;
|
|
362
687
|
const ops = [];
|
|
363
688
|
const allPrimitives = [];
|
|
@@ -395,7 +720,7 @@ export class Store {
|
|
|
395
720
|
* was brought in for, and the narrower of the two wins.
|
|
396
721
|
*/
|
|
397
722
|
for (const call of calls) {
|
|
398
|
-
const refusal = this.refusesAgent(call, author);
|
|
723
|
+
const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call);
|
|
399
724
|
if (refusal) {
|
|
400
725
|
tellTheWatchOfARefusal(refusal, author.id);
|
|
401
726
|
throw new PermissionDeniedError(refusal);
|
|
@@ -424,7 +749,14 @@ export class Store {
|
|
|
424
749
|
seq: this.log.length,
|
|
425
750
|
batch,
|
|
426
751
|
author,
|
|
427
|
-
|
|
752
|
+
/*
|
|
753
|
+
* THE ACT'S OWN SENTENCE, kept; what the gesture was for goes
|
|
754
|
+
* beside it (FR-18). An act with no `describe` has no sentence of
|
|
755
|
+
* its own, only `close-item(id="deposit")`, and there the caller's
|
|
756
|
+
* words ("Close it", the button's) are the better one.
|
|
757
|
+
*/
|
|
758
|
+
intent: definition.describe !== undefined ? compiled.intent : (options.intent ?? compiled.intent),
|
|
759
|
+
...(options.intent !== undefined ? { batchIntent: options.intent } : {}),
|
|
428
760
|
mutation: call,
|
|
429
761
|
/*
|
|
430
762
|
* NORMALISED on the way into the record: a patch that clears a
|
|
@@ -437,6 +769,7 @@ export class Store {
|
|
|
437
769
|
reads: compiled.reads,
|
|
438
770
|
writes: compiled.writes,
|
|
439
771
|
at: this.now(),
|
|
772
|
+
...(options.via !== undefined ? { via: options.via } : {}),
|
|
440
773
|
};
|
|
441
774
|
this.graph.applyPrimitives(op.primitives);
|
|
442
775
|
this.log.append(op);
|
|
@@ -506,23 +839,169 @@ export class Store {
|
|
|
506
839
|
return [];
|
|
507
840
|
const before = this.graph.snapshot();
|
|
508
841
|
const landed = [];
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
842
|
+
/*
|
|
843
|
+
* ALL OR NOTHING, ACROSS THE OPS TOO (FR-26). Every op's primitives go
|
|
844
|
+
* on first; one that fails puts the graph back as it was — `load`, so a
|
|
845
|
+
* listener that heard the ops before it hears them reversed — and
|
|
846
|
+
* throws a ReceiveError naming that op, before any of them joins the
|
|
847
|
+
* log. A store handed a bad op is the store it was.
|
|
848
|
+
*
|
|
849
|
+
* ALREADY IN EFFECT HERE (`applied`). A client that applied a call
|
|
850
|
+
* provisionally and is now handed the server's op for it has the graph
|
|
851
|
+
* the op describes; applying the primitives again would add the node
|
|
852
|
+
* twice. The op still joins the log — it is the one everybody else has
|
|
853
|
+
* — so undo names it and the activity shows it, and the graph stays put.
|
|
854
|
+
*/
|
|
855
|
+
if (!options.applied) {
|
|
856
|
+
for (const op of fresh) {
|
|
857
|
+
try {
|
|
858
|
+
// Somebody's undo puts back what was there, here as there (FR-28).
|
|
859
|
+
this.graph.applyPrimitives(op.primitives, { restoring: op.undoes !== undefined });
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
this.graph.load(before);
|
|
863
|
+
throw new ReceiveError(op, error);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const seq = this.log.length;
|
|
868
|
+
for (const [index, op] of fresh.entries()) {
|
|
869
|
+
const here = { ...op, seq: seq + index };
|
|
520
870
|
this.log.append(here);
|
|
521
871
|
landed.push(here);
|
|
522
872
|
}
|
|
523
873
|
this.notify(diffSnapshots(before, this.graph.snapshot()), landed);
|
|
524
874
|
return landed;
|
|
525
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* OPS A HOST MADE, landed as ordinary history (FR-18): a template's
|
|
878
|
+
* example content, a seeded beginning, a repair or an import the host
|
|
879
|
+
* built itself. Unlike `receive`, which takes ops somebody else's store
|
|
880
|
+
* already made and skips the ones it has, these are new here, so the
|
|
881
|
+
* store fills in what the host left out the way it fills its own: the
|
|
882
|
+
* seq, an id, one batch for the call (`options.batch`, else a fresh one),
|
|
883
|
+
* the time, the inverse (from the primitives), what they write, and no
|
|
884
|
+
* mutation. What the host did say is kept.
|
|
885
|
+
*
|
|
886
|
+
* Each op lands as a write (a default is filled in on the way in, as a
|
|
887
|
+
* fold would), or, when it `undoes` another, as a putting-back. No policy
|
|
888
|
+
* is asked: there is no act to judge, and whoever calls this is the
|
|
889
|
+
* authority, as with `applyPrimitives`. All or nothing: an op that does
|
|
890
|
+
* not fit puts the store back as it was and throws a `ReceiveError`
|
|
891
|
+
* naming it, and an id the log already holds is refused before anything
|
|
892
|
+
* lands. Subscribers hear one change, and every op undoes like any other.
|
|
893
|
+
*/
|
|
894
|
+
append(ops, options = {}) {
|
|
895
|
+
if (ops.length === 0)
|
|
896
|
+
return [];
|
|
897
|
+
const taken = new Set(this.log.all().map((op) => op.id));
|
|
898
|
+
let batch;
|
|
899
|
+
const built = ops.map((made, index) => {
|
|
900
|
+
const primitives = made.primitives.map(normalise);
|
|
901
|
+
const op = {
|
|
902
|
+
id: made.id ?? this.nextId(),
|
|
903
|
+
seq: this.log.length + index,
|
|
904
|
+
batch: made.batch ?? options.batch ?? (batch ??= this.mintBatch("batch")),
|
|
905
|
+
author: made.author,
|
|
906
|
+
intent: made.intent,
|
|
907
|
+
...(made.batchIntent !== undefined ? { batchIntent: made.batchIntent } : {}),
|
|
908
|
+
mutation: made.mutation ?? null,
|
|
909
|
+
primitives,
|
|
910
|
+
inverse: made.inverse ?? [...primitives].reverse().map(invert),
|
|
911
|
+
reads: made.reads ?? [],
|
|
912
|
+
writes: made.writes ?? [...new Set(primitives.flatMap(writesOf))],
|
|
913
|
+
at: made.at ?? this.now(),
|
|
914
|
+
...(made.undoes !== undefined ? { undoes: made.undoes } : {}),
|
|
915
|
+
...(made.via !== undefined ? { via: made.via } : {}),
|
|
916
|
+
};
|
|
917
|
+
if (taken.has(op.id))
|
|
918
|
+
throw new GraphError(`Op "${op.id}" (${op.intent}) is already in the log`, "A host-made op needs an id of its own; leave `id` out and the store mints one.");
|
|
919
|
+
taken.add(op.id);
|
|
920
|
+
return op;
|
|
921
|
+
});
|
|
922
|
+
const before = this.graph.snapshot();
|
|
923
|
+
for (const op of built) {
|
|
924
|
+
try {
|
|
925
|
+
this.graph.applyPrimitives(op.primitives, { restoring: op.undoes !== undefined });
|
|
926
|
+
}
|
|
927
|
+
catch (error) {
|
|
928
|
+
this.graph.load(before);
|
|
929
|
+
throw new ReceiveError(op, error);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
for (const op of built)
|
|
933
|
+
this.log.append(op);
|
|
934
|
+
for (const op of built)
|
|
935
|
+
tellTheWatchOfAnAuthor(op.author.id);
|
|
936
|
+
this.notify(diffSnapshots(before, this.graph.snapshot()), built);
|
|
937
|
+
return built;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* PRIMITIVES AS AN ORDINARY CHANGE (FR-21): one batch, one op, logged,
|
|
941
|
+
* attributed to `author` and undoable like any act. For changes that are
|
|
942
|
+
* not a declared mutation — a repair from `repairPlan`, a host's own fix —
|
|
943
|
+
* so they land in the history rather than beside it.
|
|
944
|
+
*
|
|
945
|
+
* No policy is asked: there is no act to judge, so whoever calls this is
|
|
946
|
+
* the authority, the way a migration engine is. All or nothing (FR-26): a
|
|
947
|
+
* primitive that does not fit leaves the store as it was and throws.
|
|
948
|
+
*/
|
|
949
|
+
applyPrimitives(primitives, options = {}) {
|
|
950
|
+
const batch = options.batch ?? this.mintBatch("batch");
|
|
951
|
+
const author = options.author ?? HUMAN;
|
|
952
|
+
const before = this.violations();
|
|
953
|
+
const rollback = this.graph.snapshot();
|
|
954
|
+
const intent = options.intent ?? "Apply a change";
|
|
955
|
+
const recorded = primitives.map(normalise);
|
|
956
|
+
const ops = [];
|
|
957
|
+
if (recorded.length > 0) {
|
|
958
|
+
const op = {
|
|
959
|
+
id: this.nextId(),
|
|
960
|
+
seq: this.log.length,
|
|
961
|
+
batch,
|
|
962
|
+
author,
|
|
963
|
+
intent,
|
|
964
|
+
mutation: null,
|
|
965
|
+
primitives: recorded,
|
|
966
|
+
inverse: [...recorded].reverse().map(invert),
|
|
967
|
+
reads: [],
|
|
968
|
+
writes: [...new Set(recorded.flatMap(writesOf))],
|
|
969
|
+
at: this.now(),
|
|
970
|
+
...(options.via !== undefined ? { via: options.via } : {}),
|
|
971
|
+
};
|
|
972
|
+
this.graph.applyPrimitives(op.primitives);
|
|
973
|
+
this.log.append(op);
|
|
974
|
+
ops.push(op);
|
|
975
|
+
tellTheWatchOfAnAuthor(author.id);
|
|
976
|
+
}
|
|
977
|
+
const after = this.violations();
|
|
978
|
+
const beforeKeys = new Set(before.map(violationKey));
|
|
979
|
+
const afterKeys = new Set(after.map(violationKey));
|
|
980
|
+
const diff = diffSnapshots(rollback, this.graph.snapshot());
|
|
981
|
+
this.notify(diff, ops);
|
|
982
|
+
return {
|
|
983
|
+
batch,
|
|
984
|
+
ops,
|
|
985
|
+
diff,
|
|
986
|
+
primitives: recorded,
|
|
987
|
+
reads: [],
|
|
988
|
+
writes: ops[0]?.writes ?? [],
|
|
989
|
+
intent,
|
|
990
|
+
introduces: after.filter((v) => !beforeKeys.has(violationKey(v))),
|
|
991
|
+
resolves: before.filter((v) => !afterKeys.has(violationKey(v))),
|
|
992
|
+
violationsAfter: after,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* What in this store no longer fits its declaration (FR-21): the graph
|
|
997
|
+
* as held, judged by `validateGraph` against the schema and the rules
|
|
998
|
+
* this workspace has on. A clean store has none.
|
|
999
|
+
*/
|
|
1000
|
+
findings() {
|
|
1001
|
+
return validateGraph({ schema: this.schema, invariants: this.allInvariants() }, this.graph.snapshot(), {
|
|
1002
|
+
invariantOptions: this.invariantOptions,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
526
1005
|
batches() {
|
|
527
1006
|
return this.log.batches();
|
|
528
1007
|
}
|
|
@@ -550,6 +1029,7 @@ export class Store {
|
|
|
550
1029
|
reads: [...new Set(check.ops.flatMap((op) => [...op.reads]))],
|
|
551
1030
|
writes: [...writes],
|
|
552
1031
|
intent: `Undo: ${check.ops.map((op) => op.intent).join("; ")}`,
|
|
1032
|
+
restoring: check.ops.every(putsBack),
|
|
553
1033
|
...(context === undefined ? {} : { context }),
|
|
554
1034
|
}),
|
|
555
1035
|
};
|
|
@@ -561,12 +1041,32 @@ export class Store {
|
|
|
561
1041
|
* than a stack pop.
|
|
562
1042
|
*/
|
|
563
1043
|
undo(batchIds, options = {}) {
|
|
1044
|
+
return this.undoing(batchIds, options);
|
|
1045
|
+
}
|
|
1046
|
+
/** `undo` itself, kept apart for the same reason as `applying`. */
|
|
1047
|
+
undoing(batchIds, options) {
|
|
564
1048
|
const ids = typeof batchIds === "string" ? [batchIds] : batchIds;
|
|
565
|
-
const check = this.canUndo(ids);
|
|
566
|
-
if (!check.ok)
|
|
567
|
-
throw new GraphError(check.message);
|
|
568
|
-
const batch = options.batch ?? `undo:${++this.counter}`;
|
|
569
1049
|
const author = options.author ?? HUMAN;
|
|
1050
|
+
/*
|
|
1051
|
+
* JUDGED FIRST AS THIS SEAT SEES THE LOG (FR-16, FR-02). A change it
|
|
1052
|
+
* may not see is not its to take back, and when one stands in the way
|
|
1053
|
+
* the refusal says a change you cannot see does — the full check's
|
|
1054
|
+
* sentence would quote it. The two agree on what blocks: a withheld op
|
|
1055
|
+
* keeps the ids this seat sees, and those are all it can overlap.
|
|
1056
|
+
*/
|
|
1057
|
+
const sighted = (this.policy?.sees?.length ?? 0) > 0 && !isSystem(author);
|
|
1058
|
+
if (sighted) {
|
|
1059
|
+
const seen = checkUndo(readingOf(redact(this.log.all(), seesId(this, author)), () => this.log.epochs()), ids);
|
|
1060
|
+
if (!seen.ok)
|
|
1061
|
+
throw new UndoBlockedError(seen);
|
|
1062
|
+
}
|
|
1063
|
+
const check = this.canUndo(ids);
|
|
1064
|
+
if (!check.ok) {
|
|
1065
|
+
throw new UndoBlockedError(sighted
|
|
1066
|
+
? { ok: false, ops: [], blockedBy: [], includeBatches: [], message: "Cannot undo on its own — a later change you cannot see depends on it, which only somebody who can see it can take back." }
|
|
1067
|
+
: check);
|
|
1068
|
+
}
|
|
1069
|
+
const batch = options.batch ?? this.mintBatch("undo");
|
|
570
1070
|
/*
|
|
571
1071
|
* Undo is a CHANGE, and it is judged like one.
|
|
572
1072
|
*
|
|
@@ -600,7 +1100,8 @@ export class Store {
|
|
|
600
1100
|
seq: this.log.length,
|
|
601
1101
|
batch,
|
|
602
1102
|
author,
|
|
603
|
-
intent:
|
|
1103
|
+
intent: `Undo: ${target.intent}`,
|
|
1104
|
+
...(options.intent !== undefined ? { batchIntent: options.intent } : {}),
|
|
604
1105
|
mutation: null,
|
|
605
1106
|
primitives: target.inverse,
|
|
606
1107
|
inverse: [...target.inverse].reverse().map(invert),
|
|
@@ -608,8 +1109,9 @@ export class Store {
|
|
|
608
1109
|
writes: target.writes,
|
|
609
1110
|
at: this.now(),
|
|
610
1111
|
undoes: target.id,
|
|
1112
|
+
...(options.via !== undefined ? { via: options.via } : {}),
|
|
611
1113
|
};
|
|
612
|
-
this.graph.applyPrimitives(op.primitives);
|
|
1114
|
+
this.graph.applyPrimitives(op.primitives, { restoring: putsBack(target) });
|
|
613
1115
|
this.log.append(op);
|
|
614
1116
|
ops.push(op);
|
|
615
1117
|
}
|
|
@@ -647,14 +1149,272 @@ export class Store {
|
|
|
647
1149
|
this.listeners.add(listener);
|
|
648
1150
|
return () => this.listeners.delete(listener);
|
|
649
1151
|
}
|
|
1152
|
+
/**
|
|
1153
|
+
* TELLS EVERY SUBSCRIBER OF A CHANGE: the diff, and the ops that made it.
|
|
1154
|
+
* The store calls this for each change it makes; it is public for a host
|
|
1155
|
+
* that changes the graph some other way and owes its subscribers the same
|
|
1156
|
+
* news (the rebase item of FR-05). A change with no ops and an empty diff
|
|
1157
|
+
* is nothing to tell.
|
|
1158
|
+
*/
|
|
650
1159
|
notify(diff, ops) {
|
|
651
|
-
if (
|
|
1160
|
+
if (this.quiet > 0)
|
|
1161
|
+
return;
|
|
1162
|
+
if (ops.length === 0 && isEmptyDiff(diff))
|
|
652
1163
|
return;
|
|
653
1164
|
for (const listener of this.listeners)
|
|
654
1165
|
listener(diff, ops);
|
|
655
1166
|
}
|
|
1167
|
+
/**
|
|
1168
|
+
* AN OPTIMISTIC CLIENT'S ROLLBACK (the rebase item of FR-05).
|
|
1169
|
+
*
|
|
1170
|
+
* A client applies a press at once and sends it; the server's op is the
|
|
1171
|
+
* one that stays. When anything arrives from the server — somebody else's
|
|
1172
|
+
* ops, or the verdict on one of this client's — the client's log must
|
|
1173
|
+
* read `[confirmed…, pending…]` again. So, in one step:
|
|
1174
|
+
*
|
|
1175
|
+
* 1. every op of the `pending` and `drop` batches is rolled back, newest
|
|
1176
|
+
* first, and cut from the log (they must be its tail: an op after
|
|
1177
|
+
* them that is neither is refused, before anything moves);
|
|
1178
|
+
* 2. the `confirmed` ops land in the server's order, skipped by id when
|
|
1179
|
+
* the log already holds them, all or nothing (`ReceiveError`);
|
|
1180
|
+
* 3. the `pending` batches are applied again on top, in the order given,
|
|
1181
|
+
* under the same batch ids, author, intent and channel: each call
|
|
1182
|
+
* judged and compiled afresh against the graph the server's ops left.
|
|
1183
|
+
* One that no longer applies there is left off and said in `refused`;
|
|
1184
|
+
* the server's verdict on it will say the rest.
|
|
1185
|
+
*
|
|
1186
|
+
* `drop` is the batches the server has answered (its own op for them is
|
|
1187
|
+
* among `confirmed`) or refused. Subscribers hear ONE change, the net
|
|
1188
|
+
* diff, with the confirmed ops and the re-applied ones, so an interface
|
|
1189
|
+
* never flickers through the states in between.
|
|
1190
|
+
*/
|
|
1191
|
+
rebase(change) {
|
|
1192
|
+
const settling = new Set([...change.pending, ...(change.drop ?? [])]);
|
|
1193
|
+
const all = this.log.all();
|
|
1194
|
+
const first = all.findIndex((op) => settling.has(op.batch));
|
|
1195
|
+
const tail = all.slice(first < 0 ? all.length : first);
|
|
1196
|
+
// The seq the log is cut back to: indexes into `all()` count from the horizon (FR-23).
|
|
1197
|
+
const cut = this.log.horizon + (first < 0 ? all.length : first);
|
|
1198
|
+
const stray = tail.find((op) => !settling.has(op.batch));
|
|
1199
|
+
if (stray) {
|
|
1200
|
+
throw new GraphError(`Cannot rebase: op "${stray.id}" (${stray.intent}) comes after a pending batch and is neither pending nor dropped`, "While anything is pending, land the server's ops through rebase, so the log stays confirmed ops then pending ones.");
|
|
1201
|
+
}
|
|
1202
|
+
const epoch = this.log.lastEpoch();
|
|
1203
|
+
if (epoch && epoch.seq > cut)
|
|
1204
|
+
throw new GraphError(`Cannot rebase across an epoch: one begins at seq ${epoch.seq}, after pending op "${tail[0].id}"`);
|
|
1205
|
+
const before = this.graph.snapshot();
|
|
1206
|
+
const landed = [];
|
|
1207
|
+
const replayed = [];
|
|
1208
|
+
const refused = [];
|
|
1209
|
+
this.quiet++;
|
|
1210
|
+
try {
|
|
1211
|
+
// 1. Back to the confirmed prefix, putting back exactly what each op took.
|
|
1212
|
+
// In place: what a pending op took out goes back where it stood, or this copy and the server's disagree on order.
|
|
1213
|
+
for (const op of [...tail].reverse())
|
|
1214
|
+
this.graph.applyPrimitives(op.inverse, { restoring: true, inPlace: true });
|
|
1215
|
+
this.log.truncate(cut);
|
|
1216
|
+
// 2. What the server says, in its order.
|
|
1217
|
+
const known = new Set(this.log.all().map((op) => op.id));
|
|
1218
|
+
for (const op of change.confirmed) {
|
|
1219
|
+
if (known.has(op.id))
|
|
1220
|
+
continue;
|
|
1221
|
+
try {
|
|
1222
|
+
this.graph.applyPrimitives(op.primitives, { restoring: op.undoes !== undefined });
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
throw new ReceiveError(op, error);
|
|
1226
|
+
}
|
|
1227
|
+
const here = { ...op, seq: this.log.length };
|
|
1228
|
+
this.log.append(here);
|
|
1229
|
+
known.add(op.id);
|
|
1230
|
+
landed.push(here);
|
|
1231
|
+
}
|
|
1232
|
+
// 3. What is still pending, again, on top.
|
|
1233
|
+
for (const batch of change.pending) {
|
|
1234
|
+
const ops = tail.filter((op) => op.batch === batch);
|
|
1235
|
+
if (ops.length === 0)
|
|
1236
|
+
continue;
|
|
1237
|
+
try {
|
|
1238
|
+
replayed.push(...this.replay(batch, ops));
|
|
1239
|
+
}
|
|
1240
|
+
catch (error) {
|
|
1241
|
+
refused.push({ batch, error });
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
catch (error) {
|
|
1246
|
+
// A store handed a bad op is the store it was.
|
|
1247
|
+
this.log.truncate(cut);
|
|
1248
|
+
for (const op of tail)
|
|
1249
|
+
this.log.append(op);
|
|
1250
|
+
this.graph.load(before);
|
|
1251
|
+
throw error;
|
|
1252
|
+
}
|
|
1253
|
+
finally {
|
|
1254
|
+
this.quiet--;
|
|
1255
|
+
}
|
|
1256
|
+
const diff = diffSnapshots(before, this.graph.snapshot());
|
|
1257
|
+
const ops = [...landed, ...replayed];
|
|
1258
|
+
this.notify(diff, ops);
|
|
1259
|
+
return { diff, confirmed: landed, pending: replayed, refused };
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* A CLIENT THAT RESYNCED TAKES THE SERVER'S WHOLE STATE (FR-53).
|
|
1263
|
+
*
|
|
1264
|
+
* `rebase` lands the server's ops on the confirmed prefix this store
|
|
1265
|
+
* holds. When that prefix cannot be trusted — this copy drifted, and an
|
|
1266
|
+
* op of the server's no longer fits it, or the server compacted past
|
|
1267
|
+
* where this client left off, so `/graview/since` cannot bring the ops in
|
|
1268
|
+
* between — the client takes the server's graph and log as they are:
|
|
1269
|
+
*
|
|
1270
|
+
* 1. the ops of the `pending` batches are read off the log as it stands;
|
|
1271
|
+
* 2. the log becomes `log` (with its `epochs` and `horizon`, checked as
|
|
1272
|
+
* `OperationLog.from` checks them; one that is not intact is refused
|
|
1273
|
+
* before anything moves) and the graph becomes `snapshot`, in the
|
|
1274
|
+
* server's order — not put back op by op, as `rebase` puts it;
|
|
1275
|
+
* 3. the `pending` batches are applied again on top, in the order given,
|
|
1276
|
+
* exactly as `rebase` applies them: under the same batch ids, judged
|
|
1277
|
+
* and compiled afresh. One that no longer applies is said in `refused`.
|
|
1278
|
+
*
|
|
1279
|
+
* `drop` names the batches the server has answered or refused: they are
|
|
1280
|
+
* not applied again (nor is any batch left unnamed: the log is the
|
|
1281
|
+
* server's). Subscribers hear ONE change, the net diff, with the ops this
|
|
1282
|
+
* store did not hold before and the re-applied ones.
|
|
1283
|
+
*/
|
|
1284
|
+
adopt(state) {
|
|
1285
|
+
const dropped = new Set(state.drop ?? []);
|
|
1286
|
+
const pending = (state.pending ?? []).filter((batch) => !dropped.has(batch));
|
|
1287
|
+
const held = this.log.all();
|
|
1288
|
+
const before = this.graph.snapshot();
|
|
1289
|
+
const was = { ops: [...held], epochs: [...this.log.epochs()], horizon: this.log.horizon };
|
|
1290
|
+
const known = new Set(held.map((op) => op.id));
|
|
1291
|
+
const tails = new Map(pending.map((batch) => [batch, held.filter((op) => op.batch === batch)]));
|
|
1292
|
+
// A log that is not intact is refused here, before the graph moves.
|
|
1293
|
+
if (state.log.length > 0)
|
|
1294
|
+
this.log.replace(state.log, state.epochs, state.horizon !== undefined ? { horizon: state.horizon } : {});
|
|
1295
|
+
else {
|
|
1296
|
+
const at = state.horizon ?? 0;
|
|
1297
|
+
this.log.replace([], state.epochs ?? [{ seq: at, base: state.snapshot }], { horizon: at });
|
|
1298
|
+
}
|
|
1299
|
+
const replayed = [];
|
|
1300
|
+
const refused = [];
|
|
1301
|
+
this.quiet++;
|
|
1302
|
+
try {
|
|
1303
|
+
this.graph.load(state.snapshot);
|
|
1304
|
+
this.projected = undefined;
|
|
1305
|
+
this.countPastTheLog();
|
|
1306
|
+
for (const batch of pending) {
|
|
1307
|
+
const ops = tails.get(batch) ?? [];
|
|
1308
|
+
if (ops.length === 0)
|
|
1309
|
+
continue;
|
|
1310
|
+
try {
|
|
1311
|
+
replayed.push(...this.replay(batch, ops));
|
|
1312
|
+
}
|
|
1313
|
+
catch (error) {
|
|
1314
|
+
refused.push({ batch, error });
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
catch (error) {
|
|
1319
|
+
// A store that could not adopt is the store it was.
|
|
1320
|
+
this.log.replace(was.ops, was.epochs, { horizon: was.horizon });
|
|
1321
|
+
this.graph.load(before);
|
|
1322
|
+
this.projected = undefined;
|
|
1323
|
+
throw error;
|
|
1324
|
+
}
|
|
1325
|
+
finally {
|
|
1326
|
+
this.quiet--;
|
|
1327
|
+
}
|
|
1328
|
+
const adopted = this.log.all().filter((op) => !known.has(op.id) && !replayed.includes(op));
|
|
1329
|
+
const diff = diffSnapshots(before, this.graph.snapshot());
|
|
1330
|
+
this.notify(diff, [...adopted, ...replayed]);
|
|
1331
|
+
return { diff, adopted, pending: replayed, refused };
|
|
1332
|
+
}
|
|
1333
|
+
/** One pending batch, applied again as it was made: its calls, its undo, or its primitives. */
|
|
1334
|
+
replay(batch, ops) {
|
|
1335
|
+
const head = ops[0];
|
|
1336
|
+
const options = {
|
|
1337
|
+
author: head.author,
|
|
1338
|
+
batch,
|
|
1339
|
+
...(head.batchIntent !== undefined ? { intent: head.batchIntent } : {}),
|
|
1340
|
+
...(head.via !== undefined ? { via: head.via } : {}),
|
|
1341
|
+
};
|
|
1342
|
+
if (head.undoes !== undefined) {
|
|
1343
|
+
const targets = ops.map((op) => this.log.get(op.undoes ?? ""));
|
|
1344
|
+
const missing = ops.find((_op, index) => targets[index] === undefined);
|
|
1345
|
+
if (missing)
|
|
1346
|
+
throw new GraphError(`What "${missing.intent}" took back is no longer in the log`);
|
|
1347
|
+
return this.undoing([...new Set(targets.map((op) => op.batch))], options).ops;
|
|
1348
|
+
}
|
|
1349
|
+
if (ops.every((op) => op.mutation !== null)) {
|
|
1350
|
+
return this.applying(ops.map((op) => op.mutation), options).ops;
|
|
1351
|
+
}
|
|
1352
|
+
return this.append(ops.map(({ seq: _seq, ...op }) => op));
|
|
1353
|
+
}
|
|
656
1354
|
snapshot() {
|
|
657
1355
|
return this.graph.snapshot();
|
|
658
1356
|
}
|
|
1357
|
+
/**
|
|
1358
|
+
* WHETHER THE GRAPH IS WHAT ITS LOG SAYS (FR-20).
|
|
1359
|
+
*
|
|
1360
|
+
* Refolds the log from its last epoch (FR-27), or from empty when it has
|
|
1361
|
+
* none, and compares the result with the graph held, by `snapshotHash`. Agreement returns the hash; disagreement returns both
|
|
1362
|
+
* hashes and the op after which they part, so a host that finds a store
|
|
1363
|
+
* drifted knows where to look rather than only that it should.
|
|
1364
|
+
*/
|
|
1365
|
+
verify() {
|
|
1366
|
+
const from = this.log.lastEpoch();
|
|
1367
|
+
return verifyFold(this.schema, this.log.opsFrom(from?.seq ?? 0), this.graph.snapshot(), {
|
|
1368
|
+
validate: this.validate,
|
|
1369
|
+
...(from ? { base: from.base } : {}),
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* THE CHECKPOINT A COMPACTION WOULD MAKE (FR-23), or `undefined` when
|
|
1374
|
+
* nothing is old enough to archive. The horizon is `seq` when given, or
|
|
1375
|
+
* else whichever of two keeps more: the ops made in the last `keepDays`
|
|
1376
|
+
* days before `now` (default 90), or the last `keepOps` ops (default
|
|
1377
|
+
* 1000). It is moved back to the start of the gesture it would split, so
|
|
1378
|
+
* a batch is never half behind it.
|
|
1379
|
+
*
|
|
1380
|
+
* Nothing moves until `compact` is handed the checkpoint, so a host can
|
|
1381
|
+
* archive what is behind it first. A store whose graph does not verify
|
|
1382
|
+
* against its log is not compacted: the checkpoint would vouch for a
|
|
1383
|
+
* history that does not lead to the graph held.
|
|
1384
|
+
*/
|
|
1385
|
+
checkpoint(options = {}) {
|
|
1386
|
+
const ops = this.log.all();
|
|
1387
|
+
let seq;
|
|
1388
|
+
if (options.seq !== undefined)
|
|
1389
|
+
seq = options.seq;
|
|
1390
|
+
else {
|
|
1391
|
+
const byOps = this.log.length - (options.keepOps ?? 1000);
|
|
1392
|
+
const now = Date.parse(options.now ?? this.now());
|
|
1393
|
+
const cutoff = now - (options.keepDays ?? 90) * 86_400_000;
|
|
1394
|
+
const kept = ops.find((op) => Date.parse(op.at) > cutoff);
|
|
1395
|
+
const byDays = kept ? kept.seq : this.log.length;
|
|
1396
|
+
seq = Math.min(byOps, byDays);
|
|
1397
|
+
}
|
|
1398
|
+
seq = Math.min(Math.max(seq, this.log.horizon), this.log.length);
|
|
1399
|
+
// Never inside a gesture: back to where the batch it would split began.
|
|
1400
|
+
while (seq > this.log.horizon && seq < this.log.length && ops[seq - this.log.horizon].batch === ops[seq - 1 - this.log.horizon].batch)
|
|
1401
|
+
seq--;
|
|
1402
|
+
if (seq <= this.log.horizon)
|
|
1403
|
+
return undefined;
|
|
1404
|
+
const verified = this.verify();
|
|
1405
|
+
if (!verified.ok)
|
|
1406
|
+
throw new GraphError(`Cannot compact a store that does not verify: ${verified.reason}`);
|
|
1407
|
+
return this.log.checkpointAt(this.schema, seq, { validate: this.validate });
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* MOVES THE UNDO HORIZON TO `checkpoint` (FR-23): the ops and epochs
|
|
1411
|
+
* before it leave the log, which begins at its seq from now on, and are
|
|
1412
|
+
* returned for an adapter to archive. The graph is unchanged and still
|
|
1413
|
+
* verifies; undo of anything behind the horizon is refused, naming it.
|
|
1414
|
+
* Ship's `opened.compact()` archives first and calls this after.
|
|
1415
|
+
*/
|
|
1416
|
+
compact(checkpoint) {
|
|
1417
|
+
return this.log.compact(checkpoint);
|
|
1418
|
+
}
|
|
659
1419
|
}
|
|
660
1420
|
//# sourceMappingURL=store.js.map
|