@lunora/platform-node 1.0.0-alpha.11 → 1.0.0-alpha.111

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 +297 -75
  3. package/dist/index.d.ts +297 -75
  4. package/dist/index.mjs +1 -1
  5. package/dist/packem_shared/createNodeGlobalStore-DiGxjmD7.mjs +1 -0
  6. package/dist/packem_shared/createNodePlatform-B6LVGo8q.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-DqOqumS8.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
package/dist/index.d.ts CHANGED
@@ -14,13 +14,14 @@ type NodeGlobalContextDatabaseOptions = Omit<SqlCtxDbOptions, "dialect" | "exec"
14
14
  /**
15
15
  * Wrap a `better-sqlite3` connection as the async exec the store core consumes.
16
16
  *
17
- * `batch` is deliberately **not** implemented. The contract lets an exec that
18
- * omits it fall back to a sequential `run()` loop, and that fallback is already
19
- * optimal here: `batch` exists to collapse network round trips (D1 does it
20
- * atomically in one request; the Hyperdrive adapters dispatch concurrently over
21
- * a pool), and an embedded database has no round trip to collapse. Declaring it
22
- * would buy nothing and would opt this exec into the "MAY reorder or
23
- * parallelize" licence for no reason.
17
+ * `batch` runs the statements in order inside one `better-sqlite3`
18
+ * transaction, which is synchronous, so nothing else runs between them. There is
19
+ * no round trip to save; atomicity is the point. The store core's FTS5 search
20
+ * companion writes each document as an ordered list of statements, and without
21
+ * this two async writers interleaved at every `await` between them: one could
22
+ * move a document's mapping while the other's entry was mid-write, leaving an
23
+ * entry no later write or purge could reach. The `sqlite` dialect's contract
24
+ * requires an ordered, atomic `batch` for exactly that reason.
24
25
  */
25
26
  declare const createNodeSqlExec: (database: Database.Database) => SqlCtxExec;
26
27
  /** Options for {@link createNodeGlobalStore}. */
