@lunora/platform-node 0.0.0 → 1.0.0-alpha.1

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.
@@ -0,0 +1,517 @@
1
+ import { SchemaLike } from '@lunora/shard-engine';
2
+ import { SqlCtxDbOptions, SqlCtxExec, createSqlCtxDb } from '@lunora/sql-store';
3
+ import Database from 'better-sqlite3';
4
+ import { ShardKvStore, QueueBindingLike, MessageBatchLike, SchedulerHost, ShardHost, SocketHost, DirectShardDirectory, PlatformCapabilities, ShardDirectory, R2BucketLike, SocketHandle } from '@lunora/platform';
5
+ import { WorkflowBindingLike } from '@lunora/workflow';
6
+ import { WorkflowRuntime, WorkflowStore } from '@visulima/workflow';
7
+ /**
8
+ * The Node store options — the shared store options minus the two this binding
9
+ * owns. `dialect` is fixed (SQLite) and `exec` is the store's own connection;
10
+ * letting a caller pass either would let them point the writer at a different
11
+ * engine or a different database than the one `migrate` just provisioned.
12
+ */
13
+ type NodeGlobalContextDatabaseOptions = Omit<SqlCtxDbOptions, "dialect" | "exec">;
14
+ /**
15
+ * Wrap a `better-sqlite3` connection as the async exec the store core consumes.
16
+ *
17
+ * `batch` is deliberately **not** implemented. The contract lets an exec that
18
+ * omits it fall back to a sequential `run()` loop, and that fallback is already
19
+ * optimal here: `batch` exists to collapse network round trips (D1 does it
20
+ * atomically in one request; the Hyperdrive adapters dispatch concurrently over
21
+ * a pool), and an embedded database has no round trip to collapse. Declaring it
22
+ * would buy nothing and would opt this exec into the "MAY reorder or
23
+ * parallelize" licence for no reason.
24
+ */
25
+ declare const createNodeSqlExec: (database: Database.Database) => SqlCtxExec;
26
+ /** Options for {@link createNodeGlobalStore}. */
27
+ interface NodeGlobalStoreOptions {
28
+ /**
29
+ * SQLite file backing the `.global()` tables. Defaults to `:memory:`.
30
+ *
31
+ * Deliberately a *separate* database from any shard's: a global table is
32
+ * shared by every shard, so keeping it inside one shard's file would make
33
+ * that shard's lifecycle (and its single-writer gate) the global store's
34
+ * too.
35
+ */
36
+ path?: string;
37
+ }
38
+ /** A `.global()` store for the Node target, plus the handles to run it down. */
39
+ interface NodeGlobalStore {
40
+ /** The underlying connection, for callers that need to run migrations or inspect it. */
41
+ database: Database.Database;
42
+ /** Close the connection. Safe to call more than once. */
43
+ dispose: () => void;
44
+ /** The async exec the store core (and the migration helpers) consume. */
45
+ exec: SqlCtxExec;
46
+ /**
47
+ * Create the schema's `.global()` tables and every companion table
48
+ * (aggregate, rank, search) they need, then backfill the search indexes.
49
+ * Idempotent — every statement is `CREATE TABLE IF NOT EXISTS`-shaped — so
50
+ * it is safe to run on each boot, which is what a dev server does.
51
+ */
52
+ migrate: (schema: SchemaLike, options?: {
53
+ cdc?: boolean;
54
+ }) => Promise<void>;
55
+ /** Build a `.global()` writer bound to this store. */
56
+ writer: (options: NodeGlobalContextDatabaseOptions) => ReturnType<typeof createSqlCtxDb>;
57
+ }
58
+ /**
59
+ * Stand up the `.global()` backend for a Node target.
60
+ *
61
+ * The returned `writer` is the same `createSqlCtxDb` every other backend
62
+ * returns, so the engine above it cannot tell which target it is running on —
63
+ * which is the whole point of the dialect seam.
64
+ */
65
+ declare const createNodeGlobalStore: (options?: NodeGlobalStoreOptions) => NodeGlobalStore;
66
+ /** Build a `ShardKvStore` over a `better-sqlite3` table in the shard's database. */
67
+ declare const createNodeShardKvStore: (database: Database.Database) => ShardKvStore;
68
+ /** Options for {@link createNodeQueueHost}. */
69
+ interface NodeQueueHostOptions<Queues extends Record<string, {
70
+ isLunoraQueue: true;
71
+ }>> {
72
+ /** Base env merged under the derived `QUEUE_*` bindings. */
73
+ env?: Record<string, unknown>;
74
+ /**
75
+ * The host's clock, in epoch ms. Producers and `poll()` share it, so a
76
+ * `delaySeconds` written by `send` is measured against the same reading
77
+ * `poll` compares it to. Defaults to `Date.now`.
78
+ *
79
+ * Injectable because otherwise the two diverge: a test that captures
80
+ * `Date.now()`, sends, and then polls at `captured + delay` misses by however
81
+ * many milliseconds elapsed in between — and the dead-letter re-enqueue,
82
+ * which stamps `enqueued_at` from the poll clock, mixes two clocks in one
83
+ * column.
84
+ */
85
+ now?: () => number;
86
+ /**
87
+ * Deliver one assembled batch. Wire this to `dispatchQueueBatch` from
88
+ * `@lunora/queue` — the host owns storage and batching, not routing, which is
89
+ * the same split the Cloudflare host has.
90
+ *
91
+ * A rejection retries every message the handler left undecided, matching
92
+ * workerd; a normal return acks them.
93
+ */
94
+ onBatch: (batch: MessageBatchLike) => Promise<void> | void;
95
+ /** The declared queues keyed by their `lunora/queues.ts` export name. */
96
+ queues: Queues;
97
+ /** How long a claimed batch stays invisible before redelivery. Defaults to 30s. */
98
+ visibilityTimeoutMs?: number;
99
+ }
100
+ /** A fully-wired Node queue host. */
101
+ interface NodeQueueHost<Queues extends Record<string, {
102
+ isLunoraQueue: true;
103
+ }>> {
104
+ /** Per-export-name `QueueBindingLike` — the map `ctx.queues` consumes. */
105
+ readonly bindings: { [K in keyof Queues]: QueueBindingLike; };
106
+ /**
107
+ * The parked messages — those that exhausted `maxRetries` with no
108
+ * `deadLetterQueue` declared.
109
+ *
110
+ * `list` + `requeue`, mirroring `SchedulerHost.deadLetter` rather than
111
+ * inspection alone: this host justifies parking with "a dropped message with
112
+ * no trace is the thing that makes a queue impossible to debug", and a trace
113
+ * you can read but not act on is only half of that.
114
+ */
115
+ deadLetters: {
116
+ list: (queue: string) => {
117
+ attempts: number;
118
+ body: unknown;
119
+ id: string;
120
+ }[];
121
+ /** Return a parked message to its queue with `attempts` reset. `false` when no such message is parked. */
122
+ requeue: (id: string) => boolean;
123
+ };
124
+ /** The caller's `env` plus one `QUEUE_<EXPORT>` binding per queue. */
125
+ readonly env: Record<string, unknown>;
126
+ /**
127
+ * Deliver every batch currently due, one per queue per call, and return how
128
+ * many batches were delivered. Drives the consumer — nothing here runs on a
129
+ * timer, because this host has no dev server to own one yet.
130
+ */
131
+ poll: (now?: number) => Promise<number>;
132
+ }
133
+ /**
134
+ * Create a Node queue host: one durable table behind every declared queue, a
135
+ * producer binding per queue, and `poll()` to drive the consumer.
136
+ */
137
+ declare const createNodeQueueHost: <Queues extends Record<string, {
138
+ isLunoraQueue: true;
139
+ }>>(database: Database.Database, options: NodeQueueHostOptions<Queues>) => NodeQueueHost<Queues>;
140
+ /** Options for {@link createNodeSchedulerHost}. */
141
+ interface NodeSchedulerHostOptions {
142
+ /**
143
+ * Called when a job or cron tick comes due — this host's stand-in for the
144
+ * Worker fetch `SchedulerDO` makes when its alarm drains the queue.
145
+ *
146
+ * A handler that **throws** marks the delivery failed, which is what drives
147
+ * the retry/dead-letter machinery: the job's `attempts` increments and it is
148
+ * either re-armed with backoff or parked. A handler that resolves marks the
149
+ * job delivered and removes it.
150
+ *
151
+ * Omitting it makes every delivery trivially succeed, which means a caller
152
+ * who forgets to pass one gets a scheduler that quietly drops work — the
153
+ * precise failure plan 267 caught. Supply one in anything real; the
154
+ * conformance host supplies one purely so the TCK can tell a delivery apart
155
+ * from an expired timer.
156
+ */
157
+ onDispatch?: (functionPath: string, args: Record<string, unknown>, job: {
158
+ attempts: number;
159
+ id: string;
160
+ }) => Promise<void> | void;
161
+ }
162
+ /**
163
+ * The scheduler half of a Node platform instance, plus the test-only hook the
164
+ * TCK's dead-letter legs need.
165
+ */
166
+ interface NodeSchedulerHost {
167
+ /**
168
+ * Clear every armed `setTimeout` (one-shot jobs and cron ticks alike) and
169
+ * put the host into a terminal, closed state: afterwards `schedule()` and
170
+ * `cron()` throw rather than arm a fresh timer nothing would ever clear.
171
+ *
172
+ * Rows are left in place on purpose: `dispose()` is shutdown, not deletion,
173
+ * and the next construction over the same database re-arms exactly what was
174
+ * still pending. An armed timer is also a handle that keeps the event loop —
175
+ * and transitively the process — alive until it fires, which is what
176
+ * produces the "worker process failed to exit gracefully" delay if it is
177
+ * left uncleared.
178
+ */
179
+ dispose: () => void;
180
+ scheduler: SchedulerHost;
181
+ /**
182
+ * Drive a pending job straight to its dead-letter state without waiting out
183
+ * a real retry budget. Exactly the hook `ConformanceHost.simulateDeadLetter`
184
+ * describes: the observable invariants of dead-lettering are contract-level,
185
+ * while how many failures it takes to get there is host policy.
186
+ */
187
+ simulateDeadLetter: (id: string) => Promise<boolean>;
188
+ }
189
+ /** Build a durable, SQLite-backed scheduler over `database`. */
190
+ declare const createNodeSchedulerHost: (database: Database.Database, options?: NodeSchedulerHostOptions) => NodeSchedulerHost;
191
+ /** Options for {@link createNodeShardHost}. */
192
+ interface NodeShardHostOptions {
193
+ /**
194
+ * Called when a durable alarm comes due — this host's stand-in for the
195
+ * `alarm()` method workerd invokes on a Durable Object. Fires for alarms
196
+ * set during this process's lifetime AND for one restored from
197
+ * `_lunora_alarm` when the host is constructed over an existing database,
198
+ * including an alarm whose time elapsed while nothing was running.
199
+ *
200
+ * A handler that throws is isolated to its own delivery; it never reaches
201
+ * the caller that set the alarm, because by then that call has long
202
+ * returned.
203
+ */
204
+ onAlarm?: () => Promise<void> | void;
205
+ /**
206
+ * SQLite database file. Defaults to `:memory:` (matching the reference
207
+ * host) — pass a real path to exercise cross-process persistence, the one
208
+ * axis the in-memory reference host cannot.
209
+ */
210
+ path?: string;
211
+ /**
212
+ * The shard key this host serves, threaded through to `ShardHost.shardKey`.
213
+ * Cloudflare derives this from `state.id.name`; a Node host has no
214
+ * equivalent addressing scheme, so the caller supplies it.
215
+ */
216
+ shardKey?: string;
217
+ }
218
+ /**
219
+ * Build a `ShardHost` over a real `better-sqlite3` database.
220
+ *
221
+ * `runSerialized` chains onto a single `tail` promise rather than the
222
+ * reference host's explicit job-array-plus-drain-loop: every queued closure
223
+ * runs once `tail` settles, and `tail` is reset to a version that always
224
+ * resolves (never rejects) so one job's failure cannot wedge the queue for
225
+ * every job after it — the same "no two closures interleave" guarantee,
226
+ * fewer moving parts to get wrong.
227
+ *
228
+ * `transaction` issues raw `BEGIN`/`COMMIT`/`ROLLBACK` — legal here (unlike
229
+ * inside a Cloudflare Durable Object, where the runtime forbids it and
230
+ * callers must use `storage.transaction`) because better-sqlite3 is a plain
231
+ * embedded database with no platform-level transaction primitive layered over
232
+ * it. It runs on its own private `transactionTail` chain, the same shape as
233
+ * `runSerialized`'s but never shared with it: two bare, overlapping
234
+ * `transaction()` calls on this host serialize against each other rather than
235
+ * corrupting each other's commits (raw `BEGIN` on a connection already inside
236
+ * a transaction throws, or worse, interleaves). Routing `transaction` through
237
+ * `runSerialized` itself would deadlock, since the engine already composes
238
+ * `runSerialized(() => transaction(work))` — the inner enqueue would then wait
239
+ * on the outer closure awaiting it.
240
+ *
241
+ * The returned `dispose()` is this host's lifecycle owner: it clears the
242
+ * pending alarm `setTimeout` (so it can never fire against a connection this
243
+ * call is about to close) and then closes the database. Call it whenever a
244
+ * caller is done with the host — composition roots (`createNodePlatform`,
245
+ * the conformance host) route their own `close()`/`cleanup()` through it
246
+ * rather than closing `database` directly, so the alarm timer and the
247
+ * connection are always retired together.
248
+ */
249
+ declare const createNodeShardHost: (options?: NodeShardHostOptions) => {
250
+ database: Database.Database;
251
+ dispose: () => void;
252
+ drain: () => Promise<void>;
253
+ host: ShardHost;
254
+ };
255
+ /** The contracts scoped to one shard key. */
256
+ interface NodeShard {
257
+ /** Durable key-value storage on this shard's database. */
258
+ kv: ShardKvStore;
259
+ /** Single-writer execution, local SQL, transactions, durable alarms. */
260
+ shard: ShardHost;
261
+ /** The key this shard serves. */
262
+ shardKey: string;
263
+ /** This shard's socket registry, with SQLite-persisted attachments. */
264
+ sockets: SocketHost;
265
+ }
266
+ /** Options for {@link createNodeShardRegistry}. */
267
+ interface NodeShardRegistryOptions {
268
+ /**
269
+ * Directory to hold one SQLite file per shard. Omit for in-memory shards,
270
+ * which is right for tests and wrong for anything that must survive a
271
+ * restart — an in-memory registry also has no files to seed its key set
272
+ * from, so its fan-out set is empty until each shard is touched again.
273
+ */
274
+ directory?: string;
275
+ /** Called when a shard's durable alarm comes due. */
276
+ onAlarm?: (shard: NodeShard) => Promise<void> | void;
277
+ /**
278
+ * The app's per-shard request handler — the Node equivalent of
279
+ * `ShardDO.fetch`. This is what a resolved stub dispatches to, and what the
280
+ * query coordinator reaches on every fan-out leg.
281
+ *
282
+ * Omitted, a resolved stub answers with the shard key. That is not a
283
+ * placeholder for convenience: it is the exact behaviour the TCK's
284
+ * directory legs assert (same key ⇒ same body, and a stub is dispatchable),
285
+ * so the conformance run exercises this registry rather than a second
286
+ * implementation kept alive just for tests.
287
+ */
288
+ onFetch?: (request: Request, shard: NodeShard) => Promise<Response> | Response;
289
+ }
290
+ /** An in-process shard registry: placement, hosting, and the fan-out key set. */
291
+ interface NodeShardRegistry {
292
+ /** Dispose every live shard. Safe to call more than once. */
293
+ close: () => void;
294
+ /** The `ShardDirectory` to hand the runtime. */
295
+ directory: DirectShardDirectory;
296
+ /**
297
+ * Every shard key this registry knows about — the set `@lunora/runtime`'s
298
+ * query coordinator fans out over.
299
+ *
300
+ * The `table` argument is accepted and ignored, which is a real
301
+ * approximation worth stating: Cloudflare's dynamic registry tracks which
302
+ * shard keys hold rows for which table, while this one knows only that a
303
+ * shard exists. Answering with every shard is a *superset*, so results stay
304
+ * correct — a shard holding no rows for the table contributes none — at the
305
+ * cost of visiting shards that had nothing to say.
306
+ */
307
+ listShardKeys: (table?: string) => ReadonlyArray<string>;
308
+ /** Materialize (or reuse) the shard serving `key`. */
309
+ shardFor: (key: string) => NodeShard;
310
+ }
311
+ /** Build the in-process shard registry. */
312
+ declare const createNodeShardRegistry: (options?: NodeShardRegistryOptions) => NodeShardRegistry;
313
+ /** Every contract this package provides, composed for one Node process. */
314
+ interface NodePlatform<Queues extends Record<string, {
315
+ isLunoraQueue: true;
316
+ }> = Record<string, never>> {
317
+ /** `using platform = createNodePlatform(...)` support — delegates to `close()`. */
318
+ [Symbol.dispose]: () => void;
319
+ /** What this target supports — see `NODE_CAPABILITIES` in `@lunora/platform`. */
320
+ capabilities: PlatformCapabilities;
321
+ /**
322
+ * Tear this platform instance down: clears the shard host's pending alarm
323
+ * timer and closes its `better-sqlite3` database (and, with it, `kv`'s
324
+ * table — both live on the same connection), then clears every armed
325
+ * scheduler job timer. Nothing in this package closes these resources on
326
+ * its own — a `NodePlatform` a caller stops using without calling `close()`
327
+ * leaks the open file handle (plus its WAL/SHM sidecar files) and keeps
328
+ * the process alive on outstanding timers. Safe to call more than once.
329
+ *
330
+ * **`close()` is a terminal state, not merely a cleanup step.** After it
331
+ * runs: `scheduler.schedule()` throws instead of arming a fresh timer
332
+ * nothing would ever clear; `scheduler.list()` returns `[]` and
333
+ * `scheduler.cancel()` answers `false` (the job map is empty, so this
334
+ * happens naturally, without a throw — keeping teardown-order races
335
+ * benign); `shard.alarms.set()`/`delete()` throw before mutating any
336
+ * in-memory state (checked against the connection's own open/closed state,
337
+ * the single source of truth); `shard.alarms.get()` keeps answering
338
+ * whatever it last held. A no-op instead of a throw would be
339
+ * indistinguishable from a working call — exactly the silent-vanishing
340
+ * this lifecycle exists to end.
341
+ */
342
+ close: () => void;
343
+ /** In-process shard directory (see `createNodeShardRegistry` for what it can and cannot do). */
344
+ directory: ShardDirectory;
345
+ /**
346
+ * Wait for every promise handed to `shard.waitUntil` to settle.
347
+ *
348
+ * Separate from `close()` because the two answer different questions:
349
+ * `close()` releases handles and stays synchronous (it backs
350
+ * `Symbol.dispose`), while draining is inherently awaitable. A graceful
351
+ * shutdown is `await platform.drain()` then `platform.close()`.
352
+ */
353
+ drain: () => Promise<void>;
354
+ /** Durable key-value storage backed by the same `better-sqlite3` database as `shard`. */
355
+ kv: ShardKvStore;
356
+ /**
357
+ * The declared queues, or `undefined` when the caller declared none.
358
+ *
359
+ * Present only when `queues` is passed: with no declarations there is
360
+ * nothing to bind, and handing back an empty host would suggest `ctx.queues`
361
+ * works when no queue exists to send to.
362
+ */
363
+ queues?: NodeQueueHost<Queues>;
364
+ /** Delayed jobs and crons, persisted to the same database and re-armed on construction. */
365
+ scheduler: SchedulerHost;
366
+ /** Single-writer execution, local SQL, transactions, durable alarms. */
367
+ shard: ShardHost;
368
+ /** Socket registry with mutable tags and SQLite-persisted attachments. */
369
+ sockets: SocketHost;
370
+ }
371
+ /**
372
+ * Options for {@link createNodePlatform} — the shard host's (`path`,
373
+ * `shardKey`, `onAlarm`), the scheduler's (`onDispatch`), and the shard
374
+ * registry's (`directory`, `onAlarm`, `onFetch`). Both delivery hooks are
375
+ * optional and both are what make the durable halves useful: a re-armed alarm
376
+ * or job with nowhere to land is bookkeeping. `directory` makes the shards the
377
+ * directory resolves for fan-out file-backed too, and `onAlarm` gives their
378
+ * durable alarms somewhere to land.
379
+ */
380
+ type NodePlatformOptions<Queues extends Record<string, {
381
+ isLunoraQueue: true;
382
+ }> = Record<string, never>> = {
383
+ /**
384
+ * Deliver one assembled queue batch — wire this to `dispatchQueueBatch`.
385
+ * Required alongside `queues`; without it the messages would be stored
386
+ * and never consumed.
387
+ */
388
+ onQueueBatch?: NodeQueueHostOptions<Queues>["onBatch"];
389
+ /** The app's `defineQueue` results, keyed by export name. Omit when the app declares no queues. */
390
+ queues?: Queues;
391
+ } & NodeSchedulerHostOptions & NodeShardHostOptions & NodeShardRegistryOptions;
392
+ /** Compose every contract this package provides over one `better-sqlite3` database. */
393
+ declare const createNodePlatform: <Queues extends Record<string, {
394
+ isLunoraQueue: true;
395
+ }> = Record<string, never>>(options?: NodePlatformOptions<Queues>) => NodePlatform<Queues>;
396
+ /** Options for {@link createNodeR2Bucket}. */
397
+ interface NodeR2BucketOptions {
398
+ /** The bucket directory — created on first write. Objects live here, one file per key. */
399
+ directory: string;
400
+ }
401
+ /**
402
+ * Create an `R2BucketLike` over the local filesystem. Any object shape
403
+ * `createStorage({ bucket })` accepts — `put`/`get`/`head`/`delete`/`list` —
404
+ * maps directly onto a file operation.
405
+ */
406
+ declare const createNodeR2Bucket: (options: NodeR2BucketOptions) => R2BucketLike;
407
+ /**
408
+ * The `DurableObjectState` subset `ShardDO` consumes.
409
+ *
410
+ * Declared here rather than imported so this package keeps no dependency on
411
+ * `@lunora/do` (which depends on `@lunora/platform-cloudflare`, and through it
412
+ * on Cloudflare's types). Structural typing does the rest: a value of this type
413
+ * satisfies `ShardDO`'s constructor parameter without the two ever meeting at
414
+ * the type level.
415
+ */
416
+ interface NodeShardState {
417
+ acceptWebSocket: (socket: unknown, tags?: string[]) => void;
418
+ blockConcurrencyWhile: <T>(callback: () => Promise<T>) => Promise<T>;
419
+ getWebSockets: (tag?: string) => unknown[];
420
+ id: {
421
+ name?: string;
422
+ };
423
+ storage: {
424
+ delete: (key: string) => Promise<boolean>;
425
+ deleteAlarm: () => Promise<void>;
426
+ get: <T = unknown>(key: string) => Promise<T | undefined>;
427
+ getAlarm: () => Promise<number | null>;
428
+ list: <T = unknown>(options?: {
429
+ prefix?: string;
430
+ }) => Promise<Map<string, T>>;
431
+ put: (key: string, value: unknown) => Promise<void>;
432
+ setAlarm: (scheduledTime: Date | number) => Promise<void>;
433
+ sql: {
434
+ readonly databaseSize?: number;
435
+ exec: (query: string, ...bindings: unknown[]) => unknown;
436
+ };
437
+ transaction: <T>(closure: () => Promise<T>) => Promise<T>;
438
+ };
439
+ waitUntil: (promise: Promise<unknown>) => void;
440
+ }
441
+ /**
442
+ * Project a shard's platform contracts into the `DurableObjectState` shape.
443
+ *
444
+ * Deliberately **not** exposing the native-PITR members
445
+ * (`getBookmarkForTime` / `getCurrentBookmark` /
446
+ * `onNextSessionRestoreBookmark`) or `setWebSocketAutoResponse`: those are
447
+ * Cloudflare runtime features with no Node equivalent, and `ShardDO` probes for
448
+ * each before use. Omitting them takes the documented degraded path — the same
449
+ * one local `wrangler dev` takes — whereas supplying a stub that resolves would
450
+ * report a point-in-time restore that never happened.
451
+ */
452
+ declare const createNodeShardState: (shard: NodeShard) => NodeShardState;
453
+ /**
454
+ * The socket-host half of a Node platform instance, plus the test-only hooks
455
+ * `@lunora/platform/conformance`'s `ConformanceHost` needs to drive a recycle
456
+ * from inside a test.
457
+ */
458
+ interface NodeSocketHost {
459
+ /** Read back the frames sent to a socket, oldest first, text frames only. */
460
+ readFrames: (handle: SocketHandle) => string[];
461
+ /**
462
+ * Re-create a runtime socket from durable state.
463
+ *
464
+ * `attachment` is a fallback only: this host restores what it persisted,
465
+ * because that is what a real wake looks like. The argument covers an id
466
+ * this host never durably tracked (a synthetic id a test constructs).
467
+ */
468
+ restoreSocket: (id: string, attachment: unknown) => SocketHandle;
469
+ /** Drop the runtime socket map while keeping durable attachments/tags. */
470
+ simulateRecycle: () => void;
471
+ /** The `SocketHost` contract implementation. */
472
+ socket: SocketHost;
473
+ }
474
+ /** Build the socket registry, persisting attachments and tags to `database`. */
475
+ declare const createNodeSocketHost: (database: Database.Database) => NodeSocketHost;
476
+ /** Options for {@link createNodeWorkflowHost}. */
477
+ interface NodeWorkflowHostOptions<Workflows extends Record<string, {
478
+ isLunoraWorkflow: true;
479
+ }>> {
480
+ /** Base env merged under the derived `WORKFLOW_*` bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
481
+ env?: Record<string, unknown>;
482
+ /** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
483
+ leaseTtlMs?: number;
484
+ /** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
485
+ store: WorkflowStore;
486
+ /** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
487
+ workflows: Workflows;
488
+ }
489
+ /** A fully-wired Node workflow host. */
490
+ interface NodeWorkflowHost<Workflows extends Record<string, {
491
+ isLunoraWorkflow: true;
492
+ }>> {
493
+ /** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
494
+ readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
495
+ /**
496
+ * The caller's `env` plus one `WORKFLOW_&lt;EXPORT>` binding per workflow —
497
+ * merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
498
+ * children through the same runtime.
499
+ */
500
+ readonly env: Record<string, unknown>;
501
+ /** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
502
+ readonly runtime: WorkflowRuntime;
503
+ }
504
+ /**
505
+ * Create a Node workflow host: compile every declared Lunora workflow onto the
506
+ * visulima engine, derive the `WORKFLOW_*` env, and expose the per-workflow
507
+ * `WorkflowBindingLike` handles.
508
+ */
509
+ declare const createNodeWorkflowHost: <Workflows extends Record<string, {
510
+ isLunoraWorkflow: true;
511
+ }>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
512
+ /**
513
+ * Build a durable {@link WorkflowStore} over a `better-sqlite3` connection.
514
+ * Pass the result as `createNodeWorkflowHost({ store })`.
515
+ */
516
+ declare const createNodeWorkflowStore: (database: Database.Database) => WorkflowStore;
517
+ export { type NodeGlobalContextDatabaseOptions, type NodeGlobalStore, type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformOptions, type NodeQueueHost, type NodeQueueHostOptions, type NodeR2BucketOptions, type NodeSchedulerHost, type NodeSchedulerHostOptions, type NodeShard, type NodeShardHostOptions, type NodeShardRegistry, type NodeShardRegistryOptions, type NodeShardState, type NodeSocketHost, type NodeWorkflowHost, type NodeWorkflowHostOptions, createNodeGlobalStore, createNodePlatform, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };