@volter/world-core 2.0.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/LICENSE +202 -0
- package/README.md +29 -0
- package/app-route.cjs +154 -0
- package/app-route.d.cts +7 -0
- package/attach.cjs +80 -0
- package/dist/app-route.cjs +154 -0
- package/dist/app-route.d.cts +7 -0
- package/dist/attach.cjs +80 -0
- package/dist/generated/pack-facts.json +4306 -0
- package/dist/inject.cjs +1097 -0
- package/dist/network-policy.cjs +92 -0
- package/dist/network-policy.d.cts +10 -0
- package/dist/src/actions.d.ts +276 -0
- package/dist/src/actions.js +436 -0
- package/dist/src/ancestry.d.ts +22 -0
- package/dist/src/ancestry.js +238 -0
- package/dist/src/args.d.ts +3 -0
- package/dist/src/args.js +12 -0
- package/dist/src/blob-store.d.ts +55 -0
- package/dist/src/blob-store.js +186 -0
- package/dist/src/brand-tokens.d.ts +2 -0
- package/dist/src/brand-tokens.js +17 -0
- package/dist/src/changeset.d.ts +431 -0
- package/dist/src/changeset.js +0 -0
- package/dist/src/client-bundle.d.ts +1 -0
- package/dist/src/client-bundle.js +28 -0
- package/dist/src/credential.d.ts +38 -0
- package/dist/src/credential.js +114 -0
- package/dist/src/derived-core.d.ts +452 -0
- package/dist/src/derived-core.js +782 -0
- package/dist/src/derived.d.ts +84 -0
- package/dist/src/derived.js +122 -0
- package/dist/src/emit.d.ts +106 -0
- package/dist/src/emit.js +157 -0
- package/dist/src/executor.d.ts +120 -0
- package/dist/src/executor.js +387 -0
- package/dist/src/file-response.d.ts +3 -0
- package/dist/src/file-response.js +22 -0
- package/dist/src/fork.d.ts +26 -0
- package/dist/src/fork.js +68 -0
- package/dist/src/git/history.d.ts +36 -0
- package/dist/src/git/history.js +298 -0
- package/dist/src/git/index.d.ts +6 -0
- package/dist/src/git/index.js +6 -0
- package/dist/src/git/inflate.d.ts +11 -0
- package/dist/src/git/inflate.js +194 -0
- package/dist/src/git/objects.d.ts +64 -0
- package/dist/src/git/objects.js +161 -0
- package/dist/src/git/pack.d.ts +14 -0
- package/dist/src/git/pack.js +199 -0
- package/dist/src/git/refs.d.ts +19 -0
- package/dist/src/git/refs.js +35 -0
- package/dist/src/git/smart-http.d.ts +45 -0
- package/dist/src/git/smart-http.js +223 -0
- package/dist/src/hash.d.ts +38 -0
- package/dist/src/hash.js +48 -0
- package/dist/src/head.d.ts +140 -0
- package/dist/src/head.js +313 -0
- package/dist/src/history.d.ts +76 -0
- package/dist/src/history.js +322 -0
- package/dist/src/index.d.ts +73 -0
- package/dist/src/index.js +98 -0
- package/dist/src/lifecycle.d.ts +1 -0
- package/dist/src/lifecycle.js +8 -0
- package/dist/src/log.d.ts +254 -0
- package/dist/src/log.js +801 -0
- package/dist/src/mirror-shell.d.ts +2 -0
- package/dist/src/mirror-shell.js +13 -0
- package/dist/src/observe.d.ts +49 -0
- package/dist/src/observe.js +148 -0
- package/dist/src/pack-assets.d.ts +30 -0
- package/dist/src/pack-assets.js +88 -0
- package/dist/src/packRegistry.d.ts +374 -0
- package/dist/src/packRegistry.js +142 -0
- package/dist/src/placeholder-remote.d.ts +22 -0
- package/dist/src/placeholder-remote.js +86 -0
- package/dist/src/proxy.d.ts +25 -0
- package/dist/src/proxy.js +155 -0
- package/dist/src/rateBudget.d.ts +367 -0
- package/dist/src/rateBudget.js +925 -0
- package/dist/src/references.d.ts +18 -0
- package/dist/src/references.js +27 -0
- package/dist/src/remote-execute.d.ts +22 -0
- package/dist/src/remote-execute.js +1 -0
- package/dist/src/resource-blob.d.ts +10 -0
- package/dist/src/resource-blob.js +56 -0
- package/dist/src/scenario.d.ts +197 -0
- package/dist/src/scenario.js +425 -0
- package/dist/src/schemas.d.ts +78 -0
- package/dist/src/schemas.js +50 -0
- package/dist/src/serve-http.d.ts +48 -0
- package/dist/src/serve-http.js +340 -0
- package/dist/src/serve.d.ts +147 -0
- package/dist/src/serve.js +507 -0
- package/dist/src/shared-blob-index.d.ts +4 -0
- package/dist/src/shared-blob-index.js +126 -0
- package/dist/src/state-system.d.ts +70 -0
- package/dist/src/state-system.js +90 -0
- package/dist/src/storage.d.ts +101 -0
- package/dist/src/storage.js +337 -0
- package/dist/src/twin-fetch.d.ts +64 -0
- package/dist/src/twin-fetch.js +91 -0
- package/dist/src/types.d.ts +40 -0
- package/dist/src/types.js +1 -0
- package/dist/src/v1-removed.d.ts +159 -0
- package/dist/src/v1-removed.js +124 -0
- package/dist/src/volter-home.d.ts +5 -0
- package/dist/src/volter-home.js +10 -0
- package/dist/src/world-clock.d.ts +4 -0
- package/dist/src/world-clock.js +32 -0
- package/dist/src/world-env.d.ts +3 -0
- package/dist/src/world-env.js +22 -0
- package/dist/src/world-store-sql.d.ts +27 -0
- package/dist/src/world-store-sql.js +86 -0
- package/dist/src/world-store.d.ts +168 -0
- package/dist/src/world-store.js +475 -0
- package/dist/src/worldConfig.d.ts +9 -0
- package/dist/src/worldConfig.js +17 -0
- package/dist/stream-bridge.cjs +80 -0
- package/dist/vendor-hosts.cjs +200 -0
- package/generated/pack-facts.json +4306 -0
- package/inject.cjs +1097 -0
- package/network-policy.cjs +92 -0
- package/network-policy.d.cts +10 -0
- package/package.json +103 -0
- package/src/actions.ts +564 -0
- package/src/ancestry.ts +213 -0
- package/src/args.ts +14 -0
- package/src/blob-store.ts +185 -0
- package/src/brand-tokens.ts +17 -0
- package/src/changeset.ts +1032 -0
- package/src/client-bundle.ts +29 -0
- package/src/credential.ts +140 -0
- package/src/derived-core.ts +1004 -0
- package/src/derived.ts +176 -0
- package/src/emit.ts +242 -0
- package/src/executor.ts +431 -0
- package/src/file-response.ts +22 -0
- package/src/fork.ts +89 -0
- package/src/git/history.ts +177 -0
- package/src/git/index.ts +6 -0
- package/src/git/inflate.ts +125 -0
- package/src/git/objects.ts +110 -0
- package/src/git/pack.ts +105 -0
- package/src/git/refs.ts +25 -0
- package/src/git/smart-http.ts +149 -0
- package/src/hash.ts +66 -0
- package/src/head.ts +318 -0
- package/src/history.ts +246 -0
- package/src/index.ts +323 -0
- package/src/lifecycle.ts +8 -0
- package/src/log.ts +793 -0
- package/src/mirror-shell.ts +15 -0
- package/src/observe.ts +130 -0
- package/src/pack-assets.ts +81 -0
- package/src/packRegistry.ts +408 -0
- package/src/placeholder-remote.ts +81 -0
- package/src/proxy.ts +183 -0
- package/src/rateBudget.ts +1115 -0
- package/src/references.ts +46 -0
- package/src/remote-execute.ts +26 -0
- package/src/resource-blob.ts +57 -0
- package/src/scenario.ts +479 -0
- package/src/schemas.ts +56 -0
- package/src/serve-http.ts +299 -0
- package/src/serve.ts +618 -0
- package/src/shared-blob-index.ts +108 -0
- package/src/state-system.ts +115 -0
- package/src/storage.ts +407 -0
- package/src/twin-fetch.ts +147 -0
- package/src/types.ts +50 -0
- package/src/v1-removed.ts +172 -0
- package/src/volter-home.ts +11 -0
- package/src/world-clock.ts +33 -0
- package/src/world-env.ts +18 -0
- package/src/world-store-sql.ts +118 -0
- package/src/world-store.ts +572 -0
- package/src/worldConfig.ts +27 -0
- package/stream-bridge.cjs +80 -0
- package/vendor-hosts.cjs +200 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WORLD_NETWORK_POLICY_ENV = 'VOLTER_WORLD_NETWORK_POLICY';
|
|
4
|
+
|
|
5
|
+
function object(value, keys, label) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
7
|
+
|| Object.keys(value).some(key => !keys.includes(key))) {
|
|
8
|
+
throw new Error(`${label} has an invalid shape`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function origins(value) {
|
|
14
|
+
if (!Array.isArray(value) || value.length > 256) throw new Error('World network egress must be an array of at most 256 exact HTTPS origins');
|
|
15
|
+
const normalized = value.map(origin => {
|
|
16
|
+
if (typeof origin !== 'string' || origin.length > 2048) throw new Error('World network egress origins must be strings');
|
|
17
|
+
let url;
|
|
18
|
+
try { url = new URL(origin); } catch { throw new Error(`Invalid World network origin: ${origin}`); }
|
|
19
|
+
if (url.protocol !== 'https:' || url.origin !== origin || url.username || url.password
|
|
20
|
+
|| !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(url.hostname)
|
|
21
|
+
|| url.hostname.split('.').some(label => !label || label.length > 63 || label.startsWith('-') || label.endsWith('-'))
|
|
22
|
+
|| /^[\d.]+$/.test(url.hostname) || url.hostname === 'localhost' || url.hostname.endsWith('.localhost')) {
|
|
23
|
+
throw new Error(`World network requires an exact canonical HTTPS origin: ${origin}`);
|
|
24
|
+
}
|
|
25
|
+
return origin;
|
|
26
|
+
});
|
|
27
|
+
return Object.freeze([...new Set(normalized)].sort());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeWorldNetwork(value) {
|
|
31
|
+
const network = object(value, ['egress'], 'World network');
|
|
32
|
+
return Object.freeze({ egress: origins(network.egress) });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createWorldNetworkPolicy(world, network) {
|
|
36
|
+
if (typeof world !== 'string' || !world || world.length > 256) throw new Error('World network policy requires its World identity');
|
|
37
|
+
return Object.freeze({ version: 1, world, egress: normalizeWorldNetwork(network).egress });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseWorldNetworkPolicy(value) {
|
|
41
|
+
const policy = object(value, ['version', 'world', 'egress'], 'World network policy');
|
|
42
|
+
if (policy.version !== 1) throw new Error('Unsupported World network policy version');
|
|
43
|
+
return createWorldNetworkPolicy(policy.world, { egress: policy.egress });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function worldNetworkPolicyFromEnv(env) {
|
|
47
|
+
const serialized = env[WORLD_NETWORK_POLICY_ENV];
|
|
48
|
+
if (serialized === undefined) return undefined;
|
|
49
|
+
if (typeof serialized !== 'string' || serialized.length > 65536) throw new Error('Invalid published World network policy');
|
|
50
|
+
const policy = parseWorldNetworkPolicy(JSON.parse(serialized));
|
|
51
|
+
if (!env.VOLTER_WORLD || policy.world !== env.VOLTER_WORLD) throw new Error('World network policy does not belong to the attached World');
|
|
52
|
+
return policy;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function allowsWorldNetworkEgress(policy, value) {
|
|
56
|
+
let url;
|
|
57
|
+
try { url = new URL(String(value)); } catch { return false; }
|
|
58
|
+
return url.protocol === 'https:' && !url.username && !url.password && policy.egress.includes(url.origin);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Loopback and private addresses stay inside the machine a World runs on: never refused. */
|
|
62
|
+
function isWorldInternalHost(hostname) {
|
|
63
|
+
const h = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
64
|
+
if (!h) return true;
|
|
65
|
+
if (h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.test') || h === '0.0.0.0' || h === '::1') return true;
|
|
66
|
+
const v4 = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(h);
|
|
67
|
+
if (v4) {
|
|
68
|
+
const a = Number(v4[1]); const b = Number(v4[2]);
|
|
69
|
+
return a === 127 || a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254);
|
|
70
|
+
}
|
|
71
|
+
return h.includes(':') && (h.startsWith('fc') || h.startsWith('fd') || h.startsWith('fe80:'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Why this World refuses an outbound request to `value`, or null when it allows it. The one rule
|
|
76
|
+
* every enforcement point applies: the injector for application processes, and a twin's own
|
|
77
|
+
* World-internal outbound calls (a queue delivering to its destination), which run in a host the
|
|
78
|
+
* injector does not attach to. Internal addresses pass; strict egress refuses everything else; a
|
|
79
|
+
* published network policy refuses what it does not list. An unreadable policy refuses.
|
|
80
|
+
*/
|
|
81
|
+
function worldEgressRefusal(value, env = process.env) {
|
|
82
|
+
let url;
|
|
83
|
+
try { url = new URL(String(value)); } catch { return `unparseable destination ${String(value)}`; }
|
|
84
|
+
if (isWorldInternalHost(url.hostname)) return null;
|
|
85
|
+
if (env.VOLTER_TWIN_STRICT_EGRESS === '1' || env.VOLTER_TWIN_STRICT_EGRESS === 'true') return `sealed World refuses untwinned host ${url.hostname}`;
|
|
86
|
+
let policy;
|
|
87
|
+
try { policy = worldNetworkPolicyFromEnv(env); } catch (error) { return `World network policy unreadable (${error.message}); refusing ${url.hostname}`; }
|
|
88
|
+
if (policy !== undefined && !allowsWorldNetworkEgress(policy, url)) return `World network policy does not allow ${url.origin}`;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { isWorldInternalHost, worldEgressRefusal, WORLD_NETWORK_POLICY_ENV, normalizeWorldNetwork, createWorldNetworkPolicy, parseWorldNetworkPolicy, worldNetworkPolicyFromEnv, allowsWorldNetworkEgress };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface WorldNetwork { readonly egress: readonly string[] }
|
|
2
|
+
export interface WorldNetworkPolicy extends WorldNetwork { readonly version: 1; readonly world: string }
|
|
3
|
+
export const WORLD_NETWORK_POLICY_ENV: 'VOLTER_WORLD_NETWORK_POLICY';
|
|
4
|
+
export function normalizeWorldNetwork(value: unknown): WorldNetwork;
|
|
5
|
+
export function createWorldNetworkPolicy(world: string, network: WorldNetwork): WorldNetworkPolicy;
|
|
6
|
+
export function parseWorldNetworkPolicy(value: unknown): WorldNetworkPolicy;
|
|
7
|
+
export function worldNetworkPolicyFromEnv(env: Readonly<Record<string, string | undefined>>): WorldNetworkPolicy | undefined;
|
|
8
|
+
export function allowsWorldNetworkEgress(policy: WorldNetworkPolicy, value: string | URL): boolean;
|
|
9
|
+
export function isWorldInternalHost(hostname: string): boolean;
|
|
10
|
+
export function worldEgressRefusal(value: string | URL, env?: Readonly<Record<string, string | undefined>>): string | null;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { SubjectFields } from './hash.js';
|
|
2
|
+
import { type Receipt } from './log.js';
|
|
3
|
+
import type { TwinResource } from './serve.js';
|
|
4
|
+
export type TwinActionOp = 'set' | 'revert';
|
|
5
|
+
export type TwinActionPreconditionOp = 'exists' | 'not_exists' | 'eq' | 'neq' | 'version_eq';
|
|
6
|
+
export type TwinActionPrecondition = {
|
|
7
|
+
subject: {
|
|
8
|
+
type: string;
|
|
9
|
+
id: string;
|
|
10
|
+
};
|
|
11
|
+
field: string;
|
|
12
|
+
op: TwinActionPreconditionOp;
|
|
13
|
+
value?: unknown;
|
|
14
|
+
};
|
|
15
|
+
export type TwinActionRevertSpec = {
|
|
16
|
+
strategy: 'inverse' | 'suppress' | 'compensating-action';
|
|
17
|
+
operation?: string;
|
|
18
|
+
fields?: SubjectFields;
|
|
19
|
+
};
|
|
20
|
+
export type ProjectedResource = {
|
|
21
|
+
type: string;
|
|
22
|
+
id: string;
|
|
23
|
+
fields: SubjectFields;
|
|
24
|
+
};
|
|
25
|
+
export type ProjectedResourcePatch = {
|
|
26
|
+
type: string;
|
|
27
|
+
id: string;
|
|
28
|
+
fields: SubjectFields;
|
|
29
|
+
};
|
|
30
|
+
export type ProjectedResourceRef = {
|
|
31
|
+
type: string;
|
|
32
|
+
id: string;
|
|
33
|
+
};
|
|
34
|
+
export type ProjectedDelivery = {
|
|
35
|
+
kind: 'webhook' | 'event' | 'notification';
|
|
36
|
+
target: string;
|
|
37
|
+
payload: Record<string, unknown>;
|
|
38
|
+
};
|
|
39
|
+
export type ActionProjection = {
|
|
40
|
+
creates?: ProjectedResource[];
|
|
41
|
+
updates?: ProjectedResourcePatch[];
|
|
42
|
+
deletes?: ProjectedResourceRef[];
|
|
43
|
+
emits?: ProjectedDelivery[];
|
|
44
|
+
};
|
|
45
|
+
export type TwinAction = {
|
|
46
|
+
id: string;
|
|
47
|
+
service: string;
|
|
48
|
+
op: TwinActionOp;
|
|
49
|
+
subject: {
|
|
50
|
+
type: string;
|
|
51
|
+
id: string;
|
|
52
|
+
};
|
|
53
|
+
occurredAt: string;
|
|
54
|
+
actor?: {
|
|
55
|
+
kind: 'agent' | 'human' | 'bot' | 'system';
|
|
56
|
+
id?: string;
|
|
57
|
+
};
|
|
58
|
+
/** Optional vendor operation name, e.g. `issue.update` or `message.send`. */
|
|
59
|
+
operation?: string;
|
|
60
|
+
/** The caller's own request key, when it asked for at-most-once. Provenance, AND the marker that
|
|
61
|
+
* makes replay identity ignore `occurredAt`: the whole point of a request key is that a retry
|
|
62
|
+
* arriving at a different millisecond is the SAME request, so comparing the two bodies must not
|
|
63
|
+
* fail on the timestamp. Without this, a caller that did not pin `occurredAt` got a thrown
|
|
64
|
+
* "Conflicting duplicate twin action" — a 500 where it had asked for idempotence. */
|
|
65
|
+
idempotencyKey?: string;
|
|
66
|
+
/** Raw operation input (provenance); not projected — `fields`/`projection` carry the state change. */
|
|
67
|
+
input?: Record<string, unknown>;
|
|
68
|
+
/** Preconditions are evaluated against the current projected twin state before append. */
|
|
69
|
+
preconditions?: TwinActionPrecondition[];
|
|
70
|
+
fields?: SubjectFields;
|
|
71
|
+
projection?: ActionProjection;
|
|
72
|
+
revertsActionId?: string;
|
|
73
|
+
/** A pending row a snapshot import planted (contract "A snapshot import never plants a push"):
|
|
74
|
+
* still local state the projection serves, never awaiting push until released on purpose. */
|
|
75
|
+
quarantined?: {
|
|
76
|
+
at: string;
|
|
77
|
+
by: string;
|
|
78
|
+
};
|
|
79
|
+
/** Optional machine-readable hint for how a UI/pack should construct a revert. */
|
|
80
|
+
revert?: TwinActionRevertSpec;
|
|
81
|
+
/**
|
|
82
|
+
* Request-scoped correlation id (D3 — the purpose-3 audit trail: "who reviewed the
|
|
83
|
+
* change that caused this real write"). Always present on an appended action —
|
|
84
|
+
* appendAction/appendActionIfAbsent generate one when the caller doesn't supply it —
|
|
85
|
+
* and threaded through to the push-ledger row(s) a push against this action produces
|
|
86
|
+
* (see pushLedger.ts), so an action row and its push-ledger row(s) join on this id
|
|
87
|
+
* alone, with no dependence on actionId/pushId naming conventions.
|
|
88
|
+
*/
|
|
89
|
+
correlationId?: string;
|
|
90
|
+
};
|
|
91
|
+
export type TwinTransactionCommit = TwinAction;
|
|
92
|
+
export type TwinTransactionCommitOp = TwinActionOp;
|
|
93
|
+
export type TwinTransactionPrecondition = TwinActionPrecondition;
|
|
94
|
+
export type TwinTransactionRevertSpec = TwinActionRevertSpec;
|
|
95
|
+
export declare class TwinActionPreconditionError extends Error {
|
|
96
|
+
readonly actionId: string;
|
|
97
|
+
readonly failed: TwinActionPrecondition;
|
|
98
|
+
constructor(actionId: string, failed: TwinActionPrecondition);
|
|
99
|
+
}
|
|
100
|
+
/** Subjects a `set` action touches: its own subject, every projection resource, and every
|
|
101
|
+
* precondition subject — a precondition established the action's validity against that
|
|
102
|
+
* subject's state, so remote movement there is drift for this action too. */
|
|
103
|
+
export declare function touchedSubjects(action: TwinAction): Array<{
|
|
104
|
+
type: string;
|
|
105
|
+
id: string;
|
|
106
|
+
}>;
|
|
107
|
+
export declare function observeAppends(observer: ((action: TwinAction, root: string | undefined) => void) | undefined): void;
|
|
108
|
+
export declare function runWithCorrelationId<T>(id: string, fn: () => T): T;
|
|
109
|
+
export declare function currentCorrelationId(): string | undefined;
|
|
110
|
+
export declare function appendAction(action: TwinAction, root?: string): TwinAction;
|
|
111
|
+
/** Append `action` only if no exact action with the same id already exists — the whole
|
|
112
|
+
* replay/precondition/append decision runs under the service projection + actions locks, so it's
|
|
113
|
+
* atomic across processes (two concurrent identical writes converge to ONE action; distinct
|
|
114
|
+
* writes both land). A reused id with different content fails loudly. */
|
|
115
|
+
export declare function appendActionIfAbsent(action: TwinAction, root?: string): {
|
|
116
|
+
action: TwinAction;
|
|
117
|
+
appended: boolean;
|
|
118
|
+
placeholder?: true;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Append `build(n)` as an OCCURRENCE: the nth time this exact write has been made.
|
|
122
|
+
*
|
|
123
|
+
* A local vendor write is an occurrence, not a replay — two calls are two actions, even
|
|
124
|
+
* byte-identical in the same instant (docs/contributing/adding-a-twin.md#5-build-on-the-shared-kernel--dont-reinvent,
|
|
125
|
+
* "A local write is an occurrence"). `appendActionIfAbsent` cannot express that: its identity is content, and
|
|
126
|
+
* content provably cannot separate "the same request delivered twice" from "the same change made
|
|
127
|
+
* twice". So a caller wanting at-most-once passes an explicit key and uses that function; a caller
|
|
128
|
+
* recording what actually happened uses this one.
|
|
129
|
+
*
|
|
130
|
+
* The ordinal keeps every EXISTING action id byte-identical: occurrence 0 is `build(0)`'s own id,
|
|
131
|
+
* and only a genuine repeat becomes `<id>#1`, `#2`. It is derived under the same projection +
|
|
132
|
+
* actions locks as the append, so two processes racing take different ordinals rather than
|
|
133
|
+
* colliding, and it is deterministic — replaying one sequence of writes onto a fresh root yields
|
|
134
|
+
* the same ordinals, which is what serve-path determinism requires.
|
|
135
|
+
*/
|
|
136
|
+
export declare function appendActionOccurrence(base: TwinAction, root?: string): {
|
|
137
|
+
action: TwinAction;
|
|
138
|
+
appended: boolean;
|
|
139
|
+
placeholder?: true;
|
|
140
|
+
};
|
|
141
|
+
/** Occurrence 0 IS the base id — so nothing that exists today changes shape. The appender owns
|
|
142
|
+
* this math; a caller passing an already-ordinalled id would stack them (`…#1#1`). */
|
|
143
|
+
export declare function occurrenceId(base: string, ordinal: number): string;
|
|
144
|
+
export type AtomicActionDecision<T> = {
|
|
145
|
+
kind: 'skip';
|
|
146
|
+
value: T;
|
|
147
|
+
} | {
|
|
148
|
+
kind: 'append';
|
|
149
|
+
action: TwinAction;
|
|
150
|
+
value: T;
|
|
151
|
+
/** `'occurrence'` (default) appends every call, with the ordinal and the first occurrence's
|
|
152
|
+
* merge base. `'caller'` is at-most-once on the caller's own identity — what an
|
|
153
|
+
* `idempotencyKey` or `actionId` means. It rides on the DECISION rather than on the
|
|
154
|
+
* function's arguments because only the callback knows which write it chose. */
|
|
155
|
+
identity?: 'occurrence' | 'caller';
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Evaluate current projected state and optionally append one action under ONE service projection
|
|
159
|
+
* transaction (the shared projection lock plus the action-log lock). This is the narrow
|
|
160
|
+
* state-dependent seam for vendor operations whose acceptance and resulting fields depend on the
|
|
161
|
+
* latest observed + local projection (for example, immutable version publish).
|
|
162
|
+
*
|
|
163
|
+
* The callback must remain synchronous and side-effect free: it computes a decision from the
|
|
164
|
+
* supplied snapshot. Durable state changes only through the returned action, which this helper
|
|
165
|
+
* appends before releasing the lock.
|
|
166
|
+
*/
|
|
167
|
+
export declare function decideAndAppendAction<T>(service: string, decide: (resources: TwinResource[]) => AtomicActionDecision<T>, root?: string): {
|
|
168
|
+
value: T;
|
|
169
|
+
action?: TwinAction;
|
|
170
|
+
appended: boolean;
|
|
171
|
+
placeholder?: true;
|
|
172
|
+
};
|
|
173
|
+
export declare function appendTransactionCommit(commit: TwinTransactionCommit, root?: string): TwinTransactionCommit;
|
|
174
|
+
export declare function listActions(service: string, root?: string): TwinAction[];
|
|
175
|
+
/** The kernel's ONE deterministic check: does `actual` satisfy the precondition expression?
|
|
176
|
+
* Shared by write-time preconditions (here), plan conflict detection (plan.ts) and changeset
|
|
177
|
+
* verifiers (changeset.ts) — one evaluator, so a check means the same thing everywhere. */
|
|
178
|
+
export declare function checkPrecondition(precondition: TwinActionPrecondition, actual: unknown): boolean;
|
|
179
|
+
/** Look a precondition subject's field up in projected resources (undefined = no resource or
|
|
180
|
+
* no field — exactly what `exists`/`not_exists` distinguish). */
|
|
181
|
+
export declare function projectedPreconditionValue(precondition: TwinActionPrecondition, resources: TwinResource[]): unknown;
|
|
182
|
+
/**
|
|
183
|
+
* Project the action log over the observed mirror → current twin resources.
|
|
184
|
+
* `set` actions overlay fields (creating subjects that don't exist in the mirror);
|
|
185
|
+
* reverted and confirmed actions are skipped (confirmed facts come from the
|
|
186
|
+
* observed log instead, so they are not projected twice).
|
|
187
|
+
*/
|
|
188
|
+
/** THE TREE (log.ts): what a read sees — the nearest checkpoint plus the entries since; the parent's
|
|
189
|
+
* entries, then this branch's, skipping any the parent already holds. `until` folds only the
|
|
190
|
+
* branch entries BEFORE that id — the state a write saw at its own append (the rebase's
|
|
191
|
+
* evaluation point). */
|
|
192
|
+
export declare function projectResources(service: string, root?: string, opts?: {
|
|
193
|
+
until?: string;
|
|
194
|
+
}): TwinResource[];
|
|
195
|
+
/** The local → vendor id aliases the landed copies carry: a read by the id a caller was handed before
|
|
196
|
+
* its write was performed resolves to the row the vendor now owns. */
|
|
197
|
+
export declare function subjectAliases(service: string, root?: string): Map<string, string>;
|
|
198
|
+
/** Resolve a subject id through the aliases: the vendor's id when a confirm rebound it, else the id itself. */
|
|
199
|
+
export declare function resolveSubjectId(service: string, type: string, id: string, root?: string): string;
|
|
200
|
+
/**
|
|
201
|
+
* Confirm a local action after it was pushed to the real vendor (R18): record the
|
|
202
|
+
* confirmed fields as an OBSERVED event (origin 'external' — it's now real) and
|
|
203
|
+
* append a `confirm` action mapping the local action → that observed event id.
|
|
204
|
+
* Projection then drops the local action (the fact lives in the observed log), so
|
|
205
|
+
* the change is counted exactly once. Returns the observed event id.
|
|
206
|
+
*/
|
|
207
|
+
export declare function confirmAction(opts: {
|
|
208
|
+
service: string;
|
|
209
|
+
actionId: string;
|
|
210
|
+
subject: {
|
|
211
|
+
type: string;
|
|
212
|
+
id: string;
|
|
213
|
+
};
|
|
214
|
+
fields: SubjectFields;
|
|
215
|
+
/** Additional observed resources landed by the same compound action, under the same lock. */
|
|
216
|
+
additionalObservations?: Array<{
|
|
217
|
+
subject: {
|
|
218
|
+
type: string;
|
|
219
|
+
id: string;
|
|
220
|
+
};
|
|
221
|
+
fields: SubjectFields;
|
|
222
|
+
}>;
|
|
223
|
+
/** The subject id the VENDOR minted for this write (the push outcome's externalId). When it
|
|
224
|
+
* differs from the local id, the landed copy carries the vendor's id and `aliasOf` names the
|
|
225
|
+
* local one, so a read by either id finds the row. */
|
|
226
|
+
vendorSubjectId?: string;
|
|
227
|
+
/** the receipt on the landed copy; `deployed` when omitted (the push performed it) */
|
|
228
|
+
receipt?: Partial<Receipt>;
|
|
229
|
+
occurredAt: string;
|
|
230
|
+
root?: string;
|
|
231
|
+
}): {
|
|
232
|
+
observedEventId: string;
|
|
233
|
+
observedEventIds: string[];
|
|
234
|
+
};
|
|
235
|
+
export type RevertOutcome = {
|
|
236
|
+
status: 'reverted';
|
|
237
|
+
revertId: string;
|
|
238
|
+
} | {
|
|
239
|
+
status: 'already-reverted';
|
|
240
|
+
} | {
|
|
241
|
+
status: 'confirmed';
|
|
242
|
+
} | {
|
|
243
|
+
status: 'not-found';
|
|
244
|
+
} | {
|
|
245
|
+
status: 'not-revertable';
|
|
246
|
+
op: TwinActionOp;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* Revert one pending `set` action — decision AND append under the service projection +
|
|
250
|
+
* actions locks, so a concurrent writer (a connector confirming the same action from
|
|
251
|
+
* another process) cannot land between the check and the revert: the whole read-decide-
|
|
252
|
+
* append is ONE critical section. Idempotent: a repeat answers 'already-reverted'.
|
|
253
|
+
* Only `set` rows are revertable — reverting a revert or a confirm is a category error.
|
|
254
|
+
*/
|
|
255
|
+
export declare function revertAction(opts: {
|
|
256
|
+
service: string;
|
|
257
|
+
actionId: string;
|
|
258
|
+
occurredAt: string;
|
|
259
|
+
root?: string;
|
|
260
|
+
}): RevertOutcome;
|
|
261
|
+
/** The LOCAL OVERLAY: pending actions (set, not reverted, not yet confirmed) — the divergence
|
|
262
|
+
* from the mirror, what a twin's projection folds over the observed state. A QUARANTINED row
|
|
263
|
+
* (contract "A snapshot import never plants a push") is still local state and stays here; it
|
|
264
|
+
* leaves only the PUSHABLE suffix below. */
|
|
265
|
+
export declare function pendingActions(service: string, root?: string): TwinAction[];
|
|
266
|
+
/** A twin's OWN bookkeeping in its action log — a subject type beginning with `_` (stripe's
|
|
267
|
+
* `_idempotency` store, for one): part of the overlay the twin serves from, never a change the
|
|
268
|
+
* app made. It is not in the log a user reads, not in a diff, not in a changeset, not pushed. */
|
|
269
|
+
export declare function isTwinBookkeeping(action: Pick<TwinAction, 'subject'>): boolean;
|
|
270
|
+
/** The PUSHABLE suffix — `log origin..HEAD` as the push arm, the pending door, plans and the
|
|
271
|
+
* unpushed count read it: the overlay minus quarantined rows (an import is a copy, not a
|
|
272
|
+
* decision; releasing a quarantined row is an explicit act, never a scheduler's) and minus the
|
|
273
|
+
* twin's own bookkeeping. */
|
|
274
|
+
export declare function pushablePendingActions(service: string, root?: string): TwinAction[];
|
|
275
|
+
export declare const listTransactionCommits: typeof listActions;
|
|
276
|
+
export declare const pendingTransactionCommits: typeof pendingActions;
|