@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,147 @@
|
|
|
1
|
+
// THE ONE HTTP ADAPTATION (runtime contract R12b): most packs' serve path is the same
|
|
2
|
+
// ~20 lines of glue — the keyless GET /twin manifest door, the body read, a header map,
|
|
3
|
+
// the dispatch into `handle<Vendor>TwinRequest` with the WORLD instant, and a JSON
|
|
4
|
+
// response. That glue is a MECHANISM, so it lives here once; a pack's differences are
|
|
5
|
+
// VALUES (its manifest, its header threading, its static handler options, its extra
|
|
6
|
+
// response headers). A pack whose wire genuinely exceeds this shape (SSE streaming,
|
|
7
|
+
// multipart adaptation, raw-bytes responses) writes its own fetch instead — openai is
|
|
8
|
+
// that reference; this adapter is the reference for everyone else.
|
|
9
|
+
//
|
|
10
|
+
// Workerd-clean by construction: no Bun APIs, no fs, no clock but worldNow() — the same
|
|
11
|
+
// closure serves under Bun.serve locally and mounted in-process on Cloudflare.
|
|
12
|
+
import { runWithCorrelationId } from './actions.ts';
|
|
13
|
+
import { worldNow } from './world-clock.ts';
|
|
14
|
+
|
|
15
|
+
/** The header a host sets when it mounts a twin under a path (a served World's wire:
|
|
16
|
+
* `/<org>/<world>/<vendor>`), the reverse-proxy convention; with it the host sets
|
|
17
|
+
* `x-forwarded-host` and `x-forwarded-proto` to where the World is reached. */
|
|
18
|
+
export const TWIN_PREFIX_HEADER = 'x-forwarded-prefix';
|
|
19
|
+
|
|
20
|
+
/** Where this twin is reached for this request: the request's origin plus the path a host mounted
|
|
21
|
+
* the twin under. A pack mints every live URL it hands back (an upload target, a file link, a
|
|
22
|
+
* callback) from this, never from the bare origin, so the URL works behind a served World. */
|
|
23
|
+
export function twinPublicBase(request: Request): string {
|
|
24
|
+
const prefix = request.headers.get(TWIN_PREFIX_HEADER);
|
|
25
|
+
if (!prefix || !/^\/[A-Za-z0-9._~\/-]*$/.test(prefix)) return new URL(request.url).origin;
|
|
26
|
+
// the host that mounted the twin says where it is reached: a local World forwards to the twin's
|
|
27
|
+
// own port, so the request's own origin is not the public one
|
|
28
|
+
const host = request.headers.get('x-forwarded-host');
|
|
29
|
+
const proto = request.headers.get('x-forwarded-proto');
|
|
30
|
+
const origin = host && /^[A-Za-z0-9.-]+(:\d+)?$/.test(host) && (proto === 'http' || proto === 'https') ? `${proto}://${host}` : new URL(request.url).origin;
|
|
31
|
+
return `${origin}${prefix.replace(/\/+$/, '')}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A twin's BYTE-STREAM door: a pack whose clients speak a TCP protocol (SMTP, the MySQL wire) serves
|
|
35
|
+
* it over whatever carries the bytes — a local listener's socket, or a WebSocket to a hosted World.
|
|
36
|
+
* The pack owns framing and protocol; the host owns the transport. `open` greets through the sink;
|
|
37
|
+
* `data` hands the connection bytes in arrival order; `settled` resolves when every byte handed so
|
|
38
|
+
* far has been answered; `close` is the peer going away. */
|
|
39
|
+
export type TwinStreamSink = { write(bytes: Uint8Array): void; end(): void };
|
|
40
|
+
export type TwinStreamConnection = { data(chunk: Uint8Array): void; settled(): Promise<void>; close(): void };
|
|
41
|
+
export type TwinStream = (sink: TwinStreamSink, peer: string) => TwinStreamConnection;
|
|
42
|
+
|
|
43
|
+
export type TwinFetchHandlerResult = {
|
|
44
|
+
status: number;
|
|
45
|
+
body: unknown;
|
|
46
|
+
headers?: Record<string, string>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** The common handler request shape. Extra vendor fields ride `handlerOptions` (static,
|
|
50
|
+
* per instance) and `extras` (derived per request); handlers ignore keys they don't
|
|
51
|
+
* declare, so the union is safe to pass wholesale. */
|
|
52
|
+
export type TwinFetchHandlerRequest = {
|
|
53
|
+
method: string;
|
|
54
|
+
path: string;
|
|
55
|
+
body: string;
|
|
56
|
+
headers: Record<string, string>;
|
|
57
|
+
readOnly: boolean;
|
|
58
|
+
occurredAt: string;
|
|
59
|
+
root?: string;
|
|
60
|
+
} & Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
export interface TwinFetchAdapterConfig {
|
|
63
|
+
root?: string;
|
|
64
|
+
readOnly?: boolean;
|
|
65
|
+
/** The keyless GET /twin body — a value (stateful packs: statefulTwinManifest(...)) or
|
|
66
|
+
* a thunk (generative packs build theirs around the scenario engine). */
|
|
67
|
+
manifest: unknown | (() => unknown);
|
|
68
|
+
/** GET /twin/scenario body (thunk) — scenario-carrying packs only. */
|
|
69
|
+
scenarioStatus?: () => unknown;
|
|
70
|
+
/** THE STORE DOOR (R5c, mirror purity R3): named, deterministic projections over stored
|
|
71
|
+
* state, served at `GET /twin/store/<name>` and listed in the manifest as `stores`. A
|
|
72
|
+
* mirror reads twin state ONLY through this door — never by importing the handler or a
|
|
73
|
+
* `-twin-internal` module — so the vendor API lacking a listing endpoint (resend has no
|
|
74
|
+
* list-emails) no longer breeds a bespoke `/_twin/*` route per mirror. Keyed in the skin
|
|
75
|
+
* like every state read (only bare `GET /twin` is keyless). */
|
|
76
|
+
stores?: Record<string, () => unknown | Promise<unknown>>;
|
|
77
|
+
/** Per-request vendor extras (header threading: stripe-version, notion-version, …). */
|
|
78
|
+
extras?: (request: Request, url: URL) => Record<string, unknown>;
|
|
79
|
+
/** Static per-instance handler options (e.g. rateLimitPerSecond). */
|
|
80
|
+
handlerOptions?: Record<string, unknown>;
|
|
81
|
+
/** Extra response headers on every non-door reply (e.g. stripe's request-id, and
|
|
82
|
+
* postmark's `connection: close` Bun-socket workaround — inert under workerd). */
|
|
83
|
+
responseHeaders?: Record<string, string>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createTwinFetchFromHandler(
|
|
87
|
+
handler: (request: TwinFetchHandlerRequest) => TwinFetchHandlerResult | Promise<TwinFetchHandlerResult>,
|
|
88
|
+
config: TwinFetchAdapterConfig,
|
|
89
|
+
): (request: Request) => Promise<Response> {
|
|
90
|
+
const readOnly = config.readOnly ?? false;
|
|
91
|
+
return async function twinFetch(request: Request): Promise<Response> {
|
|
92
|
+
const url = new URL(request.url);
|
|
93
|
+
const cleanPath = url.pathname.replace(/\/+$/, '') || '/';
|
|
94
|
+
if (request.method === 'GET' && cleanPath === '/twin') {
|
|
95
|
+
const manifest = typeof config.manifest === 'function' ? (config.manifest as () => unknown)() : config.manifest;
|
|
96
|
+
const storeNames = Object.keys(config.stores ?? {}).sort();
|
|
97
|
+
if (storeNames.length > 0 && manifest !== null && typeof manifest === 'object') {
|
|
98
|
+
const m = manifest as Record<string, unknown>;
|
|
99
|
+
const doors = m.doors !== null && typeof m.doors === 'object' ? (m.doors as Record<string, unknown>) : {};
|
|
100
|
+
// The manifest EDUCATES: it lists the store names and names the door's path.
|
|
101
|
+
return Response.json({ ...m, stores: storeNames, doors: { ...doors, store: 'GET /twin/store/<name>' } });
|
|
102
|
+
}
|
|
103
|
+
return Response.json(manifest);
|
|
104
|
+
}
|
|
105
|
+
if (request.method === 'GET' && cleanPath === '/twin/scenario' && config.scenarioStatus !== undefined) {
|
|
106
|
+
return Response.json(config.scenarioStatus());
|
|
107
|
+
}
|
|
108
|
+
if (request.method === 'GET' && cleanPath.startsWith('/twin/store/')) {
|
|
109
|
+
const name = cleanPath.slice('/twin/store/'.length);
|
|
110
|
+
const store = config.stores?.[name];
|
|
111
|
+
if (store === undefined) return Response.json({ error: 'unknown store', store: name, stores: Object.keys(config.stores ?? {}).sort() }, { status: 404 });
|
|
112
|
+
return Response.json(await store()); // a projection may read through the handler (async) — the door awaits it
|
|
113
|
+
}
|
|
114
|
+
const body = request.method === 'GET' || request.method === 'HEAD' ? '' : await request.text();
|
|
115
|
+
const headers: Record<string, string> = {};
|
|
116
|
+
request.headers.forEach((value, key) => { headers[key] = value; });
|
|
117
|
+
// FIXED FIELDS WIN (review B1): handlerOptions/extras spread FIRST, so no config —
|
|
118
|
+
// and via extras, no WIRE INPUT — can override method/path/body/readOnly or the
|
|
119
|
+
// world instant. A client-settable occurredAt would be a direct R9 hole.
|
|
120
|
+
// D3 — the wire's request id becomes the correlation of every action this handler appends.
|
|
121
|
+
const requestId = request.headers.get('x-twins-request-id') ?? undefined;
|
|
122
|
+
const invoke = () => handler({
|
|
123
|
+
...config.handlerOptions,
|
|
124
|
+
...config.extras?.(request, url),
|
|
125
|
+
method: request.method,
|
|
126
|
+
path: url.pathname + (url.search || ''),
|
|
127
|
+
body,
|
|
128
|
+
headers,
|
|
129
|
+
readOnly,
|
|
130
|
+
occurredAt: worldNow(),
|
|
131
|
+
...(config.root !== undefined ? { root: config.root } : {}),
|
|
132
|
+
});
|
|
133
|
+
const result = await (requestId ? runWithCorrelationId(requestId, invoke) : invoke());
|
|
134
|
+
// A null/undefined body is an EMPTY reply (`JSON.stringify(null)` would serve the
|
|
135
|
+
// four bytes "null") — and so is the estate's own 204 idiom, `body: ''` with a
|
|
136
|
+
// null-body status: workerd THROWS on any body with 204/205/304 (review B2), where
|
|
137
|
+
// Bun silently serves '""' — either way a wire break on exactly one lane.
|
|
138
|
+
const nullBodyStatus = result.status === 204 || result.status === 205 || result.status === 304;
|
|
139
|
+
if (result.body === null || result.body === undefined || (nullBodyStatus && result.body === '')) {
|
|
140
|
+
return new Response(null, { status: result.status, headers: { ...config.responseHeaders, ...result.headers } });
|
|
141
|
+
}
|
|
142
|
+
return new Response(JSON.stringify(result.body), {
|
|
143
|
+
status: result.status,
|
|
144
|
+
headers: { 'content-type': 'application/json', ...config.responseHeaders, ...result.headers },
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type {
|
|
3
|
+
GenericWorldStateSchema,
|
|
4
|
+
WorldServiceEventSchema,
|
|
5
|
+
} from './schemas.ts';
|
|
6
|
+
|
|
7
|
+
export type WorldServiceEvent = z.infer<typeof WorldServiceEventSchema>;
|
|
8
|
+
export type GenericWorldState = z.infer<typeof GenericWorldStateSchema>;
|
|
9
|
+
|
|
10
|
+
export type AppendEventResult = {
|
|
11
|
+
event: WorldServiceEvent;
|
|
12
|
+
appended: boolean;
|
|
13
|
+
duplicateOf?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type QueuedWorldServiceEvent = {
|
|
17
|
+
id: string;
|
|
18
|
+
service: string;
|
|
19
|
+
receivedAt: string;
|
|
20
|
+
source: 'listener' | 'webhook' | 'broker' | 'capture' | 'manual' | 'replay';
|
|
21
|
+
event: WorldServiceEvent;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type EnqueueEventResult = {
|
|
25
|
+
queued: QueuedWorldServiceEvent;
|
|
26
|
+
appended: boolean;
|
|
27
|
+
duplicateOf?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CommitQueuedEventsResult = {
|
|
31
|
+
service: string;
|
|
32
|
+
queued: number;
|
|
33
|
+
committed: number;
|
|
34
|
+
skipped: number;
|
|
35
|
+
eventIds: string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type WorldPaths = {
|
|
39
|
+
root: string;
|
|
40
|
+
service: string;
|
|
41
|
+
dir: string;
|
|
42
|
+
events: string;
|
|
43
|
+
state: string;
|
|
44
|
+
resources: string;
|
|
45
|
+
cursors: string;
|
|
46
|
+
ingests: string;
|
|
47
|
+
eventQueue: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type WorldReducer<State> = (state: State, event: WorldServiceEvent) => State;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// v1 LEFT THE KERNEL (company contract "Just like Neon", 5 — ruled 2026-09-06). These names are what
|
|
2
|
+
// a protocol 1 pack still imports: the push ledger, the shadow refs and bases, observed deltas as a
|
|
3
|
+
// row kind, egress intents, plans, leases, reconcile, remote refs, the queue lifecycle, the v1 status,
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
|
+
// validation and inspector, the poll runner, the v1 apply. Each throws on its first call. A pack
|
|
6
|
+
// that reaches one is out of date; the sweep moves it (protocol 2: the head, the fold, the tree).
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
// Reaching a tombstone is RECORDED, process-wide (one list whichever copy of the kernel the caller
|
|
9
|
+
// resolved), so a verify() that swallows the throw and answers false is still known to have reached
|
|
10
|
+
// v1 machinery: the capability runner reads this list around each verify and says "out of date".
|
|
11
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
12
|
+
|
|
13
|
+
const REACHED = Symbol.for('volter.v1.reached');
|
|
14
|
+
export function v1Reached(): string[] { return ((globalThis as Record<symbol, unknown>)[REACHED] ??= []) as string[]; }
|
|
15
|
+
// …and per SCOPE, so concurrent verifies attribute a reach to the one that made it (the runner opens a
|
|
16
|
+
// scope around each verify); one storage per process, whichever copy of the kernel the caller resolved
|
|
17
|
+
const SCOPE = Symbol.for('volter.v1.reached.scope');
|
|
18
|
+
// created on first use, never at import (a browser bundle has no AsyncLocalStorage; see observe.ts)
|
|
19
|
+
const scope = (): AsyncLocalStorage<string[]> => ((globalThis as Record<symbol, unknown>)[SCOPE] ??= new AsyncLocalStorage<string[]>()) as AsyncLocalStorage<string[]>;
|
|
20
|
+
/** Run `fn` and learn which v1 names it reached (directly or through anything it awaited). */
|
|
21
|
+
export async function v1ReachedIn<T>(fn: () => Promise<T>): Promise<{ value: T; reached: string[] }> {
|
|
22
|
+
const reached: string[] = [];
|
|
23
|
+
const value = await scope().run(reached, fn);
|
|
24
|
+
return { value, reached };
|
|
25
|
+
}
|
|
26
|
+
const gone = (name: string): any => { v1Reached().push(name); if (typeof AsyncLocalStorage === 'function') scope().getStore()?.push(name); throw new Error(`v1 removed: ${name} — protocol 2 replaces it (the head performs, the kernel folds observations, the tree is read); this pack is out of date until the sweep moves it`); };
|
|
27
|
+
|
|
28
|
+
export function appendEventLocked(..._args: any[]): any { return gone('appendEventLocked'); }
|
|
29
|
+
export function eventsLockPath(..._args: any[]): any { return gone('eventsLockPath'); }
|
|
30
|
+
export function appendPushRecord(..._args: any[]): any { return gone('appendPushRecord'); }
|
|
31
|
+
export function listPushLedger(..._args: any[]): any { return gone('listPushLedger'); }
|
|
32
|
+
export function latestPushByActionId(..._args: any[]): any { return gone('latestPushByActionId'); }
|
|
33
|
+
export function unconfirmedPushes(..._args: any[]): any { return gone('unconfirmedPushes'); }
|
|
34
|
+
export function assertFastForward(..._args: any[]): any { return gone('assertFastForward'); }
|
|
35
|
+
export function pushTransaction(..._args: any[]): any { return gone('pushTransaction'); }
|
|
36
|
+
export function abandonPush(..._args: any[]): any { return gone('abandonPush'); }
|
|
37
|
+
export function syncPull(..._args: any[]): any { return gone('syncPull'); }
|
|
38
|
+
export function syncPush(..._args: any[]): any { return gone('syncPush'); }
|
|
39
|
+
export function currentResources(..._args: any[]): any { return gone('currentResources'); }
|
|
40
|
+
export function isEgressEventType(..._args: any[]): any { return gone('isEgressEventType'); }
|
|
41
|
+
export function recordWriteIntent(..._args: any[]): any { return gone('recordWriteIntent'); }
|
|
42
|
+
export function performExternalWrite(..._args: any[]): any { return gone('performExternalWrite'); }
|
|
43
|
+
export function listUnreconciledWriteIntents(..._args: any[]): any { return gone('listUnreconciledWriteIntents'); }
|
|
44
|
+
export function listEgressLedger(..._args: any[]): any { return gone('listEgressLedger'); }
|
|
45
|
+
export function findWriteResultByExternal(..._args: any[]): any { return gone('findWriteResultByExternal'); }
|
|
46
|
+
export function transactionSetId(..._args: any[]): any { return gone('transactionSetId'); }
|
|
47
|
+
export function listPlanReviews(..._args: any[]): any { return gone('listPlanReviews'); }
|
|
48
|
+
export function latestPlanReview(..._args: any[]): any { return gone('latestPlanReview'); }
|
|
49
|
+
export function buildLocalActionPlan(..._args: any[]): any { return gone('buildLocalActionPlan'); }
|
|
50
|
+
export function recordPlanReview(..._args: any[]): any { return gone('recordPlanReview'); }
|
|
51
|
+
export function approvedPendingActionSet(..._args: any[]): any { return gone('approvedPendingActionSet'); }
|
|
52
|
+
export function assertPendingActionsApproved(..._args: any[]): any { return gone('assertPendingActionsApproved'); }
|
|
53
|
+
export function pendingConflicts(..._args: any[]): any { return gone('pendingConflicts'); }
|
|
54
|
+
export function buildApplyPlan(..._args: any[]): any { return gone('buildApplyPlan'); }
|
|
55
|
+
export function writePlan(..._args: any[]): any { return gone('writePlan'); }
|
|
56
|
+
export function readPlan(..._args: any[]): any { return gone('readPlan'); }
|
|
57
|
+
export function listPlans(..._args: any[]): any { return gone('listPlans'); }
|
|
58
|
+
export function planRequiresApproval(..._args: any[]): any { return gone('planRequiresApproval'); }
|
|
59
|
+
export function applyPlan(..._args: any[]): any { return gone('applyPlan'); }
|
|
60
|
+
export function listLeases(..._args: any[]): any { return gone('listLeases'); }
|
|
61
|
+
export function isLeaseActive(..._args: any[]): any { return gone('isLeaseActive'); }
|
|
62
|
+
export function activeLease(..._args: any[]): any { return gone('activeLease'); }
|
|
63
|
+
export function acquireLease(..._args: any[]): any { return gone('acquireLease'); }
|
|
64
|
+
export function releaseLease(..._args: any[]): any { return gone('releaseLease'); }
|
|
65
|
+
export function reconcile(..._args: any[]): any { return gone('reconcile'); }
|
|
66
|
+
export function isCleanlyReconcilable(..._args: any[]): any { return gone('isCleanlyReconcilable'); }
|
|
67
|
+
export function reconcileRequiresApproval(..._args: any[]): any { return gone('reconcileRequiresApproval'); }
|
|
68
|
+
export function writeRemoteRef(..._args: any[]): any { return gone('writeRemoteRef'); }
|
|
69
|
+
export function readRemoteRef(..._args: any[]): any { return gone('readRemoteRef'); }
|
|
70
|
+
export function listRemoteRefs(..._args: any[]): any { return gone('listRemoteRefs'); }
|
|
71
|
+
export function writeLocalRef(..._args: any[]): any { return gone('writeLocalRef'); }
|
|
72
|
+
export function readLocalRef(..._args: any[]): any { return gone('readLocalRef'); }
|
|
73
|
+
export function isBaseStale(..._args: any[]): any { return gone('isBaseStale'); }
|
|
74
|
+
export function setQueueRowStatus(..._args: any[]): any { return gone('setQueueRowStatus'); }
|
|
75
|
+
export function queueRowStatus(..._args: any[]): any { return gone('queueRowStatus'); }
|
|
76
|
+
export function listQueueWithStatus(..._args: any[]): any { return gone('listQueueWithStatus'); }
|
|
77
|
+
export function pendingQueueRows(..._args: any[]): any { return gone('pendingQueueRows'); }
|
|
78
|
+
export function queueCounts(..._args: any[]): any { return gone('queueCounts'); }
|
|
79
|
+
export function commitQueueRow(..._args: any[]): any { return gone('commitQueueRow'); }
|
|
80
|
+
export function ignoreQueueRow(..._args: any[]): any { return gone('ignoreQueueRow'); }
|
|
81
|
+
export function supersedeQueueRow(..._args: any[]): any { return gone('supersedeQueueRow'); }
|
|
82
|
+
export function poisonQueueRow(..._args: any[]): any { return gone('poisonQueueRow'); }
|
|
83
|
+
export function commitPendingQueue(..._args: any[]): any { return gone('commitPendingQueue'); }
|
|
84
|
+
export function worldStatus(..._args: any[]): any { return gone('worldStatus'); }
|
|
85
|
+
export function formatStatus(..._args: any[]): any { return gone('formatStatus'); }
|
|
86
|
+
export function discoverWorldServices(..._args: any[]): any { return gone('discoverWorldServices'); }
|
|
87
|
+
export function validateWorldService(..._args: any[]): any { return gone('validateWorldService'); }
|
|
88
|
+
export function summarizeWorldFindings(..._args: any[]): any { return gone('summarizeWorldFindings'); }
|
|
89
|
+
export function validateWorld(..._args: any[]): any { return gone('validateWorld'); }
|
|
90
|
+
export function renderTwinHtml(..._args: any[]): any { return gone('renderTwinHtml'); }
|
|
91
|
+
export function createVisualizerServer(..._args: any[]): any { return gone('createVisualizerServer'); }
|
|
92
|
+
export function pushPending(..._args: any[]): any { return gone('pushPending'); }
|
|
93
|
+
export function pushOperationOf(..._args: any[]): any { return gone('pushOperationOf'); }
|
|
94
|
+
export function pushIdempotencyKeyOf(..._args: any[]): any { return gone('pushIdempotencyKeyOf'); }
|
|
95
|
+
export function remoteRefs(..._args: any[]): any { return gone('remoteRefs'); }
|
|
96
|
+
export function buildShadowState(..._args: any[]): any { return gone('buildShadowState'); }
|
|
97
|
+
export function recordObservedDelta(..._args: any[]): any { return gone('recordObservedDelta'); }
|
|
98
|
+
export function recordObservedDeltaLocked(..._args: any[]): any { return gone('recordObservedDeltaLocked'); }
|
|
99
|
+
export function pollCursorPath(..._args: any[]): any { return gone('pollCursorPath'); }
|
|
100
|
+
export function loadPollCursor(..._args: any[]): any { return gone('loadPollCursor'); }
|
|
101
|
+
export function savePollCursor(..._args: any[]): any { return gone('savePollCursor'); }
|
|
102
|
+
export function runConnectorPoll(..._args: any[]): any { return gone('runConnectorPoll'); }
|
|
103
|
+
export function runConnectorSweep(..._args: any[]): any { return gone('runConnectorSweep'); }
|
|
104
|
+
export function applyChangeset(..._args: any[]): any { return gone('applyChangeset'); }
|
|
105
|
+
export function forkDivergence(..._args: any[]): any { return gone('forkDivergence'); }
|
|
106
|
+
export function auditForkNoRealWrites(..._args: any[]): any { return gone('auditForkNoRealWrites'); }
|
|
107
|
+
export function discardFork(..._args: any[]): any { return gone('discardFork'); }
|
|
108
|
+
export function cherryPickActions(..._args: any[]): any { return gone('cherryPickActions'); }
|
|
109
|
+
export function mergeForks(..._args: any[]): any { return gone('mergeForks'); }
|
|
110
|
+
export function resetFork(..._args: any[]): any { return gone('resetFork'); }
|
|
111
|
+
export function rebaseFork(..._args: any[]): any { return gone('rebaseFork'); }
|
|
112
|
+
export const WRITE_INTENT_SUFFIX = '';
|
|
113
|
+
export const WRITE_RESULT_SUFFIX = '';
|
|
114
|
+
export class UnreconciledPushError extends Error { constructor(message = 'v1 removed: UnreconciledPushError') { super(message); this.name = 'UnreconciledPushError'; } }
|
|
115
|
+
export class NonFastForwardPushError extends Error { constructor(message = 'v1 removed: NonFastForwardPushError') { super(message); this.name = 'NonFastForwardPushError'; } }
|
|
116
|
+
export class UnreconciledWriteIntentError extends Error { constructor(message = 'v1 removed: UnreconciledWriteIntentError') { super(message); this.name = 'UnreconciledWriteIntentError'; } }
|
|
117
|
+
export class LeaseHeldError extends Error { constructor(message = 'v1 removed: LeaseHeldError') { super(message); this.name = 'LeaseHeldError'; } }
|
|
118
|
+
export type PushStatus = any;
|
|
119
|
+
export type WorldPushRecord = any;
|
|
120
|
+
export type SyncResource = any;
|
|
121
|
+
export type PullResult = any;
|
|
122
|
+
export type PushItemResult = any;
|
|
123
|
+
export type PushResult = any;
|
|
124
|
+
export type EgressActor = any;
|
|
125
|
+
export type EgressWriteRequest = any;
|
|
126
|
+
export type EgressWriteOutcome = any;
|
|
127
|
+
export type EgressWriteResult = any;
|
|
128
|
+
export type EgressLedgerEntry = any;
|
|
129
|
+
export type ProviderCall = any;
|
|
130
|
+
export type WorldApplyPlan = any;
|
|
131
|
+
export type PlanReviewDecision = any;
|
|
132
|
+
export type PlanReviewRecord = any;
|
|
133
|
+
export type LocalActionPlan = any;
|
|
134
|
+
export type ApprovedPendingActionSet = any;
|
|
135
|
+
export type ActionMapper = any;
|
|
136
|
+
export type ApplyResult = any;
|
|
137
|
+
export type WorldApplyLease = any;
|
|
138
|
+
export type ReconcilePolicy = any;
|
|
139
|
+
export type FieldDecision = any;
|
|
140
|
+
export type SubjectReconcile = any;
|
|
141
|
+
export type ReconcilePlan = any;
|
|
142
|
+
export type WorldRemoteRef = any;
|
|
143
|
+
export type WorldLocalRef = any;
|
|
144
|
+
export type QueueRowStatus = any;
|
|
145
|
+
export type QueueStatusTransition = any;
|
|
146
|
+
export type QueueRowWithStatus = any;
|
|
147
|
+
export type QueueCounts = any;
|
|
148
|
+
export type WorldStatus = any;
|
|
149
|
+
export type WorldValidationFinding = any;
|
|
150
|
+
export type WorldValidationSummary = any;
|
|
151
|
+
export type WorldValidationReport = any;
|
|
152
|
+
export type PushReport = any;
|
|
153
|
+
export type RemoteRefsMode = any;
|
|
154
|
+
export type ShadowState = any;
|
|
155
|
+
export type SubjectFieldExtractor = any;
|
|
156
|
+
export type DeltaObservation = any;
|
|
157
|
+
export type DeltaResult = any;
|
|
158
|
+
export type ConnectorObservation = any;
|
|
159
|
+
export type WorldConnector = any;
|
|
160
|
+
export type ConnectorPollResult = any;
|
|
161
|
+
export type SweepConnector = any;
|
|
162
|
+
export type ConnectorSweepResult = any;
|
|
163
|
+
export type CopyResult = any;
|
|
164
|
+
export type ForkAudit = any;
|
|
165
|
+
export type ForkDivergence = any;
|
|
166
|
+
export type SubjectDivergence = any;
|
|
167
|
+
export type FieldDivergence = any;
|
|
168
|
+
export type ApplyTarget = any;
|
|
169
|
+
export type ApplyChangesetOutcome = any;
|
|
170
|
+
export type TwinProxy = any;
|
|
171
|
+
export type TwinProxyOptions = any;
|
|
172
|
+
export type VendorRoute = any;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** The directory host-owned state lives under — rate-budget ledgers, the branch-ancestry lock and
|
|
5
|
+
* removals, the shared blob index, World lifecycle records: `VOLTER_HOME`, else `~/.volter`. A
|
|
6
|
+
* verification run points it at a directory of its own (scripts/isolated-home.ts), so a run
|
|
7
|
+
* spends no other actor's allowance and deletes none of their records. Read on every call. */
|
|
8
|
+
export function volterHome(): string {
|
|
9
|
+
const set = process.env.VOLTER_HOME;
|
|
10
|
+
return set !== undefined && set !== '' ? set : join(homedir(), '.volter');
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// THE WORLD CLOCK — the one piece of physics every twin consults (TWIN-PROGRAMMING-MODEL P3).
|
|
2
|
+
// Time is a GLOBAL fact requiring simultaneous agreement: TTL/expiry logic reads it at serve
|
|
3
|
+
// time, so no seed-side workaround exists. The clock is a FILE, not a service — every twin
|
|
4
|
+
// reads it per request (a tiny local read; the serve-path determinism tooth bans network
|
|
5
|
+
// egress, not filesystem reads), the world runtime owns writing it, and `volter-world clock`
|
|
6
|
+
// is the operator door (set / advance / show). No file, or no env → real wall-clock time,
|
|
7
|
+
// exactly the pre-clock behavior; a world only has scripted time when its operator says so.
|
|
8
|
+
//
|
|
9
|
+
// File format: a single ISO-8601 line (a FROZEN instant). Every read returns exactly that
|
|
10
|
+
// instant — deterministic by construction. Advancing time is an explicit operator act, never
|
|
11
|
+
// a background drift.
|
|
12
|
+
// Read through the ACTIVE WorldStore, not node:fs: the clock file is world state, and a
|
|
13
|
+
// hydrated serverless namespace must carry its frozen instant with it (runtime contract R11–R14
|
|
14
|
+
// — a MemoryWorldStore world that read the clock off the host filesystem would silently
|
|
15
|
+
// serve the HOST's time, the exact two-services-disagreeing-about-now defect below).
|
|
16
|
+
import { getActiveWorldStore } from './world-store.ts';
|
|
17
|
+
|
|
18
|
+
export const WORLD_CLOCK_ENV = 'TWIN_WORLD_CLOCK_FILE';
|
|
19
|
+
|
|
20
|
+
/** The world's "now": the frozen instant in the clock file when one is configured and
|
|
21
|
+
* parseable; real wall-clock time otherwise. */
|
|
22
|
+
export function worldNow(): string {
|
|
23
|
+
const file = process.env[WORLD_CLOCK_ENV];
|
|
24
|
+
if (file && getActiveWorldStore().exists(file)) {
|
|
25
|
+
const raw = (getActiveWorldStore().read(file) ?? '').trim();
|
|
26
|
+
const parsed = Date.parse(raw);
|
|
27
|
+
if (!Number.isNaN(parsed)) return new Date(parsed).toISOString();
|
|
28
|
+
// A configured-but-corrupt clock is a WORLD defect — fail loudly, never silently drift
|
|
29
|
+
// back to wall-clock (two services disagreeing about now breaks the world).
|
|
30
|
+
throw new Error(`world clock: ${file} does not contain a parseable ISO-8601 instant (got ${JSON.stringify(raw.slice(0, 40))})`);
|
|
31
|
+
}
|
|
32
|
+
return new Date().toISOString();
|
|
33
|
+
}
|
package/src/world-env.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A WORLD'S OWN ENV VALUES, as a twin reads them. A World hands its env to every twin it runs (a spawned process, a
|
|
2
|
+
// colocated host, a worker) on top of the caller's own, which only `runtime.environment.strip` keeps out. The runtime
|
|
3
|
+
// names the variables the World itself sets in WORLD_ENV_NAMES_ENV; a twin that must agree with the app (a signing
|
|
4
|
+
// secret the app verifies with) reads only those, so a developer's own exported secret never becomes World state.
|
|
5
|
+
// Read per call: an import reads nothing, and a runtime with no `process` (a browser tab) has no World env.
|
|
6
|
+
|
|
7
|
+
export const WORLD_ENV_NAMES_ENV = 'VOLTER_WORLD_ENV_NAMES';
|
|
8
|
+
|
|
9
|
+
/** The value the World set for `name`; undefined when the World sets none (whatever the caller's shell holds). */
|
|
10
|
+
export function worldEnvValue(name: string): string | undefined {
|
|
11
|
+
const env = typeof process === 'undefined' ? undefined : process.env;
|
|
12
|
+
const raw = env?.[WORLD_ENV_NAMES_ENV];
|
|
13
|
+
if (!raw) return undefined;
|
|
14
|
+
let names: unknown;
|
|
15
|
+
try { names = JSON.parse(raw); } catch { return undefined; }
|
|
16
|
+
const value = Array.isArray(names) && names.includes(name) ? env![name] : undefined;
|
|
17
|
+
return value ? value : undefined;
|
|
18
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// SQL WORLD STORE — the WorldStore primitives over a SYNCHRONOUS SQLite handle.
|
|
2
|
+
//
|
|
3
|
+
// A Cloudflare Durable Object's `ctx.storage.sql` is synchronous, so the kernel's synchronous
|
|
4
|
+
// store seam maps onto it directly: no hydrate-the-world-then-flush boundary, no snapshot cap, and
|
|
5
|
+
// a request touches only the rows it reads. The handle is injected (`SqlExec` is the one method the
|
|
6
|
+
// store calls), so this module imports nothing and stays safe in every bundle.
|
|
7
|
+
//
|
|
8
|
+
// Layout: `ws_files` holds one row per file (byte size, version); `ws_chunks` holds its content in
|
|
9
|
+
// order. An append adds a chunk; a write replaces them. Content is split so no row nears SQLite's
|
|
10
|
+
// per-row limit on the Durable Object backend (2 MB). Directories are implicit in the path set, as
|
|
11
|
+
// in MemoryWorldStore, whose semantics this store matches method for method.
|
|
12
|
+
//
|
|
13
|
+
// Versions: `stat().mtimeMs` is the file's version, drawn from a counter persisted in the database.
|
|
14
|
+
// storage.ts's append-dedupe cache lives across requests in an isolate and is validated by
|
|
15
|
+
// (size, mtimeMs), so a version may never repeat for a path — not across requests, not across a
|
|
16
|
+
// restarted object.
|
|
17
|
+
import type { WorldStat, WorldStore } from './world-store.ts';
|
|
18
|
+
|
|
19
|
+
type SqlValue = string | number | null | ArrayBuffer;
|
|
20
|
+
/** The slice of a SQLite handle the store uses — Durable Objects' `SqlStorage` satisfies it. */
|
|
21
|
+
export type SqlExec = { exec(query: string, ...bindings: unknown[]): { toArray(): Record<string, SqlValue>[] } };
|
|
22
|
+
|
|
23
|
+
/** Characters per stored chunk: at most 4 UTF-8 bytes each, well under a 2 MB row. */
|
|
24
|
+
const CHUNK = 256 * 1024;
|
|
25
|
+
const encoder = new TextEncoder();
|
|
26
|
+
|
|
27
|
+
/** `[low, high)` bounds selecting every path strictly under `dir`. */
|
|
28
|
+
function under(dir: string): [string, string] {
|
|
29
|
+
const base = dir.endsWith('/') ? dir.slice(0, -1) : dir;
|
|
30
|
+
return [`${base}/`, `${base}0`]; // '0' is the code point after '/'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class SqlWorldStore implements WorldStore {
|
|
34
|
+
private version: number;
|
|
35
|
+
|
|
36
|
+
constructor(private readonly sql: SqlExec) {
|
|
37
|
+
sql.exec('CREATE TABLE IF NOT EXISTS ws_files (path TEXT PRIMARY KEY, size INTEGER NOT NULL, version INTEGER NOT NULL, chunks INTEGER NOT NULL)');
|
|
38
|
+
sql.exec('CREATE TABLE IF NOT EXISTS ws_chunks (path TEXT NOT NULL, seq INTEGER NOT NULL, data TEXT NOT NULL, PRIMARY KEY (path, seq))');
|
|
39
|
+
this.version = Number(sql.exec('SELECT COALESCE(MAX(version), 0) AS v FROM ws_files').toArray()[0]?.v ?? 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private file(path: string): { size: number; version: number; chunks: number } | null {
|
|
43
|
+
const row = this.sql.exec('SELECT size, version, chunks FROM ws_files WHERE path = ?', path).toArray()[0];
|
|
44
|
+
return row ? { size: Number(row.size), version: Number(row.version), chunks: Number(row.chunks) } : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private put(path: string, data: string, from: number, size: number): void {
|
|
48
|
+
let seq = from;
|
|
49
|
+
for (let i = 0; i < data.length; i += CHUNK) this.sql.exec('INSERT INTO ws_chunks (path, seq, data) VALUES (?, ?, ?)', path, seq++, data.slice(i, i + CHUNK));
|
|
50
|
+
this.version += 1;
|
|
51
|
+
this.sql.exec('INSERT INTO ws_files (path, size, version, chunks) VALUES (?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET size = excluded.size, version = excluded.version, chunks = excluded.chunks', path, size, this.version, seq);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
read(path: string): string | null {
|
|
55
|
+
if (!this.file(path)) return null;
|
|
56
|
+
return this.sql.exec('SELECT data FROM ws_chunks WHERE path = ? ORDER BY seq', path).toArray().map((r) => String(r.data)).join('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
readLines(path: string): string[] {
|
|
60
|
+
const content = this.read(path);
|
|
61
|
+
return content === null ? [] : content.split('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
append(path: string, data: string): void {
|
|
65
|
+
const existing = this.file(path);
|
|
66
|
+
this.put(path, data, existing?.chunks ?? 0, (existing?.size ?? 0) + encoder.encode(data).length);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
write(path: string, data: string): void {
|
|
70
|
+
this.sql.exec('DELETE FROM ws_chunks WHERE path = ?', path);
|
|
71
|
+
this.put(path, data, 0, encoder.encode(data).length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
writeAtomic(path: string, data: string): void {
|
|
75
|
+
// One synchronous run of SQL with no await inside: no reader interleaves, and the object's
|
|
76
|
+
// output gate holds the response until the write is durable.
|
|
77
|
+
this.write(path, data);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
exists(path: string): boolean {
|
|
81
|
+
if (this.file(path)) return true;
|
|
82
|
+
const [low, high] = under(path);
|
|
83
|
+
return this.sql.exec('SELECT 1 FROM ws_files WHERE path >= ? AND path < ? LIMIT 1', low, high).toArray().length > 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
remove(path: string): void {
|
|
87
|
+
const [low, high] = under(path);
|
|
88
|
+
this.sql.exec('DELETE FROM ws_chunks WHERE path = ? OR (path >= ? AND path < ?)', path, low, high);
|
|
89
|
+
this.sql.exec('DELETE FROM ws_files WHERE path = ? OR (path >= ? AND path < ?)', path, low, high);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
mkdir(_dirPath: string): void {
|
|
93
|
+
// Directories are implicit in the path set.
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
list(dirPath: string): string[] {
|
|
97
|
+
const [low, high] = under(dirPath);
|
|
98
|
+
const children = new Set<string>();
|
|
99
|
+
for (const row of this.sql.exec('SELECT path FROM ws_files WHERE path >= ? AND path < ?', low, high).toArray()) {
|
|
100
|
+
const rest = String(row.path).slice(low.length);
|
|
101
|
+
const slash = rest.indexOf('/');
|
|
102
|
+
children.add(slash === -1 ? rest : rest.slice(0, slash));
|
|
103
|
+
}
|
|
104
|
+
return [...children];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
stat(path: string): WorldStat | null {
|
|
108
|
+
const file = this.file(path);
|
|
109
|
+
if (file) return { size: file.size, mtimeMs: file.version, isDirectory: false };
|
|
110
|
+
return this.exists(path) ? { size: 0, mtimeMs: 0, isDirectory: true } : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
withLock<T>(_lockPath: string, fn: () => T): T {
|
|
114
|
+
// A Durable Object runs one synchronous stretch of JavaScript at a time and `fn` is synchronous,
|
|
115
|
+
// so the kernel's read-then-append critical sections are already exclusive.
|
|
116
|
+
return fn();
|
|
117
|
+
}
|
|
118
|
+
}
|