@volter/world-core 3.0.155 → 3.0.156
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/generated/pack-facts.json +143 -12
- package/dist/inject.cjs +150 -86
- package/dist/src/actions.d.ts +15 -0
- package/dist/src/actions.js +82 -21
- package/dist/src/ancestry.d.ts +5 -4
- package/dist/src/ancestry.js +53 -28
- package/dist/src/changeset.js +1 -1
- package/dist/src/clickhouse/compression.d.ts +28 -0
- package/dist/src/clickhouse/compression.js +281 -0
- package/dist/src/clickhouse/index.d.ts +1 -0
- package/dist/src/clickhouse/index.js +1 -0
- package/dist/src/derived-core.d.ts +12 -1
- package/dist/src/derived-core.js +145 -11
- package/dist/src/derived.js +3 -0
- package/dist/src/fork.js +1 -1
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/h2.js +63 -10
- package/dist/src/head.d.ts +3 -0
- package/dist/src/head.js +9 -0
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +7 -7
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +5 -1
- package/dist/src/log.d.ts +23 -3
- package/dist/src/log.js +160 -37
- package/dist/src/managed-original-inputs.d.ts +101 -0
- package/dist/src/managed-original-inputs.js +125 -0
- package/dist/src/mongodb/client.d.ts +8 -0
- package/dist/src/mongodb/client.js +27 -0
- package/dist/src/mongodb/index.d.ts +2 -0
- package/dist/src/mongodb/index.js +5 -0
- package/dist/src/mongodb/stream.d.ts +74 -0
- package/dist/src/mongodb/stream.js +206 -0
- package/dist/src/observe.js +1 -1
- package/dist/src/pack-fetch.js +20 -8
- package/dist/src/packRegistry.d.ts +10 -14
- package/dist/src/packRegistry.js +2 -13
- package/dist/src/protocol.d.ts +13 -0
- package/dist/src/protocol.js +13 -0
- package/dist/src/redis/engine.d.ts +10 -4
- package/dist/src/redis/engine.js +103 -32
- package/dist/src/redis/member-storage.js +20 -8
- package/dist/src/redis/service-times.d.ts +25 -0
- package/dist/src/redis/service-times.js +87 -0
- package/dist/src/redis/stream.d.ts +2 -1
- package/dist/src/redis/stream.js +109 -20
- package/dist/src/runtime-state.d.ts +11 -0
- package/dist/src/runtime-state.js +13 -0
- package/dist/src/runtime.d.ts +4 -2
- package/dist/src/runtime.js +5 -2
- package/dist/src/serve-http.d.ts +2 -0
- package/dist/src/serve-http.js +21 -4
- package/dist/src/serve.d.ts +10 -0
- package/dist/src/serve.js +43 -3
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.js +6 -3
- package/dist/src/world-boot.d.ts +1 -0
- package/dist/src/world-boot.js +6 -0
- package/dist/src/world-clock.js +6 -2
- package/dist/src/world-store.d.ts +108 -7
- package/dist/src/world-store.js +883 -103
- package/dist/src/xml.d.ts +30 -0
- package/dist/src/xml.js +74 -0
- package/generated/pack-facts.json +143 -12
- package/inject.cjs +150 -86
- package/package.json +20 -1
- package/src/actions.ts +78 -21
- package/src/ancestry.ts +46 -24
- package/src/changeset.ts +1 -1
- package/src/clickhouse/compression.ts +172 -0
- package/src/clickhouse/index.ts +2 -0
- package/src/derived-core.ts +142 -11
- package/src/derived.ts +3 -0
- package/src/fork.ts +1 -1
- package/src/grpc-wire.ts +2 -1
- package/src/h2.ts +46 -5
- package/src/head.ts +9 -0
- package/src/history.ts +7 -7
- package/src/index.ts +6 -1
- package/src/log.ts +147 -32
- package/src/managed-original-inputs.ts +163 -0
- package/src/mongodb/client.ts +27 -0
- package/src/mongodb/index.ts +5 -0
- package/src/mongodb/stream.ts +213 -0
- package/src/observe.ts +1 -1
- package/src/pack-fetch.ts +20 -8
- package/src/packRegistry.ts +3 -15
- package/src/protocol.ts +13 -0
- package/src/redis/engine.ts +100 -57
- package/src/redis/member-storage.ts +17 -9
- package/src/redis/service-times.ts +81 -0
- package/src/redis/stream.ts +66 -14
- package/src/runtime-state.ts +14 -0
- package/src/runtime.ts +6 -1
- package/src/serve-http.ts +23 -4
- package/src/serve.ts +45 -3
- package/src/storage.ts +5 -3
- package/src/world-boot.ts +6 -0
- package/src/world-clock.ts +6 -2
- package/src/world-store.ts +704 -91
- package/src/xml.ts +71 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { getActiveWorldStore, removeCarriedLocks } from './world-store.js';
|
|
2
|
+
export type { WorldStore } from './world-store.js';
|
|
3
|
+
export { stateDirName, withFileLock, worldPaths, worldStateRoot, WORLD_DATA_ENV } from './storage.js';
|
|
4
|
+
export { assertStateRemovable, withAncestryLock, withStateRemoval } from './ancestry.js';
|
|
5
|
+
export { readBranchMeta } from './log.js';
|
|
6
|
+
export { volterHome } from './volter-home.js';
|
|
7
|
+
export { installedPackageDir, installedVendorPacks, isDerivedPack, packRepository, vendorOfPackModule } from './pack-paths.js';
|
|
8
|
+
export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from './protocol.js';
|
|
9
|
+
export { WORLD_BOOT_PATH } from './serve-http.js';
|
|
10
|
+
export { WORLD_ENV_NAMES_ENV } from './world-env.js';
|
|
11
|
+
export { beginPlaceholderPull, endPlaceholderPull, placeholderPullActive } from './placeholder-remote.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The lifecycle's state/identity primitives. Re-export their canonical modules
|
|
2
|
+
// so the active store, locks and scopes are shared with the full runtime and packs.
|
|
3
|
+
// Booting an empty World does not require pack registration or derived engines.
|
|
4
|
+
export { getActiveWorldStore, removeCarriedLocks } from "./world-store.js";
|
|
5
|
+
export { stateDirName, withFileLock, worldPaths, worldStateRoot, WORLD_DATA_ENV } from "./storage.js";
|
|
6
|
+
export { assertStateRemovable, withAncestryLock, withStateRemoval } from "./ancestry.js";
|
|
7
|
+
export { readBranchMeta } from "./log.js";
|
|
8
|
+
export { volterHome } from "./volter-home.js";
|
|
9
|
+
export { installedPackageDir, installedVendorPacks, isDerivedPack, packRepository, vendorOfPackModule } from "./pack-paths.js";
|
|
10
|
+
export { PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding } from "./protocol.js";
|
|
11
|
+
export { WORLD_BOOT_PATH } from "./serve-http.js";
|
|
12
|
+
export { WORLD_ENV_NAMES_ENV } from "./world-env.js";
|
|
13
|
+
export { beginPlaceholderPull, endPlaceholderPull, placeholderPullActive } from "./placeholder-remote.js";
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export { SqlWorldStore } from './world-store-sql.js';
|
|
|
38
38
|
export type { SqlExec } from './world-store-sql.js';
|
|
39
39
|
export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActiveBlobStore, withBlobStore, readBlobRange, } from './blob-store.js';
|
|
40
40
|
export type { BlobStore } from './blob-store.js';
|
|
41
|
-
export { applyTwinWrite, applyTwinWriteAtomic, createTwinServer, CALLER_HEADER, callerForHeader, callerFromHeader, journalTwinRequest, readTwinRequestJournalFrom, type TwinRequestJournalCursor, twinRequestJournalFailures, twinRequestCredentials, readTwinRequestJournal, recordedInput, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from './serve.js';
|
|
41
|
+
export { applyTwinWrite, applyTwinWrites, applyTwinWriteAtomic, createTwinServer, CALLER_HEADER, callerForHeader, callerFromHeader, journalTwinRequest, readTwinRequestJournalFrom, type TwinRequestJournalCursor, twinRequestJournalFailures, twinRequestCredentials, readTwinRequestJournal, recordedInput, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from './serve.js';
|
|
42
42
|
export type { AtomicTwinWriteDecision, TwinRequestJournalEntry, TwinResource, TwinWriteInput, TwinWriteResult, } from './serve.js';
|
|
43
43
|
export { createTwinProxy } from './proxy.js';
|
|
44
44
|
export type { TwinProxy, TwinProxyOptions, VendorRoute } from './proxy.js';
|
|
@@ -52,6 +52,7 @@ export { getActivePackAssets, setActivePackAssets, packAsset, assetContentType,
|
|
|
52
52
|
export { installedPackageDir, installedVendorPacks, packVendor, packRepository, isDerivedPack, vendorPackageDir, vendorOfPackModule } from './pack-paths.js';
|
|
53
53
|
export * as git from './git/index.js';
|
|
54
54
|
export * as s3 from './s3/wire.js';
|
|
55
|
+
export * as xml from './xml.js';
|
|
55
56
|
export { PLACEHOLDER_REMOTE, beginPlaceholderPull, endPlaceholderPull, placeholderPullActive, placeholderPullMarkerPath, withPlaceholderPull, placeholderEventsFor } from './placeholder-remote.js';
|
|
56
57
|
export { aliasesFrom, ownFields, subjectHistory, rebaseBranch, cutCheckpoint, appendParentEntry, wholeLog, branchEntries, branchLogPath, branchMetaPath, CHECKPOINT_EVERY, dropCheckpoint, foldEntries, landedCopy, landedIds, parentEntries, parentLogPath, position, readBranchMeta, readTree, toEntry, toEvent, unpushedEntries, writeBranchMeta } from './log.js';
|
|
57
58
|
export type { BranchMeta, Entry, EntryOp, Receipt } from './log.js';
|
|
@@ -69,7 +70,7 @@ export { observeResource, observeResources, collectObservations, foldObservation
|
|
|
69
70
|
export type { ObservedResource, ObserveReport, Observation } from './observe.js';
|
|
70
71
|
export { readTreeMap, readParentTreeMap, treeStamp, treeChangesSince, originEntries, appendOriginEntry, originLogPath, isUrlParent, parentPosition, positionAt, splitsBatch, assertBatchBoundary } from './log.js';
|
|
71
72
|
export { referenceField, registerReferences, packReferences, resolveReferences, type ReferenceDeclaration } from './references.js';
|
|
72
|
-
export { serveHttp, serveStream, connectWebSocket, nodeBuiltin, WORLD_BOOT_PATH, type HttpServer, type ServeHttpOptions, type HttpHandler } from './serve-http.js';
|
|
73
|
+
export { serveHttp, serveStream, connectWebSocket, nodeBuiltin, servedHandlerOf, WORLD_BOOT_PATH, type HttpServer, type ServeHttpOptions, type HttpHandler } from './serve-http.js';
|
|
73
74
|
export { fileResponse, contentTypeOf } from './file-response.js';
|
|
74
75
|
export { bundleClient, filePathOf } from './client-bundle.js';
|
|
75
76
|
export { brandTokensResponse } from './brand-tokens.js';
|
|
@@ -86,3 +87,4 @@ export { foldHistory } from './log.js';
|
|
|
86
87
|
export { volterHome } from './volter-home.js';
|
|
87
88
|
export type { MachinePool, MachineSpec, MachineProcess, MachineStatus, MachineFiles, MachineFileStat, GuestImage, LightPoolOptions } from './machines.js';
|
|
88
89
|
export { boardFrameKey } from './board-keys.js';
|
|
90
|
+
export * as managedOriginalInputs from './managed-original-inputs.js';
|
package/dist/src/runtime.js
CHANGED
|
@@ -55,7 +55,7 @@ export { SqlWorldStore } from "./world-store-sql.js";
|
|
|
55
55
|
// The blob seam (runtime contract R11): byte storage behind byte-carrying handlers, so a
|
|
56
56
|
// serverless namespace puts bytes in object storage while local worlds keep today's layout.
|
|
57
57
|
export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActiveBlobStore, withBlobStore, readBlobRange, } from "./blob-store.js";
|
|
58
|
-
export { applyTwinWrite, applyTwinWriteAtomic, createTwinServer, CALLER_HEADER, callerForHeader, callerFromHeader, journalTwinRequest, readTwinRequestJournalFrom, twinRequestJournalFailures, twinRequestCredentials, readTwinRequestJournal, recordedInput, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from "./serve.js";
|
|
58
|
+
export { applyTwinWrite, applyTwinWrites, applyTwinWriteAtomic, createTwinServer, CALLER_HEADER, callerForHeader, callerFromHeader, journalTwinRequest, readTwinRequestJournalFrom, twinRequestJournalFailures, twinRequestCredentials, readTwinRequestJournal, recordedInput, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from "./serve.js";
|
|
59
59
|
export { createTwinProxy } from "./proxy.js";
|
|
60
60
|
export { forkTwin, isFork, readForkMeta, } from "./fork.js";
|
|
61
61
|
export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, projectOwnerResources, OwnerStoreAmbiguousError, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from "./actions.js";
|
|
@@ -73,6 +73,8 @@ export { installedPackageDir, installedVendorPacks, packVendor, packRepository,
|
|
|
73
73
|
export * as git from "./git/index.js";
|
|
74
74
|
// S3's wire (contract "S3's wire is a kernel library too"): its XML, CORS, lifecycle, delete batches, ETags, body bytes.
|
|
75
75
|
export * as s3 from "./s3/wire.js";
|
|
76
|
+
// Generic strict XML grammar; vendor interpretation remains in its consumer.
|
|
77
|
+
export * as xml from "./xml.js";
|
|
76
78
|
// The push arm: the kernel's transaction over a pack's one `perform<Name>Action` (contract section of that name).
|
|
77
79
|
// The placeholder remote (contract section of that name): the seed is a pull, never a pending write.
|
|
78
80
|
export { PLACEHOLDER_REMOTE, beginPlaceholderPull, endPlaceholderPull, placeholderPullActive, placeholderPullMarkerPath, withPlaceholderPull, placeholderEventsFor } from "./placeholder-remote.js";
|
|
@@ -88,7 +90,7 @@ export { observeResource, observeResources, collectObservations, foldObservation
|
|
|
88
90
|
export { readTreeMap, readParentTreeMap, treeStamp, treeChangesSince, originEntries, appendOriginEntry, originLogPath, isUrlParent, parentPosition, positionAt, splitsBatch, assertBatchBoundary } from "./log.js";
|
|
89
91
|
export { referenceField, registerReferences, packReferences, resolveReferences } from "./references.js";
|
|
90
92
|
// the HTTP server seam: the one place the serve path meets the runtime (Bun or Node)
|
|
91
|
-
export { serveHttp, serveStream, connectWebSocket, nodeBuiltin, WORLD_BOOT_PATH } from "./serve-http.js";
|
|
93
|
+
export { serveHttp, serveStream, connectWebSocket, nodeBuiltin, servedHandlerOf, WORLD_BOOT_PATH } from "./serve-http.js";
|
|
92
94
|
// two runtime-neutral helpers for a serve path: a file as a Response, a page's client bundle
|
|
93
95
|
export { fileResponse, contentTypeOf } from "./file-response.js";
|
|
94
96
|
export { bundleClient, filePathOf } from "./client-bundle.js";
|
|
@@ -104,3 +106,4 @@ export { captureHistory, captureParentHistory, historyChanges, historyAtInstant,
|
|
|
104
106
|
export { foldHistory } from "./log.js";
|
|
105
107
|
export { volterHome } from "./volter-home.js";
|
|
106
108
|
export { boardFrameKey } from "./board-keys.js";
|
|
109
|
+
export * as managedOriginalInputs from "./managed-original-inputs.js";
|
package/dist/src/serve-http.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export type HttpServer = {
|
|
|
45
45
|
export declare function setServeDecorator(fn: ((options: ServeHttpOptions) => HttpHandler) | undefined): void;
|
|
46
46
|
/** The path a World asks a service it booted, when that service's port answers, which boot it belongs to. */
|
|
47
47
|
export declare const WORLD_BOOT_PATH = "/__volter/world-boot";
|
|
48
|
+
/** The handler serving `port` in this process, when a server made through the seam listens there. */
|
|
49
|
+
export declare function servedHandlerOf(port: number): HttpHandler | undefined;
|
|
48
50
|
export declare function serveHttp(options: ServeHttpOptions): Promise<HttpServer>;
|
|
49
51
|
/** A Node builtin without a static import — `process.getBuiltinModule` (Node 22.3+, Bun): what a
|
|
50
52
|
* module that also rides into a browser bundle reaches `node:crypto` with. A bare `require()`
|
package/dist/src/serve-http.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// as a standard Request and the handler's Response written back. Async, because binding a port is
|
|
5
5
|
// asynchronous on Node — Bun's synchronous bind is the special case.
|
|
6
6
|
//
|
|
7
|
-
// Nothing here runs at module scope, and `node:http` is reached through `process.getBuiltinModule`
|
|
7
|
+
// Nothing here runs at module scope (the served-handler registry is made on first use), and `node:http` is reached through `process.getBuiltinModule`
|
|
8
8
|
// rather than a static import: this module rides into browser client bundles through the kernel's
|
|
9
9
|
// entrypoint, where a static `node:` import is the 2026-09-06 class of break.
|
|
10
|
+
import { worldBootAnswer } from "./world-boot.js";
|
|
10
11
|
/** One socket library at the host seam, shared by inbound upgrades and authenticated outbound relays. */
|
|
11
12
|
const socketLibrary = () => import('ws');
|
|
12
13
|
export async function connectWebSocket(url, headers, protocols = []) {
|
|
@@ -19,6 +20,22 @@ export function setServeDecorator(fn) { decorate = fn; }
|
|
|
19
20
|
const bunRuntime = () => { const b = globalThis.Bun; return b && typeof b.serve === 'function' ? b : undefined; };
|
|
20
21
|
/** The path a World asks a service it booted, when that service's port answers, which boot it belongs to. */
|
|
21
22
|
export const WORLD_BOOT_PATH = '/__volter/world-boot';
|
|
23
|
+
/** Each listening server's own handler, by its port, as the seam serves it (the boot door answered, nothing journaled):
|
|
24
|
+
* the World's co-located host warms a twin in-process through it (world-runtime host.ts warmInProcess), so its warm-up is
|
|
25
|
+
* never a request on the wire nor in any journal. A server leaves it when it stops. Kept on globalThis under a registered
|
|
26
|
+
* symbol, as the reference registry is (references.ts), so a pack that resolved its own copy of this module shares it. */
|
|
27
|
+
const servedHandlers = () => (globalThis[Symbol.for('volter.servedHandlers')] ??= new Map());
|
|
28
|
+
/** The handler serving `port` in this process, when a server made through the seam listens there. */
|
|
29
|
+
export function servedHandlerOf(port) { return servedHandlers().get(port); }
|
|
30
|
+
/** A server's handler kept by its port until the server stops. */
|
|
31
|
+
function keepHandler(server, handler) {
|
|
32
|
+
const port = server.port;
|
|
33
|
+
if (!port)
|
|
34
|
+
return server;
|
|
35
|
+
servedHandlers().set(port, handler);
|
|
36
|
+
return { ...server, stop: async (closeActive) => { if (servedHandlers().get(port) === handler)
|
|
37
|
+
servedHandlers().delete(port); await server.stop(closeActive); } };
|
|
38
|
+
}
|
|
22
39
|
export async function serveHttp(options) {
|
|
23
40
|
// Inside a World every server made through the seam answers which boot it belongs to, so the World can tell its own
|
|
24
41
|
// twin from a leftover World's that holds the same port. Outside a World nothing changes.
|
|
@@ -33,7 +50,7 @@ export async function serveHttp(options) {
|
|
|
33
50
|
const worldBoot = globalThis.process?.env?.VOLTER_WORLD_BOOT_ID;
|
|
34
51
|
if (worldBoot) {
|
|
35
52
|
const served = options.fetch;
|
|
36
|
-
options = { ...options, fetch: (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ?
|
|
53
|
+
options = { ...options, fetch: (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? worldBootAnswer(worldBoot) : served(request)) };
|
|
37
54
|
}
|
|
38
55
|
// loopback by default: an unnamed interface is never every interface
|
|
39
56
|
const hostname = options.hostname ?? '127.0.0.1';
|
|
@@ -60,9 +77,9 @@ export async function serveHttp(options) {
|
|
|
60
77
|
} : {}),
|
|
61
78
|
});
|
|
62
79
|
const port = server.port ?? 0;
|
|
63
|
-
return { hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => { await server.stop(closeActive); } };
|
|
80
|
+
return keepHandler({ hostname, port, url: server.url instanceof URL ? server.url : new URL(`http://${urlHost}:${port}/`), stop: async (closeActive = true) => { await server.stop(closeActive); } }, options.fetch);
|
|
64
81
|
}
|
|
65
|
-
return serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch });
|
|
82
|
+
return keepHandler(await serveOnNode({ ...options, hostname, urlHost, fetch: decorate ? decorate(options) : options.fetch }), options.fetch);
|
|
66
83
|
}
|
|
67
84
|
/** A Node builtin without a static import — `process.getBuiltinModule` (Node 22.3+, Bun): what a
|
|
68
85
|
* module that also rides into a browser bundle reaches `node:crypto` with. A bare `require()`
|
package/dist/src/serve.d.ts
CHANGED
|
@@ -184,6 +184,16 @@ export declare function applyTwinWrite(service: string, write: TwinWriteInput, r
|
|
|
184
184
|
result: TwinWriteResult;
|
|
185
185
|
resource: TwinResource;
|
|
186
186
|
}>;
|
|
187
|
+
/**
|
|
188
|
+
* Several local writes of one service, landed as `applyTwinWrite` lands each called in turn — the same actions, ids and
|
|
189
|
+
* order — under one lock and one append (actions.ts `appendActionOccurrences`). Writes that need the one-at-a-time
|
|
190
|
+
* path take it: a caller's identity (`idempotencyKey`/`actionId`), preconditions, or a head that performs each entry as
|
|
191
|
+
* it lands. An input is offloaded once per distinct value, as each write's own offload would name it.
|
|
192
|
+
*/
|
|
193
|
+
export declare function applyTwinWrites(service: string, writes: readonly TwinWriteInput[], root?: string): Promise<Array<{
|
|
194
|
+
result: TwinWriteResult;
|
|
195
|
+
resource: TwinResource;
|
|
196
|
+
}>>;
|
|
187
197
|
export declare function createTwinServer(options: {
|
|
188
198
|
service: string;
|
|
189
199
|
root?: string;
|
package/dist/src/serve.js
CHANGED
|
@@ -12,12 +12,13 @@ import { parseExactJson } from "./exact-json.js";
|
|
|
12
12
|
// reality). Forking is a separate data operation (fork.ts), never a serve mode.
|
|
13
13
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
14
14
|
import { serveHttp, setServeDecorator, WORLD_BOOT_PATH } from "./serve-http.js";
|
|
15
|
-
import { performAtHead } from "./head.js";
|
|
15
|
+
import { performAtHead, performsAtHeadNow } from "./head.js";
|
|
16
16
|
import { withHeaders } from "./with-headers.js";
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
import { hashFieldValue } from "./hash.js";
|
|
20
|
-
import { appendActionIfAbsent, appendActionOccurrence, decideAndAppendAction, projectResources } from "./actions.js";
|
|
20
|
+
import { appendActionIfAbsent, appendActionOccurrence, appendActionOccurrences, decideAndAppendAction, projectResources } from "./actions.js";
|
|
21
|
+
import { readSubject } from "./log.js";
|
|
21
22
|
import { observeWorldPaths, worldPaths } from "./storage.js";
|
|
22
23
|
import { getActiveWorldStore } from "./world-store.js";
|
|
23
24
|
import { getActiveBlobStore } from "./blob-store.js";
|
|
@@ -596,11 +597,50 @@ export async function applyTwinWrite(service, write, root) {
|
|
|
596
597
|
return {
|
|
597
598
|
result,
|
|
598
599
|
get resource() {
|
|
599
|
-
return resolved ??=
|
|
600
|
+
return resolved ??= readSubject(service, write.subjectType, answerId, root)
|
|
600
601
|
?? { id: answerId, type: write.subjectType, updatedAt: landed.occurredAt, ...write.fields };
|
|
601
602
|
},
|
|
602
603
|
};
|
|
603
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Several local writes of one service, landed as `applyTwinWrite` lands each called in turn — the same actions, ids and
|
|
607
|
+
* order — under one lock and one append (actions.ts `appendActionOccurrences`). Writes that need the one-at-a-time
|
|
608
|
+
* path take it: a caller's identity (`idempotencyKey`/`actionId`), preconditions, or a head that performs each entry as
|
|
609
|
+
* it lands. An input is offloaded once per distinct value, as each write's own offload would name it.
|
|
610
|
+
*/
|
|
611
|
+
export async function applyTwinWrites(service, writes, root) {
|
|
612
|
+
if (writes.length <= 1 || performsAtHeadNow(service, root) || writes.some((write) => write.idempotencyKey || write.actionId || write.preconditions?.length)) {
|
|
613
|
+
const out = [];
|
|
614
|
+
for (const write of writes)
|
|
615
|
+
out.push(await applyTwinWrite(service, write, root));
|
|
616
|
+
return out;
|
|
617
|
+
}
|
|
618
|
+
// Each distinct input offloaded once (S2 R17): `offloadInput` is content-addressed (a string's blob is named by its
|
|
619
|
+
// SHA-256), so the result is the one each write's own offload would give, and a command's writes, which share one
|
|
620
|
+
// recorded request, hash and look up its blob once instead of once per write.
|
|
621
|
+
const offloaded = new Map();
|
|
622
|
+
for (const write of writes)
|
|
623
|
+
if (write.input !== undefined && !offloaded.has(write.input))
|
|
624
|
+
offloaded.set(write.input, await offloadInput(service, write.input, root));
|
|
625
|
+
const committed = appendActionOccurrences(writes.map((write) => actionForTwinWrite(service, write.input === undefined ? write : { ...write, input: offloaded.get(write.input) })), root);
|
|
626
|
+
const out = [];
|
|
627
|
+
for (const [index, { appended, action: landed, placeholder }] of committed.entries()) {
|
|
628
|
+
const write = writes[index];
|
|
629
|
+
// the head does not perform here (`performsAtHeadNow` was false): this marks the request's deferred group as written
|
|
630
|
+
if (appended && !placeholder)
|
|
631
|
+
await performAtHead(service, landed, root);
|
|
632
|
+
const answerId = write.subjectId;
|
|
633
|
+
let resolved;
|
|
634
|
+
out.push({
|
|
635
|
+
result: { status: appended ? 'performed' : 'replayed', actionId: landed.id },
|
|
636
|
+
get resource() {
|
|
637
|
+
return resolved ??= readSubject(service, write.subjectType, answerId, root)
|
|
638
|
+
?? { id: answerId, type: write.subjectType, updatedAt: landed.occurredAt, ...write.fields };
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
604
644
|
export async function createTwinServer(options) {
|
|
605
645
|
const readOnly = options.readOnly ?? false;
|
|
606
646
|
const serveOptions = {
|
package/dist/src/storage.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WorldLockOptions } from './world-store.js';
|
|
1
2
|
import type { AppendEventResult, GenericWorldState, WorldPaths, WorldReducer, WorldServiceEvent } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Name of the per-project state directory holding world data
|
|
@@ -60,7 +61,7 @@ export declare function twinLog(kind: string, details: Record<string, unknown>):
|
|
|
60
61
|
* single-instance store it is a synchronous pass-through. Used to make read-then-append
|
|
61
62
|
* critical sections atomic. Public so world-runtime can guard concurrent `upWorld`
|
|
62
63
|
* claims of one instance dir with the SAME lock semantics the event log uses (TWIN-36). */
|
|
63
|
-
export declare function withFileLock<T>(lockPath: string, fn: () => T): T;
|
|
64
|
+
export declare function withFileLock<T>(lockPath: string, fn: () => T, options?: WorldLockOptions): T;
|
|
64
65
|
/** The parent log as observed rows: every entry the branch inherited or landed, as
|
|
65
66
|
* observed rows (log.ts `toEvent`). A branched world sees its base world's logs to the branch
|
|
66
67
|
* position first. */
|
package/dist/src/storage.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
|
3
3
|
import { withStateLock, withStateRemoval } from "./ancestry.js";
|
|
4
4
|
import { appendParentEntry, toEntry } from "./log.js";
|
|
5
5
|
import { GenericWorldStateSchema, WorldServiceEventSchema, } from "./schemas.js";
|
|
6
|
-
import { getActiveWorldStore } from "./world-store.js";
|
|
6
|
+
import { getActiveWorldStore, storageLockScope, TREE_GENERATION } from "./world-store.js";
|
|
7
7
|
import { refuseReadOnlyWrite } from "./request-scope.js";
|
|
8
8
|
import { parentEntries, toEvent } from "./log.js";
|
|
9
9
|
function nowIso() {
|
|
@@ -181,8 +181,8 @@ function appendJsonl(path, value) {
|
|
|
181
181
|
* single-instance store it is a synchronous pass-through. Used to make read-then-append
|
|
182
182
|
* critical sections atomic. Public so world-runtime can guard concurrent `upWorld`
|
|
183
183
|
* claims of one instance dir with the SAME lock semantics the event log uses (TWIN-36). */
|
|
184
|
-
export function withFileLock(lockPath, fn) {
|
|
185
|
-
return withStateLock(lockPath, fn);
|
|
184
|
+
export function withFileLock(lockPath, fn, options = {}) {
|
|
185
|
+
return withStateLock(lockPath, fn, storageLockScope(options));
|
|
186
186
|
}
|
|
187
187
|
function writeJsonAtomic(path, value) {
|
|
188
188
|
getActiveWorldStore().writeAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -327,6 +327,9 @@ function isKnownServiceEntry(name) {
|
|
|
327
327
|
// log.ts: the branch pointer and the checkpoint
|
|
328
328
|
if (name === 'branch.json' || name === 'checkpoints')
|
|
329
329
|
return true;
|
|
330
|
+
// world-store.ts: the generation a file store keeps beside a tree's files
|
|
331
|
+
if (name === TREE_GENERATION)
|
|
332
|
+
return true;
|
|
330
333
|
return KNOWN_SERVICE_ENTRIES.has(name) || name.endsWith('.tmp') || /\.stale\.\d+\.\d+$/.test(name);
|
|
331
334
|
}
|
|
332
335
|
/** A service dir "looks like" one of ours when every entry in it is something
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function worldBootAnswer(boot: string): Response;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// One private boot answer for the kernel's HTTP and gRPC serve seams.
|
|
2
|
+
// Reading process identity happens on request; importing this helper has no side effects.
|
|
3
|
+
export function worldBootAnswer(boot) {
|
|
4
|
+
const pid = globalThis.process?.pid;
|
|
5
|
+
return Response.json({ boot, pid });
|
|
6
|
+
}
|
package/dist/src/world-clock.js
CHANGED
|
@@ -23,10 +23,14 @@ const machineNow = () => {
|
|
|
23
23
|
* now); real wall-clock time otherwise. */
|
|
24
24
|
export function worldNow() {
|
|
25
25
|
const file = process.env[WORLD_CLOCK_ENV];
|
|
26
|
-
|
|
26
|
+
// One read answers both whether the clock file is there and what it holds.
|
|
27
|
+
// Kept while the clock file's own facts stand (world-store.ts readKept): every operation asks the time.
|
|
28
|
+
const store = getActiveWorldStore();
|
|
29
|
+
const held = file ? (store.readKept ? store.readKept(file) : store.read(file)) : null;
|
|
30
|
+
if (held !== null) {
|
|
27
31
|
// A configured-but-corrupt clock is a WORLD defect — parseClock fails loudly, never silently drifting back to
|
|
28
32
|
// wall-clock (two services disagreeing about now breaks the world).
|
|
29
|
-
const clock = clockForm.parseClock(
|
|
33
|
+
const clock = clockForm.parseClock(held);
|
|
30
34
|
return new Date(clockForm.clockNowMs(clock, machineNow())).toISOString();
|
|
31
35
|
}
|
|
32
36
|
return new Date(machineNow()).toISOString();
|
|
@@ -7,6 +7,17 @@ export type WorldStat = {
|
|
|
7
7
|
isDirectory: boolean;
|
|
8
8
|
isSymbolicLink?: boolean;
|
|
9
9
|
};
|
|
10
|
+
/** Diagnostic context only; it does not change lock acquisition or stale-owner policy. */
|
|
11
|
+
export type WorldLockOptions = {
|
|
12
|
+
retainLiveOwner?: boolean;
|
|
13
|
+
operation?: string;
|
|
14
|
+
caller?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Capture at the outermost lock helper, then carry the same caller through nested helpers. */
|
|
17
|
+
export declare function storageLockScope(options?: WorldLockOptions): WorldLockOptions & {
|
|
18
|
+
operation: string;
|
|
19
|
+
caller: string;
|
|
20
|
+
};
|
|
10
21
|
/**
|
|
11
22
|
* The synchronous persistence seam. Every path is an ABSOLUTE key the caller already
|
|
12
23
|
* computed (via `worldPaths`/`worldStateRoot`); the store treats it as an opaque key,
|
|
@@ -51,13 +62,57 @@ export interface WorldStore {
|
|
|
51
62
|
readRange?(path: string, start: number): string | null;
|
|
52
63
|
/** Resolve filesystem aliases for durable ownership; absent on opaque-key stores. */
|
|
53
64
|
canonicalPath?(path: string): string;
|
|
65
|
+
/** `read`, for a small file every operation needs and few change (a branch's pointer, the World's clock): the text
|
|
66
|
+
* kept from the last read while the path's own facts stand, so an unchanged file costs one check and no read.
|
|
67
|
+
* Optional: a store without it is read. */
|
|
68
|
+
readKept?(path: string): string | null;
|
|
69
|
+
/** The facts of the generation kept beside a tree's files in `dirPath` (TREE_FILES): one string that differs after
|
|
70
|
+
* any write of one of them through a store, by this process or another; `null` where the directory has none yet,
|
|
71
|
+
* which says nothing. Optional: a store without it is asked for each file's own facts. */
|
|
72
|
+
generation?(dirPath: string): string | null;
|
|
73
|
+
/** The generation in `dirPath` now, when exactly one write of a tree's file through a store has moved it since
|
|
74
|
+
* `before` (a generation this store answered for it); `null` when it cannot say so. A writer that read `before` just
|
|
75
|
+
* ahead of its own write, holding the directory's history lock, learns that its write was the only one (log.ts
|
|
76
|
+
* appendToBranchLog). Optional, beside `generation`. */
|
|
77
|
+
generationAfterOwnWrite?(dirPath: string, before: string): string | null;
|
|
54
78
|
/** Run `fn` holding an exclusive lock on `lockPath`, releasing it afterwards. The fs
|
|
55
79
|
* backend uses the historical cross-process reclaimable file lock; an in-memory,
|
|
56
80
|
* single-instance backend is a synchronous pass-through. */
|
|
57
|
-
withLock<T>(lockPath: string, fn: () => T, options?:
|
|
58
|
-
retainLiveOwner?: boolean;
|
|
59
|
-
}): T;
|
|
81
|
+
withLock<T>(lockPath: string, fn: () => T, options?: WorldLockOptions): T;
|
|
60
82
|
}
|
|
83
|
+
/** The files a service's tree is folded from, by name in its state directory (log.ts: the parent log, the branch log,
|
|
84
|
+
* the branch's pointer), and the generation a store keeps beside them. */
|
|
85
|
+
export declare const TREE_FILES: {
|
|
86
|
+
readonly parent: "events.jsonl";
|
|
87
|
+
readonly branch: "actions.jsonl";
|
|
88
|
+
readonly pointer: "branch.json";
|
|
89
|
+
};
|
|
90
|
+
export declare const TREE_GENERATION = "tree.generation";
|
|
91
|
+
/**
|
|
92
|
+
* A WORLD'S CARRIED LOCKS, REMOVED AT ITS OPEN (S2 K4). A World booted over its kept state (`up --keep-state`, a
|
|
93
|
+
* tab's restored archive) carries whatever lock files its last life left in its services' state directories. One whose
|
|
94
|
+
* holder record cannot be read is no holder's once its taker has gone, and a restore gives it the restore's file time,
|
|
95
|
+
* so the empty-record grace and the 60 s age ran from the restore: every waiter waited them, or gave up at 10 s (Twenty
|
|
96
|
+
* e946aa4c run 2: anthropic's projection.lock, "holder record unreadable", 10,086 ms, then its credentials 500 and the
|
|
97
|
+
* server 10 s late). A carried `.recovery` that is not empty and does not parse was never ended by R13's rule at all.
|
|
98
|
+
*
|
|
99
|
+
* A file is carried only when all of these hold: it is a world-core lock or its reclaim record (`*.lock`,
|
|
100
|
+
* `*.lock.recovery`) in a service's state directory (`<root>/<service>/<stateDir>/world`, never a yarn.lock or any
|
|
101
|
+
* project file); its text is empty or a holder record's beginning (`{"pid"`) that does not parse; and its file time is
|
|
102
|
+
* before `before`, the start of this boot's claim. A restore sets the time before the claim; a taker that is live now
|
|
103
|
+
* (a CLI's changeset rebase, a branch World keeping this one as its parent's storage, a two-call taker between its
|
|
104
|
+
* create and its record) made its file after it. Each removal is still a reclaimer's: a lock's under its `.recovery`,
|
|
105
|
+
* taken with its record (createRecorded), a held `.recovery` skipped; the file read again, renamed aside, the aside copy
|
|
106
|
+
* read again and linked back where it now holds a record (EEXIST there is the path taken since, left to its holder).
|
|
107
|
+
* A record that parses is kept, under its rules (a dead holder's, our own pid's, the 60 s age, which a restore resets:
|
|
108
|
+
* not this rule's). Answers the paths removed.
|
|
109
|
+
*/
|
|
110
|
+
export declare function removeCarriedLocks(roots: readonly string[], options: {
|
|
111
|
+
stateDir: string;
|
|
112
|
+
before: number;
|
|
113
|
+
}): string[];
|
|
114
|
+
/** Name a bookkeeping phase within an existing acquisition; never acquire another lock. */
|
|
115
|
+
export declare function withStorageLockOperation<T>(path: string, operation: string, fn: () => T): T;
|
|
61
116
|
/**
|
|
62
117
|
* The default store: a thin, faithful pass-through to `node:fs`. Every method preserves
|
|
63
118
|
* the exact behavior the kernel had before the seam existed — the durable append
|
|
@@ -66,6 +121,51 @@ export interface WorldStore {
|
|
|
66
121
|
*/
|
|
67
122
|
export declare class FsWorldStore implements WorldStore {
|
|
68
123
|
read(path: string): string | null;
|
|
124
|
+
/**
|
|
125
|
+
* A small file's text, kept while its own facts stand. THE GUARANTEE: the key is the path's own lstat (device,
|
|
126
|
+
* inode, size, modification and change times); this process's own writes through the store drop what is kept at
|
|
127
|
+
* once; another process's write is seen when any of those facts moves. That is the guarantee a tree's stamp
|
|
128
|
+
* already rests on for its logs (their size and modification time), with the inode and change time added: a
|
|
129
|
+
* replaced file (writeAtomic) is another inode, a file written in place has another time. What it cannot see is a
|
|
130
|
+
* rewrite to the same size on the same or a reused inode within one tick of the file system's clock, with no check
|
|
131
|
+
* between; the logs' stamp has the same limit. A link is read through, and not kept.
|
|
132
|
+
* A text read after a write that came between the lstat and the read is kept under the OLDER key, so the next
|
|
133
|
+
* check finds the facts moved and reads again: what is kept is never newer than its key, and never served past it.
|
|
134
|
+
*/
|
|
135
|
+
private readonly kept;
|
|
136
|
+
readKept(path: string): string | null;
|
|
137
|
+
/**
|
|
138
|
+
* A TREE'S GENERATION: one file beside a tree's three files, a byte longer after every write of any of them, so a
|
|
139
|
+
* reader that has the tree's stamp asks ONE question to know it still stands (log.ts treeFacts), where it asked
|
|
140
|
+
* three, two of them of files a World with no parent does not have.
|
|
141
|
+
*
|
|
142
|
+
* WHAT MAKES IT SOUND. A writer appends a byte BEFORE its write and another AFTER it; a reader takes the
|
|
143
|
+
* generation's facts BEFORE it reads the files' own. The file only grows, so its size never repeats on one inode,
|
|
144
|
+
* and a replaced or removed file is another inode or none. So for any write W and any reader that kept a stamp S
|
|
145
|
+
* under generation G: if W's first byte is after G was read, the kept G is behind and S is not served; if it is
|
|
146
|
+
* before, then either W itself was before S's own reads (S holds it), or W's second byte is after G was read, and
|
|
147
|
+
* again G is behind. An append is one call the system orders among all writers: each byte is one append of its
|
|
148
|
+
* own, whole or not at all, on a POSIX filesystem and in a tab's one kernel (browser-substrate
|
|
149
|
+
* packages/node/src/kernel-virtual-filesystem.ts appendFileSync, an O_APPEND open and writeAll, which sends a write
|
|
150
|
+
* of at most DATA_BYTES as one SYS_write request the kernel serves whole; or, by a file step, its table's own
|
|
151
|
+
* O_APPEND open and write of the one byte, filesystem.ts appendFileBytes). The proof rests on that; log.ts
|
|
152
|
+
* appendToBranchLog's (`generationAfterOwnWrite`: one write is exactly two bytes) as well.
|
|
153
|
+
*
|
|
154
|
+
* WHERE THAT STOPS. (1) A writer that dies between its write and its second byte, with a reader's whole check
|
|
155
|
+
* between its first byte and its write: that reader keeps the older stamp until the next write of the tree. (2) A
|
|
156
|
+
* writer of these files that does not come through a store of this version (an older package, a copy made over a
|
|
157
|
+
* live directory by another program): the generation does not move. A directory made or removed whole has no
|
|
158
|
+
* generation or another's, which serves nothing kept. (3) A stamp that names files outside its own directory (a
|
|
159
|
+
* World with a parent: its view and its ancestors' logs) is never kept under a generation (log.ts).
|
|
160
|
+
*/
|
|
161
|
+
generation(dirPath: string): string | null;
|
|
162
|
+
/** One write is `moved` twice, a byte each: the same file, those two bytes longer. */
|
|
163
|
+
generationAfterOwnWrite(dirPath: string, before: string): string | null;
|
|
164
|
+
/** One more byte on the generation beside `path`, where `path` is one of a tree's files. A directory that is not
|
|
165
|
+
* there has no reader to tell: the write that makes it is followed by the byte that makes its generation. */
|
|
166
|
+
private moved;
|
|
167
|
+
/** A write of `path`, with the generation beside it moved before and after where it is one of a tree's files. */
|
|
168
|
+
private writing;
|
|
69
169
|
readLines(path: string): string[];
|
|
70
170
|
readRange(path: string, start: number): string | null;
|
|
71
171
|
append(path: string, data: string): void;
|
|
@@ -79,17 +179,18 @@ export declare class FsWorldStore implements WorldStore {
|
|
|
79
179
|
remove(path: string): void;
|
|
80
180
|
mkdir(dirPath: string): void;
|
|
81
181
|
list(dirPath: string): string[];
|
|
182
|
+
/** The real path of each path already resolved: a state directory's does not move under a running process, and
|
|
183
|
+
* every history lock asked for it again. A path that does not exist yet is not kept. */
|
|
184
|
+
private readonly canonical;
|
|
82
185
|
canonicalPath(path: string): string;
|
|
83
186
|
stat(path: string): WorldStat | null;
|
|
84
187
|
/** Run `fn` holding an exclusive cross-process file lock. The lockfile records
|
|
85
|
-
*
|
|
188
|
+
* identity, operation, caller and any nested wait; a contender that finds a stale lock (dead pid or age >
|
|
86
189
|
* LOCK_STALE_MS) reclaims it by atomically renaming it aside — so a crashed holder
|
|
87
190
|
* can't wedge the world forever. Reclaim is race-safe: only the process that wins
|
|
88
191
|
* the rename clears the stale inode, and the exclusive `wx` create still decides the
|
|
89
192
|
* winner. (Moved verbatim from storage.ts `withFileLock`.) */
|
|
90
|
-
withLock<T>(lockPath: string, fn: () => T, options?:
|
|
91
|
-
retainLiveOwner?: boolean;
|
|
92
|
-
}): T;
|
|
193
|
+
withLock<T>(lockPath: string, fn: () => T, options?: WorldLockOptions): T;
|
|
93
194
|
}
|
|
94
195
|
/**
|
|
95
196
|
* An in-memory store. Files are entries in a `Map<path, string>`; directories are
|