@lunora/platform-node 1.0.0-alpha.7 → 1.0.0-alpha.71

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 (29) hide show
  1. package/dist/conformance/index.mjs +1 -1
  2. package/dist/index.d.mts +288 -68
  3. package/dist/index.d.ts +288 -68
  4. package/dist/index.mjs +1 -1
  5. package/dist/packem_shared/createNodeGlobalStore-nAJ4WO7w.mjs +1 -0
  6. package/dist/packem_shared/createNodePlatform-D5wfsfSD.mjs +1 -0
  7. package/dist/packem_shared/createNodeQueueHost-CttDb5sY.mjs +17 -0
  8. package/dist/packem_shared/createNodeR2Bucket-CajTvuOy.mjs +1 -0
  9. package/dist/packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs +15 -0
  10. package/dist/packem_shared/createNodeShardHost-Da19DFX0.mjs +1 -0
  11. package/dist/packem_shared/createNodeShardKvStore-DVi1OsvH.mjs +1 -0
  12. package/dist/packem_shared/createNodeShardRegistry-D-KxwdUc.mjs +1 -0
  13. package/dist/packem_shared/createNodeShardState-D6XMWbFs.mjs +1 -0
  14. package/dist/packem_shared/createNodeSocketHost-DQs85G7W.mjs +2 -0
  15. package/dist/packem_shared/createNodeWorkflowHost-B5Aiwj3L.mjs +1 -0
  16. package/dist/packem_shared/{createNodeWorkflowStore-jlNR6Y3h.mjs → createNodeWorkflowStore-CL7RYP6H.mjs} +5 -5
  17. package/dist/packem_shared/{to-array-buffer-CP4NlF-y.mjs → to-array-buffer-DVyXU4Ep.mjs} +1 -1
  18. package/package.json +13 -10
  19. package/dist/packem_shared/createNodeGlobalStore-BvteXWRK.mjs +0 -1
  20. package/dist/packem_shared/createNodePlatform-BQimmgT9.mjs +0 -1
  21. package/dist/packem_shared/createNodeQueueHost-CLQ5hOBf.mjs +0 -17
  22. package/dist/packem_shared/createNodeR2Bucket-Wb8pyi2X.mjs +0 -1
  23. package/dist/packem_shared/createNodeSchedulerHost-DRjZhuxe.mjs +0 -15
  24. package/dist/packem_shared/createNodeShardHost-DEHe5Vcm.mjs +0 -1
  25. package/dist/packem_shared/createNodeShardKvStore-BfKCQTjE.mjs +0 -1
  26. package/dist/packem_shared/createNodeShardRegistry-0iSMdi9-.mjs +0 -1
  27. package/dist/packem_shared/createNodeShardState-DdOj9FzJ.mjs +0 -1
  28. package/dist/packem_shared/createNodeSocketHost-CytowrYN.mjs +0 -2
  29. package/dist/packem_shared/createNodeWorkflowHost-Dht_jgSv.mjs +0 -1
