@lunora/platform-node 1.0.0-alpha.15 → 1.0.0-alpha.150

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 (30) hide show
  1. package/README.md +7 -0
  2. package/dist/conformance/index.mjs +1 -1
  3. package/dist/index.d.mts +279 -124
  4. package/dist/index.d.ts +279 -124
  5. package/dist/index.mjs +1 -1
  6. package/dist/packem_shared/NODE_SCHEDULER_DISPATCH_PATH-CNBAtztq.mjs +1 -0
  7. package/dist/packem_shared/createNodeGlobalStore-DhHyIT8I.mjs +1 -0
  8. package/dist/packem_shared/createNodePlatform-D-vCAW6Z.mjs +1 -0
  9. package/dist/packem_shared/createNodeProfileHandler-MX5xKPg0.mjs +1 -0
  10. package/dist/packem_shared/createNodeQueueHost-CttDb5sY.mjs +17 -0
  11. package/dist/packem_shared/createNodeR2Bucket-6I2HobcB.mjs +1 -0
  12. package/dist/packem_shared/createNodeSchedulerHost-BIJ4H38-.mjs +15 -0
  13. package/dist/packem_shared/createNodeShardHost-B6mt1S-h.mjs +1 -0
  14. package/dist/packem_shared/createNodeShardRegistry-q-46Oj8M.mjs +1 -0
  15. package/dist/packem_shared/createNodeShardState-D6XMWbFs.mjs +1 -0
  16. package/dist/packem_shared/createNodeSocketHost-DQs85G7W.mjs +2 -0
  17. package/dist/packem_shared/createNodeWorkflowHost-B76TKiw_.mjs +1 -0
  18. package/dist/packem_shared/hmac-url-BrwGfbCr.mjs +1 -0
  19. package/dist/packem_shared/prepared-statement-3XBQZXtC.mjs +1 -0
  20. package/package.json +13 -8
  21. package/dist/packem_shared/createNodeGlobalStore-Cwi33XOk.mjs +0 -1
  22. package/dist/packem_shared/createNodePlatform-CfKfpghn.mjs +0 -1
  23. package/dist/packem_shared/createNodeQueueHost--mBLGSxF.mjs +0 -17
  24. package/dist/packem_shared/createNodeR2Bucket-CmHYlIni.mjs +0 -1
  25. package/dist/packem_shared/createNodeSchedulerHost-vZgv1CEo.mjs +0 -15
  26. package/dist/packem_shared/createNodeShardHost-ByDrsNFb.mjs +0 -1
  27. package/dist/packem_shared/createNodeShardRegistry-Zy8eZoF9.mjs +0 -1
  28. package/dist/packem_shared/createNodeShardState-CpUzvWwd.mjs +0 -1
  29. package/dist/packem_shared/createNodeSocketHost-Ce4aH2Kq.mjs +0 -2
  30. package/dist/packem_shared/createNodeWorkflowHost-CTVBYiuj.mjs +0 -1
package/dist/index.d.ts CHANGED
@@ -4,6 +4,39 @@ import Database from 'better-sqlite3';
4
4
  import { ShardKvStore, QueueBindingLike, MessageBatchLike, SchedulerHost, ShardHost, SocketHost, DirectShardDirectory, PlatformCapabilities, ShardDirectory, R2BucketLike, SocketHandle } from '@lunora/platform';
5
5
  import { WorkflowBindingLike } from '@lunora/workflow';
6
6
  import { WorkflowRuntime, WorkflowStore } from '@visulima/workflow';
7
+ /** One function call the route has authenticated and parsed. */
8
+ interface NodeDispatchInvocation {
9
+ args: Record<string, unknown>;
10
+ functionPath: string;
11
+ /** The scheduler record or delivery id, used as the shard's idempotency key. */
12
+ id: string | undefined;
13
+ /** Identity the trusted caller forwarded, if any. */
14
+ identity: {
15
+ identity?: string;
16
+ userId?: string;
17
+ } | undefined;
18
+ shardKey: string;
19
+ traceparent: string | undefined;
20
+ }
21
+ interface NodeDispatchRouteOptions {
22
+ /** `LUNORA_ADMIN_TOKEN`. Used as the bearer when no HMAC signature is presented. */
23
+ adminToken?: string;
24
+ /** The shard a body without `shardKey` is sent to. */
25
+ defaultShardKey: string;
26
+ /** Runs the call in-process and returns the shard's response, unchanged. */
27
+ invoke: (call: NodeDispatchInvocation) => Promise<Response>;
28
+ /** `LUNORA_SCHEDULER_SECRET`. When set, a signed request is verified against it. */
29
+ schedulerSecret?: string;
30
+ }
31
+ /**
32
+ * Build the dispatch route. The returned function answers any request it is
33
+ * handed; mounting it at {@link NODE_SCHEDULER_DISPATCH_PATH} is the caller's job.
34
+ * With neither secret configured every request is refused, so an unconfigured
35
+ * route fails closed.
36
+ */
37
+ declare const createNodeDispatchRoute: (options: NodeDispatchRouteOptions) => ((request: Request) => Promise<Response>);
38
+ /** The path `@lunora/dispatch` and `@lunora/scheduler` POST to. */
39
+ declare const NODE_SCHEDULER_DISPATCH_PATH: "/_lunora/scheduler/dispatch";
7
40
  /**
8
41
  * The Node store options — the shared store options minus the two this binding
9
42
  * owns. `dialect` is fixed (SQLite) and `exec` is the store's own connection;
@@ -14,13 +47,14 @@ type NodeGlobalContextDatabaseOptions = Omit<SqlCtxDbOptions, "dialect" | "exec"
14
47
  /**
15
48
  * Wrap a `better-sqlite3` connection as the async exec the store core consumes.
16
49
  *
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.
50
+ * `batch` runs the statements in order inside one `better-sqlite3`
51
+ * transaction, which is synchronous, so nothing else runs between them. There is
52
+ * no round trip to save; atomicity is the point. The store core's FTS5 search
53
+ * companion writes each document as an ordered list of statements, and without
54
+ * this two async writers interleaved at every `await` between them: one could
55
+ * move a document's mapping while the other's entry was mid-write, leaving an
56
+ * entry no later write or purge could reach. The `sqlite` dialect's contract
57
+ * requires an ordered, atomic `batch` for exactly that reason.
24
58
  */
25
59
  declare const createNodeSqlExec: (database: Database.Database) => SqlCtxExec;
26
60
  /** Options for {@link createNodeGlobalStore}. */
