@mcp-b/do-runtime 0.1.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +110 -0
  3. package/LICENSE.workerd +176 -0
  4. package/NOTICE +7 -0
  5. package/README.md +282 -0
  6. package/dist/backends/node-sqlite.d.ts +38 -0
  7. package/dist/backends/node-sqlite.js +335 -0
  8. package/dist/backends/node-sqlite.js.map +1 -0
  9. package/dist/backends/sqlite-wasm.d.ts +130 -0
  10. package/dist/backends/sqlite-wasm.js +259 -0
  11. package/dist/backends/sqlite-wasm.js.map +1 -0
  12. package/dist/chunks/sqlite-DFg92Tgt.js +498 -0
  13. package/dist/chunks/sqlite-DFg92Tgt.js.map +1 -0
  14. package/dist/cloudflare-workers.js +351 -0
  15. package/dist/cloudflare-workers.js.map +1 -0
  16. package/dist/conformance/host.d.ts +58 -0
  17. package/dist/conformance.js +18 -0
  18. package/dist/conformance.js.map +1 -0
  19. package/dist/index.js +7184 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/server/alarm-scheduler.js +513 -0
  22. package/dist/server/alarm-scheduler.js.map +1 -0
  23. package/dist/src/api/actor-state.d.ts +396 -0
  24. package/dist/src/api/actor.d.ts +306 -0
  25. package/dist/src/api/cloudflare-workers.d.ts +259 -0
  26. package/dist/src/api/export-loopback.d.ts +264 -0
  27. package/dist/src/api/global-scope.d.ts +262 -0
  28. package/dist/src/api/http.d.ts +52 -0
  29. package/dist/src/api/sql.d.ts +188 -0
  30. package/dist/src/api/sync-kv.d.ts +51 -0
  31. package/dist/src/api/web-socket.d.ts +93 -0
  32. package/dist/src/api/worker-loader.d.ts +354 -0
  33. package/dist/src/index.d.ts +130 -0
  34. package/dist/src/io/actor-cache.d.ts +203 -0
  35. package/dist/src/io/actor-id.d.ts +74 -0
  36. package/dist/src/io/actor-sqlite.d.ts +298 -0
  37. package/dist/src/io/io-channels.d.ts +191 -0
  38. package/dist/src/io/io-context.d.ts +451 -0
  39. package/dist/src/io/io-gate.d.ts +298 -0
  40. package/dist/src/io/worker-source.d.ts +108 -0
  41. package/dist/src/io/worker.d.ts +88 -0
  42. package/dist/src/server/actor-container.d.ts +525 -0
  43. package/dist/src/server/actor-id-impl.d.ts +118 -0
  44. package/dist/src/server/alarm-scheduler.d.ts +201 -0
  45. package/dist/src/server/facet-deletion.d.ts +156 -0
  46. package/dist/src/server/facet-tree-index.d.ts +94 -0
  47. package/dist/src/server/sha256.d.ts +39 -0
  48. package/dist/src/transport/rpc-session.d.ts +34 -0
  49. package/dist/src/util/sqlite-kv.d.ts +98 -0
  50. package/dist/src/util/sqlite-metadata.d.ts +46 -0
  51. package/dist/src/util/sqlite.d.ts +291 -0
  52. package/package.json +111 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * ← workerd `src/workerd/server/alarm-scheduler.{h,c++}`
