@substrat-run/kernel 0.135.0 → 0.137.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/async-invocation-log.d.ts +61 -0
- package/dist/async-invocation-log.d.ts.map +1 -0
- package/dist/async-invocation-log.js +149 -0
- package/dist/async-invocation-log.js.map +1 -0
- package/dist/base64url.d.ts +6 -0
- package/dist/base64url.d.ts.map +1 -0
- package/dist/base64url.js +19 -0
- package/dist/base64url.js.map +1 -0
- package/dist/capability.d.ts +4 -0
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +7 -8
- package/dist/capability.js.map +1 -1
- package/dist/carried-copy.d.ts +117 -0
- package/dist/carried-copy.d.ts.map +1 -0
- package/dist/carried-copy.js +134 -0
- package/dist/carried-copy.js.map +1 -0
- package/dist/index.d.ts +101 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -14
- package/dist/index.js.map +1 -1
- package/dist/invocation-log.d.ts +115 -9
- package/dist/invocation-log.d.ts.map +1 -1
- package/dist/invocation-log.js +78 -15
- package/dist/invocation-log.js.map +1 -1
- package/dist/job-run.d.ts +77 -15
- package/dist/job-run.d.ts.map +1 -1
- package/dist/job-run.js +107 -24
- package/dist/job-run.js.map +1 -1
- package/dist/lifecycle-flow.d.ts.map +1 -1
- package/dist/lifecycle-flow.js +2 -1
- package/dist/lifecycle-flow.js.map +1 -1
- package/dist/list-index.d.ts +19 -4
- package/dist/list-index.d.ts.map +1 -1
- package/dist/list-index.js +150 -8
- package/dist/list-index.js.map +1 -1
- package/dist/permission-eval.d.ts +9 -0
- package/dist/permission-eval.d.ts.map +1 -1
- package/dist/permission-eval.js +26 -0
- package/dist/permission-eval.js.map +1 -1
- package/dist/platform-call.d.ts +4 -37
- package/dist/platform-call.d.ts.map +1 -1
- package/dist/platform-call.js +5 -37
- package/dist/platform-call.js.map +1 -1
- package/dist/platform-sweep.d.ts +17 -0
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +22 -4
- package/dist/platform-sweep.js.map +1 -1
- package/dist/provider-error.d.ts.map +1 -1
- package/dist/provider-error.js +2 -1
- package/dist/provider-error.js.map +1 -1
- package/dist/refusal-query.d.ts +4 -1
- package/dist/refusal-query.d.ts.map +1 -1
- package/dist/refusal-query.js +6 -4
- package/dist/refusal-query.js.map +1 -1
- package/dist/refusals.d.ts +80 -8
- package/dist/refusals.d.ts.map +1 -1
- package/dist/refusals.js +141 -22
- package/dist/refusals.js.map +1 -1
- package/dist/routed-node.d.ts +3 -2
- package/dist/routed-node.d.ts.map +1 -1
- package/dist/routed-node.js.map +1 -1
- package/dist/scope-copy.d.ts +122 -0
- package/dist/scope-copy.d.ts.map +1 -0
- package/dist/scope-copy.js +187 -0
- package/dist/scope-copy.js.map +1 -0
- package/dist/scope-host.d.ts +95 -27
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +5 -14
- package/dist/scope-host.js.map +1 -1
- package/dist/scope-lifecycle.d.ts +70 -0
- package/dist/scope-lifecycle.d.ts.map +1 -0
- package/dist/scope-lifecycle.js +124 -0
- package/dist/scope-lifecycle.js.map +1 -0
- package/dist/system-switch-record.d.ts +27 -11
- package/dist/system-switch-record.d.ts.map +1 -1
- package/dist/system-switch-record.js +59 -24
- package/dist/system-switch-record.js.map +1 -1
- package/dist/system-switch.d.ts +41 -11
- package/dist/system-switch.d.ts.map +1 -1
- package/dist/system-switch.js +48 -15
- package/dist/system-switch.js.map +1 -1
- package/package.json +2 -2
package/dist/refusals.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Refused transitions, recorded (#1745).
|
|
2
|
+
* Refused transitions and refusing guards, recorded (#1745).
|
|
3
3
|
*
|
|
4
4
|
* An illegal move is refused inside the operation by `assertTransition`, which throws a
|
|
5
|
-
* `conflict
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* `conflict`; a manifest-declared guard (K-38) refuses the operation it stands before the
|
|
6
|
+
* same way, before the handler runs. The operation's transaction rolls back with either, so
|
|
7
|
+
* nothing about the attempt survives — the denial log holds permission refusals only, and
|
|
8
|
+
* the outbox only what committed. That made "someone tried to reopen a closed conversation
|
|
9
|
+
* twelve times" a fact nobody could see: not on the process map, not in a finding.
|
|
9
10
|
*
|
|
10
11
|
* Recorded the way a denial is (K-35): on the failure path, AFTER the rollback, as a write
|
|
11
12
|
* of its own, so the attempt survives the transaction that refused it. Kernel-owned and
|
|
@@ -14,53 +15,171 @@
|
|
|
14
15
|
*
|
|
15
16
|
* Only a refusal that FAILED the operation is recorded. One a vertical caught inside
|
|
16
17
|
* `ctx.atomic` and recovered from is the vertical's business — its operation committed.
|
|
18
|
+
*
|
|
19
|
+
* **What a row holds, and what it never does.** Keys and vocabulary only: the record's type
|
|
20
|
+
* (held to the entity-type grammar, `refusalEntityType`) and id, state names, operation and
|
|
21
|
+
* predicate names, the problem code (held to the `problemReason` grammar), the actor's id
|
|
22
|
+
* and the call's. Never the error's message, the operation's input, or a guard's config —
|
|
23
|
+
* any of those can quote what a person typed. So a row has nothing for a subject erasure to rewrite
|
|
24
|
+
* (master-plan §5.3: pseudonymous keys and transaction facts remain), which is the denial
|
|
25
|
+
* log's position too, and it is kept the way the denial log is: `drained_at` marks a shipped
|
|
26
|
+
* row, and rows drain rather than expire.
|
|
17
27
|
*/
|
|
18
|
-
import { refusedTransitionOf } from '@substrat-run/contracts';
|
|
28
|
+
import { errorCodeOf, INVALID_TRANSITION, problemReason, UNRECOGNIZED_REFUSAL_REASON, refusalEntityType, refusedRecordOf, refusedTransitionOf, } from '@substrat-run/contracts';
|
|
19
29
|
import { ulid } from './ulid.js';
|
|
20
|
-
|
|
30
|
+
/** The table alone — what `REFUSALS_REBUILD` creates under its scratch name. */
|
|
31
|
+
export const REFUSALS_TABLE_DDL = `
|
|
21
32
|
CREATE TABLE IF NOT EXISTS _substrat_refusals (
|
|
22
33
|
id TEXT PRIMARY KEY,
|
|
23
|
-
-- 'transition'
|
|
34
|
+
-- 'transition' (assertTransition) or 'guard' (a manifest-declared guard, K-38).
|
|
24
35
|
kind TEXT NOT NULL,
|
|
25
36
|
tenant_id TEXT NOT NULL,
|
|
26
37
|
scope_id TEXT,
|
|
27
|
-
-- The record the
|
|
28
|
-
--
|
|
38
|
+
-- The record the refusal was about. NULL when the refusing code did not name it: the
|
|
39
|
+
-- attempt is still recorded, and cannot be counted against an entity.
|
|
29
40
|
entity_type TEXT,
|
|
30
41
|
entity_id TEXT,
|
|
31
|
-
|
|
42
|
+
-- The state the record was in. NULL on a guard row: a guard stands before an operation,
|
|
43
|
+
-- and the kernel that ran it cannot know the state of the record behind it.
|
|
44
|
+
from_state TEXT,
|
|
32
45
|
-- Where the operation leads where it IS legal; NULL when that is not one state.
|
|
33
46
|
attempted_state TEXT,
|
|
34
|
-
-- The lifecycle's operation (what the edge is keyed by)
|
|
35
|
-
--
|
|
47
|
+
-- The lifecycle's operation (what the edge is keyed by), or the one a guard stands
|
|
48
|
+
-- before, and the invoke() string the call ran as.
|
|
36
49
|
operation TEXT NOT NULL,
|
|
37
50
|
invoked_operation TEXT,
|
|
51
|
+
-- The guard's named predicate; NULL on a transition row.
|
|
52
|
+
guard TEXT,
|
|
53
|
+
-- The problem code the operation failed with (the 409's reason). NULL on a row written
|
|
54
|
+
-- before the column, where the kind implies it.
|
|
55
|
+
reason TEXT,
|
|
38
56
|
actor TEXT NOT NULL,
|
|
39
57
|
impersonation TEXT,
|
|
40
58
|
invocation_id TEXT,
|
|
41
59
|
at TEXT NOT NULL,
|
|
42
60
|
drained_at TEXT
|
|
43
61
|
);
|
|
44
|
-
|
|
62
|
+
`;
|
|
63
|
+
/** The process map's read: one entity type's refusals in a window. */
|
|
64
|
+
export const REFUSALS_INDEX = `
|
|
45
65
|
CREATE INDEX IF NOT EXISTS _substrat_refusals_entity_at ON _substrat_refusals (entity_type, at);
|
|
46
66
|
`;
|
|
67
|
+
/**
|
|
68
|
+
* The table and its index. The adapters interpolate the two literals above instead, which
|
|
69
|
+
* is what `lint:spine-ddl` can resolve; this is for a caller that wants the whole thing.
|
|
70
|
+
*/
|
|
71
|
+
export const REFUSALS_DDL = REFUSALS_TABLE_DDL + REFUSALS_INDEX;
|
|
72
|
+
/**
|
|
73
|
+
* `_substrat_refusals` rebuilt to the shape above on a store created before guard refusals:
|
|
74
|
+
* `from_state` loses its NOT NULL and `guard` / `reason` join. Create-copy-drop-rename,
|
|
75
|
+
* because SQLite cannot relax a constraint in place — `SCHEDULE_STATE_REBUILD`'s shape and
|
|
76
|
+
* reasons (#1288), detected the same way (`refusalsAdmitGuards`, off `sqlite_master.sql`,
|
|
77
|
+
* which DO SQLite serves and `PRAGMA` does not).
|
|
78
|
+
*
|
|
79
|
+
* The new table is `REFUSALS_TABLE_DDL` under a temporary name, so the rebuilt shape cannot drift
|
|
80
|
+
* from the created one, and every row is copied verbatim: the two new columns are NULL on a
|
|
81
|
+
* transition row written before them, which the reader takes as "implied by the kind". The
|
|
82
|
+
* index is dropped with the old table and created again on the renamed one.
|
|
83
|
+
*
|
|
84
|
+
* **Run inside the adapter's transaction API**, as both callers do: un-wrapped, a stop
|
|
85
|
+
* between the DROP and the RENAME leaves the next wake's `CREATE TABLE IF NOT EXISTS` an
|
|
86
|
+
* empty table of the new shape, detection reads it as migrated, and the copied rows are
|
|
87
|
+
* orphaned in the scratch table. The leading `DROP TABLE IF EXISTS` makes a re-run start
|
|
88
|
+
* clean. A restore never reaches this: it builds the table from the current DDL and loads a
|
|
89
|
+
* legacy dump's rows by column name.
|
|
90
|
+
*/
|
|
91
|
+
export const REFUSALS_REBUILD = `
|
|
92
|
+
DROP TABLE IF EXISTS _substrat_refusals_new;
|
|
93
|
+
${REFUSALS_TABLE_DDL.replace('CREATE TABLE IF NOT EXISTS _substrat_refusals (', 'CREATE TABLE _substrat_refusals_new (')}
|
|
94
|
+
INSERT INTO _substrat_refusals_new
|
|
95
|
+
(id, kind, tenant_id, scope_id, entity_type, entity_id, from_state, attempted_state,
|
|
96
|
+
operation, invoked_operation, actor, impersonation, invocation_id, at, drained_at)
|
|
97
|
+
SELECT id, kind, tenant_id, scope_id, entity_type, entity_id, from_state, attempted_state,
|
|
98
|
+
operation, invoked_operation, actor, impersonation, invocation_id, at, drained_at
|
|
99
|
+
FROM _substrat_refusals;
|
|
100
|
+
DROP TABLE _substrat_refusals;
|
|
101
|
+
ALTER TABLE _substrat_refusals_new RENAME TO _substrat_refusals;
|
|
102
|
+
${REFUSALS_INDEX}
|
|
103
|
+
`;
|
|
104
|
+
/**
|
|
105
|
+
* Whether a store's `_substrat_refusals` already admits a guard row, read off the `sql`
|
|
106
|
+
* column of `sqlite_master`. `false` means `REFUSALS_REBUILD` is due.
|
|
107
|
+
*/
|
|
108
|
+
export function refusalsAdmitGuards(tableSql) {
|
|
109
|
+
// Line-anchored: the stored DDL keeps its comments, and a comment may say "guard".
|
|
110
|
+
return /^\s*guard TEXT,?\s*$/m.test(tableSql);
|
|
111
|
+
}
|
|
112
|
+
const REFUSED_GUARD = Symbol.for('substrat.refused-guard');
|
|
113
|
+
/**
|
|
114
|
+
* Mark a guard predicate's throw as a refusal, when it is one — called by both adapters'
|
|
115
|
+
* `runGuards` around each predicate, so the two cannot disagree about what counts.
|
|
116
|
+
*
|
|
117
|
+
* A refusal is a `conflict`: what a predicate throws to say "not yet" (`protocol_required`).
|
|
118
|
+
* Anything else — a validation failure, a bug, a denied check (which the denial log has) —
|
|
119
|
+
* is not a guard refusing, and is left unmarked. So is a throw that already carries a
|
|
120
|
+
* refused transition: it is recorded once, as the transition it is.
|
|
121
|
+
*/
|
|
122
|
+
export function markGuardRefusal(err, predicate, operation) {
|
|
123
|
+
if (err === null || typeof err !== 'object' || refusedTransitionOf(err))
|
|
124
|
+
return;
|
|
125
|
+
if (errorCodeOf(err) !== 'conflict')
|
|
126
|
+
return;
|
|
127
|
+
const record = refusedRecordOf(err);
|
|
128
|
+
err[REFUSED_GUARD] = {
|
|
129
|
+
kind: 'guard',
|
|
130
|
+
predicate,
|
|
131
|
+
operation,
|
|
132
|
+
reason: keptReason(err.extensions?.reason),
|
|
133
|
+
entityType: record?.entityType ?? null,
|
|
134
|
+
entityId: record?.entityId ?? null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The reason a row keeps: the problem's code when it is one (`problemReason`), the fixed
|
|
139
|
+
* `UNRECOGNIZED_REFUSAL_REASON` when it is anything else, null when there was none. The
|
|
140
|
+
* wire accepts any non-empty string here, so a predicate could put request text in it.
|
|
141
|
+
*/
|
|
142
|
+
function keptReason(reason) {
|
|
143
|
+
if (reason === undefined || reason === null)
|
|
144
|
+
return null;
|
|
145
|
+
return problemReason.safeParse(reason).success ? reason : UNRECOGNIZED_REFUSAL_REASON;
|
|
146
|
+
}
|
|
147
|
+
/** The refusal an operation failed with — a transition, a guard, or null for anything else. */
|
|
148
|
+
export function refusalOf(err) {
|
|
149
|
+
const transition = refusedTransitionOf(err);
|
|
150
|
+
if (transition)
|
|
151
|
+
return transition;
|
|
152
|
+
if (err === null || typeof err !== 'object')
|
|
153
|
+
return null;
|
|
154
|
+
const g = err[REFUSED_GUARD];
|
|
155
|
+
return g !== null && typeof g === 'object' ? g : null;
|
|
156
|
+
}
|
|
47
157
|
/** The one INSERT both adapters run, so the rows they write cannot differ. */
|
|
48
158
|
export function refusalInsert(row) {
|
|
159
|
+
const r = row.refused;
|
|
160
|
+
const guard = 'kind' in r && r.kind === 'guard' ? r : null;
|
|
161
|
+
const transition = guard ? null : r;
|
|
49
162
|
return {
|
|
50
163
|
sql: `INSERT INTO _substrat_refusals
|
|
51
164
|
(id, kind, tenant_id, scope_id, entity_type, entity_id, from_state, attempted_state,
|
|
52
|
-
operation, invoked_operation, actor, impersonation, invocation_id, at)
|
|
53
|
-
VALUES (?,
|
|
165
|
+
operation, invoked_operation, guard, reason, actor, impersonation, invocation_id, at)
|
|
166
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
54
167
|
params: [
|
|
55
168
|
ulid(),
|
|
169
|
+
guard ? 'guard' : 'transition',
|
|
56
170
|
row.tenantId,
|
|
57
171
|
row.scopeId,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
172
|
+
// The type reaches here from code that may be handed request data; kept only when it
|
|
173
|
+
// is spelled as one. The id stays: it is the record's key, the join every log shares.
|
|
174
|
+
refusalEntityType(r.entityType),
|
|
175
|
+
r.entityId,
|
|
176
|
+
transition?.from ?? null,
|
|
177
|
+
transition?.attempted ?? null,
|
|
178
|
+
r.operation,
|
|
63
179
|
row.invokedOperation,
|
|
180
|
+
guard?.predicate ?? null,
|
|
181
|
+
// Bounded here too, the one INSERT, so no caller of this function can keep raw text.
|
|
182
|
+
guard ? keptReason(guard.reason) : INVALID_TRANSITION,
|
|
64
183
|
row.actor,
|
|
65
184
|
row.impersonation,
|
|
66
185
|
row.invocationId,
|
|
@@ -68,6 +187,6 @@ export function refusalInsert(row) {
|
|
|
68
187
|
],
|
|
69
188
|
};
|
|
70
189
|
}
|
|
71
|
-
/** The refused transition an operation failed with, or null
|
|
190
|
+
/** The refused transition an operation failed with, or null. */
|
|
72
191
|
export { refusedTransitionOf };
|
|
73
192
|
//# sourceMappingURL=refusals.js.map
|
package/dist/refusals.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refusals.js","sourceRoot":"","sources":["../src/refusals.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"refusals.js","sourceRoot":"","sources":["../src/refusals.ts"],"names":[],"mappings":"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"}
|
package/dist/routed-node.d.ts
CHANGED
|
@@ -16,8 +16,9 @@ export declare const ROUTED_ID: RegExp;
|
|
|
16
16
|
*
|
|
17
17
|
* The router resolves `hostname → (tenant, scope, surface)` and forwards over a
|
|
18
18
|
* service binding with that resolution in headers. This is the vertical's side of
|
|
19
|
-
* that contract,
|
|
20
|
-
*
|
|
19
|
+
* that contract, shared because every vertical needs it and none of them should be
|
|
20
|
+
* re-deriving how to trust it. Moving from the kernel to `vertical-host` (#1978), which
|
|
21
|
+
* already re-exports it.
|
|
21
22
|
*
|
|
22
23
|
* Web-standard only and structurally typed: the base tsconfig is `ES2023` with no
|
|
23
24
|
* DOM lib, deliberately, so no package assumes a browser. Taking the one method we
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routed-node.d.ts","sourceRoot":"","sources":["../src/routed-node.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,QAA6B,CAAC;AAEpD
|
|
1
|
+
{"version":3,"file":"routed-node.d.ts","sourceRoot":"","sources":["../src/routed-node.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,QAA6B,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AAEH,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,oFAAoF;AACpF,qBAAa,oBAAqB,SAAQ,KAAK;CAAG;AAElD,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE,qBAA0B,GAClC,UAAU,GAAG,IAAI,CA6BnB"}
|
package/dist/routed-node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routed-node.js","sourceRoot":"","sources":["../src/routed-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"routed-node.js","sourceRoot":"","sources":["../src/routed-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,0BAA0B,CAAC;AA8BpD,oFAAoF;AACpF,MAAM,OAAO,oBAAqB,SAAQ,KAAK;CAAG;AAwBlD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAqB,EACrB,OAAO,GAA0B,EAAE;IAEnC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACjD,IAAI,CAAC,SAAS,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,EAAE,cAAc,EAAE,aAAa,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1D,IAAI,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE,cAAc,CAAC,EAAE,CAAC;YACrE,MAAM,IAAI,oBAAoB,CAAC,kDAAkD,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;SAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1B,MAAM,IAAI,oBAAoB,CAC5B,kFAAkF,CACnF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,SAAS,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC5B,MAAM,IAAI,oBAAoB,CAAC,gCAAgC,CAAC,CAAC;IACnE,CAAC;IAED,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,oBAAoB,CAAC,yCAAyC,CAAC,CAAC;IAC5E,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,SAAqB;QAC/B,OAAO,EAAE,QAAmB;QAC5B,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,KAAK;QACnD,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;KACjD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { SwitchSql } from './system-switch.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a dump leaves behind when it lands in a scope other than the one it was captured from
|
|
4
|
+
* (#1686). One definition for both adapters' loaders, so the DO and the pure host agree.
|
|
5
|
+
*
|
|
6
|
+
* A load is a **copy** when the destination is not the source: a fork, a snapshot, a preview, or
|
|
7
|
+
* one scope's backup restored onto another. It is a **return** when the dump goes back into the
|
|
8
|
+
* scope it came from: a backup restore, a carry onto a new version, adopt, rebind. A copy runs as
|
|
9
|
+
* a scope of its own, so whatever in the dump is bound to being the source (a live link's secret,
|
|
10
|
+
* a side effect the source asked for) must not work in the copy. A return keeps all of it, because
|
|
11
|
+
* restoring a backup must not end live links or lose requests that were waiting.
|
|
12
|
+
*
|
|
13
|
+
* An unknown source (`sourceScopeId` or `destScopeId` absent) counts as a copy: a load that cannot
|
|
14
|
+
* say where the dump came from cannot show it is a return. Only a control plane that predates the
|
|
15
|
+
* field sends a restore with no source.
|
|
16
|
+
*/
|
|
17
|
+
export declare const isCopyLoad: (destScopeId: string | undefined, sourceScopeId: string | undefined) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* A dump's tables as a load into `destScopeId` takes them: **capability rows never cross a scope
|
|
20
|
+
* id.** A copy loads both capability tables empty, so a live link share opens the scope it was
|
|
21
|
+
* minted in and never a copy of it; a return keeps them.
|
|
22
|
+
*
|
|
23
|
+
* Dropped, not loaded as revoked: the copy never minted those links, so a revocation recorded
|
|
24
|
+
* there would be evidence of something that did not happen in it. Names are matched without
|
|
25
|
+
* case, as SQLite resolves a table name.
|
|
26
|
+
*/
|
|
27
|
+
export declare function capabilitiesForLoad<T extends {
|
|
28
|
+
name: string;
|
|
29
|
+
rows: readonly unknown[];
|
|
30
|
+
}>(tables: T[], destScopeId: string | undefined, sourceScopeId: string | undefined): T[];
|
|
31
|
+
/**
|
|
32
|
+
* Where a scope's data came from, and whether it is a copy: at most one row, which holds two
|
|
33
|
+
* facts that are set and corrected apart (#2009).
|
|
34
|
+
*
|
|
35
|
+
* - **`events_through`, the copied-events mark.** The highest event id a load brought in from
|
|
36
|
+
* another scope, or `''` when it brought none. Every event at or below it was emitted in another
|
|
37
|
+
* scope; every event the scope emits itself sorts above it, because a loader re-seeds the
|
|
38
|
+
* scope's event-id floor from `MAX(id)` (#1335) once the rows are in. Written by a load into a
|
|
39
|
+
* scope id other than the dump's (`settleCopiedWork`), and by nothing else.
|
|
40
|
+
* - **`is_copy`, the classification.** Whether the scope is a copy (a fork, a snapshot, a
|
|
41
|
+
* preview), which a host with no control-plane directory reads for primacy and holds inert
|
|
42
|
+
* (#2005). Written on the directory's word only (`markCopyOrigin`), never inferred from a load:
|
|
43
|
+
* one install's backup restored onto another install is a cross-scope load, so it carries the
|
|
44
|
+
* source's events mark, yet the directory still says primary, and its own effects must run.
|
|
45
|
+
* Cleared by staff's correction (`clearCopyMarker`) without touching the events mark.
|
|
46
|
+
*
|
|
47
|
+
* A scope that was never loaded from another scope and never marked holds no row. A row written
|
|
48
|
+
* before the column existed holds `is_copy` NULL, and NULL reads as a copy: such a row meant both
|
|
49
|
+
* facts at once, so every legacy store keeps exactly the behaviour it had (`IS_COPY_SQL`). The
|
|
50
|
+
* column is nullable with no DEFAULT for the reason every additive spine column is (#1883,
|
|
51
|
+
* `lint:spine-ddl`), so its "default" lives in that read. Kernel-owned and shared by both
|
|
52
|
+
* adapters' `KERNEL_DDL`, so the two cannot part company.
|
|
53
|
+
*/
|
|
54
|
+
export declare const COPY_ORIGIN_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_copy_origin (\n -- Always 1: one origin per scope. No CHECK, because lint:spine-ddl compares none.\n id INTEGER PRIMARY KEY,\n source_scope_id TEXT,\n events_through TEXT NOT NULL,\n copied_at TEXT NOT NULL,\n -- #2009: 1 a copy, 0 not one (a load's events mark only, or a cleared marker), NULL a row\n -- from before the column, which read as a copy and still does.\n is_copy INTEGER\n );\n";
|
|
55
|
+
/**
|
|
56
|
+
* The predicate every read that turns an outbox row into WORK carries (consumer dispatch, executor
|
|
57
|
+
* dispatch, the Tier-2 drain): the event was emitted in this scope, not copied into it. `alias` is
|
|
58
|
+
* the outbox's alias in the caller's query (`'o.'`), or empty. A scope that was never a copy has no
|
|
59
|
+
* key, and every id compares above the empty string.
|
|
60
|
+
*/
|
|
61
|
+
export declare const emittedHere: (alias?: string) => string;
|
|
62
|
+
/**
|
|
63
|
+
* On a copy, nothing that originated in the source produces an effect in the destination. Run
|
|
64
|
+
* inside the load's transaction, after the rows are in and BEFORE the loader queues any work of
|
|
65
|
+
* its own (the attachment-text re-extraction is a job run), or that work is settled with the
|
|
66
|
+
* source's. The history stays: every row is still
|
|
67
|
+
* there and reads as what happened at the source; only its power to cause something here goes.
|
|
68
|
+
*
|
|
69
|
+
* The platform's drain walks every active scope, and a fork, a snapshot or a preview is one, so
|
|
70
|
+
* each of these would otherwise run a second time from the copy (an email sent twice, a connector
|
|
71
|
+
* delivery repeated, a usage line billed twice):
|
|
72
|
+
*
|
|
73
|
+
* - **Pending intents** settle `failed`, "not carried", attributed to the platform.
|
|
74
|
+
* - **Executor retries** (`_substrat_deliveries` rows with a `next_attempt_at`) become terminal,
|
|
75
|
+
* with "not carried" as their error, as a dead letter reads.
|
|
76
|
+
* - **Running job runs** settle `failed`, "not carried"; their step ledger stays as evidence.
|
|
77
|
+
* - **Events no consumer or executor has reached yet** cannot be settled row by row: an executor is
|
|
78
|
+
* registered on the coordinator, not in the scope, so the loader cannot know who still owes one.
|
|
79
|
+
* Instead the copy records the highest id it brought in (`_substrat_copy_origin`), and every
|
|
80
|
+
* read that dispatches work carries `emittedHere()`. That also keeps the copy from shipping the
|
|
81
|
+
* source's events to Tier 2 a second time.
|
|
82
|
+
*
|
|
83
|
+
* A return leaves all of it as it was, because the scope that asked for the work is the one it is
|
|
84
|
+
* back in. Settled rather than dropped: a journal row saying "not carried" explains why the event
|
|
85
|
+
* that raised it caused nothing here, where a missing row would leave a gap.
|
|
86
|
+
*/
|
|
87
|
+
export declare function settleCopiedWork(sql: SwitchSql, destScopeId: string | undefined, sourceScopeId: string | undefined, now: string): void;
|
|
88
|
+
/**
|
|
89
|
+
* Mark this scope a copy (#2005), on the directory's word that it is not primary: a load the
|
|
90
|
+
* platform flags, the `mark-copies` repair, a reactivation, a carry's wipe or release. Sets the
|
|
91
|
+
* classification and nothing else (#2009): an origin row a load wrote keeps its events mark and
|
|
92
|
+
* source, and a scope with no row gets one whose events mark is `''`, which passes every event
|
|
93
|
+
* exactly as no row does, so dispatch is unchanged. Idempotent: answers whether this call changed
|
|
94
|
+
* anything, and a store that already reads as a copy (a legacy row included) is left as it is.
|
|
95
|
+
*/
|
|
96
|
+
export declare function markCopyOrigin(sql: SwitchSql, now: string): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The one statement that marks a store a copy (#2005, #2009): the origin row with no events mark
|
|
99
|
+
* where none exists, or the classification set on the row that does. It only ever makes a store
|
|
100
|
+
* MORE restricted — a copy's executors are held inert — and changes no data and no events mark,
|
|
101
|
+
* which is why the scope DO lets this statement, and no other, through without advancing the
|
|
102
|
+
* write revision a carry fences on (#1722, `isCopyMarkInsert`). Clearing a marker is the opposite
|
|
103
|
+
* and is a write like any other.
|
|
104
|
+
*/
|
|
105
|
+
export declare const MARK_COPY_ORIGIN_SQL: string;
|
|
106
|
+
/**
|
|
107
|
+
* Correct a MISTAKEN copy classification (#2005, #2009): for a scope the directory says is
|
|
108
|
+
* primary, marked by a misclassification, a race, an operator, or a row from before #2009 that
|
|
109
|
+
* a cross-scope load wrote (one install's backup restored onto another). Clears the
|
|
110
|
+
* classification only: the events mark stays, so the source's queued work is still never run
|
|
111
|
+
* here (`emittedHere()`), and the scope's own effects run again. `absent` when the store does not
|
|
112
|
+
* read as a copy.
|
|
113
|
+
*/
|
|
114
|
+
export declare function clearCopyMarker(sql: SwitchSql): 'cleared' | 'absent';
|
|
115
|
+
/**
|
|
116
|
+
* Whether this scope is a copy (#2005, #2009): the classification on its copy-origin row, with
|
|
117
|
+
* a row from before the column (NULL) read as one. The one primacy fact a host with no
|
|
118
|
+
* control-plane directory can read from the scope's own storage — a CP-less hosted vertical's
|
|
119
|
+
* coordinator asks it before it runs an executor.
|
|
120
|
+
*/
|
|
121
|
+
export declare const IS_COPY_SQL = "SELECT 1 AS copy FROM _substrat_copy_origin WHERE id = 1 AND COALESCE(is_copy, 1) = 1";
|
|
122
|
+
//# sourceMappingURL=scope-copy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-copy.d.ts","sourceRoot":"","sources":["../src/scope-copy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,gBAAiB,MAAM,GAAG,SAAS,iBAAiB,MAAM,GAAG,SAAS,KAAG,OACpC,CAAC;AAK7D;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;CAAE,EACtF,MAAM,EAAE,CAAC,EAAE,EACX,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,aAAa,EAAE,MAAM,GAAG,SAAS,GAChC,CAAC,EAAE,CAGL;AAcD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,eAAe,8cAW3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,sBAAiB,MACqD,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,SAAS,EACd,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,GAAG,EAAE,MAAM,GACV,IAAI,CAiDN;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAInE;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,QAEa,CAAC;AAE/C;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAIpE;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,0FAA0F,CAAC"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { CAPABILITY_TABLE_NAMES } from './capability.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a dump leaves behind when it lands in a scope other than the one it was captured from
|
|
4
|
+
* (#1686). One definition for both adapters' loaders, so the DO and the pure host agree.
|
|
5
|
+
*
|
|
6
|
+
* A load is a **copy** when the destination is not the source: a fork, a snapshot, a preview, or
|
|
7
|
+
* one scope's backup restored onto another. It is a **return** when the dump goes back into the
|
|
8
|
+
* scope it came from: a backup restore, a carry onto a new version, adopt, rebind. A copy runs as
|
|
9
|
+
* a scope of its own, so whatever in the dump is bound to being the source (a live link's secret,
|
|
10
|
+
* a side effect the source asked for) must not work in the copy. A return keeps all of it, because
|
|
11
|
+
* restoring a backup must not end live links or lose requests that were waiting.
|
|
12
|
+
*
|
|
13
|
+
* An unknown source (`sourceScopeId` or `destScopeId` absent) counts as a copy: a load that cannot
|
|
14
|
+
* say where the dump came from cannot show it is a return. Only a control plane that predates the
|
|
15
|
+
* field sends a restore with no source.
|
|
16
|
+
*/
|
|
17
|
+
export const isCopyLoad = (destScopeId, sourceScopeId) => destScopeId === undefined || sourceScopeId !== destScopeId;
|
|
18
|
+
/** The capability tables, as `capabilitiesForLoad` matches a dump's (lowercased) names. */
|
|
19
|
+
const CAPABILITY_TABLES = new Set(CAPABILITY_TABLE_NAMES);
|
|
20
|
+
/**
|
|
21
|
+
* A dump's tables as a load into `destScopeId` takes them: **capability rows never cross a scope
|
|
22
|
+
* id.** A copy loads both capability tables empty, so a live link share opens the scope it was
|
|
23
|
+
* minted in and never a copy of it; a return keeps them.
|
|
24
|
+
*
|
|
25
|
+
* Dropped, not loaded as revoked: the copy never minted those links, so a revocation recorded
|
|
26
|
+
* there would be evidence of something that did not happen in it. Names are matched without
|
|
27
|
+
* case, as SQLite resolves a table name.
|
|
28
|
+
*/
|
|
29
|
+
export function capabilitiesForLoad(tables, destScopeId, sourceScopeId) {
|
|
30
|
+
if (!isCopyLoad(destScopeId, sourceScopeId))
|
|
31
|
+
return tables;
|
|
32
|
+
return tables.map((t) => (CAPABILITY_TABLES.has(t.name.toLowerCase()) ? { ...t, rows: [] } : t));
|
|
33
|
+
}
|
|
34
|
+
/** The reason copied work is settled with. Names the source, so the journal says where it runs. */
|
|
35
|
+
const notCarried = (sourceScopeId) => `not carried: copied from ${sourceScopeId ? `scope ${sourceScopeId}` : 'another scope'} before it ran; ` +
|
|
36
|
+
'it runs in the scope that asked for it, never in a copy';
|
|
37
|
+
/** A copied intent's failure: the platform's own refusal, never the provider's answer. */
|
|
38
|
+
const NOT_CARRIED_FAILURE = JSON.stringify({
|
|
39
|
+
origin: 'platform',
|
|
40
|
+
code: 'precondition_failed',
|
|
41
|
+
permission: null,
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Where a scope's data came from, and whether it is a copy: at most one row, which holds two
|
|
45
|
+
* facts that are set and corrected apart (#2009).
|
|
46
|
+
*
|
|
47
|
+
* - **`events_through`, the copied-events mark.** The highest event id a load brought in from
|
|
48
|
+
* another scope, or `''` when it brought none. Every event at or below it was emitted in another
|
|
49
|
+
* scope; every event the scope emits itself sorts above it, because a loader re-seeds the
|
|
50
|
+
* scope's event-id floor from `MAX(id)` (#1335) once the rows are in. Written by a load into a
|
|
51
|
+
* scope id other than the dump's (`settleCopiedWork`), and by nothing else.
|
|
52
|
+
* - **`is_copy`, the classification.** Whether the scope is a copy (a fork, a snapshot, a
|
|
53
|
+
* preview), which a host with no control-plane directory reads for primacy and holds inert
|
|
54
|
+
* (#2005). Written on the directory's word only (`markCopyOrigin`), never inferred from a load:
|
|
55
|
+
* one install's backup restored onto another install is a cross-scope load, so it carries the
|
|
56
|
+
* source's events mark, yet the directory still says primary, and its own effects must run.
|
|
57
|
+
* Cleared by staff's correction (`clearCopyMarker`) without touching the events mark.
|
|
58
|
+
*
|
|
59
|
+
* A scope that was never loaded from another scope and never marked holds no row. A row written
|
|
60
|
+
* before the column existed holds `is_copy` NULL, and NULL reads as a copy: such a row meant both
|
|
61
|
+
* facts at once, so every legacy store keeps exactly the behaviour it had (`IS_COPY_SQL`). The
|
|
62
|
+
* column is nullable with no DEFAULT for the reason every additive spine column is (#1883,
|
|
63
|
+
* `lint:spine-ddl`), so its "default" lives in that read. Kernel-owned and shared by both
|
|
64
|
+
* adapters' `KERNEL_DDL`, so the two cannot part company.
|
|
65
|
+
*/
|
|
66
|
+
export const COPY_ORIGIN_DDL = `
|
|
67
|
+
CREATE TABLE IF NOT EXISTS _substrat_copy_origin (
|
|
68
|
+
-- Always 1: one origin per scope. No CHECK, because lint:spine-ddl compares none.
|
|
69
|
+
id INTEGER PRIMARY KEY,
|
|
70
|
+
source_scope_id TEXT,
|
|
71
|
+
events_through TEXT NOT NULL,
|
|
72
|
+
copied_at TEXT NOT NULL,
|
|
73
|
+
-- #2009: 1 a copy, 0 not one (a load's events mark only, or a cleared marker), NULL a row
|
|
74
|
+
-- from before the column, which read as a copy and still does.
|
|
75
|
+
is_copy INTEGER
|
|
76
|
+
);
|
|
77
|
+
`;
|
|
78
|
+
/**
|
|
79
|
+
* The predicate every read that turns an outbox row into WORK carries (consumer dispatch, executor
|
|
80
|
+
* dispatch, the Tier-2 drain): the event was emitted in this scope, not copied into it. `alias` is
|
|
81
|
+
* the outbox's alias in the caller's query (`'o.'`), or empty. A scope that was never a copy has no
|
|
82
|
+
* key, and every id compares above the empty string.
|
|
83
|
+
*/
|
|
84
|
+
export const emittedHere = (alias = '') => `${alias}id > COALESCE((SELECT events_through FROM _substrat_copy_origin WHERE id = 1), '')`;
|
|
85
|
+
/**
|
|
86
|
+
* On a copy, nothing that originated in the source produces an effect in the destination. Run
|
|
87
|
+
* inside the load's transaction, after the rows are in and BEFORE the loader queues any work of
|
|
88
|
+
* its own (the attachment-text re-extraction is a job run), or that work is settled with the
|
|
89
|
+
* source's. The history stays: every row is still
|
|
90
|
+
* there and reads as what happened at the source; only its power to cause something here goes.
|
|
91
|
+
*
|
|
92
|
+
* The platform's drain walks every active scope, and a fork, a snapshot or a preview is one, so
|
|
93
|
+
* each of these would otherwise run a second time from the copy (an email sent twice, a connector
|
|
94
|
+
* delivery repeated, a usage line billed twice):
|
|
95
|
+
*
|
|
96
|
+
* - **Pending intents** settle `failed`, "not carried", attributed to the platform.
|
|
97
|
+
* - **Executor retries** (`_substrat_deliveries` rows with a `next_attempt_at`) become terminal,
|
|
98
|
+
* with "not carried" as their error, as a dead letter reads.
|
|
99
|
+
* - **Running job runs** settle `failed`, "not carried"; their step ledger stays as evidence.
|
|
100
|
+
* - **Events no consumer or executor has reached yet** cannot be settled row by row: an executor is
|
|
101
|
+
* registered on the coordinator, not in the scope, so the loader cannot know who still owes one.
|
|
102
|
+
* Instead the copy records the highest id it brought in (`_substrat_copy_origin`), and every
|
|
103
|
+
* read that dispatches work carries `emittedHere()`. That also keeps the copy from shipping the
|
|
104
|
+
* source's events to Tier 2 a second time.
|
|
105
|
+
*
|
|
106
|
+
* A return leaves all of it as it was, because the scope that asked for the work is the one it is
|
|
107
|
+
* back in. Settled rather than dropped: a journal row saying "not carried" explains why the event
|
|
108
|
+
* that raised it caused nothing here, where a missing row would leave a gap.
|
|
109
|
+
*/
|
|
110
|
+
export function settleCopiedWork(sql, destScopeId, sourceScopeId, now) {
|
|
111
|
+
if (!isCopyLoad(destScopeId, sourceScopeId))
|
|
112
|
+
return;
|
|
113
|
+
const reason = notCarried(sourceScopeId);
|
|
114
|
+
sql.run(`UPDATE _substrat_platform_requests
|
|
115
|
+
SET status = 'failed', last_error = ?, last_failure = ?, settled_at = ?
|
|
116
|
+
WHERE status = 'pending'`, reason, NOT_CARRIED_FAILURE, now);
|
|
117
|
+
sql.run(`UPDATE _substrat_deliveries SET next_attempt_at = NULL, error = ?, delivered_at = ?
|
|
118
|
+
WHERE next_attempt_at IS NOT NULL`, reason, now);
|
|
119
|
+
sql.run(`UPDATE _substrat_job_runs
|
|
120
|
+
SET status = 'failed', last_error = ?, next_attempt_at = NULL, updated_at = ?, ended_at = ?
|
|
121
|
+
WHERE status = 'running'`, reason, now, now);
|
|
122
|
+
const highest = sql.all('SELECT MAX(id) AS id FROM _substrat_outbox')[0]?.id;
|
|
123
|
+
// Written for an empty copy too: `''` is below every id, so `emittedHere()` passes all of the
|
|
124
|
+
// scope's own events exactly as no row would. An origin the dump already carried with the same
|
|
125
|
+
// source and mark is kept as it was, its `copied_at` included, so re-loading the same export
|
|
126
|
+
// changes nothing in it.
|
|
127
|
+
//
|
|
128
|
+
// `is_copy` is 0 (#2009): the load moves the events mark and does not classify. Whatever the
|
|
129
|
+
// dump's row said described the scope it came FROM; whether THIS scope is a copy is the
|
|
130
|
+
// directory's to say, through `markCopyOrigin`, which the loader runs after this when the
|
|
131
|
+
// directory says so.
|
|
132
|
+
sql.run(`INSERT INTO _substrat_copy_origin (id, source_scope_id, events_through, copied_at, is_copy) VALUES (1, ?, ?, ?, 0)
|
|
133
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
134
|
+
source_scope_id = excluded.source_scope_id,
|
|
135
|
+
events_through = excluded.events_through,
|
|
136
|
+
copied_at = excluded.copied_at,
|
|
137
|
+
is_copy = excluded.is_copy
|
|
138
|
+
WHERE _substrat_copy_origin.source_scope_id IS NOT excluded.source_scope_id
|
|
139
|
+
OR _substrat_copy_origin.events_through IS NOT excluded.events_through
|
|
140
|
+
OR _substrat_copy_origin.is_copy IS NOT excluded.is_copy`, sourceScopeId ?? null, typeof highest === 'string' ? highest : '', now);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Mark this scope a copy (#2005), on the directory's word that it is not primary: a load the
|
|
144
|
+
* platform flags, the `mark-copies` repair, a reactivation, a carry's wipe or release. Sets the
|
|
145
|
+
* classification and nothing else (#2009): an origin row a load wrote keeps its events mark and
|
|
146
|
+
* source, and a scope with no row gets one whose events mark is `''`, which passes every event
|
|
147
|
+
* exactly as no row does, so dispatch is unchanged. Idempotent: answers whether this call changed
|
|
148
|
+
* anything, and a store that already reads as a copy (a legacy row included) is left as it is.
|
|
149
|
+
*/
|
|
150
|
+
export function markCopyOrigin(sql, now) {
|
|
151
|
+
if (sql.all(IS_COPY_SQL).length > 0)
|
|
152
|
+
return false;
|
|
153
|
+
sql.run(MARK_COPY_ORIGIN_SQL, now);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The one statement that marks a store a copy (#2005, #2009): the origin row with no events mark
|
|
158
|
+
* where none exists, or the classification set on the row that does. It only ever makes a store
|
|
159
|
+
* MORE restricted — a copy's executors are held inert — and changes no data and no events mark,
|
|
160
|
+
* which is why the scope DO lets this statement, and no other, through without advancing the
|
|
161
|
+
* write revision a carry fences on (#1722, `isCopyMarkInsert`). Clearing a marker is the opposite
|
|
162
|
+
* and is a write like any other.
|
|
163
|
+
*/
|
|
164
|
+
export const MARK_COPY_ORIGIN_SQL = "INSERT INTO _substrat_copy_origin (id, source_scope_id, events_through, copied_at, is_copy) VALUES (1, NULL, '', ?, 1) " +
|
|
165
|
+
'ON CONFLICT (id) DO UPDATE SET is_copy = 1';
|
|
166
|
+
/**
|
|
167
|
+
* Correct a MISTAKEN copy classification (#2005, #2009): for a scope the directory says is
|
|
168
|
+
* primary, marked by a misclassification, a race, an operator, or a row from before #2009 that
|
|
169
|
+
* a cross-scope load wrote (one install's backup restored onto another). Clears the
|
|
170
|
+
* classification only: the events mark stays, so the source's queued work is still never run
|
|
171
|
+
* here (`emittedHere()`), and the scope's own effects run again. `absent` when the store does not
|
|
172
|
+
* read as a copy.
|
|
173
|
+
*/
|
|
174
|
+
export function clearCopyMarker(sql) {
|
|
175
|
+
if (sql.all(IS_COPY_SQL).length === 0)
|
|
176
|
+
return 'absent';
|
|
177
|
+
sql.run('UPDATE _substrat_copy_origin SET is_copy = 0 WHERE id = 1');
|
|
178
|
+
return 'cleared';
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Whether this scope is a copy (#2005, #2009): the classification on its copy-origin row, with
|
|
182
|
+
* a row from before the column (NULL) read as one. The one primacy fact a host with no
|
|
183
|
+
* control-plane directory can read from the scope's own storage — a CP-less hosted vertical's
|
|
184
|
+
* coordinator asks it before it runs an executor.
|
|
185
|
+
*/
|
|
186
|
+
export const IS_COPY_SQL = 'SELECT 1 AS copy FROM _substrat_copy_origin WHERE id = 1 AND COALESCE(is_copy, 1) = 1';
|
|
187
|
+
//# sourceMappingURL=scope-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-copy.js","sourceRoot":"","sources":["../src/scope-copy.ts"],"names":[],"mappings":"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"}
|