@@ -197,9 +231,12 @@ interface NodeShardHostOptions {
197
231
  * `_lunora_alarm` when the host is constructed over an existing database,
198
232
  * including an alarm whose time elapsed while nothing was running.
199
233
  *
200
- * A handler that throws is isolated to its own delivery; it never reaches
234
+ * A handler that throws is isolated to its own delivery — it never reaches
201
235
  * the caller that set the alarm, because by then that call has long
202
- * returned.
236
+ * returned — and the wakeup is then re-delivered with backoff up to
237
+ * {@link ALARM_RETRY_LIMIT} times, the way workerd retries a throwing
238
+ * `alarm()`. Delivery is at-least-once, so the handler must tolerate
239
+ * running twice for one scheduled timestamp.
203
240
  */
204
241
  onAlarm?: () => Promise<void> | void;
205
242
  /**
@@ -218,25 +255,59 @@ interface NodeShardHostOptions {
218
255
  /**
219
256
  * Build a `ShardHost` over a real `better-sqlite3` database.
220
257
  *
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.
258
+ * `runSerialized` and `transaction` share **one** boundary lock, because they
259
+ * write **one** `better-sqlite3` connection. `transaction` issues raw
260
+ * `BEGIN`/`COMMIT`/`ROLLBACK` — legal here (unlike inside a Cloudflare Durable
261
+ * Object, where the runtime forbids it and callers must use
262
+ * `storage.transaction`) because better-sqlite3 is a plain embedded database
263
+ * with no platform-level transaction primitive layered over it.
264
+ *
265
+ * ## Why one lane and not two
266
+ *
267
+ * This host shipped with two private tail chains, one per entry point, so each
268
+ * serialized against itself and neither against the other. `runSerialized`
269
+ * issues no `BEGIN` of its own, so one of its closures overlapping a bare
270
+ * `transaction()` wrote straight into that transaction's span. `assertOwnTurn`
271
+ * below catches the write — it is the only reason this host never silently lost
272
+ * one the way an unguarded connection would — but catching it is not the same
273
+ * as preventing it, and the refusal lands in the wrong place:
274
+ *
275
+ * 1. A `runSerialized` closure writes a row. Nothing is open, so it commits.
276
+ * 2. It awaits (a scheduler hop, an `onShardInit` read, any real I/O).
277
+ * 3. A bare `transaction()` runs `BEGIN` while it is parked. `transaction` is
278
+ * a public contract surface, and `./node-shard-state` re-exports it as the
279
+ * `storage.transaction` member `ShardDO` documents as "the platform
280
+ * primitive", so a subclass reaches one without going through the engine.
281
+ * 4. The closure resumes and its next write is refused with
282
+ * `SHARD_UNAVAILABLE`, so `runSerialized` **rejects** — with step 1 already
283
+ * durable.
227
284
  *
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.
285
+ * The boundary the contract promises is atomic-or-nothing to its caller tore in
286
+ * half: a rejection the caller will retry, on top of a write the retry now
287
+ * re-applies. That is `runSerialized`'s "no two closures interleave" guarantee
288
+ * broken by a `transaction` it was never serialized against.
289
+ *
290
+ * ## Why the lock is re-entrant, and why `AsyncLocalStorage` is what makes it so
291
+ *
292
+ * The engine composes the two, and stacks them: `ShardRunner.runInTransaction`
293
+ * is `runSerialized(() => transaction(work))` (`@lunora/shard-engine`), and
294
+ * `ShardDO.fetch` widens a mutation dispatch carrying `x-lunora-mutation-id`
295
+ * into a *further* `runSerialized` span around it (`@lunora/do`). Under two
296
+ * plain FIFO chains that outer span is already fatal on this host — the inner
297
+ * `runSerialized` waits on a `tail` that only settles when the outer closure it
298
+ * is running inside resolves, and because nothing ever resets that `tail`, the
299
+ * shard's write lane is wedged for the life of the process rather than for the
300
+ * life of the request.
301
+ *
302
+ * So the lock is skipped for a boundary opened from inside a boundary, and
303
+ * `AsyncLocalStorage` is the only thing that can tell that case from the one
304
+ * that must queue: its store follows a single call's own await chain without
305
+ * leaking into a sibling chain. A held boolean cannot — it reads `true` for an
306
+ * unrelated *second top-level* `transaction()` too, which would then nest a raw
307
+ * `BEGIN` on a connection already inside one. This is the same distinction
308
+ * workerd's `blockConcurrencyWhile` draws natively with "does not queue events
309
+ * initiated as part of the callback itself", which is what
310
+ * `@lunora/platform-cloudflare` leans on for the identical composition.
240
311
  *
241
312
  * The returned `dispose()` is this host's lifecycle owner: it clears the
242
313
  * pending alarm `setTimeout` (so it can never fire against a connection this
@@ -310,22 +381,65 @@ interface NodeShardRegistry {
310
381
  }
311
382
  /** Build the in-process shard registry. */
312
383
  declare const createNodeShardRegistry: (options?: NodeShardRegistryOptions) => NodeShardRegistry;
384
+ /** Options for {@link createNodeWorkflowHost}. */
385
+ interface NodeWorkflowHostOptions<Workflows extends Record<string, {
386
+ isLunoraWorkflow: true;
387
+ }>> {
388
+ /** Base env merged under the derived workflow bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
389
+ env?: Record<string, unknown>;
390
+ /** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
391
+ leaseTtlMs?: number;
392
+ /** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
393
+ store: WorkflowStore;
394
+ /** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
395
+ workflows: Workflows;
396
+ }
397
+ /** A fully-wired Node workflow host. */
398
+ interface NodeWorkflowHost<Workflows extends Record<string, {
399
+ isLunoraWorkflow: true;
400
+ }>> {
401
+ /** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
402
+ readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
403
+ /**
404
+ * The caller's `env` plus one binding per workflow under its class name
405
+ * (`OrderPipelineWorkflow`), the key `ctx.exports` would carry on Cloudflare —
406
+ * merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
407
+ * children through the same runtime.
408
+ */
409
+ readonly env: Record<string, unknown>;
410
+ /** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
411
+ readonly runtime: WorkflowRuntime;
412
+ }
413
+ /**
414
+ * Create a Node workflow host: compile every declared Lunora workflow onto the
415
+ * visulima engine, derive the workflow env, and expose the per-workflow
416
+ * `WorkflowBindingLike` handles.
417
+ */
418
+ declare const createNodeWorkflowHost: <Workflows extends Record<string, {
419
+ isLunoraWorkflow: true;
420
+ }>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
313
421
  /** Every contract this package provides, composed for one Node process. */
314
422
  interface NodePlatform<Queues extends Record<string, {
315
423
  isLunoraQueue: true;
424
+ }> = Record<string, never>, Workflows extends Record<string, {
425
+ isLunoraWorkflow: true;
316
426
  }> = Record<string, never>> {
317
427
  /** `using platform = createNodePlatform(...)` support — delegates to `close()`. */
318
428
  [Symbol.dispose]: () => void;
319
429
  /** What this target supports — see `NODE_CAPABILITIES` in `@lunora/platform`. */
320
430
  capabilities: PlatformCapabilities;
321
431
  /**
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.
432
+ * Tear this platform instance down: every resource this root built, in
433
+ * reverse construction order — the global store's own connection, every
434
+ * armed scheduler job timer, the registry's live shards, and last the shard
435
+ * host's pending alarm timer and its `better-sqlite3` database (and, with
436
+ * it, `kv`'s table — both live on the same connection). Nothing in this
437
+ * package closes these resources on its own — a `NodePlatform` a caller
438
+ * stops using without calling `close()` leaks the open file handles (plus
439
+ * their WAL/SHM sidecar files) and keeps the process alive on outstanding
440
+ * timers. Safe to call more than once. A `createNodePlatform` that throws
441
+ * unwinds the same list itself, so a failed construction leaks nothing and
442
+ * leaves nothing to call this on.
329
443
  *
330
444
  * **`close()` is a terminal state, not merely a cleanup step.** After it
331
445
  * runs: `scheduler.schedule()` throws instead of arming a fresh timer
@@ -335,13 +449,25 @@ interface NodePlatform<Queues extends Record<string, {
335
449
  * benign); `shard.alarms.set()`/`delete()` throw before mutating any
336
450
  * in-memory state (checked against the connection's own open/closed state,
337
451
  * 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
452
+ * whatever it last held; `sockets.accept()`/`setTag()`/`removeTag()` throw
453
+ * the same way, checked against the same connection state, before touching
454
+ * any runtime socket map or durable row. A no-op instead of a throw would
455
+ * be indistinguishable from a working call — exactly the silent-vanishing
340
456
  * this lifecycle exists to end.
341
457
  */
342
458
  close: () => void;
343
459
  /** In-process shard directory (see `createNodeShardRegistry` for what it can and cannot do). */
344
460
  directory: ShardDirectory;
461
+ /**
462
+ * The scheduler dispatch route, served in-process: `Request` in, shard
463
+ * response out, no listener. It answers the path `@lunora/dispatch` and
464
+ * `@lunora/scheduler` POST to. Pass it to `dispatchQueueBatch` as
465
+ * `fetchImpl` so a queue handler's `message.run` / `ctx.run` reaches the
466
+ * shard; the env you pass there must carry `LUNORA_ORIGIN_URL` and the same
467
+ * `LUNORA_ADMIN_TOKEN` as `dispatch.adminToken`. Every request is refused
468
+ * unless `dispatch` supplies a secret.
469
+ */
470
+ dispatch: (request: Request) => Promise<Response>;
345
471
  /**
346
472
  * Wait for every promise handed to `shard.waitUntil` to settle.
347
473
  *
@@ -351,8 +477,28 @@ interface NodePlatform<Queues extends Record<string, {
351
477
  * shutdown is `await platform.drain()` then `platform.close()`.
352
478
  */
353
479
  drain: () => Promise<void>;
480
+ /**
481
+ * The `.global()` backend, or `undefined` when the caller named no database
482
+ * file for it. There is no default path, because a global store silently
483
+ * rooted at `:memory:` loses every row when the process exits.
484
+ *
485
+ * **A building block, not a wiring.** Unlike its three siblings, nothing
486
+ * downstream of this composition root reads it: a `.global()` read or write
487
+ * reaches its backend through exactly one seam, `createShardCtxDb({ globalDb
488
+ * })`, and the only thing that passes `globalDb` is the generated `shard.ts`,
489
+ * from its `d1` / `hyperdriveGlobal` config thunks. `createNodePlatform`
490
+ * constructs no shard DO, so it cannot make that hop itself. A caller that
491
+ * wants `.global()` on this host makes it: after `migrate(schema)` has
492
+ * provisioned the tables, give the generated `createShardDO` a `d1` thunk
493
+ * returning `platform.globalTables.writer({ schema, … })` — `writer` builds
494
+ * the `createSqlCtxDb` facade that seam expects, and `node-platform.test.ts`
495
+ * round-trips a row through exactly that object.
496
+ */
497
+ globalTables?: NodeGlobalStore;
354
498
  /** Durable key-value storage backed by the same `better-sqlite3` database as `shard`. */
355
499
  kv: ShardKvStore;
500
+ /** The local-filesystem bucket, or `undefined` when the caller named no bucket directory. */
501
+ objectStorage?: R2BucketLike;
356
502
  /**
357
503
  * The declared queues, or `undefined` when the caller declared none.
358
504
  *
@@ -367,6 +513,28 @@ interface NodePlatform<Queues extends Record<string, {
367
513
  shard: ShardHost;
368
514
  /** Socket registry with mutable tags and SQLite-persisted attachments. */
369
515
  sockets: SocketHost;
516
+ /**
517
+ * The declared workflows, or `undefined` when the caller declared none.
518
+ * Runs are persisted to the same `better-sqlite3` database as the shard, so
519
+ * they survive a restart without a second store to configure.
520
+ */
521
+ workflows?: NodeWorkflowHost<Workflows>;
522
+ }
523
+ /**
524
+ * The secrets the dispatch route authenticates with. With neither, the route
525
+ * refuses everything.
526
+ *
527
+ * Queue and topic delivery authenticates with `adminToken` alone:
528
+ * `@lunora/dispatch` always sends `Bearer <LUNORA_ADMIN_TOKEN>` and never signs,
529
+ * so the value here must match the token the caller puts on the env it passes
530
+ * `dispatchQueueBatch`. `schedulerSecret` only verifies a signed body, which the
531
+ * scheduler's own delivery sends; queue delivery never does.
532
+ */
533
+ interface NodePlatformDispatchOptions {
534
+ /** `LUNORA_ADMIN_TOKEN`. The bearer queue and topic delivery sends, so it must match the caller's env. */
535
+ adminToken?: string;
536
+ /** `LUNORA_SCHEDULER_SECRET`. When set, a signed body is verified against it. Queue delivery does not sign. */
537
+ schedulerSecret?: string;
370
538
  }
371
539
  /**
372
540
  * Options for {@link createNodePlatform} — the shard host's (`path`,
@@ -376,10 +544,31 @@ interface NodePlatform<Queues extends Record<string, {
376
544
  * or job with nowhere to land is bookkeeping. `directory` makes the shards the
377
545
  * directory resolves for fan-out file-backed too, and `onAlarm` gives their
378
546
  * durable alarms somewhere to land.
547
+ *
548
+ * `queues`, `workflows`, `objectStorageDirectory` and `globalTablesPath` are the
549
+ * four declarations — each is absent from the returned platform when omitted.
379
550
  */
380
551
  type NodePlatformOptions<Queues extends Record<string, {
381
552
  isLunoraQueue: true;
553
+ }> = Record<string, never>, Workflows extends Record<string, {
554
+ isLunoraWorkflow: true;
382
555
  }> = Record<string, never>> = {
556
+ /** Secrets for the dispatch route. Omit to leave the route refusing every request. */
557
+ dispatch?: NodePlatformDispatchOptions;
558
+ /**
559
+ * Database file the `.global()` tables live in — its own file, never a
560
+ * shard's, because a table every shard reads must not be inside any one of
561
+ * them. Omit when the app declares no `.global()` table; there is no
562
+ * default, for the same reason the bucket has none.
563
+ */
564
+ globalTablesPath?: string;
565
+ /**
566
+ * Directory the object-storage bucket keeps its objects in, one file per
567
+ * key. Omit when the app declares no buckets — there is no default,
568
+ * because a bucket silently rooted at the process's working directory is
569
+ * worse than an absent one.
570
+ */
571
+ objectStorageDirectory?: string;
383
572
  /**
384
573
  * Deliver one assembled queue batch — wire this to `dispatchQueueBatch`.
385
574
  * Required alongside `queues`; without it the messages would be stored
@@ -388,11 +577,22 @@ type NodePlatformOptions<Queues extends Record<string, {
388
577
  onQueueBatch?: NodeQueueHostOptions<Queues>["onBatch"];
389
578
  /** The app's `defineQueue` results, keyed by export name. Omit when the app declares no queues. */
390
579
  queues?: Queues;
580
+ /** The app's `defineWorkflow` results, keyed by export name. Omit when the app declares no workflows. */
581
+ workflows?: Workflows;
391
582
  } & NodeSchedulerHostOptions & NodeShardHostOptions & NodeShardRegistryOptions;
392
583
  /** Compose every contract this package provides over one `better-sqlite3` database. */
393
584
  declare const createNodePlatform: <Queues extends Record<string, {
394
585
  isLunoraQueue: true;
395
- }> = Record<string, never>>(options?: NodePlatformOptions<Queues>) => NodePlatform<Queues>;
586
+ }> = Record<string, never>, Workflows extends Record<string, {
587
+ isLunoraWorkflow: true;
588
+ }> = Record<string, never>>(options?: NodePlatformOptions<Queues, Workflows>) => NodePlatform<Queues, Workflows>;
589
+ interface NodeProfileHandlerOptions {
590
+ /** The bearer a caller must present. Required and non-empty: an empty token would make the endpoint open to anyone. */
591
+ token: string;
592
+ }
593
+ type NodeProfileHandler = (request: Request) => Promise<Response>;
594
+ /** The public handler: the real profiler, guarded by `token`. */
595
+ declare const createNodeProfileHandler: ({ token }: NodeProfileHandlerOptions) => NodeProfileHandler;
396
596
  /** Options for {@link createNodeR2Bucket}. */
397
597
  interface NodeR2BucketOptions {
398
598
  /** The bucket directory — created on first write. Objects live here, one file per key. */
@@ -400,7 +600,7 @@ interface NodeR2BucketOptions {
400
600
  }
401
601
  /**
402
602
  * Create an `R2BucketLike` over the local filesystem. Any object shape
403
- * `createStorage({ bucket })` accepts — `put`/`get`/`head`/`delete`/`list` —
603
+ * `createStorage({ bucket, bucketName })` accepts — `put`/`get`/`head`/`delete`/`list` —
404
604
  * maps directly onto a file operation.
405
605
  */
406
606
  declare const createNodeR2Bucket: (options: NodeR2BucketOptions) => R2BucketLike;
@@ -473,48 +673,12 @@ interface NodeSocketHost {
473
673
  }
474
674
  /** Build the socket registry, persisting attachments and tags to `database`. */
475
675
  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
676
  /**
513
677
  * Build a durable {@link WorkflowStore} over a `better-sqlite3` connection.
514
678
  * Pass the result as `createNodeWorkflowHost({ store })`.
515
679
  */
516
680
  declare const createNodeWorkflowStore: (database: Database.Database) => WorkflowStore;
517
- export {
681
+ export { NODE_SCHEDULER_DISPATCH_PATH,
518
682
  /**
519
683
  * `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
520
684
  * host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
@@ -534,49 +698,29 @@ export {
534
698
  * TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
535
699
  * lifecycle/global-store tests.
536
700
  *
537
- * **Still missing**, and rated accordingly in `NODE_CAPABILITIES`
538
- * (`@lunora/platform`): a dev server — so `lunora dev --target node` does not
539
- * exist yet — plus most Cloudflare product bindings (Queues, Vectorize, Workers
540
- * AI, Browser Rendering, Containers, Analytics Engine, Pipelines, Secrets Store,
541
- * Hyperdrive). R2 buckets are emulated over the local filesystem
542
- * (`./node-r2-bucket`) and Workflows over the `@visulima/workflow` engine
543
- * (`./node-workflow-host`). Cross-shard fan-out is emulated via
544
- * `@lunora/runtime`'s query coordinator over the in-process shard registry, and
545
- * a `@lunora/config` deploy driver makes `--target node` resolve. See
546
- * `plans/234-node-host-findings.md`.
547
- */
548
- type NodeGlobalContextDatabaseOptions,
549
- /**
550
- * `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
551
- * host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
552
- * `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
553
- * scheduler registry.
701
+ * **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
702
+ * (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
703
+ * buckets over the local filesystem (`./node-r2-bucket`), workflows over the
704
+ * `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
705
+ * via `@lunora/runtime`'s query coordinator over the in-process shard registry.
706
+ * `createNodePlatform` binds all three declarations (`queues`, `workflows`,
707
+ * `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
708
+ * is the one combination that fails at runtime with no diagnostic before it.
554
709
  *
555
- * Promoted from `@lunora/platform`'s `node:sqlite` reference host
556
- * (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
557
- * durability half of each contract actually holds: alarms and scheduler jobs
558
- * are persisted **and re-armed on construction**, socket attachments and tags
559
- * live in SQLite rather than a `Map`, and `.global()` tables run the real
560
- * `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
561
- * a restart test — a second host over the same database file — not only by the
562
- * TCK's simulated recycle.
710
+ * **Still missing:** a dev server. There is no `lunora dev --target node`, and
711
+ * nothing here owns a timer, so queue delivery is driven by an explicit
712
+ * `poll()`. Also absent are the Cloudflare product bindings with no local
713
+ * equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
714
+ * Engine, Pipelines, Secrets Store, Hyperdrive.
563
715
  *
564
- * Three suites run against this package: `@lunora/platform/conformance`'s host
565
- * TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
566
- * lifecycle/global-store tests.
567
- *
568
- * **Still missing**, and rated accordingly in `NODE_CAPABILITIES`
569
- * (`@lunora/platform`): a dev server — so `lunora dev --target node` does not
570
- * exist yet — plus most Cloudflare product bindings (Queues, Vectorize, Workers
571
- * AI, Browser Rendering, Containers, Analytics Engine, Pipelines, Secrets Store,
572
- * Hyperdrive). R2 buckets are emulated over the local filesystem
573
- * (`./node-r2-bucket`) and Workflows over the `@visulima/workflow` engine
574
- * (`./node-workflow-host`). Cross-shard fan-out is emulated via
575
- * `@lunora/runtime`'s query coordinator over the in-process shard registry, and
576
- * a `@lunora/config` deploy driver makes `--target node` resolve. See
716
+ * `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
717
+ * for `provision` — which reports, once, which declared features this target
718
+ * cannot serve, and writes nothing: there is no hosted control plane to deploy
719
+ * to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
720
+ * server, which is why both statements above hold at once. See
577
721
  * `plans/234-node-host-findings.md`.
578
722
  */
579
- type NodeGlobalStore,
723
+ type NodeDispatchInvocation,
580
724
  /**
581
725
  * `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
582
726
  * host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
@@ -596,15 +740,26 @@ type NodeGlobalStore,
596
740
  * TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
597
741
  * lifecycle/global-store tests.
598
742
  *
599
- * **Still missing**, and rated accordingly in `NODE_CAPABILITIES`
600
- * (`@lunora/platform`): a dev server — so `lunora dev --target node` does not
601
- * exist yet — plus most Cloudflare product bindings (Queues, Vectorize, Workers
602
- * AI, Browser Rendering, Containers, Analytics Engine, Pipelines, Secrets Store,
603
- * Hyperdrive). R2 buckets are emulated over the local filesystem
604
- * (`./node-r2-bucket`) and Workflows over the `@visulima/workflow` engine
605
- * (`./node-workflow-host`). Cross-shard fan-out is emulated via
606
- * `@lunora/runtime`'s query coordinator over the in-process shard registry, and
607
- * a `@lunora/config` deploy driver makes `--target node` resolve. See
743
+ * **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
744
+ * (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
745
+ * buckets over the local filesystem (`./node-r2-bucket`), workflows over the
746
+ * `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
747
+ * via `@lunora/runtime`'s query coordinator over the in-process shard registry.
748
+ * `createNodePlatform` binds all three declarations (`queues`, `workflows`,
749
+ * `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
750
+ * is the one combination that fails at runtime with no diagnostic before it.
751
+ *
752
+ * **Still missing:** a dev server. There is no `lunora dev --target node`, and
753
+ * nothing here owns a timer, so queue delivery is driven by an explicit
754
+ * `poll()`. Also absent are the Cloudflare product bindings with no local
755
+ * equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
756
+ * Engine, Pipelines, Secrets Store, Hyperdrive.
757
+ *
758
+ * `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
759
+ * for `provision` — which reports, once, which declared features this target
760
+ * cannot serve, and writes nothing: there is no hosted control plane to deploy
761
+ * to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
762
+ * server, which is why both statements above hold at once. See
608
763
  * `plans/234-node-host-findings.md`.
609
764
  */
610
- 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 };
765
+ type NodeDispatchRouteOptions, type NodeGlobalContextDatabaseOptions, type NodeGlobalStore, type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformDispatchOptions, type NodePlatformOptions, type NodeProfileHandler, type NodeProfileHandlerOptions, 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, createNodeDispatchRoute, createNodeGlobalStore, createNodePlatform, createNodeProfileHandler, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createNodeGlobalStore as r,createNodeSqlExec as t}from"./packem_shared/createNodeGlobalStore-Cwi33XOk.mjs";import{createNodeShardKvStore as d}from"./packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodePlatform as f}from"./packem_shared/createNodePlatform-CfKfpghn.mjs";import{createNodeQueueHost as x}from"./packem_shared/createNodeQueueHost--mBLGSxF.mjs";import{createNodeR2Bucket as p}from"./packem_shared/createNodeR2Bucket-CmHYlIni.mjs";import{createNodeSchedulerHost as l}from"./packem_shared/createNodeSchedulerHost-vZgv1CEo.mjs";import{createNodeShardHost as h}from"./packem_shared/createNodeShardHost-ByDrsNFb.mjs";import{createNodeShardRegistry as k}from"./packem_shared/createNodeShardRegistry-Zy8eZoF9.mjs";import{createNodeShardState as w}from"./packem_shared/createNodeShardState-CpUzvWwd.mjs";import{createNodeSocketHost as W}from"./packem_shared/createNodeSocketHost-Ce4aH2Kq.mjs";import{createNodeWorkflowHost as g}from"./packem_shared/createNodeWorkflowHost-CTVBYiuj.mjs";import{createNodeWorkflowStore as q}from"./packem_shared/createNodeWorkflowStore-CL7RYP6H.mjs";export{r as createNodeGlobalStore,f as createNodePlatform,x as createNodeQueueHost,p as createNodeR2Bucket,l as createNodeSchedulerHost,h as createNodeShardHost,d as createNodeShardKvStore,k as createNodeShardRegistry,w as createNodeShardState,W as createNodeSocketHost,t as createNodeSqlExec,g as createNodeWorkflowHost,q as createNodeWorkflowStore};
1
+ import{NODE_SCHEDULER_DISPATCH_PATH as r,createNodeDispatchRoute as t}from"./packem_shared/NODE_SCHEDULER_DISPATCH_PATH-CNBAtztq.mjs";import{createNodeGlobalStore as d,createNodeSqlExec as c}from"./packem_shared/createNodeGlobalStore-DhHyIT8I.mjs";import{createNodeShardKvStore as N}from"./packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodePlatform as p}from"./packem_shared/createNodePlatform-D-vCAW6Z.mjs";import{createNodeProfileHandler as S}from"./packem_shared/createNodeProfileHandler-MX5xKPg0.mjs";import{createNodeQueueHost as H}from"./packem_shared/createNodeQueueHost-CttDb5sY.mjs";import{createNodeR2Bucket as h}from"./packem_shared/createNodeR2Bucket-6I2HobcB.mjs";import{createNodeSchedulerHost as k}from"./packem_shared/createNodeSchedulerHost-BIJ4H38-.mjs";import{createNodeShardHost as E}from"./packem_shared/createNodeShardHost-B6mt1S-h.mjs";import{createNodeShardRegistry as R}from"./packem_shared/createNodeShardRegistry-q-46Oj8M.mjs";import{createNodeShardState as _}from"./packem_shared/createNodeShardState-D6XMWbFs.mjs";import{createNodeSocketHost as A}from"./packem_shared/createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as T}from"./packem_shared/createNodeWorkflowHost-B76TKiw_.mjs";import{createNodeWorkflowStore as b}from"./packem_shared/createNodeWorkflowStore-CL7RYP6H.mjs";export{r as NODE_SCHEDULER_DISPATCH_PATH,t as createNodeDispatchRoute,d as createNodeGlobalStore,p as createNodePlatform,S as createNodeProfileHandler,H as createNodeQueueHost,h as createNodeR2Bucket,k as createNodeSchedulerHost,E as createNodeShardHost,N as createNodeShardKvStore,R as createNodeShardRegistry,_ as createNodeShardState,A as createNodeSocketHost,c as createNodeSqlExec,T as createNodeWorkflowHost,b as createNodeWorkflowStore};
@@ -0,0 +1 @@
1
+ import{LunoraError as i}from"@lunora/errors";import{toErrorResponse as h,resolveShard as u}from"@lunora/runtime";import{s as l}from"./hmac-url-BrwGfbCr.mjs";const f=async(t,e)=>{if(!t.body)return"";const n=t.body.getReader(),r=new TextDecoder;let a=0,o="";for(;;){const{done:s,value:d}=await n.read();if(s)break;if(d){if(a+=d.byteLength,a>e)return await n.cancel().catch(()=>{}),null;o+=r.decode(d,{stream:!0})}}return o+=r.decode(),o},c=(t,e)=>{const n=Math.max(t.length,e.length);let r=t.length^e.length;for(let a=0;a<n;a+=1){const o=a<t.length?t.charCodeAt(a):0,s=a<e.length?e.charCodeAt(a):0;r|=o^s}return r===0},y="/_lunora/scheduler/dispatch",g="__lunora_relation__:",p=t=>t.startsWith(g),w=(t,e)=>{if(!e||e.length===0)return!1;const n=t.headers.get("authorization");if(!n)return!1;const[r,...a]=n.split(" ");return r?.toLowerCase()!=="bearer"?!1:c(e,a.join(" ").trim())},_=async(t,e,n)=>{const r=t.headers.get("x-lunora-scheduler-signature"),{schedulerSecret:a}=n;return r!==null&&r.length>0&&a!==void 0&&a.length>0?c(await l(a,e),r):w(t,n.adminToken)},A="x-lunora-paywall",S="1",E=({forwardedIdentity:t,mutationId:e,traceparent:n})=>{const r={"content-type":"application/json",[A]:S,"x-lunora-system":"1"};return t?.userId!==void 0&&t.userId.length>0&&(r["x-lunora-userid"]=t.userId),t?.identity!==void 0&&t.identity.length>0&&(r["x-lunora-identity"]=t.identity),e!==void 0&&e.length>0&&(r["x-lunora-mutation-id"]=e),n!==void 0&&n.length>0&&(r.traceparent=n),r},T=t=>{const e=t.headers.get("x-lunora-userid"),n=t.headers.get("x-lunora-identity");if(!(e===null&&n===null))return{...n===null?{}:{identity:n},...e===null?{}:{userId:e}}},P=1048576,m=async t=>{const e=await f(t,P);if(e===null)throw new i("PAYLOAD_TOO_LARGE","Scheduler dispatch body is too large");return e},D=async(t,e)=>{if(t.method!=="POST")throw new i("METHOD_NOT_ALLOWED","Scheduler dispatch endpoint requires POST",{status:405});const n=await m(t);if(!await _(t,n,{adminToken:e.adminToken,schedulerSecret:e.schedulerSecret}))throw new i("DISPATCH_UNAUTHENTICATED","Scheduler dispatch requires a valid signature or admin bearer");let a;try{a=JSON.parse(n)}catch{throw new i("BAD_REQUEST","Scheduler dispatch body must be valid JSON")}const o=a??{};if(typeof o.workflow=="string"||o.pool!==void 0)throw new i("SERVICE_UNAVAILABLE","Scheduler dispatch for a workflow or pool target is not served by the Node dispatch route");if(typeof o.functionPath!="string"||o.functionPath.length===0)throw new i("BAD_REQUEST","Scheduler dispatch is missing `functionPath`");if(p(o.functionPath))throw new i("FORBIDDEN","`__lunora_relation__:*` is a fan-out-only reserved RPC and cannot be dispatched to a single shard");const s=typeof o.shardKey=="string"&&o.shardKey.length>0?o.shardKey:e.defaultShardKey;return e.invoke({args:o.args??{},functionPath:o.functionPath,id:typeof o.id=="string"&&o.id.length>0?o.id:void 0,identity:T(t),shardKey:s,traceparent:t.headers.get("traceparent")??void 0})},L=(t,e)=>n=>{if(!e)throw new i("SERVICE_UNAVAILABLE","@lunora/platform-node: the dispatch route needs `onFetch` to run a function; without it a shard only echoes its key");const r=new Request("https://shard.internal/rpc",{body:JSON.stringify({args:n.args,functionPath:n.functionPath}),headers:E({forwardedIdentity:n.identity,mutationId:n.id,traceparent:n.traceparent}),method:"POST"});return u(t,n.shardKey).fetch(r)},O=t=>async e=>{try{return await D(e,t)}catch(n){return h(n)}},N=y;export{N as NODE_SCHEDULER_DISPATCH_PATH,O as createNodeDispatchRoute,L as createShardInvoke};
@@ -0,0 +1 @@
1
+ import{sqliteDialect as l}from"@lunora/d1";import{createSqlCtxDb as c,runSqlGlobalTableMigrations as s,runSqlAggregateMigrations as p,runSqlRankMigrations as u,runSqlSearchMigrations as f,backfillSqlSearchIndexes as g,runSqlCdcMigration as m}from"@lunora/sql-store";import S from"better-sqlite3";import{p as n}from"./prepared-statement-3XBQZXtC.mjs";const a={...l,searchBackfillHint:"run the global store's migrate(), which backfills every search index to completion"},d=o=>({all:async(t,r)=>n(o,t).all(...r),batch:async t=>{o.transaction(()=>{for(const{params:r,sql:e}of t)n(o,e).run(...r)})()},run:async(t,r)=>({rowsAffected:n(o,t).run(...r).changes})}),x=(o={})=>{const t=new S(o.path??":memory:");t.pragma("journal_mode = WAL");const r=d(t);return{database:t,dispose:()=>{t.open&&t.close()},exec:r,migrate:async(e,i={})=>{await s(r,e,a),await p(r,e,a),await u(r,e,a),await f(r,e,a),await g(r,e,a),i.cdc===!0&&await m(r,a)},writer:e=>c({...e,dialect:a,exec:r,provisionScope:r})}};export{x as createNodeGlobalStore,d as createNodeSqlExec};
@@ -0,0 +1 @@
1
+ import{LunoraError as k}from"@lunora/errors";import{NODE_CAPABILITIES as N}from"@lunora/platform";import{createNodeDispatchRoute as b,createShardInvoke as g}from"./NODE_SCHEDULER_DISPATCH_PATH-CNBAtztq.mjs";import{createNodeGlobalStore as y}from"./createNodeGlobalStore-DhHyIT8I.mjs";import{createNodeShardKvStore as R}from"./createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeQueueHost as T}from"./createNodeQueueHost-CttDb5sY.mjs";import{createNodeR2Bucket as q}from"./createNodeR2Bucket-6I2HobcB.mjs";import{createNodeSchedulerHost as B}from"./createNodeSchedulerHost-BIJ4H38-.mjs";import{createNodeShardHost as I}from"./createNodeShardHost-B6mt1S-h.mjs";import{createNodeShardRegistry as _}from"./createNodeShardRegistry-q-46Oj8M.mjs";import{createNodeSocketHost as D}from"./createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as H}from"./createNodeWorkflowHost-B76TKiw_.mjs";import{createNodeWorkflowStore as P}from"./createNodeWorkflowStore-CL7RYP6H.mjs";const z=(e={})=>{const o=[],{database:r,dispose:u,drain:i,host:l}=I(e);o.push(u);try{const a=R(r),t=_(e);o.push(()=>{t.close()});const{directory:d}=t,m=b({adminToken:e.dispatch?.adminToken,defaultShardKey:"__root__",invoke:g(d,e.onFetch!==void 0),schedulerSecret:e.dispatch?.schedulerSecret}),{socket:f}=D(r),{dispose:n,scheduler:p}=B(r,e);o.push(n);const S=e.queues===void 0?void 0:T(r,{onBatch:e.onQueueBatch??(()=>{throw new k("VALIDATION_ERROR","@lunora/platform-node: createNodePlatform was given `queues` without `onQueueBatch`, so a delivered batch has nowhere to go — pass dispatchQueueBatch from @lunora/queue")}),queues:e.queues}),v=e.objectStorageDirectory===void 0?void 0:q({directory:e.objectStorageDirectory}),c=e.globalTablesPath===void 0?void 0:y({path:e.globalTablesPath});c!==void 0&&o.push(()=>{c.dispose()});const w=e.workflows===void 0?void 0:H({store:P(r),workflows:e.workflows}),h=()=>{for(const s of o.toReversed())s()};return{capabilities:N,close:h,directory:d,dispatch:s=>m(s),drain:i,globalTables:c,kv:a,objectStorage:v,queues:S,scheduler:p,shard:l,sockets:f,workflows:w,[Symbol.dispose]:h}}catch(a){for(const t of o.toReversed())try{t()}catch{}throw a}};export{z as createNodePlatform};
@@ -0,0 +1 @@
1
+ import{timingSafeEqual as B,createHash as L}from"node:crypto";import{LunoraError as l,toErrorBody as H}from"@lunora/errors";import{Session as $}from"node:inspector";import{gzipSync as C}from"node:zlib";import{StringTable as x,Profile as j}from"pprof-format";const z="(anonymous)",T=e=>e>=0?e+1:0,R=e=>{const t=new x,n=[],o=[],p=new Map,i=new Map,f=(r,a)=>{const c=`${r}\0${a.url}\0${String(a.lineNumber)}\0${String(a.columnNumber)}`,s=p.get(c);if(s!==void 0)return s;const u=n.length+1;return p.set(c,u),n.push({filename:t.dedup(a.url),id:u,name:t.dedup(r),startLine:T(a.lineNumber),systemName:t.dedup(r)}),u},h=r=>{const a=r.functionName===""?z:r.functionName,c=f(a,r),s=T(r.lineNumber),u=T(r.columnNumber),y=`${String(c)}\0${String(s)}\0${String(u)}`,I=i.get(y);if(I!==void 0)return I;const w=o.length+1;return i.set(y,w),o.push({id:w,line:[{column:u,functionId:c,line:s}]}),w},S=new Map,U=r=>{const a=S.get(r);if(a!==void 0)return a;const c=[];let s=e.frames.get(r);for(;s?.parentId!==void 0;)c.push(h(s.callFrame)),s=e.frames.get(s.parentId);return S.set(r,c),c},g=new Map;for(const r of e.samples){const a=U(r.nodeId),c=a.join(","),s=g.get(c);s===void 0?g.set(c,{locationId:a,values:[...r.values]}):s.values=s.values.map((u,y)=>u+(r.values[y]??0))}const k=[...g.values()].map(r=>({locationId:r.locationId,value:r.values}));return new j({durationNanos:e.durationNanos,function:n,location:o,period:e.period,periodType:{type:t.dedup(e.periodType.type),unit:t.dedup(e.periodType.unit)},sample:k,sampleType:e.sampleTypes.map(r=>({type:t.dedup(r.type),unit:t.dedup(r.unit)})),stringTable:t,timeNanos:e.startNanos})},W=e=>{const t=new Map;for(const n of e.nodes)for(const o of n.children??[])t.set(o,n.id);return new Map(e.nodes.map(n=>[n.id,{callFrame:n.callFrame,parentId:t.get(n.id)}]))},Y=e=>{const t=new Map,n=[{node:e,parentId:void 0}];for(let o=n.pop();o!==void 0;o=n.pop()){t.set(o.node.id,{callFrame:o.node.callFrame,parentId:o.parentId});for(const p of o.node.children)n.push({node:p,parentId:o.node.id})}return t},G=[{type:"samples",unit:"count"},{type:"cpu",unit:"nanoseconds"}],Q=[{type:"space",unit:"bytes"}],P=e=>e*1e6,J=(e,t)=>R({durationNanos:(e.endTime-e.startTime)*1e3,frames:W(e),period:t.intervalMicros*1e3,periodType:{type:"cpu",unit:"nanoseconds"},samples:e.samples.map((n,o)=>({nodeId:n,values:[1,(e.timeDeltas[o]??0)*1e3]})),sampleTypes:G,startNanos:P(t.startedAtMs)}),V=(e,t)=>R({durationNanos:P(t.durationMs),frames:Y(e.head),period:t.samplingIntervalBytes,periodType:{type:"space",unit:"bytes"},samples:e.samples.map(n=>({nodeId:n.nodeId,values:[n.size]})),sampleTypes:Q,startNanos:P(t.startedAtMs)}),M=e=>new Uint8Array(C(e.encode())),_=1e3,v=5e4,D=["cpu","heap"],K=e=>typeof e=="string"&&D.includes(e),E=1e3,A=32768;let N=!1;const b=e=>new Promise(t=>{setTimeout(t,e)}),d=(e,t,n)=>new Promise((o,p)=>{e.post(t,n,(i,f)=>{i===null?o(f):p(i)})}),F=e=>{const{durationMs:t,profileType:n}=e;if(!K(n))throw new l("BAD_REQUEST",`profile type must be one of ${D.join(", ")}`);if(typeof t!="number"||!Number.isInteger(t)||t<_||t>v)throw new l("BAD_REQUEST",`duration must be an integer from ${String(_)} to ${String(v)} ms`);return{durationMs:t,profileType:n}},X=async e=>{const t=new $,n=Date.now();let o;t.connect();try{if(e.profileType==="cpu"){await d(t,"Profiler.enable"),await d(t,"Profiler.setSamplingInterval",{interval:E}),await d(t,"Profiler.start"),o="cpu",await b(e.durationMs);const{profile:i}=await d(t,"Profiler.stop");return o=void 0,M(J(i,{intervalMicros:E,startedAtMs:n}))}await d(t,"HeapProfiler.startSampling",{samplingInterval:A}),o="heap",await b(e.durationMs);const{profile:p}=await d(t,"HeapProfiler.stopSampling");return o=void 0,M(V(p,{durationMs:e.durationMs,samplingIntervalBytes:A,startedAtMs:n}))}finally{o==="cpu"?await d(t,"Profiler.stop").catch(()=>{}):o==="heap"&&await d(t,"HeapProfiler.stopSampling").catch(()=>{}),t.disconnect()}},Z=()=>({capture:async e=>{const t=F(e);if(N)throw new l("CONFLICT","a profile capture is already running in this process",{hint:"Wait for the running capture to finish, then retry."});N=!0;try{return await X(t)}finally{N=!1}}}),q=/^Bearer\s+(?<token>\S+)$/iu,O=e=>L("sha256").update(e,"utf8").digest(),m=(e,t={})=>{const{body:n,status:o}=H(e);return Response.json({error:n},{headers:t,status:o})},ee=e=>{const t=e.headers.get("authorization");return t===null?void 0:q.exec(t)?.groups?.token},te=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),ne=(e,t)=>{if(e.length===0)throw new l("ADMIN_TOKEN_NOT_CONFIGURED","createNodeProfileHandler needs a non-empty token; refusing to expose the profiler without one");const n=O(e);return async o=>{const p=ee(o);if(p===void 0)return m(new l("UNAUTHORIZED","send the bearer token as `Authorization: Bearer <token>`"),{"WWW-Authenticate":"Bearer"});if(!B(O(p),n))return m(new l("FORBIDDEN","the bearer token is not accepted"));if(o.method!=="POST")return m(new l("METHOD_NOT_ALLOWED","the profile endpoint accepts POST only"),{Allow:"POST"});let i;try{i=await o.json()}catch{return m(new l("BAD_REQUEST","the body must be JSON: { duration_ms, profile_type }"))}if(!te(i))return m(new l("BAD_REQUEST","the body must be a JSON object: { duration_ms, profile_type }"));try{const f=F({durationMs:i.duration_ms,profileType:i.profile_type}),h=await t.capture(f);return new Response(h,{headers:{"Content-Type":"application/gzip"},status:200})}catch(f){return m(f)}}},ce=({token:e})=>ne(e,Z());export{ne as buildProfileHandler,ce as createNodeProfileHandler};
@@ -0,0 +1,17 @@
1
+ import{randomUUID as F}from"node:crypto";import{deserialize as H,serialize as W}from"node:v8";import{LunoraError as m}from"@lunora/errors";import{isQueueDefinition as X,queueDefaultName as $,queueBindingName as Q}from"@lunora/queue";const _={maxBatchSize:10,maxBatchTimeoutSeconds:5,maxRetries:3,retryDelaySeconds:0,visibilityTimeoutMs:3e4},V=43200,A=131072,S=262144,k=(e,i)=>{switch(i){case"bytes":{if(e instanceof ArrayBuffer)return Buffer.from(e);if(ArrayBuffer.isView(e))return Buffer.from(e.buffer,e.byteOffset,e.byteLength);throw new m("VALIDATION_ERROR",'@lunora/platform-node: queue contentType "bytes" needs an ArrayBuffer or a typed array')}case"text":{if(typeof e!="string")throw new m("VALIDATION_ERROR",'@lunora/platform-node: queue contentType "text" needs a string');return Buffer.from(e,"utf8")}case"v8":return W(e);default:return Buffer.from(JSON.stringify(e??null),"utf8")}},O=(e,i,R)=>{const E=k(e,i);if(E.byteLength>A)throw new m("VALIDATION_ERROR",`@lunora/platform-node: a message for queue "${R}" encodes to ${String(E.byteLength)} bytes, over the Cloudflare Queues ceiling of ${String(A)} (128 KiB) per message — store the payload and enqueue a reference to it`);return E},D=(e,i)=>{switch(i){case"bytes":return e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);case"text":return e.toString("utf8");case"v8":return H(e);default:return JSON.parse(e.toString("utf8"))}},Y=e=>{try{return D(e.body,e.content_type)}catch{return}},y=e=>e===void 0||!Number.isFinite(e)?0:Math.min(Math.max(0,Math.floor(e)),V)*1e3,K=(e,i)=>{e.pragma("journal_mode = WAL"),e.exec(`CREATE TABLE IF NOT EXISTS _lunora_queue_messages (
2
+ id TEXT PRIMARY KEY,
3
+ queue TEXT NOT NULL,
4
+ body BLOB NOT NULL,
5
+ content_type TEXT NOT NULL,
6
+ attempts INTEGER NOT NULL DEFAULT 0,
7
+ visible_at INTEGER NOT NULL,
8
+ enqueued_at INTEGER NOT NULL,
9
+ state TEXT NOT NULL DEFAULT 'pending'
10
+ )`),e.exec("CREATE INDEX IF NOT EXISTS _lunora_queue_messages_due ON _lunora_queue_messages (queue, state, visible_at)");const R=e.prepare("INSERT INTO _lunora_queue_messages (id, queue, body, content_type, visible_at, enqueued_at) VALUES (?, ?, ?, ?, ?, ?)"),E=e.prepare(`UPDATE _lunora_queue_messages
11
+ SET attempts = attempts + 1, visible_at = ?
12
+ WHERE id IN (
13
+ SELECT id FROM _lunora_queue_messages
14
+ WHERE queue = ? AND state = 'pending' AND visible_at <= ?
15
+ ORDER BY visible_at, enqueued_at LIMIT ?
16
+ )
17
+ RETURNING *`),q=e.prepare("UPDATE _lunora_queue_messages SET visible_at = ? WHERE id = ?"),h=e.prepare("DELETE FROM _lunora_queue_messages WHERE id = ?"),v=e.prepare("UPDATE _lunora_queue_messages SET state = 'dead' WHERE id = ?"),B=e.prepare("SELECT enqueued_at FROM _lunora_queue_messages WHERE queue = ? AND state = 'pending' AND visible_at <= ? ORDER BY enqueued_at LIMIT 1"),I=e.prepare("SELECT COUNT(*) AS n FROM _lunora_queue_messages WHERE queue = ? AND state = 'pending' AND visible_at <= ?"),M=e.prepare("SELECT * FROM _lunora_queue_messages WHERE queue = ? AND state = 'dead' ORDER BY enqueued_at"),b=e.prepare("UPDATE _lunora_queue_messages SET state = 'pending', attempts = 0, visible_at = ? WHERE id = ? AND state = 'dead'"),p=Object.entries(i.queues).map(([t,n])=>{if(!X(n))throw new m("VALIDATION_ERROR",`@lunora/platform-node: "${t}" is not a defineQueue result`);return{definition:n,exportName:t,name:n.name??$(t)}}),x=new Set(p.map(t=>t.name));for(const t of p){const n=t.definition.deadLetterQueue;if(n!==void 0){if(n===t.name)throw new m("VALIDATION_ERROR",`@lunora/platform-node: queue "${t.name}" names itself as its own deadLetterQueue, which would redeliver forever`);if(!x.has(n))throw new m("VALIDATION_ERROR",`@lunora/platform-node: queue "${t.name}" names deadLetterQueue "${n}", which no declared queue provides — its dead letters would be unreachable`)}}const U=i.visibilityTimeoutMs??_.visibilityTimeoutMs,T=i.now??Date.now,g=(t,n,s,u,o)=>{R.run(F(),t,n,s,o+u,o)},w=(t,n,s,u,o)=>{if(s==="ack"){h.run(t.id);return}const c=n.definition.maxRetries??_.maxRetries;if(t.attempts>c){const l=n.definition.deadLetterQueue;l===void 0?v.run(t.id):(g(l,t.body,t.content_type,0,o),h.run(t.id));return}const f=y(u??n.definition.retryDelay??_.retryDelaySeconds);q.run(o+f,t.id)},N={},L={...i.env};for(const t of p){const n={send:async(s,u)=>{const o=u?.contentType??"json";g(t.name,O(s,o,t.name),o,y(u?.delaySeconds),T())},sendBatch:async(s,u)=>{const o=T(),c=y(u?.delaySeconds),f=[...s].map(a=>{const r=a.contentType??"json";return{contentType:r,delay:a.delaySeconds===void 0?c:y(a.delaySeconds),encoded:O(a.body,r,t.name)}}),l=f.reduce((a,r)=>a+r.encoded.byteLength,0);if(l>S)throw new m("VALIDATION_ERROR",`@lunora/platform-node: a sendBatch for queue "${t.name}" encodes to ${String(l)} bytes, over the Cloudflare Queues ceiling of ${String(S)} (256 KiB) per batch — split it across more calls`);e.transaction(()=>{for(const a of f)g(t.name,a.encoded,a.contentType,a.delay,o)})()}};N[t.exportName]=n,L[Q(t.exportName)]=n}const C=async(t,n)=>{const s=Math.min(Math.max(1,t.definition.maxBatchSize??_.maxBatchSize),100),u=I.get(t.name,n)?.n??0;if(u===0)return!1;if(u<s){const r=B.get(t.name,n)?.enqueued_at,d=Math.min(Math.max(0,t.definition.maxBatchTimeout??_.maxBatchTimeoutSeconds),60)*1e3;if(r!==void 0&&n-r<d)return!1}const o=e.transaction(()=>E.all(n+U,t.name,n,s)).immediate();if(o.length===0)return!1;const c=new Map,f=o.map(r=>({ack:()=>{c.set(r.id,{outcome:"ack"})},attempts:r.attempts,body:D(r.body,r.content_type),id:r.id,retry:d=>{c.set(r.id,{delaySeconds:d?.delaySeconds,outcome:"retry"})},timestamp:new Date(r.enqueued_at)})),l={ackAll:()=>{for(const r of o)c.set(r.id,{outcome:"ack"})},messages:f,queue:t.name,retryAll:r=>{for(const d of o)c.set(d.id,{delaySeconds:r?.delaySeconds,outcome:"retry"})}};let a="ack";try{await i.onBatch(l)}catch{a="retry"}return e.transaction(()=>{for(const r of o){const d=c.get(r.id);w(r,t,d?.outcome??a,d?.delaySeconds,n)}})(),!0};return{bindings:N,deadLetters:{list:t=>M.all(t).map(n=>({attempts:n.attempts,body:Y(n),id:n.id})),requeue:t=>b.run(T(),t).changes>0},env:L,poll:async(t=T())=>{let n=0;for(const s of p)s.definition.mode!=="pull"&&await C(s,t)&&(n+=1);return n}}};export{K as createNodeQueueHost};