@@ -197,9 +198,12 @@ interface NodeShardHostOptions {
197
198
  * `_lunora_alarm` when the host is constructed over an existing database,
198
199
  * including an alarm whose time elapsed while nothing was running.
199
200
  *
200
- * A handler that throws is isolated to its own delivery; it never reaches
201
+ * A handler that throws is isolated to its own delivery — it never reaches
201
202
  * the caller that set the alarm, because by then that call has long
202
- * returned.
203
+ * returned — and the wakeup is then re-delivered with backoff up to
204
+ * {@link ALARM_RETRY_LIMIT} times, the way workerd retries a throwing
205
+ * `alarm()`. Delivery is at-least-once, so the handler must tolerate
206
+ * running twice for one scheduled timestamp.
203
207
  */
204
208
  onAlarm?: () => Promise<void> | void;
205
209
  /**
@@ -218,25 +222,59 @@ interface NodeShardHostOptions {
218
222
  /**
219
223
  * Build a `ShardHost` over a real `better-sqlite3` database.
220
224
  *
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.
225
+ * `runSerialized` and `transaction` share **one** boundary lock, because they
226
+ * write **one** `better-sqlite3` connection. `transaction` issues raw
227
+ * `BEGIN`/`COMMIT`/`ROLLBACK` — legal here (unlike inside a Cloudflare Durable
228
+ * Object, where the runtime forbids it and callers must use
229
+ * `storage.transaction`) because better-sqlite3 is a plain embedded database
230
+ * with no platform-level transaction primitive layered over it.
227
231
  *
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.
232
+ * ## Why one lane and not two
233
+ *
234
+ * This host shipped with two private tail chains, one per entry point, so each
235
+ * serialized against itself and neither against the other. `runSerialized`
236
+ * issues no `BEGIN` of its own, so one of its closures overlapping a bare
237
+ * `transaction()` wrote straight into that transaction's span. `assertOwnTurn`
238
+ * below catches the write — it is the only reason this host never silently lost
239
+ * one the way an unguarded connection would — but catching it is not the same
240
+ * as preventing it, and the refusal lands in the wrong place:
241
+ *
242
+ * 1. A `runSerialized` closure writes a row. Nothing is open, so it commits.
243
+ * 2. It awaits (a scheduler hop, an `onShardInit` read, any real I/O).
244
+ * 3. A bare `transaction()` runs `BEGIN` while it is parked. `transaction` is
245
+ * a public contract surface, and `./node-shard-state` re-exports it as the
246
+ * `storage.transaction` member `ShardDO` documents as "the platform
247
+ * primitive", so a subclass reaches one without going through the engine.
248
+ * 4. The closure resumes and its next write is refused with
249
+ * `SHARD_UNAVAILABLE`, so `runSerialized` **rejects** — with step 1 already
250
+ * durable.
251
+ *
252
+ * The boundary the contract promises is atomic-or-nothing to its caller tore in
253
+ * half: a rejection the caller will retry, on top of a write the retry now
254
+ * re-applies. That is `runSerialized`'s "no two closures interleave" guarantee
255
+ * broken by a `transaction` it was never serialized against.
256
+ *
257
+ * ## Why the lock is re-entrant, and why `AsyncLocalStorage` is what makes it so
258
+ *
259
+ * The engine composes the two, and stacks them: `ShardRunner.runInTransaction`
260
+ * is `runSerialized(() => transaction(work))` (`@lunora/shard-engine`), and
261
+ * `ShardDO.fetch` widens a mutation dispatch carrying `x-lunora-mutation-id`
262
+ * into a *further* `runSerialized` span around it (`@lunora/do`). Under two
263
+ * plain FIFO chains that outer span is already fatal on this host — the inner
264
+ * `runSerialized` waits on a `tail` that only settles when the outer closure it
265
+ * is running inside resolves, and because nothing ever resets that `tail`, the
266
+ * shard's write lane is wedged for the life of the process rather than for the
267
+ * life of the request.
268
+ *
269
+ * So the lock is skipped for a boundary opened from inside a boundary, and
270
+ * `AsyncLocalStorage` is the only thing that can tell that case from the one
271
+ * that must queue: its store follows a single call's own await chain without
272
+ * leaking into a sibling chain. A held boolean cannot — it reads `true` for an
273
+ * unrelated *second top-level* `transaction()` too, which would then nest a raw
274
+ * `BEGIN` on a connection already inside one. This is the same distinction
275
+ * workerd's `blockConcurrencyWhile` draws natively with "does not queue events
276
+ * initiated as part of the callback itself", which is what
277
+ * `@lunora/platform-cloudflare` leans on for the identical composition.
240
278
  *
241
279
  * The returned `dispose()` is this host's lifecycle owner: it clears the
242
280
  * pending alarm `setTimeout` (so it can never fire against a connection this
@@ -310,22 +348,65 @@ interface NodeShardRegistry {
310
348
  }
311
349
  /** Build the in-process shard registry. */
312
350
  declare const createNodeShardRegistry: (options?: NodeShardRegistryOptions) => NodeShardRegistry;
351
+ /** Options for {@link createNodeWorkflowHost}. */
352
+ interface NodeWorkflowHostOptions<Workflows extends Record<string, {
353
+ isLunoraWorkflow: true;
354
+ }>> {
355
+ /** Base env merged under the derived workflow bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
356
+ env?: Record<string, unknown>;
357
+ /** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
358
+ leaseTtlMs?: number;
359
+ /** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
360
+ store: WorkflowStore;
361
+ /** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
362
+ workflows: Workflows;
363
+ }
364
+ /** A fully-wired Node workflow host. */
365
+ interface NodeWorkflowHost<Workflows extends Record<string, {
366
+ isLunoraWorkflow: true;
367
+ }>> {
368
+ /** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
369
+ readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
370
+ /**
371
+ * The caller's `env` plus one binding per workflow under its class name
372
+ * (`OrderPipelineWorkflow`), the key `ctx.exports` would carry on Cloudflare —
373
+ * merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
374
+ * children through the same runtime.
375
+ */
376
+ readonly env: Record<string, unknown>;
377
+ /** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
378
+ readonly runtime: WorkflowRuntime;
379
+ }
380
+ /**
381
+ * Create a Node workflow host: compile every declared Lunora workflow onto the
382
+ * visulima engine, derive the workflow env, and expose the per-workflow
383
+ * `WorkflowBindingLike` handles.
384
+ */
385
+ declare const createNodeWorkflowHost: <Workflows extends Record<string, {
386
+ isLunoraWorkflow: true;
387
+ }>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
313
388
  /** Every contract this package provides, composed for one Node process. */
314
389
  interface NodePlatform<Queues extends Record<string, {
315
390
  isLunoraQueue: true;
391
+ }> = Record<string, never>, Workflows extends Record<string, {
392
+ isLunoraWorkflow: true;
316
393
  }> = Record<string, never>> {
317
394
  /** `using platform = createNodePlatform(...)` support — delegates to `close()`. */
318
395
  [Symbol.dispose]: () => void;
319
396
  /** What this target supports — see `NODE_CAPABILITIES` in `@lunora/platform`. */
320
397
  capabilities: PlatformCapabilities;
321
398
  /**
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.
399
+ * Tear this platform instance down: every resource this root built, in
400
+ * reverse construction order — the global store's own connection, every
401
+ * armed scheduler job timer, the registry's live shards, and last the shard
402
+ * host's pending alarm timer and its `better-sqlite3` database (and, with
403
+ * it, `kv`'s table — both live on the same connection). Nothing in this
404
+ * package closes these resources on its own — a `NodePlatform` a caller
405
+ * stops using without calling `close()` leaks the open file handles (plus
406
+ * their WAL/SHM sidecar files) and keeps the process alive on outstanding
407
+ * timers. Safe to call more than once. A `createNodePlatform` that throws
408
+ * unwinds the same list itself, so a failed construction leaks nothing and
409
+ * leaves nothing to call this on.
329
410
  *
330
411
  * **`close()` is a terminal state, not merely a cleanup step.** After it
331
412
  * runs: `scheduler.schedule()` throws instead of arming a fresh timer
@@ -335,8 +416,10 @@ interface NodePlatform<Queues extends Record<string, {
335
416
  * benign); `shard.alarms.set()`/`delete()` throw before mutating any
336
417
  * in-memory state (checked against the connection's own open/closed state,
337
418
  * 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
419
+ * whatever it last held; `sockets.accept()`/`setTag()`/`removeTag()` throw
420
+ * the same way, checked against the same connection state, before touching
421
+ * any runtime socket map or durable row. A no-op instead of a throw would
422
+ * be indistinguishable from a working call — exactly the silent-vanishing
340
423
  * this lifecycle exists to end.
341
424
  */
342
425
  close: () => void;
@@ -351,8 +434,28 @@ interface NodePlatform<Queues extends Record<string, {
351
434
  * shutdown is `await platform.drain()` then `platform.close()`.
352
435
  */
353
436
  drain: () => Promise<void>;
437
+ /**
438
+ * The `.global()` backend, or `undefined` when the caller named no database
439
+ * file for it. There is no default path, because a global store silently
440
+ * rooted at `:memory:` loses every row when the process exits.
441
+ *
442
+ * **A building block, not a wiring.** Unlike its three siblings, nothing
443
+ * downstream of this composition root reads it: a `.global()` read or write
444
+ * reaches its backend through exactly one seam, `createShardCtxDb({ globalDb
445
+ * })`, and the only thing that passes `globalDb` is the generated `shard.ts`,
446
+ * from its `d1` / `hyperdriveGlobal` config thunks. `createNodePlatform`
447
+ * constructs no shard DO, so it cannot make that hop itself. A caller that
448
+ * wants `.global()` on this host makes it: after `migrate(schema)` has
449
+ * provisioned the tables, give the generated `createShardDO` a `d1` thunk
450
+ * returning `platform.globalTables.writer({ schema, … })` — `writer` builds
451
+ * the `createSqlCtxDb` facade that seam expects, and `node-platform.test.ts`
452
+ * round-trips a row through exactly that object.
453
+ */
454
+ globalTables?: NodeGlobalStore;
354
455
  /** Durable key-value storage backed by the same `better-sqlite3` database as `shard`. */
355
456
  kv: ShardKvStore;
457
+ /** The local-filesystem bucket, or `undefined` when the caller named no bucket directory. */
458
+ objectStorage?: R2BucketLike;
356
459
  /**
357
460
  * The declared queues, or `undefined` when the caller declared none.
358
461
  *
@@ -367,6 +470,12 @@ interface NodePlatform<Queues extends Record<string, {
367
470
  shard: ShardHost;
368
471
  /** Socket registry with mutable tags and SQLite-persisted attachments. */
369
472
  sockets: SocketHost;
473
+ /**
474
+ * The declared workflows, or `undefined` when the caller declared none.
475
+ * Runs are persisted to the same `better-sqlite3` database as the shard, so
476
+ * they survive a restart without a second store to configure.
477
+ */
478
+ workflows?: NodeWorkflowHost<Workflows>;
370
479
  }
371
480
  /**
372
481
  * Options for {@link createNodePlatform} — the shard host's (`path`,
@@ -376,10 +485,29 @@ interface NodePlatform<Queues extends Record<string, {
376
485
  * or job with nowhere to land is bookkeeping. `directory` makes the shards the
377
486
  * directory resolves for fan-out file-backed too, and `onAlarm` gives their
378
487
  * durable alarms somewhere to land.
488
+ *
489
+ * `queues`, `workflows`, `objectStorageDirectory` and `globalTablesPath` are the
490
+ * four declarations — each is absent from the returned platform when omitted.
379
491
  */
380
492
  type NodePlatformOptions<Queues extends Record<string, {
381
493
  isLunoraQueue: true;
494
+ }> = Record<string, never>, Workflows extends Record<string, {
495
+ isLunoraWorkflow: true;
382
496
  }> = Record<string, never>> = {
497
+ /**
498
+ * Database file the `.global()` tables live in — its own file, never a
499
+ * shard's, because a table every shard reads must not be inside any one of
500
+ * them. Omit when the app declares no `.global()` table; there is no
501
+ * default, for the same reason the bucket has none.
502
+ */
503
+ globalTablesPath?: string;
504
+ /**
505
+ * Directory the object-storage bucket keeps its objects in, one file per
506
+ * key. Omit when the app declares no buckets — there is no default,
507
+ * because a bucket silently rooted at the process's working directory is
508
+ * worse than an absent one.
509
+ */
510
+ objectStorageDirectory?: string;
383
511
  /**
384
512
  * Deliver one assembled queue batch — wire this to `dispatchQueueBatch`.
385
513
  * Required alongside `queues`; without it the messages would be stored
@@ -388,11 +516,15 @@ type NodePlatformOptions<Queues extends Record<string, {
388
516
  onQueueBatch?: NodeQueueHostOptions<Queues>["onBatch"];
389
517
  /** The app's `defineQueue` results, keyed by export name. Omit when the app declares no queues. */
390
518
  queues?: Queues;
519
+ /** The app's `defineWorkflow` results, keyed by export name. Omit when the app declares no workflows. */
520
+ workflows?: Workflows;
391
521
  } & NodeSchedulerHostOptions & NodeShardHostOptions & NodeShardRegistryOptions;
392
522
  /** Compose every contract this package provides over one `better-sqlite3` database. */
393
523
  declare const createNodePlatform: <Queues extends Record<string, {
394
524
  isLunoraQueue: true;
395
- }> = Record<string, never>>(options?: NodePlatformOptions<Queues>) => NodePlatform<Queues>;
525
+ }> = Record<string, never>, Workflows extends Record<string, {
526
+ isLunoraWorkflow: true;
527
+ }> = Record<string, never>>(options?: NodePlatformOptions<Queues, Workflows>) => NodePlatform<Queues, Workflows>;
396
528
  /** Options for {@link createNodeR2Bucket}. */
397
529
  interface NodeR2BucketOptions {
398
530
  /** The bucket directory — created on first write. Objects live here, one file per key. */
@@ -400,7 +532,7 @@ interface NodeR2BucketOptions {
400
532
  }
401
533
  /**
402
534
  * Create an `R2BucketLike` over the local filesystem. Any object shape
403
- * `createStorage({ bucket })` accepts — `put`/`get`/`head`/`delete`/`list` —
535
+ * `createStorage({ bucket, bucketName })` accepts — `put`/`get`/`head`/`delete`/`list` —
404
536
  * maps directly onto a file operation.
405
537
  */
406
538
  declare const createNodeR2Bucket: (options: NodeR2BucketOptions) => R2BucketLike;
@@ -473,45 +605,135 @@ interface NodeSocketHost {
473
605
  }
474
606
  /** Build the socket registry, persisting attachments and tags to `database`. */
475
607
  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
608
  /**
513
609
  * Build a durable {@link WorkflowStore} over a `better-sqlite3` connection.
514
610
  * Pass the result as `createNodeWorkflowHost({ store })`.
515
611
  */
516
612
  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 };
613
+ export {
614
+ /**
615
+ * `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
616
+ * host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
617
+ * `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
618
+ * scheduler registry.
619
+ *
620
+ * Promoted from `@lunora/platform`'s `node:sqlite` reference host
621
+ * (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
622
+ * durability half of each contract actually holds: alarms and scheduler jobs
623
+ * are persisted **and re-armed on construction**, socket attachments and tags
624
+ * live in SQLite rather than a `Map`, and `.global()` tables run the real
625
+ * `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
626
+ * a restart test — a second host over the same database file — not only by the
627
+ * TCK's simulated recycle.
628
+ *
629
+ * Three suites run against this package: `@lunora/platform/conformance`'s host
630
+ * TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
631
+ * lifecycle/global-store tests.
632
+ *
633
+ * **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
634
+ * (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
635
+ * buckets over the local filesystem (`./node-r2-bucket`), workflows over the
636
+ * `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
637
+ * via `@lunora/runtime`'s query coordinator over the in-process shard registry.
638
+ * `createNodePlatform` binds all three declarations (`queues`, `workflows`,
639
+ * `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
640
+ * is the one combination that fails at runtime with no diagnostic before it.
641
+ *
642
+ * **Still missing:** a dev server. There is no `lunora dev --target node`, and
643
+ * nothing here owns a timer, so queue delivery is driven by an explicit
644
+ * `poll()`. Also absent are the Cloudflare product bindings with no local
645
+ * equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
646
+ * Engine, Pipelines, Secrets Store, Hyperdrive.
647
+ *
648
+ * `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
649
+ * for `provision` — which reports, once, which declared features this target
650
+ * cannot serve, and writes nothing: there is no hosted control plane to deploy
651
+ * to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
652
+ * server, which is why both statements above hold at once. See
653
+ * `plans/234-node-host-findings.md`.
654
+ */
655
+ type NodeGlobalContextDatabaseOptions,
656
+ /**
657
+ * `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
658
+ * host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
659
+ * `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
660
+ * scheduler registry.
661
+ *
662
+ * Promoted from `@lunora/platform`'s `node:sqlite` reference host
663
+ * (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
664
+ * durability half of each contract actually holds: alarms and scheduler jobs
665
+ * are persisted **and re-armed on construction**, socket attachments and tags
666
+ * live in SQLite rather than a `Map`, and `.global()` tables run the real
667
+ * `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
668
+ * a restart test — a second host over the same database file — not only by the
669
+ * TCK's simulated recycle.
670
+ *
671
+ * Three suites run against this package: `@lunora/platform/conformance`'s host
672
+ * TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
673
+ * lifecycle/global-store tests.
674
+ *
675
+ * **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
676
+ * (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
677
+ * buckets over the local filesystem (`./node-r2-bucket`), workflows over the
678
+ * `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
679
+ * via `@lunora/runtime`'s query coordinator over the in-process shard registry.
680
+ * `createNodePlatform` binds all three declarations (`queues`, `workflows`,
681
+ * `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
682
+ * is the one combination that fails at runtime with no diagnostic before it.
683
+ *
684
+ * **Still missing:** a dev server. There is no `lunora dev --target node`, and
685
+ * nothing here owns a timer, so queue delivery is driven by an explicit
686
+ * `poll()`. Also absent are the Cloudflare product bindings with no local
687
+ * equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
688
+ * Engine, Pipelines, Secrets Store, Hyperdrive.
689
+ *
690
+ * `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
691
+ * for `provision` — which reports, once, which declared features this target
692
+ * cannot serve, and writes nothing: there is no hosted control plane to deploy
693
+ * to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
694
+ * server, which is why both statements above hold at once. See
695
+ * `plans/234-node-host-findings.md`.
696
+ */
697
+ type NodeGlobalStore,
698
+ /**
699
+ * `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
700
+ * host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
701
+ * `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
702
+ * scheduler registry.
703
+ *
704
+ * Promoted from `@lunora/platform`'s `node:sqlite` reference host
705
+ * (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
706
+ * durability half of each contract actually holds: alarms and scheduler jobs
707
+ * are persisted **and re-armed on construction**, socket attachments and tags
708
+ * live in SQLite rather than a `Map`, and `.global()` tables run the real
709
+ * `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
710
+ * a restart test — a second host over the same database file — not only by the
711
+ * TCK's simulated recycle.
712
+ *
713
+ * Three suites run against this package: `@lunora/platform/conformance`'s host
714
+ * TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
715
+ * lifecycle/global-store tests.
716
+ *
717
+ * **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
718
+ * (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
719
+ * buckets over the local filesystem (`./node-r2-bucket`), workflows over the
720
+ * `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
721
+ * via `@lunora/runtime`'s query coordinator over the in-process shard registry.
722
+ * `createNodePlatform` binds all three declarations (`queues`, `workflows`,
723
+ * `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
724
+ * is the one combination that fails at runtime with no diagnostic before it.
725
+ *
726
+ * **Still missing:** a dev server. There is no `lunora dev --target node`, and
727
+ * nothing here owns a timer, so queue delivery is driven by an explicit
728
+ * `poll()`. Also absent are the Cloudflare product bindings with no local
729
+ * equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
730
+ * Engine, Pipelines, Secrets Store, Hyperdrive.
731
+ *
732
+ * `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
733
+ * for `provision` — which reports, once, which declared features this target
734
+ * cannot serve, and writes nothing: there is no hosted control plane to deploy
735
+ * to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
736
+ * server, which is why both statements above hold at once. See
737
+ * `plans/234-node-host-findings.md`.
738
+ */
739
+ 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 };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createNodeGlobalStore as r,createNodeSqlExec as t}from"./packem_shared/createNodeGlobalStore-BvteXWRK.mjs";import{createNodeShardKvStore as d}from"./packem_shared/createNodeShardKvStore-BfKCQTjE.mjs";import{createNodePlatform as f}from"./packem_shared/createNodePlatform-BQimmgT9.mjs";import{createNodeQueueHost as x}from"./packem_shared/createNodeQueueHost-CLQ5hOBf.mjs";import{createNodeR2Bucket as p}from"./packem_shared/createNodeR2Bucket-Wb8pyi2X.mjs";import{createNodeSchedulerHost as l}from"./packem_shared/createNodeSchedulerHost-DRjZhuxe.mjs";import{createNodeShardHost as h}from"./packem_shared/createNodeShardHost-DEHe5Vcm.mjs";import{createNodeShardRegistry as k}from"./packem_shared/createNodeShardRegistry-0iSMdi9-.mjs";import{createNodeShardState as w}from"./packem_shared/createNodeShardState-DdOj9FzJ.mjs";import{createNodeSocketHost as W}from"./packem_shared/createNodeSocketHost-CytowrYN.mjs";import{createNodeWorkflowHost as g}from"./packem_shared/createNodeWorkflowHost-Dht_jgSv.mjs";import{createNodeWorkflowStore as q}from"./packem_shared/createNodeWorkflowStore-jlNR6Y3h.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{createNodeGlobalStore as r,createNodeSqlExec as t}from"./packem_shared/createNodeGlobalStore-DiGxjmD7.mjs";import{createNodeShardKvStore as d}from"./packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodePlatform as f}from"./packem_shared/createNodePlatform-B6LVGo8q.mjs";import{createNodeQueueHost as x}from"./packem_shared/createNodeQueueHost-CttDb5sY.mjs";import{createNodeR2Bucket as p}from"./packem_shared/createNodeR2Bucket-CajTvuOy.mjs";import{createNodeSchedulerHost as l}from"./packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as h}from"./packem_shared/createNodeShardHost-Da19DFX0.mjs";import{createNodeShardRegistry as k}from"./packem_shared/createNodeShardRegistry-D-KxwdUc.mjs";import{createNodeShardState as w}from"./packem_shared/createNodeShardState-D6XMWbFs.mjs";import{createNodeSocketHost as W}from"./packem_shared/createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as g}from"./packem_shared/createNodeWorkflowHost-DqOqumS8.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};
@@ -0,0 +1 @@
1
+ import{sqliteDialect as l}from"@lunora/d1";import{createSqlCtxDb as s,runSqlGlobalTableMigrations as p,runSqlAggregateMigrations as u,runSqlRankMigrations as g,runSqlSearchMigrations as f,backfillSqlSearchIndexes as S,runSqlCdcMigration as w}from"@lunora/sql-store";import d from"better-sqlite3";const i=new WeakMap,n=(a,r)=>{let e=i.get(a);e===void 0&&(e=new Map,i.set(a,e));let t=e.get(r);return t===void 0&&(t=a.prepare(r),e.set(r,t)),t},o={...l,searchBackfillHint:"run the global store's migrate(), which backfills every search index to completion"},m=a=>({all:async(r,e)=>n(a,r).all(...e),batch:async r=>{a.transaction(()=>{for(const{params:e,sql:t}of r)n(a,t).run(...e)})()},run:async(r,e)=>({rowsAffected:n(a,r).run(...e).changes})}),x=(a={})=>{const r=new d(a.path??":memory:");r.pragma("journal_mode = WAL");const e=m(r);return{database:r,dispose:()=>{r.open&&r.close()},exec:e,migrate:async(t,c={})=>{await p(e,t,o),await u(e,t,o),await g(e,t,o),await f(e,t,o),await S(e,t,o),c.cdc===!0&&await w(e,o)},writer:t=>s({...t,dialect:o,exec:e,provisionScope:e})}};export{x as createNodeGlobalStore,m as createNodeSqlExec};
@@ -0,0 +1 @@
1
+ import{LunoraError as S}from"@lunora/errors";import{NODE_CAPABILITIES as b}from"@lunora/platform";import{createNodeGlobalStore as N}from"./createNodeGlobalStore-DiGxjmD7.mjs";import{createNodeShardKvStore as k}from"./createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeQueueHost as g}from"./createNodeQueueHost-CttDb5sY.mjs";import{createNodeR2Bucket as y}from"./createNodeR2Bucket-CajTvuOy.mjs";import{createNodeSchedulerHost as R}from"./createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as q}from"./createNodeShardHost-Da19DFX0.mjs";import{createNodeShardRegistry as B}from"./createNodeShardRegistry-D-KxwdUc.mjs";import{createNodeSocketHost as H}from"./createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as I}from"./createNodeWorkflowHost-DqOqumS8.mjs";import{createNodeWorkflowStore as P}from"./createNodeWorkflowStore-CL7RYP6H.mjs";const G=(o={})=>{const e=[],{database:r,dispose:d,drain:u,host:i}=q(o);e.push(d);try{const s=k(r),t=B(o);e.push(()=>{t.close()});const{directory:l}=t,{socket:h}=H(r),{dispose:m,scheduler:f}=R(r,o);e.push(m);const n=o.queues===void 0?void 0:g(r,{onBatch:o.onQueueBatch??(()=>{throw new S("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:o.queues}),p=o.objectStorageDirectory===void 0?void 0:y({directory:o.objectStorageDirectory}),a=o.globalTablesPath===void 0?void 0:N({path:o.globalTablesPath});a!==void 0&&e.push(()=>{a.dispose()});const w=o.workflows===void 0?void 0:I({store:P(r),workflows:o.workflows}),c=()=>{for(const v of e.toReversed())v()};return{capabilities:b,close:c,directory:l,drain:u,globalTables:a,kv:s,objectStorage:p,queues:n,scheduler:f,shard:i,sockets:h,workflows:w,[Symbol.dispose]:c}}catch(s){for(const t of e.toReversed())try{t()}catch{}throw s}};export{G as createNodePlatform};
@@ -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};
@@ -0,0 +1 @@
1
+ import{randomUUID as z,createHash as G}from"node:crypto";import{mkdir as N,open as j,rename as L,rmdir as U,unlink as W,readdir as X}from"node:fs/promises";import{join as R,dirname as v,sep as Y}from"node:path";import{LunoraError as c}from"@lunora/errors";import{t as P}from"./to-array-buffer-DVyXU4Ep.mjs";new TextEncoder;const J=Array.from({length:32},(e,t)=>t),Z=new RegExp(`[${J.map(e=>String.fromCodePoint(e)).join("")}]`,"u"),q=e=>Z.test(e),S=".lunora-tmp",H="LNR1",p=8,D=1024,B=255,M=2048,Q=/[%A-Z:]/g,k=e=>{const t=e.replaceAll(Q,n=>`%${n.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`),r=t.at(-1);return r==="."||r===" "?`${t.slice(0,-1)}%${r.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`:t},tt=e=>{try{return decodeURIComponent(e)}catch{return}},A=e=>e.split("/").map(t=>k(t)).join("/"),et=e=>{const t=[];for(const r of e.split("/")){const n=tt(r);if(n===void 0)return;t.push(n)}return t.join("/")},rt=new TextEncoder,nt=e=>rt.encode(e).length,g=e=>{if(typeof e!="string"||e.length===0)throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key must be a non-empty string");if(nt(e)>D)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key exceeds ${String(D)}-byte limit`);if(e.includes("\0"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a NUL byte");if(q(e))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a control character (including CR/LF)");if(e.includes("\\"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a backslash (a path separator on Windows)");if(e.startsWith("/"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key must not start with `/`");for(const t of e.split("/")){if(Buffer.byteLength(k(t),"utf8")>B)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key segment "${t}" exceeds ${String(B)} bytes once escaped for the filesystem`);if(t===""||t==="."||t==="..")throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key has an empty, \`.\` or \`..\` path segment ("${e}")`);if(t===S)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key is reserved (no segment may be "${S}")`)}},ot=e=>{if(e===void 0)return;let t=0;for(const[r,n]of Object.entries(e))t+=Buffer.byteLength(r,"utf8")+Buffer.byteLength(n,"utf8");if(t>M)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 customMetadata is ${String(t)} bytes, over the ${String(M)}-byte ceiling R2 applies to the summed keys and values — your metadata headers exceed the maximum allowed metadata size`)},at=/^[\da-f]{64}$/,st=e=>{if(typeof e=="string"){const t=e.toLowerCase();if(!at.test(t))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 put sha256 must be 64 hex characters or a 32-byte buffer");return t}if(e.byteLength!==32)throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 put sha256 must be 64 hex characters or a 32-byte buffer");return Buffer.from(e).toString("hex")},b=e=>e instanceof Error&&"code"in e&&e.code==="ENOENT",C=(e,t)=>{const r=t instanceof Error&&"code"in t?t.code:void 0;return r==="EEXIST"||r==="EISDIR"||r==="ENOTDIR"?new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key "${e}" collides with an existing object at one of its path prefixes — this host stores one file per key, so a key and a prefix of it cannot both hold objects`):t},it=e=>{const t=Buffer.from(JSON.stringify(e),"utf8"),r=Buffer.alloc(p);return r.writeUInt32BE(t.byteLength,0),r.write(H,4,"ascii"),Buffer.concat([t,r])},ct=e=>{if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.sha256Hex=="string"&&typeof t.size=="number"&&typeof t.uploaded=="string"},ft=async e=>{const t=await e.stat();if(!t.isFile()||t.size<p)return;const r=Buffer.alloc(p);if(await e.read(r,0,p,t.size-p),r.toString("ascii",4,p)!==H)return;const n=r.readUInt32BE(0),o=t.size-p-n;if(n===0||o<0)return;const s=Buffer.alloc(n),{bytesRead:i}=await e.read(s,0,n,o);if(i!==n)return;let a;try{a=JSON.parse(s.toString("utf8"))}catch{return}return ct(a)?{bodySize:o,meta:a}:void 0},F=async e=>{let t;try{t=await j(e,"r")}catch(r){if(b(r))return;throw r}try{const r=await ft(t);if(r===void 0){await t.close();return}return{...r,handle:t}}catch(r){throw await t.close(),r}},$=new FinalizationRegistry(e=>{e.close().catch(()=>{})}),V=async e=>{const t=await F(e);if(t!==void 0)return await t.handle.close(),{bodySize:t.bodySize,meta:t.meta}},I=(e,t)=>{const{sha256Hex:r}=t,n=Buffer.from(r,"hex");return{checksums:{sha256:P(n)},customMetadata:t.customMetadata,etag:r,httpEtag:`"${r}"`,httpMetadata:t.httpMetadata,key:e,sha256:r,sha256Base64:n.toString("base64"),size:t.size,uploaded:new Date(t.uploaded)}},O=(e,t,r)=>e===void 0||!Number.isFinite(e)?t:Math.min(Math.max(0,Math.floor(e)),r),dt=(e,t)=>{if(t===void 0)return{end:e,start:0};if("suffix"in t)return{end:e,start:e-O(t.suffix,e,e)};const r=O(t.offset,0,e);return{end:t.length===void 0||!Number.isFinite(t.length)?e:Math.min(r+O(t.length,0,e),e),start:r}},lt=(e,t,r,n)=>{const o=e.createReadStream({autoClose:!1,end:r-1,start:t})[Symbol.asyncIterator]();return new ReadableStream({cancel:async()=>{await o.return?.(),await n()},pull:async s=>{let i;try{i=await o.next()}catch(a){throw await n(),a}i.done===!0?(s.close(),await n()):s.enqueue(i.value)}})},ut=async(e,t,r)=>{const n=r-t;if(n<=0)return Buffer.alloc(0);const o=Buffer.alloc(n),{bytesRead:s}=await e.read(o,0,n,t);return o.subarray(0,s)},K=async function*(e){if(e!==null){if(typeof e=="string"){yield new TextEncoder().encode(e);return}if(e instanceof ArrayBuffer){yield new Uint8Array(e);return}if(ArrayBuffer.isView(e)){yield new Uint8Array(e.buffer,e.byteOffset,e.byteLength);return}if(e instanceof Blob){yield*K(e.stream());return}if(e instanceof ReadableStream){for await(const t of e)if(t instanceof Uint8Array)yield t;else if(t instanceof ArrayBuffer)yield new Uint8Array(t);else throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 put stream must yield byte chunks");return}throw new c("VALIDATION_ERROR","@lunora/platform-node: unsupported R2 put body")}},ht=async e=>{const t=[],r=async(n,o)=>{let s;try{s=await X(n,{withFileTypes:!0})}catch(a){if(b(a))return;throw a}const i=[];for(const a of s){if(a.name===S)continue;const l=o===""?a.name:`${o}/${a.name}`;if(a.isDirectory())i.push(r(R(n,a.name),l));else{const h=et(l);h!==void 0&&t.push(h)}}await Promise.all(i)};return await r(e,""),t.sort((n,o)=>n<o?-1:+(n>o)),t},wt=(e,t)=>{let r=0,n=e.length;for(;r<n;){const o=r+Math.floor((n-r)/2),s=e[o];s===void 0||s<=t?r=o+1:n=o}return r},mt=new Set(["EISDIR","ENOENT","ENOTDIR","EPERM"]),_=async e=>{try{await W(e)}catch(t){const r=t instanceof Error&&"code"in t?t.code:void 0;if(r===void 0||!mt.has(r))throw t}},Rt=async(e,t)=>{let r=e;for(;r!==t&&r.startsWith(`${t}${Y}`);){try{await U(r)}catch(n){if(!b(n))return}r=v(r)}},It=e=>{const{directory:t}=e;return{delete:async r=>{g(r);const n=R(t,A(r));await _(n),await Rt(n,t)},get:async(r,n)=>{g(r);const o=await F(R(t,A(r)));if(o===void 0)return null;const{bodySize:s,handle:i,meta:a}=o,{end:l,start:h}=dt(s,n?.range),u={};let f=!1;const w=async()=>{f||(f=!0,$.unregister(u),await i.close())},m=()=>{if(f)throw new c("BAD_REQUEST",`@lunora/platform-node: R2 object "${r}" body has already been consumed`)},T=async()=>{m();try{return await ut(i,h,l)}finally{await w()}},E={...I(r,a),arrayBuffer:async()=>P(await T()),get body(){return m(),l<=h?(w().catch(()=>{}),new Blob([]).stream()):lt(i,h,l,w)},text:async()=>(await T()).toString("utf8")};return $.register(E,{close:w},u),E},head:async r=>{g(r);const n=await V(R(t,A(r)));return n===void 0?null:I(r,n.meta)},list:async(r={})=>{const n=Math.max(1,O(r.limit,1e3,1e3));if(r.prefix?.includes("\0"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 list prefix contains a NUL byte");const o=await ht(t),s=r.prefix??"",i=o.filter(d=>d.startsWith(s)),{delimiter:a}=r,l=a===void 0?i:i.filter(d=>!d.slice(s.length).includes(a)),h=a===void 0?void 0:[...new Set(i.map(d=>{const x=d.slice(s.length).indexOf(a);return x===-1?void 0:d.slice(0,s.length+x+a.length)}).filter(d=>d!==void 0))].toSorted((d,y)=>d<y?-1:+(d>y));let u=r.cursor;r.startAfter!==void 0&&(u===void 0||r.startAfter>u)&&(u=r.startAfter);const f=u===void 0?0:wt(l,u),w=l.slice(f,f+n),m=f+n<l.length,E=(await Promise.all(w.map(async d=>{const y=await V(R(t,A(d)));return y===void 0?void 0:I(d,y.meta)}))).filter(d=>d!==void 0);return{cursor:m?w.at(-1):void 0,delimitedPrefixes:h,objects:E,truncated:m}},put:async(r,n,o)=>{g(r),ot(o?.customMetadata);const s=o?.sha256===void 0?void 0:st(o.sha256),i=R(t,A(r)),a=R(t,S,z());try{await N(v(i),{recursive:!0})}catch(f){throw C(r,f)}await N(v(a),{recursive:!0});const l=G("sha256");let h=0,u;try{const f=await j(a,"w");try{for await(const m of K(n))l.update(m),h+=m.byteLength,await f.write(m);const w=l.digest("hex");if(s!==void 0&&s!==w)throw new c("VALIDATION_ERROR",`@lunora/platform-node: the SHA-256 checksum you specified for R2 key "${r}" did not match what we received. You provided a SHA-256 checksum with value: ${s}. Actual SHA-256 was: ${w}`);u={customMetadata:o?.customMetadata,httpMetadata:o?.httpMetadata,sha256Hex:w,size:h,uploaded:new Date().toISOString()},await f.write(it(u))}finally{await f.close()}}catch(f){throw await _(a),f}try{await L(a,i)}catch{try{await U(i)}catch{}try{await N(v(i),{recursive:!0}),await L(a,i)}catch(f){throw await _(a),C(r,f)}}return I(r,u)}}};export{It as createNodeR2Bucket};
@@ -0,0 +1,15 @@
1
+ import{randomUUID as j}from"node:crypto";import{deserialize as L,serialize as N}from"node:v8";import{CronExpressionParser as x}from"cron-parser";const T={backoffMultiplier:2,initialDelayMs:1e3,maxAttempts:5,maxDelayMs:6e4},d=2147483647,F=(s,i)=>{const _=i?.initialDelayMs??T.initialDelayMs,m=i?.backoffMultiplier??T.backoffMultiplier,p=i?.maxDelayMs??T.maxDelayMs;return Math.min(p,_*m**Math.max(0,s-1))},H=(s,i={})=>{s.exec(`CREATE TABLE IF NOT EXISTS _lunora_scheduler_jobs (
2
+ id TEXT PRIMARY KEY,
3
+ function_path TEXT NOT NULL,
4
+ args BLOB NOT NULL,
5
+ scheduled_for INTEGER NOT NULL,
6
+ attempts INTEGER NOT NULL DEFAULT 0,
7
+ retry BLOB,
8
+ state TEXT NOT NULL DEFAULT 'pending'
9
+ )`),s.exec(`CREATE TABLE IF NOT EXISTS _lunora_scheduler_crons (
10
+ key TEXT PRIMARY KEY,
11
+ expression TEXT NOT NULL,
12
+ function_path TEXT NOT NULL,
13
+ args BLOB NOT NULL
14
+ )`);const _=s.prepare("INSERT INTO _lunora_scheduler_jobs (id, function_path, args, scheduled_for, retry) VALUES (?, ?, ?, ?, ?)"),m=s.prepare("SELECT * FROM _lunora_scheduler_jobs WHERE id = ?"),p=s.prepare("SELECT * FROM _lunora_scheduler_jobs WHERE state = ? ORDER BY scheduled_for"),R=s.prepare("DELETE FROM _lunora_scheduler_jobs WHERE id = ?"),M=s.prepare("DELETE FROM _lunora_scheduler_jobs WHERE id = ? AND state = 'pending'"),O=s.prepare("UPDATE _lunora_scheduler_jobs SET attempts = ?, scheduled_for = ? WHERE id = ?"),g=s.prepare("UPDATE _lunora_scheduler_jobs SET state = 'dead', attempts = ? WHERE id = ?"),U=s.prepare("UPDATE _lunora_scheduler_jobs SET state = 'pending', attempts = 0, scheduled_for = ? WHERE id = ? AND state = 'dead'"),S=s.prepare(`INSERT INTO _lunora_scheduler_crons (key, expression, function_path, args) VALUES (?, ?, ?, ?)
15
+ ON CONFLICT (key) DO UPDATE SET expression = excluded.expression, args = excluded.args`),b=s.prepare("SELECT * FROM _lunora_scheduler_crons"),c=()=>s.open,A=e=>e===null?void 0:L(e);let a=!1;const l=new Map,u=new Map,D=e=>({attempts:e.attempts,functionPath:e.function_path,id:e.id,scheduledFor:e.scheduled_for}),h=e=>{const t=l.get(e);t!==void 0&&(clearTimeout(t),l.delete(e))},E=(e,t)=>{h(e);const r=Math.max(0,t-Date.now());if(r>d){const o=setTimeout(()=>{E(e,t)},d);l.set(e,o);return}const n=setTimeout(()=>{k(e).catch(()=>{})},r);l.set(e,n)},k=async e=>{if(l.delete(e),!c())return;const t=m.get(e);if(t?.state!=="pending")return;const r=t.attempts+1,n=A(t.retry);try{await i.onDispatch?.(t.function_path,L(t.args),{attempts:r,id:e}),c()&&R.run(e)}catch{if(a||!c())return;const o=n?.maxAttempts??T.maxAttempts;if(r>=o){g.run(r,e);return}const y=Date.now()+F(r,n);O.run(r,y,e),E(e,y)}},f=e=>{const t=u.get(e.key);t!==void 0&&clearTimeout(t);let r;try{r=x.parse(e.expression).next().getTime()}catch{return}const n=Math.max(0,r-Date.now());if(n>d){u.set(e.key,setTimeout(()=>{f(e)},d));return}const o=setTimeout(()=>{u.delete(e.key),(async()=>{try{await i.onDispatch?.(e.function_path,L(e.args),{attempts:0,id:e.key})}catch{}!a&&c()&&f(e)})().catch(()=>{})},n);u.set(e.key,o)},I={cancel:async e=>a?!1:(h(e),M.run(e).changes>0),cron:async(e,t,r)=>{if(a)throw new Error("platform closed: cannot register a cron");x.parse(e);const n={args:N(r??{}),expression:e,function_path:t,key:`${e} ${t}`};S.run(n.key,n.expression,n.function_path,n.args),f(n)},deadLetter:{list:async()=>a||!c()?[]:p.all("dead").map(e=>D(e)),requeue:async e=>{if(a||!c())return!1;const t=Date.now();return U.run(t,e).changes===0?!1:(E(e,t),!0)}},list:async()=>a||!c()?[]:p.all("pending").map(e=>D(e)),schedule:async(e,t,r)=>{if(a)throw new Error("platform closed: cannot schedule a job");const n=`node-job-${j()}`;let o;return r?.at===void 0?o=Date.now()+(r?.delayMs??0):o=typeof r.at=="number"?r.at:r.at.getTime(),_.run(n,e,N(t),o,r?.retry===void 0?null:N(r.retry)),E(n,o),{id:n,scheduledFor:o}}};for(const e of p.all("pending"))E(e.id,e.scheduled_for);for(const e of b.all())f(e);return{dispose:()=>{for(const e of l.values())clearTimeout(e);for(const e of u.values())clearTimeout(e);l.clear(),u.clear(),a=!0},scheduler:I,simulateDeadLetter:async e=>{const t=m.get(e);if(t?.state!=="pending")return!1;h(e);const r=A(t.retry);return g.run((r?.maxAttempts??T.maxAttempts)+1,e),!0}}};export{H as createNodeSchedulerHost};
@@ -0,0 +1 @@
1
+ import{AsyncLocalStorage as S}from"node:async_hooks";import{LunoraError as v}from"@lunora/errors";import L from"better-sqlite3";const y=2147483647,R=6,O=100,I=e=>e===void 0?null:e,M=(e,t)=>({get databaseSize(){const l=e.pragma("page_count",{simple:!0}),r=e.pragma("page_size",{simple:!0});return l*r},exec:(l,...r)=>{t("run SQL");const o=e.prepare(l),s=r.map(c=>I(c)),n=o.reader?o.all(...s):(o.run(...s),[]);return{get columnNames(){return o.reader?o.columns().map(c=>c.name):void 0},[Symbol.iterator]:()=>n[Symbol.iterator](),one:()=>{if(n.length!==1)throw new Error(`expected exactly one row, got ${String(n.length)}`);return n[0]},...o.reader?{raw:()=>e.prepare(l).raw(!0).all(...s)[Symbol.iterator]()}:{},toArray:()=>[...n]}}}),N=(e,t,l)=>{let r,o,s=0,n;e.exec("CREATE TABLE IF NOT EXISTS _lunora_alarm (id INTEGER PRIMARY KEY CHECK (id = 0), scheduled_for INTEGER NOT NULL)");const c=a=>{a===void 0?e.prepare("DELETE FROM _lunora_alarm WHERE id = 0").run():e.prepare("INSERT INTO _lunora_alarm (id, scheduled_for) VALUES (0, ?) ON CONFLICT (id) DO UPDATE SET scheduled_for = excluded.scheduled_for").run(a)},p=()=>{o!==void 0&&(clearTimeout(o),o=void 0)},m=a=>{p();const d=Math.max(0,a-Date.now());if(d>y){o=setTimeout(()=>{m(a)},y);return}o=setTimeout(()=>{r=void 0,o=void 0,e.open&&(c(void 0),(async()=>{await l?.(),s=0})().catch(()=>{if(!e.open||r!==void 0)return;if(s+=1,s>R){s=0;return}const E=Date.now()+O*2**(s-1);r=E,c(E),m(E)}))},d)},f={delete:()=>{if(!e.open)throw new Error("platform closed: cannot delete an alarm");if(t.assertOwnTurn("delete an alarm"),s=0,c(void 0),t.inside()){n={at:void 0};return}r=void 0,p()},get:()=>(n===void 0?r:n.at)??null,set:a=>{if(!e.open)throw new Error("platform closed: cannot set an alarm");t.assertOwnTurn("set an alarm");const d=typeof a=="number"?a:a.getTime();if(s=0,c(d),t.inside()){n={at:d};return}r=d,m(d)}},h=()=>{if(n===void 0)return;const{at:a}=n;if(n=void 0,a===void 0){r=void 0,p();return}r=a,m(a)},T=()=>{n=void 0},w=e.prepare("SELECT scheduled_for FROM _lunora_alarm WHERE id = 0").get();return w!==void 0&&(r=w.scheduled_for,m(w.scheduled_for)),{alarms:f,commit:h,dispose:p,rollback:T}},k=(e={})=>{const t=new L(e.path??":memory:");t.pragma("journal_mode = WAL");const l=new S;let r=!1;const o=i=>{if(r&&l.getStore()!==!0)throw new v("SHARD_UNAVAILABLE",`shard busy: cannot ${i} while another task holds this shard's transaction`)},s=M(t,o),{alarms:n,commit:c,dispose:p,rollback:m}=N(t,{assertOwnTurn:o,inside:()=>l.getStore()===!0},e.onAlarm),f=new Set,h=new S;let T=Promise.resolve();const w=async i=>{if(h.getStore()===!0)return await i();const u=T;let A=()=>{};T=new Promise(g=>{A=g}),await u;try{return await h.run(!0,i)}finally{A()}},a=i=>w(i),d=async i=>{t.exec("BEGIN"),r=!0;try{const u=await l.run(!0,i);return t.exec("COMMIT"),r=!1,c(),u}catch(u){r=!1,m();try{t.exec("ROLLBACK")}catch{}throw u}},E=i=>w(async()=>d(i)),_={alarms:n,runSerialized:a,shardKey:e.shardKey,sql:s,transaction:E,waitUntil:i=>{const u=i.catch(()=>{}).finally(()=>{f.delete(u)});f.add(u)}};return{database:t,dispose:()=>{p(),t.open&&t.close()},drain:async()=>{for(;f.size>0;)await Promise.all(f)},host:_}};export{k as createNodeShardHost};
@@ -0,0 +1 @@
1
+ import{serialize as v,deserialize as u}from"node:v8";const T=r=>{r.exec("CREATE TABLE IF NOT EXISTS _lunora_kv (key TEXT PRIMARY KEY, value BLOB NOT NULL)");const l=r.prepare("SELECT value FROM _lunora_kv WHERE key = ?"),s=r.prepare("INSERT INTO _lunora_kv (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value"),a=r.prepare("DELETE FROM _lunora_kv WHERE key = ?"),E=r.prepare("SELECT key, value FROM _lunora_kv");return{delete:async e=>a.run(e).changes>0,get:async e=>{const t=l.get(e);return t===void 0?void 0:u(t.value)},list:async e=>{const t=e?.prefix??"",c=E.all(),o=new Map;for(const n of c)n.key.startsWith(t)&&o.set(n.key,u(n.value));return o},put:async(e,t)=>{s.run(e,v(t))}}};export{T as createNodeShardKvStore};
@@ -0,0 +1 @@
1
+ import{mkdirSync as y,readdirSync as S}from"node:fs";import{join as h}from"node:path";import{LunoraError as l}from"@lunora/errors";import{createNodeShardKvStore as v}from"./createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeShardHost as w}from"./createNodeShardHost-Da19DFX0.mjs";import{createNodeSocketHost as A}from"./createNodeSocketHost-DQs85G7W.mjs";const c=".sqlite3",f=r=>r.replaceAll(/[^\da-z._-]/gu,t=>[...Buffer.from(t,"utf8")].map(o=>`%${o.toString(16).padStart(2,"0").toUpperCase()}`).join("")),g=r=>decodeURIComponent(r),$=(r,t)=>{const o=t.slice(0,-c.length),d=h(r,t);let a;try{a=g(o)}catch{throw new l("SHARD_UNAVAILABLE",`@lunora/platform-node: shard database "${d}" has a basename that is not valid percent-encoding, so the shard key it holds cannot be recovered. Rename or remove the file.`)}const e=`${f(a)}${c}`;if(e!==t)throw new l("SHARD_UNAVAILABLE",`@lunora/platform-node: shard database "${d}" was written under an older shard-key encoding. It decodes to shard key "${a}", which this build stores as "${e}" — so the key would be listed for fan-out while every read opened a new, empty database. Rename it: mv "${d}" "${h(r,e)}"`);return a},I=(r={})=>{const t=new Map,o=new Set;if(r.directory!==void 0){y(r.directory,{recursive:!0});for(const e of S(r.directory))e.endsWith(c)&&o.add($(r.directory,e))}const d=e=>{const s=t.get(e);if(s!==void 0)return s.shard;const i=r.directory===void 0?void 0:h(r.directory,`${f(e)}${c}`);let n;const{database:m,dispose:u,host:p}=w({onAlarm:()=>r.onAlarm?.(n),path:i,shardKey:e});return n={kv:v(m),shard:p,shardKey:e,sockets:A(m).socket},t.set(e,{dispose:u,shard:n}),o.add(e),n},a=e=>({fetch:async s=>{const i=d(e);return r.onFetch===void 0?new Response(e):r.onFetch(s,i)}});return{close:()=>{for(const e of t.values())e.dispose();t.clear()},directory:{get:e=>a(String(e)),getByName:e=>a(e),idForName:e=>e},listShardKeys:()=>[...o],shardFor:d}};export{I as createNodeShardRegistry};
@@ -0,0 +1 @@
1
+ const c=t=>({acceptWebSocket:(e,a)=>{t.sockets.accept(e,void 0,a)},blockConcurrencyWhile:e=>t.shard.runSerialized(e),getWebSockets:e=>t.sockets.getSockets(e),id:{name:t.shardKey},storage:{delete:e=>t.kv.delete(e),deleteAlarm:async()=>{await t.shard.alarms.delete()},get:e=>t.kv.get(e),getAlarm:async()=>t.shard.alarms.get(),list:e=>t.kv.list(e),put:(e,a)=>t.kv.put(e,a),setAlarm:async e=>{await t.shard.alarms.set(e)},sql:{get databaseSize(){return t.shard.sql.databaseSize},exec:(e,...a)=>t.shard.sql.exec(e,...a)},transaction:e=>t.shard.transaction(e)},waitUntil:e=>{t.shard.waitUntil?.(e)}});export{c as createNodeShardState};
@@ -0,0 +1,2 @@
1
+ import{deserialize as A,serialize as l}from"node:v8";import{t as T}from"./to-array-buffer-DVyXU4Ep.mjs";const p=s=>typeof s=="object"&&s!==null||typeof s=="function",F=s=>{s.exec("CREATE TABLE IF NOT EXISTS _lunora_sockets (id TEXT PRIMARY KEY, attachment BLOB, tags TEXT NOT NULL DEFAULT '[]')");const f=s.prepare(`INSERT INTO _lunora_sockets (id, attachment, tags) VALUES (?, ?, ?)
2
+ ON CONFLICT (id) DO UPDATE SET attachment = excluded.attachment, tags = excluded.tags`),v=s.prepare("UPDATE _lunora_sockets SET attachment = ? WHERE id = ?"),S=s.prepare("UPDATE _lunora_sockets SET tags = ? WHERE id = ?"),k=s.prepare("SELECT attachment, tags FROM _lunora_sockets WHERE id = ?"),w=s.prepare("DELETE FROM _lunora_sockets WHERE id = ?"),i=new Map,a=new WeakMap;let d=new WeakMap;const u=(t,e)=>{s.open&&v.run(e===void 0?null:l(e),t)},g=(t,e)=>{s.open&&S.run(JSON.stringify([...e]),t)},h=t=>{const e=t;e.closed=!0,i.delete(e.id),p(e.raw)&&d.delete(e.raw),s.open&&w.run(e.id)},m=(t,e)=>{const o=t;return o.handle=e,a.set(e,o.id),e},R=(t,e)=>{const o=t,n=e,c=typeof n.close=="function"?n.close.bind(e):void 0;return typeof n.send!="function"&&(n.send=r=>{o.received.push(typeof r=="string"?r:T(r))}),n.close=(r,y)=>{h(o),c?.(r,y)},n.deserializeAttachment=()=>o.attachment,n.serializeAttachment=r=>{o.attachment=r,u(o.id,r)},m(o,e)},E=t=>{const e=t;return m(e,{close:(n,c)=>{h(e)},deserializeAttachment:()=>e.attachment,send:n=>{e.received.push(typeof n=="string"?n:T(n))},serializeAttachment:n=>{e.attachment=n,u(e.id,n)}})};return{readFrames:t=>(i.get(a.get(t)??"")?.received??[]).filter(e=>typeof e=="string"),restoreSocket:(t,e)=>{const o=s.open?k.get(t):void 0,n=o?.attachment===null?void 0:o?.attachment,c={attachment:n===void 0?e:A(n),closed:!1,handle:void 0,id:t,raw:void 0,received:[],tags:new Set(o===void 0?[]:JSON.parse(o.tags))};return i.set(t,c),o===void 0&&s.open&&f.run(t,c.attachment===void 0?null:l(c.attachment),JSON.stringify([...c.tags])),E(c)},simulateRecycle:()=>{i.clear(),d=new WeakMap},socket:{accept:(t,e,o)=>{if(!s.open)throw new Error("platform closed: cannot accept a socket");const n=crypto.randomUUID(),c=new Set(o);f.run(n,e===void 0?null:l(e),JSON.stringify([...c]));const r={attachment:e,closed:!1,handle:void 0,id:n,raw:t,received:[],tags:c};return i.set(n,r),p(t)?(d.set(t,r),R(r,t)):E(r)},getSockets:t=>{const e=[...i.values()];return(t===void 0?e:e.filter(n=>n.tags.has(t))).map(n=>n.handle)},handleFor:t=>p(t)?d.get(t)?.handle:void 0,idFor:t=>{const e=a.get(t);if(e===void 0)throw new Error("@lunora/platform-node: idFor called with a handle this host never issued");return e},removeTag:(t,e)=>{if(!s.open)throw new Error("platform closed: cannot remove a tag");const o=i.get(a.get(t)??"");o!==void 0&&(e===void 0?o.tags.clear():o.tags.delete(e),g(o.id,o.tags))},setTag:(t,e)=>{if(!s.open)throw new Error("platform closed: cannot set a tag");const o=i.get(a.get(t)??"");o!==void 0&&(o.tags.add(e),g(o.id,o.tags))}}}};export{F as createNodeSocketHost};