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