@@ -1 +1 @@
1
- import{createNodeShardKvStore as f}from"../packem_shared/createNodeShardKvStore-BfKCQTjE.mjs";import{createNodeSchedulerHost as y}from"../packem_shared/createNodeSchedulerHost-DRjZhuxe.mjs";import{createNodeShardHost as k}from"../packem_shared/createNodeShardHost-DEHe5Vcm.mjs";import{createNodeShardRegistry as D}from"../packem_shared/createNodeShardRegistry-0iSMdi9-.mjs";import{createNodeSocketHost as N}from"../packem_shared/createNodeSocketHost-CytowrYN.mjs";const b=()=>{const{database:r,dispose:c,host:i}=k(),m=f(r),a=D(),{directory:n}=a,{readFrames:h,restoreSocket:l,simulateRecycle:u,socket:p}=N(r),s=new Set,{dispose:w,scheduler:d,simulateDeadLetter:S}=y(r,{onDispatch:(e,t,o)=>{s.add(o.id)}});return{awaitAlarmFired:async e=>{await new Promise(t=>{setTimeout(t,Math.max(0,e-Date.now())+30)})},awaitJobDispatched:async e=>{const t=(await d.list?.())?.find(o=>o.id===e);return t!==void 0&&await new Promise(o=>{setTimeout(o,Math.max(0,t.scheduledFor-Date.now())+30)}),s.has(e)},cleanup:()=>{w(),c(),a.close()},directory:n,kv:m,readFrames:h,restoreSocket:l,scheduler:d,shard:i,simulateDeadLetter:S,simulateRecycle:u,socket:p}};export{b as createNodeConformanceHost};
1
+ import{createNodeShardKvStore as N}from"../packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeSchedulerHost as k}from"../packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as D}from"../packem_shared/createNodeShardHost-Da19DFX0.mjs";import{createNodeShardRegistry as H}from"../packem_shared/createNodeShardRegistry-D-KxwdUc.mjs";import{createNodeSocketHost as T}from"../packem_shared/createNodeSocketHost-DQs85G7W.mjs";const P=()=>{const{database:o,dispose:m,host:p}=D(),h=N(o),c=H(),{directory:l}=c,{readFrames:w,restoreSocket:u,simulateRecycle:S,socket:y}=T(o),d=new Set,s=new Map,{dispose:f,scheduler:i,simulateDeadLetter:g}=k(o,{onDispatch:(e,a,t)=>{d.add(t.id),s.set(e,(s.get(e)??0)+1)}}),n=()=>{f(),m(),c.close()};return{awaitAlarmFired:async e=>{await new Promise(a=>{setTimeout(a,Math.max(0,e-Date.now())+30)})},awaitJobDispatched:async e=>{const t=(await i.list?.())?.find(r=>r.id===e);return t!==void 0&&await new Promise(r=>{setTimeout(r,Math.max(0,t.scheduledFor-Date.now())+30)}),d.has(e)},cleanup:n,cronTicks:e=>s.get(e)??0,directory:l,disposeTerminally:n,kv:h,readFrames:w,restoreSocket:u,scheduler:i,shard:p,simulateDeadLetter:g,simulateRecycle:S,socket:y}};export{P as createNodeConformanceHost};
package/dist/index.d.mts CHANGED
@@ -197,9 +197,12 @@ interface NodeShardHostOptions {
197
197
  * `_lunora_alarm` when the host is constructed over an existing database,
198
198
  * including an alarm whose time elapsed while nothing was running.
199
199
  *
200
- * A handler that throws is isolated to its own delivery; it never reaches
200
+ * A handler that throws is isolated to its own delivery — it never reaches
201
201
  * the caller that set the alarm, because by then that call has long
202
- * returned.
202
+ * returned — and the wakeup is then re-delivered with backoff up to
203
+ * {@link ALARM_RETRY_LIMIT} times, the way workerd retries a throwing
204
+ * `alarm()`. Delivery is at-least-once, so the handler must tolerate
205
+ * running twice for one scheduled timestamp.
203
206
  */
204
207
  onAlarm?: () => Promise<void> | void;
205
208
  /**
@@ -218,25 +221,59 @@ interface NodeShardHostOptions {
218
221
  /**
219
222
  * Build a `ShardHost` over a real `better-sqlite3` database.
220
223
  *
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.
224
+ * `runSerialized` and `transaction` share **one** boundary lock, because they
225
+ * write **one** `better-sqlite3` connection. `transaction` issues raw
226
+ * `BEGIN`/`COMMIT`/`ROLLBACK` — legal here (unlike inside a Cloudflare Durable
227
+ * Object, where the runtime forbids it and callers must use
228
+ * `storage.transaction`) because better-sqlite3 is a plain embedded database
229
+ * with no platform-level transaction primitive layered over it.
227
230
  *
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.
231
+ * ## Why one lane and not two
232
+ *
233
+ * This host shipped with two private tail chains, one per entry point, so each
234
+ * serialized against itself and neither against the other. `runSerialized`
235
+ * issues no `BEGIN` of its own, so one of its closures overlapping a bare
236
+ * `transaction()` wrote straight into that transaction's span. `assertOwnTurn`
237
+ * below catches the write — it is the only reason this host never silently lost
238
+ * one the way an unguarded connection would — but catching it is not the same
239
+ * as preventing it, and the refusal lands in the wrong place:
240
+ *
241
+ * 1. A `runSerialized` closure writes a row. Nothing is open, so it commits.
242
+ * 2. It awaits (a scheduler hop, an `onShardInit` read, any real I/O).
243
+ * 3. A bare `transaction()` runs `BEGIN` while it is parked. `transaction` is
244
+ * a public contract surface, and `./node-shard-state` re-exports it as the
245
+ * `storage.transaction` member `ShardDO` documents as "the platform
246
+ * primitive", so a subclass reaches one without going through the engine.
247
+ * 4. The closure resumes and its next write is refused with
248
+ * `SHARD_UNAVAILABLE`, so `runSerialized` **rejects** — with step 1 already
249
+ * durable.
250
+ *
251
+ * The boundary the contract promises is atomic-or-nothing to its caller tore in
252
+ * half: a rejection the caller will retry, on top of a write the retry now
253
+ * re-applies. That is `runSerialized`'s "no two closures interleave" guarantee
254
+ * broken by a `transaction` it was never serialized against.
255
+ *
256
+ * ## Why the lock is re-entrant, and why `AsyncLocalStorage` is what makes it so
257
+ *
258
+ * The engine composes the two, and stacks them: `ShardRunner.runInTransaction`
259
+ * is `runSerialized(() => transaction(work))` (`@lunora/shard-engine`), and
260
+ * `ShardDO.fetch` widens a mutation dispatch carrying `x-lunora-mutation-id`
261
+ * into a *further* `runSerialized` span around it (`@lunora/do`). Under two
262
+ * plain FIFO chains that outer span is already fatal on this host — the inner
263
+ * `runSerialized` waits on a `tail` that only settles when the outer closure it
264
+ * is running inside resolves, and because nothing ever resets that `tail`, the
265
+ * shard's write lane is wedged for the life of the process rather than for the
266
+ * life of the request.
267
+ *
268
+ * So the lock is skipped for a boundary opened from inside a boundary, and
269
+ * `AsyncLocalStorage` is the only thing that can tell that case from the one
270
+ * that must queue: its store follows a single call's own await chain without
271
+ * leaking into a sibling chain. A held boolean cannot — it reads `true` for an
272
+ * unrelated *second top-level* `transaction()` too, which would then nest a raw
273
+ * `BEGIN` on a connection already inside one. This is the same distinction
274
+ * workerd's `blockConcurrencyWhile` draws natively with "does not queue events
275
+ * initiated as part of the callback itself", which is what
276
+ * `@lunora/platform-cloudflare` leans on for the identical composition.
240
277
  *
241
278
  * The returned `dispose()` is this host's lifecycle owner: it clears the
242
279
  * pending alarm `setTimeout` (so it can never fire against a connection this
@@ -310,22 +347,64 @@ interface NodeShardRegistry {
310
347
  }
311
348
  /** Build the in-process shard registry. */
312
349
  declare const createNodeShardRegistry: (options?: NodeShardRegistryOptions) => NodeShardRegistry;
350
+ /** Options for {@link createNodeWorkflowHost}. */
351
+ interface NodeWorkflowHostOptions<Workflows extends Record<string, {
352
+ isLunoraWorkflow: true;
353
+ }>> {
354
+ /** Base env merged under the derived `WORKFLOW_*` bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
355
+ env?: Record<string, unknown>;
356
+ /** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
357
+ leaseTtlMs?: number;
358
+ /** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
359
+ store: WorkflowStore;
360
+ /** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
361
+ workflows: Workflows;
362
+ }
363
+ /** A fully-wired Node workflow host. */
364
+ interface NodeWorkflowHost<Workflows extends Record<string, {
365
+ isLunoraWorkflow: true;
366
+ }>> {
367
+ /** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
368
+ readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
369
+ /**
370
+ * The caller's `env` plus one `WORKFLOW_&lt;EXPORT>` binding per workflow —
371
+ * merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
372
+ * children through the same runtime.
373
+ */
374
+ readonly env: Record<string, unknown>;
375
+ /** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
376
+ readonly runtime: WorkflowRuntime;
377
+ }
378
+ /**
379
+ * Create a Node workflow host: compile every declared Lunora workflow onto the
380
+ * visulima engine, derive the `WORKFLOW_*` env, and expose the per-workflow
381
+ * `WorkflowBindingLike` handles.
382
+ */
383
+ declare const createNodeWorkflowHost: <Workflows extends Record<string, {
384
+ isLunoraWorkflow: true;
385
+ }>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
313
386
  /** Every contract this package provides, composed for one Node process. */
314
387
  interface NodePlatform<Queues extends Record<string, {
315
388
  isLunoraQueue: true;
389
+ }> = Record<string, never>, Workflows extends Record<string, {
390
+ isLunoraWorkflow: true;
316
391
  }> = Record<string, never>> {
317
392
  /** `using platform = createNodePlatform(...)` support — delegates to `close()`. */
318
393
  [Symbol.dispose]: () => void;
319
394
  /** What this target supports — see `NODE_CAPABILITIES` in `@lunora/platform`. */
320
395
  capabilities: PlatformCapabilities;
321
396
  /**
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.
397
+ * Tear this platform instance down: every resource this root built, in
398
+ * reverse construction order — the global store's own connection, every
399
+ * armed scheduler job timer, the registry's live shards, and last the shard
400
+ * host's pending alarm timer and its `better-sqlite3` database (and, with
401
+ * it, `kv`'s table — both live on the same connection). Nothing in this
402
+ * package closes these resources on its own — a `NodePlatform` a caller
403
+ * stops using without calling `close()` leaks the open file handles (plus
404
+ * their WAL/SHM sidecar files) and keeps the process alive on outstanding
405
+ * timers. Safe to call more than once. A `createNodePlatform` that throws
406
+ * unwinds the same list itself, so a failed construction leaks nothing and
407
+ * leaves nothing to call this on.
329
408
  *
330
409
  * **`close()` is a terminal state, not merely a cleanup step.** After it
331
410
  * runs: `scheduler.schedule()` throws instead of arming a fresh timer
@@ -335,8 +414,10 @@ interface NodePlatform<Queues extends Record<string, {
335
414
  * benign); `shard.alarms.set()`/`delete()` throw before mutating any
336
415
  * in-memory state (checked against the connection's own open/closed state,
337
416
  * 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
417
+ * whatever it last held; `sockets.accept()`/`setTag()`/`removeTag()` throw
418
+ * the same way, checked against the same connection state, before touching
419
+ * any runtime socket map or durable row. A no-op instead of a throw would
420
+ * be indistinguishable from a working call — exactly the silent-vanishing
340
421
  * this lifecycle exists to end.
341
422
  */
342
423
  close: () => void;
@@ -351,8 +432,28 @@ interface NodePlatform<Queues extends Record<string, {
351
432
  * shutdown is `await platform.drain()` then `platform.close()`.
352
433
  */
353
434
  drain: () => Promise<void>;
435
+ /**
436
+ * The `.global()` backend, or `undefined` when the caller named no database
437
+ * file for it. There is no default path, because a global store silently
438
+ * rooted at `:memory:` loses every row when the process exits.
439
+ *
440
+ * **A building block, not a wiring.** Unlike its three siblings, nothing
441
+ * downstream of this composition root reads it: a `.global()` read or write
442
+ * reaches its backend through exactly one seam, `createShardCtxDb({ globalDb
443
+ * })`, and the only thing that passes `globalDb` is the generated `shard.ts`,
444
+ * from its `d1` / `hyperdriveGlobal` config thunks. `createNodePlatform`
445
+ * constructs no shard DO, so it cannot make that hop itself. A caller that
446
+ * wants `.global()` on this host makes it: after `migrate(schema)` has
447
+ * provisioned the tables, give the generated `createShardDO` a `d1` thunk
448
+ * returning `platform.globalTables.writer({ schema, … })` — `writer` builds
449
+ * the `createSqlCtxDb` facade that seam expects, and `node-platform.test.ts`
450
+ * round-trips a row through exactly that object.
451
+ */
452
+ globalTables?: NodeGlobalStore;
354
453
  /** Durable key-value storage backed by the same `better-sqlite3` database as `shard`. */
355
454
  kv: ShardKvStore;
455
+ /** The local-filesystem bucket, or `undefined` when the caller named no bucket directory. */
456
+ objectStorage?: R2BucketLike;
356
457
  /**
357
458
  * The declared queues, or `undefined` when the caller declared none.
358
459
  *
@@ -367,6 +468,12 @@ interface NodePlatform<Queues extends Record<string, {
367
468
  shard: ShardHost;
368
469
  /** Socket registry with mutable tags and SQLite-persisted attachments. */
369
470
  sockets: SocketHost;
471
+ /**
472
+ * The declared workflows, or `undefined` when the caller declared none.
473
+ * Runs are persisted to the same `better-sqlite3` database as the shard, so
474
+ * they survive a restart without a second store to configure.
475
+ */
476
+ workflows?: NodeWorkflowHost<Workflows>;
370
477
  }
371
478
  /**
372
479
  * Options for {@link createNodePlatform} — the shard host's (`path`,
@@ -376,10 +483,29 @@ interface NodePlatform<Queues extends Record<string, {
376
483
  * or job with nowhere to land is bookkeeping. `directory` makes the shards the
377
484
  * directory resolves for fan-out file-backed too, and `onAlarm` gives their
378
485
  * durable alarms somewhere to land.
486
+ *
487
+ * `queues`, `workflows`, `objectStorageDirectory` and `globalTablesPath` are the
488
+ * four declarations — each is absent from the returned platform when omitted.
379
489
  */
380
490
  type NodePlatformOptions<Queues extends Record<string, {
381
491
  isLunoraQueue: true;
492
+ }> = Record<string, never>, Workflows extends Record<string, {
493
+ isLunoraWorkflow: true;
382
494
  }> = Record<string, never>> = {
495
+ /**
496
+ * Database file the `.global()` tables live in — its own file, never a
497
+ * shard's, because a table every shard reads must not be inside any one of
498
+ * them. Omit when the app declares no `.global()` table; there is no
499
+ * default, for the same reason the bucket has none.
500
+ */
501
+ globalTablesPath?: string;
502
+ /**
503
+ * Directory the object-storage bucket keeps its objects in, one file per
504
+ * key. Omit when the app declares no buckets — there is no default,
505
+ * because a bucket silently rooted at the process's working directory is
506
+ * worse than an absent one.
507
+ */
508
+ objectStorageDirectory?: string;
383
509
  /**
384
510
  * Deliver one assembled queue batch — wire this to `dispatchQueueBatch`.
385
511
  * Required alongside `queues`; without it the messages would be stored
@@ -388,11 +514,15 @@ type NodePlatformOptions<Queues extends Record<string, {
388
514
  onQueueBatch?: NodeQueueHostOptions<Queues>["onBatch"];
389
515
  /** The app's `defineQueue` results, keyed by export name. Omit when the app declares no queues. */
390
516
  queues?: Queues;
517
+ /** The app's `defineWorkflow` results, keyed by export name. Omit when the app declares no workflows. */
518
+ workflows?: Workflows;
391
519
  } & NodeSchedulerHostOptions & NodeShardHostOptions & NodeShardRegistryOptions;
392
520
  /** Compose every contract this package provides over one `better-sqlite3` database. */
393
521
  declare const createNodePlatform: <Queues extends Record<string, {
394
522
  isLunoraQueue: true;
395
- }> = Record<string, never>>(options?: NodePlatformOptions<Queues>) => NodePlatform<Queues>;
523
+ }> = Record<string, never>, Workflows extends Record<string, {
524
+ isLunoraWorkflow: true;
525
+ }> = Record<string, never>>(options?: NodePlatformOptions<Queues, Workflows>) => NodePlatform<Queues, Workflows>;
396
526
  /** Options for {@link createNodeR2Bucket}. */
397
527
  interface NodeR2BucketOptions {
398
528
  /** The bucket directory — created on first write. Objects live here, one file per key. */
@@ -400,7 +530,7 @@ interface NodeR2BucketOptions {
400
530
  }
401
531
  /**
402
532
  * Create an `R2BucketLike` over the local filesystem. Any object shape
403
- * `createStorage({ bucket })` accepts — `put`/`get`/`head`/`delete`/`list` —
533
+ * `createStorage({ bucket, bucketName })` accepts — `put`/`get`/`head`/`delete`/`list` —
404
534
  * maps directly onto a file operation.
405
535
  */
406
536
  declare const createNodeR2Bucket: (options: NodeR2BucketOptions) => R2BucketLike;
@@ -473,45 +603,135 @@ interface NodeSocketHost {
473
603
  }
474
604
  /** Build the socket registry, persisting attachments and tags to `database`. */
475
605
  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
606
  /**
513
607
  * Build a durable {@link WorkflowStore} over a `better-sqlite3` connection.
514
608
  * Pass the result as `createNodeWorkflowHost({ store })`.
515
609
  */
516
610
  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 };
611
+ export {
612
+ /**
613
+ * `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
614
+ * host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
615
+ * `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
616
+ * scheduler registry.
617
+ *
618
+ * Promoted from `@lunora/platform`'s `node:sqlite` reference host
619
+ * (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
620
+ * durability half of each contract actually holds: alarms and scheduler jobs
621
+ * are persisted **and re-armed on construction**, socket attachments and tags
622
+ * live in SQLite rather than a `Map`, and `.global()` tables run the real
623
+ * `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
624
+ * a restart test — a second host over the same database file — not only by the
625
+ * TCK's simulated recycle.
626
+ *
627
+ * Three suites run against this package: `@lunora/platform/conformance`'s host
628
+ * TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
629
+ * lifecycle/global-store tests.
630
+ *
631
+ * **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
632
+ * (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
633
+ * buckets over the local filesystem (`./node-r2-bucket`), workflows over the
634
+ * `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
635
+ * via `@lunora/runtime`'s query coordinator over the in-process shard registry.
636
+ * `createNodePlatform` binds all three declarations (`queues`, `workflows`,
637
+ * `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
638
+ * is the one combination that fails at runtime with no diagnostic before it.
639
+ *
640
+ * **Still missing:** a dev server. There is no `lunora dev --target node`, and
641
+ * nothing here owns a timer, so queue delivery is driven by an explicit
642
+ * `poll()`. Also absent are the Cloudflare product bindings with no local
643
+ * equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
644
+ * Engine, Pipelines, Secrets Store, Hyperdrive.
645
+ *
646
+ * `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
647
+ * for `provision` — which reports, once, which declared features this target
648
+ * cannot serve, and writes nothing: there is no hosted control plane to deploy
649
+ * to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
650
+ * server, which is why both statements above hold at once. See
651
+ * `plans/234-node-host-findings.md`.
652
+ */
653
+ type NodeGlobalContextDatabaseOptions,
654
+ /**
655
+ * `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
656
+ * host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
657
+ * `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
658
+ * scheduler registry.
659
+ *
660
+ * Promoted from `@lunora/platform`'s `node:sqlite` reference host
661
+ * (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
662
+ * durability half of each contract actually holds: alarms and scheduler jobs
663
+ * are persisted **and re-armed on construction**, socket attachments and tags
664
+ * live in SQLite rather than a `Map`, and `.global()` tables run the real
665
+ * `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
666
+ * a restart test — a second host over the same database file — not only by the
667
+ * TCK's simulated recycle.
668
+ *
669
+ * Three suites run against this package: `@lunora/platform/conformance`'s host
670
+ * TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
671
+ * lifecycle/global-store tests.
672
+ *
673
+ * **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
674
+ * (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
675
+ * buckets over the local filesystem (`./node-r2-bucket`), workflows over the
676
+ * `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
677
+ * via `@lunora/runtime`'s query coordinator over the in-process shard registry.
678
+ * `createNodePlatform` binds all three declarations (`queues`, `workflows`,
679
+ * `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
680
+ * is the one combination that fails at runtime with no diagnostic before it.
681
+ *
682
+ * **Still missing:** a dev server. There is no `lunora dev --target node`, and
683
+ * nothing here owns a timer, so queue delivery is driven by an explicit
684
+ * `poll()`. Also absent are the Cloudflare product bindings with no local
685
+ * equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
686
+ * Engine, Pipelines, Secrets Store, Hyperdrive.
687
+ *
688
+ * `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
689
+ * for `provision` — which reports, once, which declared features this target
690
+ * cannot serve, and writes nothing: there is no hosted control plane to deploy
691
+ * to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
692
+ * server, which is why both statements above hold at once. See
693
+ * `plans/234-node-host-findings.md`.
694
+ */
695
+ type NodeGlobalStore,
696
+ /**
697
+ * `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
698
+ * host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
699
+ * `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
700
+ * scheduler registry.
701
+ *
702
+ * Promoted from `@lunora/platform`'s `node:sqlite` reference host
703
+ * (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
704
+ * durability half of each contract actually holds: alarms and scheduler jobs
705
+ * are persisted **and re-armed on construction**, socket attachments and tags
706
+ * live in SQLite rather than a `Map`, and `.global()` tables run the real
707
+ * `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
708
+ * a restart test — a second host over the same database file — not only by the
709
+ * TCK's simulated recycle.
710
+ *
711
+ * Three suites run against this package: `@lunora/platform/conformance`'s host
712
+ * TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
713
+ * lifecycle/global-store tests.
714
+ *
715
+ * **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
716
+ * (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
717
+ * buckets over the local filesystem (`./node-r2-bucket`), workflows over the
718
+ * `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
719
+ * via `@lunora/runtime`'s query coordinator over the in-process shard registry.
720
+ * `createNodePlatform` binds all three declarations (`queues`, `workflows`,
721
+ * `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
722
+ * is the one combination that fails at runtime with no diagnostic before it.
723
+ *
724
+ * **Still missing:** a dev server. There is no `lunora dev --target node`, and
725
+ * nothing here owns a timer, so queue delivery is driven by an explicit
726
+ * `poll()`. Also absent are the Cloudflare product bindings with no local
727
+ * equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
728
+ * Engine, Pipelines, Secrets Store, Hyperdrive.
729
+ *
730
+ * `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
731
+ * for `provision` — which reports, once, which declared features this target
732
+ * cannot serve, and writes nothing: there is no hosted control plane to deploy
733
+ * to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
734
+ * server, which is why both statements above hold at once. See
735
+ * `plans/234-node-host-findings.md`.
736
+ */
737
+ 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 };