3
+ *
4
+ * Delivery and retry: the `_cf_ALARM` table, the watchdog arming, the queued
5
+ * alarm, and the retry ladder. Measured on real workerd: an alarm re-armed for
6
+ * `Date.now()` from inside a running handler does NOT re-enter — delivery is
7
+ * serialised (`enter:1, exit:1, enter:2, exit:2`). That property is load
8
+ * bearing; `_cf_executingScheduleRowId` upstream is safe only because of it
9
+ * (§2.3). Here it falls out of the queued alarm: a `setAlarm` that arrives
10
+ * while a handler runs is stored on the entry and started only after the run
11
+ * finishes (`alarm-scheduler.c++:116-124`, `:220-227`).
12
+ *
13
+ * **This is runtime-internal, and it is what a host puts behind
14
+ * `ActorPorts.alarms`.** Upstream wires it the same way: `ActorSqliteHooks`
15
+ * (`server.c++:3199-3219`) is a three-line adapter whose `scheduleRun` is
16
+ * `setAlarm`/`deleteAlarm` on the scheduler, and the scheduler is built once per
17
+ * namespace (`server.c++:2325-2350`) rather than once per actor. `hooks(actorId)`
18
+ * below is that adapter, so a host composes the two instead of writing its own
19
+ * ladder.
20
+ *
21
+ * **One deliberate divergence, and it is the table's shape.** Upstream keeps the
22
+ * retry ladder in memory and reloads every alarm with its counters at zero,
23
+ * which is right for a process that lives for hours and is a regression on a
24
+ * service worker Chrome evicts after seconds — see `_cf_ALARM` below and the
25
+ * README's divergence table. Everything else here is upstream's, line for line.
26
+ *
27
+ * Spec: §1.8, §2.6, decisions 6, 11 and 16 in
28
+ * docs/decisions.md.
29
+ */
30
+ import type { AlarmOutlet } from "../io/actor-sqlite.js";
31
+ import type { Timer } from "../io/io-context.js";
32
+ import { type SqlDatabase } from "../util/sqlite.js";
33
+ /**
34
+ * ← `WorkerInterface::ALARM_RETRY_START_SECONDS` (`io/worker-interface.h:130`),
35
+ * re-declared as `AlarmScheduler::RETRY_START_SECONDS` (`alarm-scheduler.h:42`).
36
+ *
37
+ * "not a duration so we can left shift it" — upstream's own comment, and the
38
+ * reason the ladder below is a shift rather than a table.
39
+ */
40
+ export declare const ALARM_RETRY_START_SECONDS = 2;
41
+ /**
42
+ * ← `WorkerInterface::ALARM_RETRY_MAX_TRIES` (`io/worker-interface.h:131`) /
43
+ * `AlarmScheduler::RETRY_MAX_TRIES` (`alarm-scheduler.h:45`).
44
+ *
45
+ * "Max number of 'valid' retry attempts, i.e the worker returned an error."
46
+ * It bounds `countedRetry`, NOT `backoff`: a run of failures that do not count
47
+ * against the limit is retried forever, and its delay is bounded by
48
+ * `RETRY_BACKOFF_MAX` instead.
49
+ */
50
+ export declare const ALARM_RETRY_MAX_TRIES = 6;
51
+ /**
52
+ * ← `AlarmScheduler::RETRY_BACKOFF_MAX` (`alarm-scheduler.h:50`).
53
+ *
54
+ * "Bound for exponential backoff when RETRY_MAX_TRIES is exceeded due to
55
+ * internal errors. 2 << 9 is 1024 seconds, about 17 minutes. Total time spent in
56
+ * retries once the backoff limit is reached is over 30 minutes."
57
+ */
58
+ export declare const RETRY_BACKOFF_MAX = 9;
59
+ /**
60
+ * ← `AlarmScheduler::RETRY_JITTER_FACTOR` (`alarm-scheduler.h:54`).
61
+ *
62
+ * "How much jitter should be applied to retry times to avoid bundled retries
63
+ * overloading some common dependency between a set of failed alarms."
64
+ */
65
+ export declare const RETRY_JITTER_FACTOR = 0.25;
66
+ /**
67
+ * ← `(AlarmScheduler::RETRY_START_SECONDS << backoff) * kj::SECONDS`
68
+ * (`alarm-scheduler.c++:270`), before jitter.
69
+ *
70
+ * It refuses a backoff outside `[0, RETRY_BACKOFF_MAX]` rather than shifting it,
71
+ * because JS's `<<` is a 32-bit operator: an unclamped counter would wrap to a
72
+ * zero or negative delay — a hot retry loop — instead of saturating. The ladder
73
+ * clamps immediately above its own call, exactly where upstream does; this is
74
+ * what makes that clamp load bearing rather than decorative.
75
+ */
76
+ export declare function alarmRetryDelayMs(backoff: number): number;
77
+ /**
78
+ * ← `EventOutcome` (`io/outcome.capnp`), restricted to the values the alarm path
79
+ * can produce.
80
+ *
81
+ * The whole enum is a metrics type with no port — divergence 154 records that
82
+ * for `waitUntilStatus()` — but `runAlarm` reads one bit of it
83
+ * (`alarm-scheduler.c++:162`, `result.outcome != EventOutcome::OK`), so the
84
+ * values `ServiceWorkerGlobalScope::runAlarm` actually returns are named here
85
+ * and the rest are not.
86
+ */
87
+ export type EventOutcome = "ok" | "canceled" | "script-not-found" | "exception" | "exceeded-cpu" | "unknown";
88
+ /**
89
+ * ← `WorkerInterface::AlarmResult` (`io/worker-interface.h:71-81`).
90
+ *
91
+ * Upstream defaults all three fields; here every one is required, because the
92
+ * producer is `server/actor-container.ts` rather than a capnp wire default and a
93
+ * silently-defaulted `retryCountsAgainstLimit` is the difference between an
94
+ * alarm that survives a broken actor and one that is abandoned.
95
+ */
96
+ export type AlarmResult = {
97
+ readonly retry: boolean;
98
+ readonly retryCountsAgainstLimit: boolean;
99
+ readonly outcome: EventOutcome;
100
+ readonly errorDescription?: string;
101
+ };
102
+ /**
103
+ * ← the `WorkerInterface` `GetActorFn` hands back
104
+ * (`alarm-scheduler.c++:160`, `:244`), restricted to its two alarm members.
105
+ *
106
+ * `WorkerInterface` itself has no port — it is capnp dispatch, and divergence
107
+ * 176 records it collapsing into the stub the transport returns — so the seam is
108
+ * the two methods the scheduler calls. `ActorContainer` satisfies it
109
+ * structurally; `deliverAlarm` is upstream's `runAlarm` under the name Section
110
+ * 6b already gave it.
111
+ */
112
+ export interface AlarmTarget {
113
+ deliverAlarm(scheduledTime: number, retryCount: number): Promise<AlarmResult>;
114
+ /**
115
+ * ← `WorkerInterface::abandonAlarm` (`io/worker-interface.h:114`): "Returns the
116
+ * actor's stored alarm time if it differs from scheduledTime (i.e. the user
117
+ * set a new alarm), or null if the alarm was cleared or no alarm was stored."
118
+ */
119
+ abandonAlarm(scheduledTime: number): Promise<number | null>;
120
+ }
121
+ /** ← `AlarmScheduler::GetActorFn` (`alarm-scheduler.h:56`). */
122
+ export type GetActorFn = (actorId: string) => AlarmTarget;
123
+ export type AlarmSchedulerOptions = {
124
+ /**
125
+ * ← the `const kj::Clock&` and the `kj::Timer&` upstream takes separately
126
+ * (`alarm-scheduler.h:58-59`). One object here because `Timer.now()` is
127
+ * already wall-clock milliseconds — `IoContext::now()` reads the same one —
128
+ * so nothing distinguishes the two. `checkTimestamp`'s re-check loop stays,
129
+ * because a JS timer really can fire a fraction of a millisecond early
130
+ * relative to the clock it is compared against.
131
+ */
132
+ timer: Timer;
133
+ /**
134
+ * The database `_cf_ALARM` lives in — upstream's `metadata.sqlite`, one per
135
+ * namespace beside the per-actor files (`server.c++:2336-2346`).
136
+ *
137
+ * Already open, where upstream's constructor opens it from a vfs and a path:
138
+ * `SqlDatabaseProvider.open` is asynchronous and a constructor cannot await,
139
+ * which is the same reason `createActorContainer` is a promise.
140
+ */
141
+ db: SqlDatabase;
142
+ getActor: GetActorFn;
143
+ /**
144
+ * Browser hosts can mirror the earliest durable wake onto a platform watchdog
145
+ * such as `chrome.alarms`. Workerd needs no such seam because its process owns
146
+ * the scheduler timer.
147
+ */
148
+ projectWake?: (scheduledTime: number | null) => Promise<void> | void;
149
+ /**
150
+ * ← `std::default_random_engine`, seeded from the monotonic clock
151
+ * (`alarm-scheduler.c++:20-27`). A test seam on a runtime-internal class, not
152
+ * a substrate port: the jitter is the one part of the ladder that is
153
+ * deliberately not a function of its inputs.
154
+ */
155
+ random?: () => number;
156
+ };
157
+ /**
158
+ * Allows scheduling alarm executions at specific times, returning a promise
159
+ * representing the completion of the alarm event.
160
+ */
161
+ export declare class AlarmScheduler {
162
+ #private;
163
+ constructor(options: AlarmSchedulerOptions);
164
+ /**
165
+ * ← `getAlarm` (`alarm-scheduler.c++:84-99`), including its TODO: "Might be
166
+ * able to simplify AlarmScheduler somewhat, now that ActorSqlite no longer
167
+ * relies on it for getAlarm()?"
168
+ */
169
+ getAlarm(actorId: string): number | null;
170
+ /**
171
+ * ← `setAlarm` (`alarm-scheduler.c++:101-127`).
172
+ *
173
+ * The `boolean` is upstream's `query.changeCount() > 0`, and it is constant
174
+ * true against SQLite's semantics: an `INSERT … ON CONFLICT DO UPDATE` always
175
+ * reports one changed row, even when the value is unchanged. No caller reads it
176
+ * — `ActorSqliteHooks::scheduleRun` discards it — so it is kept as upstream's
177
+ * shape rather than as a signal.
178
+ */
179
+ setAlarm(actorId: string, scheduledTime: number): boolean;
180
+ /** ← `deleteAll` (`alarm-scheduler.c++:129-134`). */
181
+ deleteAll(): void;
182
+ /** ← `deleteAlarm` (`alarm-scheduler.c++:136-156`). */
183
+ deleteAlarm(actorId: string): boolean;
184
+ /**
185
+ * ← `ActorSqliteHooks` (`server.c++:3199-3219`), which is the whole of how an
186
+ * actor's storage engine reaches a scheduler: one adapter per actor, holding
187
+ * that actor's key, turning a time into `setAlarm` or `deleteAlarm`.
188
+ */
189
+ hooks(actorId: string): AlarmOutlet;
190
+ /**
191
+ * ← `taskFailed`'s `KJ_LOG(WARNING, e)` (`alarm-scheduler.c++:289-291`), and
192
+ * the two other log sites in `makeAlarmTask` that report a failure and carry
193
+ * on (`:202`, `:285`).
194
+ *
195
+ * This package has no logger, so the exception is kept instead of written —
196
+ * the same treatment divergence 154 records for `waitUntilStatus()`, and for
197
+ * the same reason: a background failure that is neither logged nor readable is
198
+ * one nothing can notice.
199
+ */
200
+ taskFailure(): unknown;
201
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * ← workerd `NO upstream correspondence`
3
+ *
4
+ * Generation-fenced deletion receipts, serialized subtree deletion, reference
5
+ * epochs. Keyed by facet id, never by an app address.
6
+ *
7
+ * **Why there is no upstream twin.** `ActorContainer::deleteFacet`
8
+ * (`server.c++:2642-2655`) aborts the child and then calls
9
+ * `directory->remove(...)` — synchronous, in-process, on a real filesystem. It
10
+ * cannot be interrupted between "the app asked" and "the bytes are gone", so it
11
+ * needs no record that it was asked. Here the same call is
12
+ * `FacetHost.deleteStorage`, which is asynchronous because it is the HOST's and a
13
+ * host's storage removal is not required to be prompt: OPFS removal is
14
+ * asynchronous in general, and the extension's supervisor crosses a worker to do
15
+ * it. `ctx.facets.delete()` is synchronous and `void` regardless — so the app's
16
+ * request outlives the process that has to carry it out. §2.7 records the browser
17
+ * host's answer to that, generation-fenced receipts, as "a genuine improvement
18
+ * over workerd, which has no equivalent because its facets are in-process". This
19
+ * is that answer, moved down into the runtime and re-keyed.
20
+ *
21
+ * In-process facets do not remove this gap. What upstream gets from them is that
22
+ * `kj::Directory::remove` is a synchronous call it can finish before returning.
23
+ * The seam here is `Promise<void>` whoever implements it, so the gap between "the
24
+ * app asked" and "the bytes are gone" is still real and still spans a possible
25
+ * teardown. The two conformance lanes happen to close that gap promptly — one
26
+ * through `rmSync`, one through the SAH pool's `unlink` — and the receipt is what
27
+ * makes a host that cannot, such as the extension's supervisor, survivable.
28
+ *
29
+ * **What changed in the move, and it is the whole of the change.** The
30
+ * extension's version (`offscreen/worker/host/facet-deletion.ts`) is keyed by an
31
+ * `AgentWorkerAddress` — a root name plus a path of `{className, name}` steps —
32
+ * and derives "is this address inside that subtree" by comparing path prefixes.
33
+ * Here the key is a `FacetId`, the small integer `server/facet-tree-index.ts`
34
+ * assigns, and ancestry is not derivable from it: id 7 says nothing about who its
35
+ * parent is. So every operation that needs a subtree is *given* one, computed
36
+ * from the index by the caller that already holds it, and this file has no
37
+ * opinion about tree shape at all. That is a smaller module, not a larger one:
38
+ * three of the extension's helpers — `isAgentAddressInTree`,
39
+ * `isAgentStorageKeyInTree`, `isAgentStorageEntryInTree` — exist only to answer
40
+ * the ancestry question from a string, and none of them has anything to do here.
41
+ *
42
+ * Storage is the facet-tree database rather than the actor's own, and that is
43
+ * load bearing: `SqliteKv::deleteAll()` calls `db.reset()`
44
+ * (`util/sqlite-kv.ts`), which replaces the actor's database file wholesale. A
45
+ * receipt recorded there would be destroyed by the very `deleteAll()` whose
46
+ * cascade it exists to make recoverable. It sits beside the tree index for the
47
+ * same reason the index does — both are facts about the tree rather than about
48
+ * any one actor's contents.
49
+ *
50
+ * Spec: §2.7, decision 14 in docs/decisions.md.
51
+ */
52
+ import { type SqlDatabase } from "../util/sqlite.js";
53
+ import type { FacetId } from "./actor-container.js";
54
+ /**
55
+ * One recorded intent to delete a facet's storage.
56
+ *
57
+ * `generation` is the fence. A receipt is cleared only if the row still carries
58
+ * the generation this receipt was issued with, so a delete that was requested
59
+ * again while the first deletion was in flight cannot have its second request
60
+ * erased by the first request's completion.
61
+ */
62
+ export type FacetDeletionReceipt = {
63
+ readonly id: FacetId;
64
+ readonly generation: number;
65
+ };
66
+ /**
67
+ * Parent-owned durable receipts for the synchronous `ctx.facets.delete()`
68
+ * boundary. The doomed child never owns its own deletion decision — it may not
69
+ * be running, and if it is, it is the thing being destroyed.
70
+ */
71
+ export declare class FacetDeletionReceiptStore {
72
+ #private;
73
+ constructor(db: SqlDatabase);
74
+ /** Bumps the generation for `id` and returns the receipt naming it. */
75
+ record(id: FacetId): FacetDeletionReceipt;
76
+ read(id: FacetId): FacetDeletionReceipt | undefined;
77
+ /**
78
+ * Every outstanding receipt, oldest facet first, for boot-time replay.
79
+ *
80
+ * The `ORDER BY` cannot be shown to matter and is kept anyway: `facet_id` is
81
+ * an `INTEGER PRIMARY KEY`, which is the rowid, so both backends scan the
82
+ * table in that order with or without it. Removing it survives the whole
83
+ * suite — a mutant that no test can kill, because killing it needs a SQLite
84
+ * that returns rows out of rowid order, and nothing this package can reach
85
+ * does. Relying on the scan order rather than saying so is the kind of thing
86
+ * that is right until a schema change makes it silently wrong.
87
+ */
88
+ list(): FacetDeletionReceipt[];
89
+ /**
90
+ * Clears the receipt if and only if it is still the one that was issued.
91
+ * Returns false when a newer request has superseded it, which is the whole
92
+ * point of the generation.
93
+ */
94
+ clear(receipt: FacetDeletionReceipt): boolean;
95
+ }
96
+ /**
97
+ * Replays and generation-fences one parent's durable child-deletion receipts.
98
+ *
99
+ * Recording is synchronous — that is the boundary `ctx.facets.delete()` has to
100
+ * hold — and only the physical deletion crosses an async boundary. A second
101
+ * delete of the same facet while the first is in flight records a newer
102
+ * generation and queues behind it rather than racing it, so a facet cannot be
103
+ * half-deleted by two overlapping attempts.
104
+ */
105
+ export declare class FacetDeletionController {
106
+ #private;
107
+ constructor(receipts: FacetDeletionReceiptStore, deleteSubtree: (receipt: FacetDeletionReceipt) => Promise<void>);
108
+ /**
109
+ * Record the intent durably now, then carry it out after the caller's actor
110
+ * ordering has settled. The record is synchronous even when the barrier is
111
+ * still pending.
112
+ */
113
+ delete(id: FacetId, waitBeforeDelete?: Promise<unknown>): Promise<void>;
114
+ /** Carry out whatever is still recorded for `id`, until nothing is. */
115
+ flush(id: FacetId): Promise<void>;
116
+ /** ← boot. Every receipt a previous session left behind is carried out before the actor runs. */
117
+ recoverAll(): Promise<void>;
118
+ }
119
+ /**
120
+ * Serializes physical subtree deletion while retaining subtree-aware waits.
121
+ *
122
+ * Two deletions that overlap in the tree must not run at once: the inner one
123
+ * would be removing files the outer one is walking. Serializing every deletion
124
+ * is the simplest thing that is correct, and deletion is not on any hot path.
125
+ * `waitFor` is the other half — anything about to *use* a facet has to wait for
126
+ * a pending deletion that covers it, and the ids each pending operation covers
127
+ * are recorded rather than derived, because a facet id does not encode its
128
+ * ancestry.
129
+ */
130
+ export declare class SerializedSubtreeDeletionQueue {
131
+ #private;
132
+ /** Runs `operation` after every operation already queued, covering `ids`. */
133
+ run(ids: Iterable<FacetId>, operation: () => Promise<void>): Promise<void>;
134
+ /** Resolves once no queued deletion covers `id`. Failures are not the waiter's to report. */
135
+ waitFor(id: FacetId): Promise<void>;
136
+ }
137
+ /**
138
+ * Epochs make every capability captured before an ancestor abort or delete
139
+ * stale.
140
+ *
141
+ * The hazard this closes has no workerd equivalent for the same reason the
142
+ * receipts do not: upstream's `abortFacet` erases the map entry and the stub
143
+ * that was handed out is refcounted against a container that is now broken, so
144
+ * a later call on it fails by itself. Here the stub is a value that outlives the
145
+ * placement, so something has to be able to say "the thing you are holding was
146
+ * torn down". Invalidation is by explicit subtree because a `FacetId` does not
147
+ * encode ancestry.
148
+ */
149
+ export declare class FacetReferenceEpochs {
150
+ #private;
151
+ /** The epoch to remember alongside a capability. */
152
+ capture(id: FacetId): number;
153
+ /** Bumps `root` and every id in `subtree`, so captures older than this call stop matching. */
154
+ invalidate(root: FacetId, subtree?: Iterable<FacetId>): void;
155
+ isCurrent(id: FacetId, epoch: number): boolean;
156
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * ← workerd `src/workerd/server/facet-tree-index.{h,c++}`
3
+ *
4
+ * Upstream's own summary: "Implements an index, stored on disk, which maps
5
+ * leaves of a tree to small integers in a stable way." One facet — id zero — is
6
+ * the root; every other facet has a parent and a name, names are unique among
7
+ * siblings but not globally, and each (parent, name) pair is assigned the next
8
+ * sequential id the first time it is seen. Deleting a facet does not release its
9
+ * id: recreating the same name under the same parent gets the same id back,
10
+ * which is what decision 14 means by "stable ids across delete-and-recreate."
11
+ *
12
+ * The whole index is held in memory, loaded at construction, because upstream
13
+ * assumes "the total number of facets created for a single Durable Object over
14
+ * its entire lifetime will never be very large" (`facet-tree-index.h:19-22`).
15
+ * That is what makes the file append-only, and the append-only format is what
16
+ * makes a torn tail safe to discard: an entry written but not synced cannot have
17
+ * been relied on, so a nonsensical entry ends the read and the remainder is
18
+ * truncated away.
19
+ *
20
+ * **The one seam: `kj::File` becomes `IndexFile`.** Upstream takes a
21
+ * `kj::Own<const kj::File>` and calls exactly four members on it —
22
+ * `readAllBytes`, `write`, `truncate` and `datasync`. There is no `kj/filesystem`
23
+ * port and no reason to build one for four methods, so those four become an
24
+ * interface and `server/` supplies it. Every method is synchronous because every
25
+ * method upstream is, and because `facets.get` is synchronous all the way down;
26
+ * both substrates can answer that (`FileSystemSyncAccessHandle` in a worker,
27
+ * `node:fs`'s sync family), which is the same shape the storage backends already
28
+ * take.
29
+ *
30
+ * **A name is its UTF-8 bytes, not its JS string.** Upstream's names are
31
+ * `kj::String`s, so the on-disk bytes *are* the identity and the ordering.
32
+ * `TextEncoder` is not injective on JS strings — every lone surrogate encodes to
33
+ * U+FFFD — so keying this index by the JS string would let two distinct names
34
+ * collide on disk and share one facet's storage file after a reload. Entries are
35
+ * therefore identified and ordered by their encoded bytes, and `forEachChild`
36
+ * reports the round-tripped name, which is what a reload would report. That also
37
+ * makes the ordering exact: upstream's `kj::TreeSet` orders by `kj::String`'s
38
+ * byte comparison, where JS `<` would order by UTF-16 code unit and disagree for
39
+ * any name mixing astral characters with U+E000..U+FFFF.
40
+ *
41
+ * **The scaffolding recorded a divergence here that is wrong, and it is not
42
+ * kept.** That header said workerd keeps one index per root actor while "our tree
43
+ * spans workers, so each parent indexes its direct children." Upstream's index
44
+ * already *is* keyed by (parent, name) — `getId(parent, name)`,
45
+ * `forEachChild(parentId, …)` — so a per-parent index changes nothing about what
46
+ * is indexed and only changes what an id *means*: upstream's ids are sequential
47
+ * across the whole tree, and they name storage files in one flat namespace,
48
+ * `<actor-id>.<facetId>.sqlite` (`server.c++:2737-2743`). Per-parent counters
49
+ * would mint id 1 under every parent and collide those files. Nor does the
50
+ * premise hold: the index is owned by the root *container*, not by an actor's
51
+ * worker (`server.c++:2680-2681`, `:2697`), and `FacetHost` already speaks a flat
52
+ * `FacetId = number` with a precomputed subtree — which only a whole-tree index
53
+ * can produce. Ported as upstream has it.
54
+ *
55
+ * Spec: §1.10, decision 14 in docs/decisions.md.
56
+ */
57
+ /**
58
+ * ← the `kj::File` members `FacetTreeIndex` calls, and nothing else.
59
+ *
60
+ * `datasync()` is not decoration: the format's recovery story is that an entry
61
+ * which was written but never synced was never relied upon, so a substrate that
62
+ * drops it turns a torn tail from "discard and reassign" into "two facets, one
63
+ * id".
64
+ */
65
+ export interface IndexFile {
66
+ /** ← `kj::File::readAllBytes()`. Called once, at construction. */
67
+ readAllBytes(): Uint8Array;
68
+ /** ← `kj::File::write(offset, data)`. Extends the file when it writes past the end. */
69
+ write(offset: number, data: Uint8Array): void;
70
+ /** ← `kj::File::truncate(size)`. Only ever shrinks, to drop a corrupted tail. */
71
+ truncate(size: number): void;
72
+ /** ← `kj::File::datasync()`. */
73
+ datasync(): void;
74
+ }
75
+ /** ← `FacetTreeIndex` (`facet-tree-index.h:50-123`). */
76
+ export declare class FacetTreeIndex {
77
+ #private;
78
+ /**
79
+ * ← the constructor (`facet-tree-index.c++:11-86`). "Construct the index,
80
+ * reading the given file to populate the initial index, and then arranging to
81
+ * append new entries to the file as needed."
82
+ */
83
+ constructor(file: IndexFile);
84
+ /** ← `FacetTreeIndex::getId`. "Gets the ID for the given facet, assigning it if needed." */
85
+ getId(parent: number, name: string): number;
86
+ /**
87
+ * ← `FacetTreeIndex::forEachChild`. "For each child of the given parent ID,
88
+ * call the callback."
89
+ *
90
+ * Upstream walks a `kj::TreeSet` range, so children arrive ordered by name
91
+ * rather than by id; the sort here is that ordering, over the same bytes.
92
+ */
93
+ forEachChild(parentId: number, callback: (childId: number, name: string) => void): void;
94
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ← workerd `NO upstream correspondence`
3
+ *
4
+ * The substrate replacement for the two includes `server/actor-id-impl.{h,c++}`
5
+ * makes — `<openssl/sha.h>` and `<openssl/hmac.h>` — and nothing else. Only
6
+ * `actor-id-impl.ts` consumes it, which is why it sits here rather than in
7
+ * `util/`: `src/util/` corresponds 1:1 to `src/workerd/util/`, and workerd has no
8
+ * digest there. `server/facet-deletion.ts` sets the precedent for a file in this
9
+ * directory with no upstream twin.
10
+ *
11
+ * **Why this exists at all.** Every method on `ActorIdFactory` is synchronous
12
+ * (`io/actor-id.h:66-71`), and the browser exposes no synchronous digest:
13
+ * `crypto.subtle.digest` returns a promise, and `node:crypto` is one lane only.
14
+ * So the algorithm is written out. It is a **substrate** divergence in decision
15
+ * 16's sense and not a semantic one — the bytes are the bytes FIPS 180-4 and RFC
16
+ * 2104 specify, which is exactly what BoringSSL computes, so an id minted here is
17
+ * the same 64 hex digits workerd mints from the same unique key. That equality is
18
+ * the whole reason for writing the real digest rather than a cheaper keyed
19
+ * function the reduced threat model would have tolerated: it keeps workerd
20
+ * available as an oracle for ids, where an invented construction would have made
21
+ * every future id question original research.
22
+ *
23
+ * No dependency was added. Nothing in the workspace ships a synchronous SHA-256,
24
+ * and the catalog's `crypto-browserify` is a CommonJS bundler shim for the
25
+ * extension that would not typecheck under this package's `WebWorker`-only lib.
26
+ */
27
+ /** ← `SHA256_DIGEST_LENGTH`. */
28
+ export declare const SHA256_DIGEST_LENGTH = 32;
29
+ /** ← `SHA256(data, length, out)`. FIPS 180-4 §6.2. */
30
+ export declare function sha256(message: Uint8Array): Uint8Array;
31
+ /**
32
+ * ← `HMAC(EVP_sha256(), key, keyLength, data, dataLength, out, &outLength)`.
33
+ * RFC 2104.
34
+ *
35
+ * Upstream's comment on why a MAC is used for something that is not
36
+ * authentication: "We're using HMAC as a keyed hash here, not actually for
37
+ * authentication, but it works" (`actor-id-impl.c++:74-75`).
38
+ */
39
+ export declare function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ← workerd `NO upstream correspondence (capnweb adaptation)`
3
+ *
4
+ * The one door onto a capnweb session, so that decision 18's identity graft
5
+ * cannot be skipped by establishing one some other way.
6
+ *
7
+ * The `RpcTarget` identity graft lives here because the guarantee belongs to
8
+ * the call that establishes a session, not to whichever sibling module happened
9
+ * to run a side effect first. It is re-applied for every session and is
10
+ * idempotent.
11
+ *
12
+ * **What this deliberately is not.** It is not a transport abstraction and takes
13
+ * no options capnweb does not: a lane or a host that needs
14
+ * `newWebSocketRpcSession` instead should call `reconcileRpcTargetIdentity()`
15
+ * itself and say so, rather than growing this into a second capnweb API. The
16
+ * `MessagePort` form is the only one this substrate uses — the extension's
17
+ * offscreen↔worker hop and the browser lane's page↔actor and page↔alarms hops are
18
+ * all `newMessagePortRpcSession` — and a port carries structured clone, which is
19
+ * what makes capnweb's `structuredClonable` encoding level available.
20
+ *
21
+ * Spec: decision 18 in docs/decisions.md.
22
+ */
23
+ import { type RpcStub } from "capnweb";
24
+ /** Make the declared Workers RpcTarget recognizable to capnweb by reference. */
25
+ export declare function reconcileRpcTargetIdentity(): void;
26
+ /**
27
+ * Establish a capnweb session over a `MessagePort`, with the `RpcTarget`
28
+ * identity reconciled first.
29
+ *
30
+ * `localMain` is what the peer reaches; the returned stub is what the peer
31
+ * exposed. Both ends call this — a session is symmetric — and either side may
32
+ * omit its main when it exports nothing.
33
+ */
34
+ export declare function newRpcSession<T = unknown>(port: MessagePort, localMain?: unknown): RpcStub<T>;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * ← workerd `src/workerd/util/sqlite-kv.{h,c++}`
3
+ *
4
+ * KV storage on top of SQLite, for Durable Object storage.
5
+ *
6
+ * The table is named `_cf_KV`. The naming is designed so that if the
7
+ * application is allowed to perform direct SQL queries, we can block it from
8
+ * accessing any table prefixed with `_cf_`.
9
+ *
10
+ * This layer is bytes in, bytes out, exactly as upstream is. The structured
11
+ * value encoding happens above it, in `api/actor-state.ts`, which is where
12
+ * upstream V8-serializes.
13
+ *
14
+ * Three translations, each of them forced:
15
+ *
16
+ * - `get`'s callback exists upstream to avoid copying bytes out of a live
17
+ * sqlite row. Our backends have already materialised the row by the time we
18
+ * see it, so there is no copy to avoid and the value is returned.
19
+ * - `delete_` is spelled `delete` — the C++ name carries a trailing underscore
20
+ * only because `delete` is a keyword there.
21
+ * - Upstream's `Uninitialized` / `Initialized` pair exists solely to hold
22
+ * thirteen `SqliteDatabase::Statement`s. Prepared statements are not part of
23
+ * our backend seam (each `exec` prepares), so the pair collapses into the
24
+ * `tableCreated` flag it already sits beside. The statements survive as
25
+ * `STMT` below: same names, same order, SQL copied verbatim, so the
26
+ * correspondence a reader needs is intact.
27
+ *
28
+ * Not ported: `SqliteKvRegulator`. Its remaining job is `shouldAddQueryStats`,
29
+ * which is row-count billing; neither backend exposes those counters.
30
+ *
31
+ * Spec: §2.4 in docs/decisions.md.
32
+ */
33
+ import { type ResetListener, type SqliteDatabase } from "./sqlite.js";
34
+ export type KeyPtr = string;
35
+ export type ValuePtr = Uint8Array;
36
+ /** ← `SqliteKv::Order`. */
37
+ export type Order = "FORWARD" | "REVERSE";
38
+ /** ← `SqliteKv::WriteOptions`. */
39
+ export type WriteOptions = {
40
+ allowUnconfirmed?: boolean;
41
+ };
42
+ /** ← `SqliteKv::ListCursor::KeyValuePair`, and the shape `put(pairs)` iterates. */
43
+ export type KeyValuePair = {
44
+ key: KeyPtr;
45
+ value: ValuePtr;
46
+ };
47
+ export declare class SqliteKv implements ResetListener {
48
+ #private;
49
+ constructor(db: SqliteDatabase);
50
+ /**
51
+ * Search for a match for the given key. Returns the value if found, undefined
52
+ * if not.
53
+ */
54
+ get(key: KeyPtr): ValuePtr | undefined;
55
+ /**
56
+ * Search for all known keys and values in a range. `end` and `limit` can be
57
+ * undefined to request no constraint be enforced.
58
+ *
59
+ * With a callback, calls it for each row seen and returns the count. Without
60
+ * one, returns a cursor which can be iterated one at a time.
61
+ */
62
+ list(begin: KeyPtr, end: KeyPtr | undefined, limit: number | undefined, order: Order): SqliteKvListCursor;
63
+ list(begin: KeyPtr, end: KeyPtr | undefined, limit: number | undefined, order: Order, callback: (key: KeyPtr, value: ValuePtr) => void): number;
64
+ /** Store a value into the table, or atomically store multiple values. */
65
+ put(key: KeyPtr, value: ValuePtr, options?: WriteOptions): void;
66
+ put(pairs: Iterable<KeyValuePair>, options: WriteOptions): void;
67
+ /** Delete the key and return whether it was matched. */
68
+ delete(key: KeyPtr, options?: WriteOptions): boolean;
69
+ deleteAll(): number;
70
+ /** ResetListener interface: we'll need to recreate the table on the next operation. */
71
+ beforeSqliteReset(): void;
72
+ /** Called by a cursor that has run out of rows, mirroring `~ListCursor::State`. */
73
+ releaseCursor(cursor: SqliteKvListCursor): void;
74
+ }
75
+ /**
76
+ * ← `SqliteKv::ListCursor`.
77
+ *
78
+ * Upstream's iterates a live sqlite statement, which is why only one may be
79
+ * open at a time and why a new `list()` cancels the previous cursor. Our rows
80
+ * arrive materialised, so nothing forces that constraint — it is kept because
81
+ * `wasCanceled()` is part of the contract above this layer, and a cursor whose
82
+ * cancellation depended on the substrate would make the browser and Node lanes
83
+ * disagree. What we do lose is streaming: an unbounded `list()` reads the whole
84
+ * range into memory, where upstream reads a row at a time.
85
+ */
86
+ export declare class SqliteKvListCursor {
87
+ #private;
88
+ constructor(parent: SqliteKv | null, rows: readonly (readonly unknown[])[] | null);
89
+ next(): KeyValuePair | undefined;
90
+ forEach(callback: (key: KeyPtr, value: ValuePtr) => void): number;
91
+ /**
92
+ * If true, the cursor was canceled due to a new list() operation starting.
93
+ * Only one list() is allowed at a time.
94
+ */
95
+ wasCanceled(): boolean;
96
+ /** Called by `SqliteKv` only. */
97
+ cancel(): void;
98
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * ← workerd `src/workerd/util/sqlite-metadata.{h,c++}`
3
+ *
4
+ * A simple metadata kv storage and cache on top of SQLite. Currently used to
5
+ * store:
6
+ *
7
+ * - Durable Object alarm times (hardcoded as key = 1);
8
+ * - a local development bookmark used to simulate the getCurrentBookmark API
9
+ * used by D1 (hardcoded as key = 2), not used in production.
10
+ *
11
+ * The table is named `_cf_METADATA`. The naming is designed so that if the
12
+ * application is allowed to perform direct SQL queries, we can block it from
13
+ * accessing any table prefixed with `_cf_`.
14
+ *
15
+ * Times are milliseconds, not nanoseconds. Upstream stores
16
+ * `(t - UNIX_EPOCH) / kj::NANOSECONDS` as an int64. A JS number runs out of
17
+ * integer precision 104 days into the epoch at nanosecond scale, so storing
18
+ * what upstream stores would silently round every alarm. Milliseconds are also
19
+ * what every caller above this already uses.
20
+ *
21
+ * The local-development bookmark IS ported, which the package's bookmark
22
+ * substrate boundary might seem to rule out. It does not: that boundary is
23
+ * `getCurrentBookmark` / `getBookmarkForTime` / `onNextSessionRestoreBookmark`,
24
+ * which need point-in-time recovery from the storage engine. Key 2 is an
25
+ * integer in a row, and D1 uses it locally precisely because it needs nothing.
26
+ *
27
+ * Spec: §1.8, §2.6 in docs/decisions.md.
28
+ */
29
+ import { type ResetListener, type SqliteDatabase } from "./sqlite.js";
30
+ export declare class SqliteMetadata implements ResetListener {
31
+ #private;
32
+ constructor(db: SqliteDatabase);
33
+ /** Return currently set alarm time, or null. */
34
+ getAlarm(): number | null;
35
+ /**
36
+ * Sets current alarm time, or null. Returns true if the value changed, false
37
+ * if it was already set to the same value.
38
+ */
39
+ setAlarm(currentTime: number | null, allowUnconfirmed: boolean): boolean;
40
+ /** Return the current local development bookmark, or null if none has been set. */
41
+ getLocalDevelopmentBookmark(): number | null;
42
+ /** Set the current ersatz bookmark. */
43
+ setLocalDevelopmentBookmark(bookmark: number): void;
44
+ /** ResetListener interface: we'll need to recreate the table on the next operation. */
45
+ beforeSqliteReset(): void;
46
+ }