@lunora/platform-node 1.0.0-alpha.37 → 1.0.0-alpha.39
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.
- package/dist/conformance/index.mjs +1 -1
- package/dist/index.d.mts +130 -65
- package/dist/index.d.ts +130 -65
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/createNodePlatform-DZ710ybu.mjs +1 -0
- package/dist/packem_shared/createNodeR2Bucket-BzNH1lBp.mjs +1 -0
- package/dist/packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs +15 -0
- package/dist/packem_shared/createNodeShardRegistry-ZWGlgv0E.mjs +1 -0
- package/dist/packem_shared/createNodeSocketHost-CoE4VZbB.mjs +2 -0
- package/package.json +6 -6
- package/dist/packem_shared/createNodePlatform-8zj2kszG.mjs +0 -1
- package/dist/packem_shared/createNodeR2Bucket-B1zKW_b8.mjs +0 -1
- package/dist/packem_shared/createNodeSchedulerHost-vZgv1CEo.mjs +0 -15
- package/dist/packem_shared/createNodeShardRegistry-BmSAs2lp.mjs +0 -1
- package/dist/packem_shared/createNodeSocketHost-DK3X5PlQ.mjs +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createNodeShardKvStore as N}from"../packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeSchedulerHost as
|
|
1
|
+
import{createNodeShardKvStore as N}from"../packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeSchedulerHost as k}from"../packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as D}from"../packem_shared/createNodeShardHost-CNoG3Fzy.mjs";import{createNodeShardRegistry as H}from"../packem_shared/createNodeShardRegistry-ZWGlgv0E.mjs";import{createNodeSocketHost as T}from"../packem_shared/createNodeSocketHost-CoE4VZbB.mjs";const P=()=>{const{database:o,dispose:m,host:p}=D(),h=N(o),c=H(),{directory:l}=c,{readFrames:w,restoreSocket:u,simulateRecycle:S,socket:y}=T(o),d=new Set,s=new Map,{dispose:f,scheduler:i,simulateDeadLetter:g}=k(o,{onDispatch:(e,a,t)=>{d.add(t.id),s.set(e,(s.get(e)??0)+1)}}),n=()=>{f(),m(),c.close()};return{awaitAlarmFired:async e=>{await new Promise(a=>{setTimeout(a,Math.max(0,e-Date.now())+30)})},awaitJobDispatched:async e=>{const t=(await i.list?.())?.find(r=>r.id===e);return t!==void 0&&await new Promise(r=>{setTimeout(r,Math.max(0,t.scheduledFor-Date.now())+30)}),d.has(e)},cleanup:n,cronTicks:e=>s.get(e)??0,directory:l,disposeTerminally:n,kv:h,readFrames:w,restoreSocket:u,scheduler:i,shard:p,simulateDeadLetter:g,simulateRecycle:S,socket:y}};export{P as createNodeConformanceHost};
|
package/dist/index.d.mts
CHANGED
|
@@ -310,9 +310,47 @@ interface NodeShardRegistry {
|
|
|
310
310
|
}
|
|
311
311
|
/** Build the in-process shard registry. */
|
|
312
312
|
declare const createNodeShardRegistry: (options?: NodeShardRegistryOptions) => NodeShardRegistry;
|
|
313
|
+
/** Options for {@link createNodeWorkflowHost}. */
|
|
314
|
+
interface NodeWorkflowHostOptions<Workflows extends Record<string, {
|
|
315
|
+
isLunoraWorkflow: true;
|
|
316
|
+
}>> {
|
|
317
|
+
/** Base env merged under the derived `WORKFLOW_*` bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
|
|
318
|
+
env?: Record<string, unknown>;
|
|
319
|
+
/** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
|
|
320
|
+
leaseTtlMs?: number;
|
|
321
|
+
/** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
|
|
322
|
+
store: WorkflowStore;
|
|
323
|
+
/** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
|
|
324
|
+
workflows: Workflows;
|
|
325
|
+
}
|
|
326
|
+
/** A fully-wired Node workflow host. */
|
|
327
|
+
interface NodeWorkflowHost<Workflows extends Record<string, {
|
|
328
|
+
isLunoraWorkflow: true;
|
|
329
|
+
}>> {
|
|
330
|
+
/** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
|
|
331
|
+
readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
|
|
332
|
+
/**
|
|
333
|
+
* The caller's `env` plus one `WORKFLOW_<EXPORT>` binding per workflow —
|
|
334
|
+
* merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
|
|
335
|
+
* children through the same runtime.
|
|
336
|
+
*/
|
|
337
|
+
readonly env: Record<string, unknown>;
|
|
338
|
+
/** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
|
|
339
|
+
readonly runtime: WorkflowRuntime;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Create a Node workflow host: compile every declared Lunora workflow onto the
|
|
343
|
+
* visulima engine, derive the `WORKFLOW_*` env, and expose the per-workflow
|
|
344
|
+
* `WorkflowBindingLike` handles.
|
|
345
|
+
*/
|
|
346
|
+
declare const createNodeWorkflowHost: <Workflows extends Record<string, {
|
|
347
|
+
isLunoraWorkflow: true;
|
|
348
|
+
}>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
|
|
313
349
|
/** Every contract this package provides, composed for one Node process. */
|
|
314
350
|
interface NodePlatform<Queues extends Record<string, {
|
|
315
351
|
isLunoraQueue: true;
|
|
352
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
353
|
+
isLunoraWorkflow: true;
|
|
316
354
|
}> = Record<string, never>> {
|
|
317
355
|
/** `using platform = createNodePlatform(...)` support — delegates to `close()`. */
|
|
318
356
|
[Symbol.dispose]: () => void;
|
|
@@ -355,6 +393,13 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
355
393
|
drain: () => Promise<void>;
|
|
356
394
|
/** Durable key-value storage backed by the same `better-sqlite3` database as `shard`. */
|
|
357
395
|
kv: ShardKvStore;
|
|
396
|
+
/**
|
|
397
|
+
* The local-filesystem bucket, or `undefined` when the caller named no
|
|
398
|
+
* bucket directory. Same reasoning as `queues`: `objectStorage` is rated
|
|
399
|
+
* `emulated`, so codegen emits `ctx.storage` for this target, and a bucket
|
|
400
|
+
* nobody bound fails at the first `put` with nothing upstream to warn.
|
|
401
|
+
*/
|
|
402
|
+
objectStorage?: R2BucketLike;
|
|
358
403
|
/**
|
|
359
404
|
* The declared queues, or `undefined` when the caller declared none.
|
|
360
405
|
*
|
|
@@ -369,6 +414,12 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
369
414
|
shard: ShardHost;
|
|
370
415
|
/** Socket registry with mutable tags and SQLite-persisted attachments. */
|
|
371
416
|
sockets: SocketHost;
|
|
417
|
+
/**
|
|
418
|
+
* The declared workflows, or `undefined` when the caller declared none.
|
|
419
|
+
* Runs are persisted to the same `better-sqlite3` database as the shard, so
|
|
420
|
+
* they survive a restart without a second store to configure.
|
|
421
|
+
*/
|
|
422
|
+
workflows?: NodeWorkflowHost<Workflows>;
|
|
372
423
|
}
|
|
373
424
|
/**
|
|
374
425
|
* Options for {@link createNodePlatform} — the shard host's (`path`,
|
|
@@ -378,10 +429,23 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
378
429
|
* or job with nowhere to land is bookkeeping. `directory` makes the shards the
|
|
379
430
|
* directory resolves for fan-out file-backed too, and `onAlarm` gives their
|
|
380
431
|
* durable alarms somewhere to land.
|
|
432
|
+
*
|
|
433
|
+
* `queues`, `workflows` and `objectStorageDirectory` are the three
|
|
434
|
+
* declarations: each binds its half of an `emulated` capability, and each is
|
|
435
|
+
* absent from the returned platform when its declaration is omitted.
|
|
381
436
|
*/
|
|
382
437
|
type NodePlatformOptions<Queues extends Record<string, {
|
|
383
438
|
isLunoraQueue: true;
|
|
439
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
440
|
+
isLunoraWorkflow: true;
|
|
384
441
|
}> = Record<string, never>> = {
|
|
442
|
+
/**
|
|
443
|
+
* Directory the object-storage bucket keeps its objects in, one file per
|
|
444
|
+
* key. Omit when the app declares no buckets — there is no default,
|
|
445
|
+
* because a bucket silently rooted at the process's working directory is
|
|
446
|
+
* worse than an absent one.
|
|
447
|
+
*/
|
|
448
|
+
objectStorageDirectory?: string;
|
|
385
449
|
/**
|
|
386
450
|
* Deliver one assembled queue batch — wire this to `dispatchQueueBatch`.
|
|
387
451
|
* Required alongside `queues`; without it the messages would be stored
|
|
@@ -390,11 +454,15 @@ type NodePlatformOptions<Queues extends Record<string, {
|
|
|
390
454
|
onQueueBatch?: NodeQueueHostOptions<Queues>["onBatch"];
|
|
391
455
|
/** The app's `defineQueue` results, keyed by export name. Omit when the app declares no queues. */
|
|
392
456
|
queues?: Queues;
|
|
457
|
+
/** The app's `defineWorkflow` results, keyed by export name. Omit when the app declares no workflows. */
|
|
458
|
+
workflows?: Workflows;
|
|
393
459
|
} & NodeSchedulerHostOptions & NodeShardHostOptions & NodeShardRegistryOptions;
|
|
394
460
|
/** Compose every contract this package provides over one `better-sqlite3` database. */
|
|
395
461
|
declare const createNodePlatform: <Queues extends Record<string, {
|
|
396
462
|
isLunoraQueue: true;
|
|
397
|
-
}> = Record<string, never
|
|
463
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
464
|
+
isLunoraWorkflow: true;
|
|
465
|
+
}> = Record<string, never>>(options?: NodePlatformOptions<Queues, Workflows>) => NodePlatform<Queues, Workflows>;
|
|
398
466
|
/** Options for {@link createNodeR2Bucket}. */
|
|
399
467
|
interface NodeR2BucketOptions {
|
|
400
468
|
/** The bucket directory — created on first write. Objects live here, one file per key. */
|
|
@@ -402,7 +470,7 @@ interface NodeR2BucketOptions {
|
|
|
402
470
|
}
|
|
403
471
|
/**
|
|
404
472
|
* Create an `R2BucketLike` over the local filesystem. Any object shape
|
|
405
|
-
* `createStorage({ bucket })` accepts — `put`/`get`/`head`/`delete`/`list` —
|
|
473
|
+
* `createStorage({ bucket, bucketName })` accepts — `put`/`get`/`head`/`delete`/`list` —
|
|
406
474
|
* maps directly onto a file operation.
|
|
407
475
|
*/
|
|
408
476
|
declare const createNodeR2Bucket: (options: NodeR2BucketOptions) => R2BucketLike;
|
|
@@ -475,42 +543,6 @@ interface NodeSocketHost {
|
|
|
475
543
|
}
|
|
476
544
|
/** Build the socket registry, persisting attachments and tags to `database`. */
|
|
477
545
|
declare const createNodeSocketHost: (database: Database.Database) => NodeSocketHost;
|
|
478
|
-
/** Options for {@link createNodeWorkflowHost}. */
|
|
479
|
-
interface NodeWorkflowHostOptions<Workflows extends Record<string, {
|
|
480
|
-
isLunoraWorkflow: true;
|
|
481
|
-
}>> {
|
|
482
|
-
/** Base env merged under the derived `WORKFLOW_*` bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
|
|
483
|
-
env?: Record<string, unknown>;
|
|
484
|
-
/** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
|
|
485
|
-
leaseTtlMs?: number;
|
|
486
|
-
/** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
|
|
487
|
-
store: WorkflowStore;
|
|
488
|
-
/** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
|
|
489
|
-
workflows: Workflows;
|
|
490
|
-
}
|
|
491
|
-
/** A fully-wired Node workflow host. */
|
|
492
|
-
interface NodeWorkflowHost<Workflows extends Record<string, {
|
|
493
|
-
isLunoraWorkflow: true;
|
|
494
|
-
}>> {
|
|
495
|
-
/** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
|
|
496
|
-
readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
|
|
497
|
-
/**
|
|
498
|
-
* The caller's `env` plus one `WORKFLOW_<EXPORT>` binding per workflow —
|
|
499
|
-
* merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
|
|
500
|
-
* children through the same runtime.
|
|
501
|
-
*/
|
|
502
|
-
readonly env: Record<string, unknown>;
|
|
503
|
-
/** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
|
|
504
|
-
readonly runtime: WorkflowRuntime;
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Create a Node workflow host: compile every declared Lunora workflow onto the
|
|
508
|
-
* visulima engine, derive the `WORKFLOW_*` env, and expose the per-workflow
|
|
509
|
-
* `WorkflowBindingLike` handles.
|
|
510
|
-
*/
|
|
511
|
-
declare const createNodeWorkflowHost: <Workflows extends Record<string, {
|
|
512
|
-
isLunoraWorkflow: true;
|
|
513
|
-
}>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
|
|
514
546
|
/**
|
|
515
547
|
* Build a durable {@link WorkflowStore} over a `better-sqlite3` connection.
|
|
516
548
|
* Pass the result as `createNodeWorkflowHost({ store })`.
|
|
@@ -536,15 +568,26 @@ export {
|
|
|
536
568
|
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
537
569
|
* lifecycle/global-store tests.
|
|
538
570
|
*
|
|
539
|
-
* **
|
|
540
|
-
* (`@lunora/platform`):
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
571
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
572
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
573
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
574
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
575
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
576
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
577
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
578
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
579
|
+
*
|
|
580
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
581
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
582
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
583
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
584
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
585
|
+
*
|
|
586
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
587
|
+
* for `provision` — which reports, once, which declared features this target
|
|
588
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
589
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
590
|
+
* server, which is why both statements above hold at once. See
|
|
548
591
|
* `plans/234-node-host-findings.md`.
|
|
549
592
|
*/
|
|
550
593
|
type NodeGlobalContextDatabaseOptions,
|
|
@@ -567,15 +610,26 @@ type NodeGlobalContextDatabaseOptions,
|
|
|
567
610
|
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
568
611
|
* lifecycle/global-store tests.
|
|
569
612
|
*
|
|
570
|
-
* **
|
|
571
|
-
* (`@lunora/platform`):
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
613
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
614
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
615
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
616
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
617
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
618
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
619
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
620
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
621
|
+
*
|
|
622
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
623
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
624
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
625
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
626
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
627
|
+
*
|
|
628
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
629
|
+
* for `provision` — which reports, once, which declared features this target
|
|
630
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
631
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
632
|
+
* server, which is why both statements above hold at once. See
|
|
579
633
|
* `plans/234-node-host-findings.md`.
|
|
580
634
|
*/
|
|
581
635
|
type NodeGlobalStore,
|
|
@@ -598,15 +652,26 @@ type NodeGlobalStore,
|
|
|
598
652
|
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
599
653
|
* lifecycle/global-store tests.
|
|
600
654
|
*
|
|
601
|
-
* **
|
|
602
|
-
* (`@lunora/platform`):
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
655
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
656
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
657
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
658
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
659
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
660
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
661
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
662
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
663
|
+
*
|
|
664
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
665
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
666
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
667
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
668
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
669
|
+
*
|
|
670
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
671
|
+
* for `provision` — which reports, once, which declared features this target
|
|
672
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
673
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
674
|
+
* server, which is why both statements above hold at once. See
|
|
610
675
|
* `plans/234-node-host-findings.md`.
|
|
611
676
|
*/
|
|
612
677
|
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.d.ts
CHANGED
|
@@ -310,9 +310,47 @@ interface NodeShardRegistry {
|
|
|
310
310
|
}
|
|
311
311
|
/** Build the in-process shard registry. */
|
|
312
312
|
declare const createNodeShardRegistry: (options?: NodeShardRegistryOptions) => NodeShardRegistry;
|
|
313
|
+
/** Options for {@link createNodeWorkflowHost}. */
|
|
314
|
+
interface NodeWorkflowHostOptions<Workflows extends Record<string, {
|
|
315
|
+
isLunoraWorkflow: true;
|
|
316
|
+
}>> {
|
|
317
|
+
/** Base env merged under the derived `WORKFLOW_*` bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
|
|
318
|
+
env?: Record<string, unknown>;
|
|
319
|
+
/** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
|
|
320
|
+
leaseTtlMs?: number;
|
|
321
|
+
/** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
|
|
322
|
+
store: WorkflowStore;
|
|
323
|
+
/** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
|
|
324
|
+
workflows: Workflows;
|
|
325
|
+
}
|
|
326
|
+
/** A fully-wired Node workflow host. */
|
|
327
|
+
interface NodeWorkflowHost<Workflows extends Record<string, {
|
|
328
|
+
isLunoraWorkflow: true;
|
|
329
|
+
}>> {
|
|
330
|
+
/** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
|
|
331
|
+
readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
|
|
332
|
+
/**
|
|
333
|
+
* The caller's `env` plus one `WORKFLOW_<EXPORT>` binding per workflow —
|
|
334
|
+
* merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
|
|
335
|
+
* children through the same runtime.
|
|
336
|
+
*/
|
|
337
|
+
readonly env: Record<string, unknown>;
|
|
338
|
+
/** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
|
|
339
|
+
readonly runtime: WorkflowRuntime;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Create a Node workflow host: compile every declared Lunora workflow onto the
|
|
343
|
+
* visulima engine, derive the `WORKFLOW_*` env, and expose the per-workflow
|
|
344
|
+
* `WorkflowBindingLike` handles.
|
|
345
|
+
*/
|
|
346
|
+
declare const createNodeWorkflowHost: <Workflows extends Record<string, {
|
|
347
|
+
isLunoraWorkflow: true;
|
|
348
|
+
}>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
|
|
313
349
|
/** Every contract this package provides, composed for one Node process. */
|
|
314
350
|
interface NodePlatform<Queues extends Record<string, {
|
|
315
351
|
isLunoraQueue: true;
|
|
352
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
353
|
+
isLunoraWorkflow: true;
|
|
316
354
|
}> = Record<string, never>> {
|
|
317
355
|
/** `using platform = createNodePlatform(...)` support — delegates to `close()`. */
|
|
318
356
|
[Symbol.dispose]: () => void;
|
|
@@ -355,6 +393,13 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
355
393
|
drain: () => Promise<void>;
|
|
356
394
|
/** Durable key-value storage backed by the same `better-sqlite3` database as `shard`. */
|
|
357
395
|
kv: ShardKvStore;
|
|
396
|
+
/**
|
|
397
|
+
* The local-filesystem bucket, or `undefined` when the caller named no
|
|
398
|
+
* bucket directory. Same reasoning as `queues`: `objectStorage` is rated
|
|
399
|
+
* `emulated`, so codegen emits `ctx.storage` for this target, and a bucket
|
|
400
|
+
* nobody bound fails at the first `put` with nothing upstream to warn.
|
|
401
|
+
*/
|
|
402
|
+
objectStorage?: R2BucketLike;
|
|
358
403
|
/**
|
|
359
404
|
* The declared queues, or `undefined` when the caller declared none.
|
|
360
405
|
*
|
|
@@ -369,6 +414,12 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
369
414
|
shard: ShardHost;
|
|
370
415
|
/** Socket registry with mutable tags and SQLite-persisted attachments. */
|
|
371
416
|
sockets: SocketHost;
|
|
417
|
+
/**
|
|
418
|
+
* The declared workflows, or `undefined` when the caller declared none.
|
|
419
|
+
* Runs are persisted to the same `better-sqlite3` database as the shard, so
|
|
420
|
+
* they survive a restart without a second store to configure.
|
|
421
|
+
*/
|
|
422
|
+
workflows?: NodeWorkflowHost<Workflows>;
|
|
372
423
|
}
|
|
373
424
|
/**
|
|
374
425
|
* Options for {@link createNodePlatform} — the shard host's (`path`,
|
|
@@ -378,10 +429,23 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
378
429
|
* or job with nowhere to land is bookkeeping. `directory` makes the shards the
|
|
379
430
|
* directory resolves for fan-out file-backed too, and `onAlarm` gives their
|
|
380
431
|
* durable alarms somewhere to land.
|
|
432
|
+
*
|
|
433
|
+
* `queues`, `workflows` and `objectStorageDirectory` are the three
|
|
434
|
+
* declarations: each binds its half of an `emulated` capability, and each is
|
|
435
|
+
* absent from the returned platform when its declaration is omitted.
|
|
381
436
|
*/
|
|
382
437
|
type NodePlatformOptions<Queues extends Record<string, {
|
|
383
438
|
isLunoraQueue: true;
|
|
439
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
440
|
+
isLunoraWorkflow: true;
|
|
384
441
|
}> = Record<string, never>> = {
|
|
442
|
+
/**
|
|
443
|
+
* Directory the object-storage bucket keeps its objects in, one file per
|
|
444
|
+
* key. Omit when the app declares no buckets — there is no default,
|
|
445
|
+
* because a bucket silently rooted at the process's working directory is
|
|
446
|
+
* worse than an absent one.
|
|
447
|
+
*/
|
|
448
|
+
objectStorageDirectory?: string;
|
|
385
449
|
/**
|
|
386
450
|
* Deliver one assembled queue batch — wire this to `dispatchQueueBatch`.
|
|
387
451
|
* Required alongside `queues`; without it the messages would be stored
|
|
@@ -390,11 +454,15 @@ type NodePlatformOptions<Queues extends Record<string, {
|
|
|
390
454
|
onQueueBatch?: NodeQueueHostOptions<Queues>["onBatch"];
|
|
391
455
|
/** The app's `defineQueue` results, keyed by export name. Omit when the app declares no queues. */
|
|
392
456
|
queues?: Queues;
|
|
457
|
+
/** The app's `defineWorkflow` results, keyed by export name. Omit when the app declares no workflows. */
|
|
458
|
+
workflows?: Workflows;
|
|
393
459
|
} & NodeSchedulerHostOptions & NodeShardHostOptions & NodeShardRegistryOptions;
|
|
394
460
|
/** Compose every contract this package provides over one `better-sqlite3` database. */
|
|
395
461
|
declare const createNodePlatform: <Queues extends Record<string, {
|
|
396
462
|
isLunoraQueue: true;
|
|
397
|
-
}> = Record<string, never
|
|
463
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
464
|
+
isLunoraWorkflow: true;
|
|
465
|
+
}> = Record<string, never>>(options?: NodePlatformOptions<Queues, Workflows>) => NodePlatform<Queues, Workflows>;
|
|
398
466
|
/** Options for {@link createNodeR2Bucket}. */
|
|
399
467
|
interface NodeR2BucketOptions {
|
|
400
468
|
/** The bucket directory — created on first write. Objects live here, one file per key. */
|
|
@@ -402,7 +470,7 @@ interface NodeR2BucketOptions {
|
|
|
402
470
|
}
|
|
403
471
|
/**
|
|
404
472
|
* Create an `R2BucketLike` over the local filesystem. Any object shape
|
|
405
|
-
* `createStorage({ bucket })` accepts — `put`/`get`/`head`/`delete`/`list` —
|
|
473
|
+
* `createStorage({ bucket, bucketName })` accepts — `put`/`get`/`head`/`delete`/`list` —
|
|
406
474
|
* maps directly onto a file operation.
|
|
407
475
|
*/
|
|
408
476
|
declare const createNodeR2Bucket: (options: NodeR2BucketOptions) => R2BucketLike;
|
|
@@ -475,42 +543,6 @@ interface NodeSocketHost {
|
|
|
475
543
|
}
|
|
476
544
|
/** Build the socket registry, persisting attachments and tags to `database`. */
|
|
477
545
|
declare const createNodeSocketHost: (database: Database.Database) => NodeSocketHost;
|
|
478
|
-
/** Options for {@link createNodeWorkflowHost}. */
|
|
479
|
-
interface NodeWorkflowHostOptions<Workflows extends Record<string, {
|
|
480
|
-
isLunoraWorkflow: true;
|
|
481
|
-
}>> {
|
|
482
|
-
/** Base env merged under the derived `WORKFLOW_*` bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
|
|
483
|
-
env?: Record<string, unknown>;
|
|
484
|
-
/** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
|
|
485
|
-
leaseTtlMs?: number;
|
|
486
|
-
/** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
|
|
487
|
-
store: WorkflowStore;
|
|
488
|
-
/** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
|
|
489
|
-
workflows: Workflows;
|
|
490
|
-
}
|
|
491
|
-
/** A fully-wired Node workflow host. */
|
|
492
|
-
interface NodeWorkflowHost<Workflows extends Record<string, {
|
|
493
|
-
isLunoraWorkflow: true;
|
|
494
|
-
}>> {
|
|
495
|
-
/** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
|
|
496
|
-
readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
|
|
497
|
-
/**
|
|
498
|
-
* The caller's `env` plus one `WORKFLOW_<EXPORT>` binding per workflow —
|
|
499
|
-
* merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
|
|
500
|
-
* children through the same runtime.
|
|
501
|
-
*/
|
|
502
|
-
readonly env: Record<string, unknown>;
|
|
503
|
-
/** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
|
|
504
|
-
readonly runtime: WorkflowRuntime;
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Create a Node workflow host: compile every declared Lunora workflow onto the
|
|
508
|
-
* visulima engine, derive the `WORKFLOW_*` env, and expose the per-workflow
|
|
509
|
-
* `WorkflowBindingLike` handles.
|
|
510
|
-
*/
|
|
511
|
-
declare const createNodeWorkflowHost: <Workflows extends Record<string, {
|
|
512
|
-
isLunoraWorkflow: true;
|
|
513
|
-
}>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
|
|
514
546
|
/**
|
|
515
547
|
* Build a durable {@link WorkflowStore} over a `better-sqlite3` connection.
|
|
516
548
|
* Pass the result as `createNodeWorkflowHost({ store })`.
|
|
@@ -536,15 +568,26 @@ export {
|
|
|
536
568
|
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
537
569
|
* lifecycle/global-store tests.
|
|
538
570
|
*
|
|
539
|
-
* **
|
|
540
|
-
* (`@lunora/platform`):
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
571
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
572
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
573
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
574
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
575
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
576
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
577
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
578
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
579
|
+
*
|
|
580
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
581
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
582
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
583
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
584
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
585
|
+
*
|
|
586
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
587
|
+
* for `provision` — which reports, once, which declared features this target
|
|
588
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
589
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
590
|
+
* server, which is why both statements above hold at once. See
|
|
548
591
|
* `plans/234-node-host-findings.md`.
|
|
549
592
|
*/
|
|
550
593
|
type NodeGlobalContextDatabaseOptions,
|
|
@@ -567,15 +610,26 @@ type NodeGlobalContextDatabaseOptions,
|
|
|
567
610
|
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
568
611
|
* lifecycle/global-store tests.
|
|
569
612
|
*
|
|
570
|
-
* **
|
|
571
|
-
* (`@lunora/platform`):
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
613
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
614
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
615
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
616
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
617
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
618
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
619
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
620
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
621
|
+
*
|
|
622
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
623
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
624
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
625
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
626
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
627
|
+
*
|
|
628
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
629
|
+
* for `provision` — which reports, once, which declared features this target
|
|
630
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
631
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
632
|
+
* server, which is why both statements above hold at once. See
|
|
579
633
|
* `plans/234-node-host-findings.md`.
|
|
580
634
|
*/
|
|
581
635
|
type NodeGlobalStore,
|
|
@@ -598,15 +652,26 @@ type NodeGlobalStore,
|
|
|
598
652
|
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
599
653
|
* lifecycle/global-store tests.
|
|
600
654
|
*
|
|
601
|
-
* **
|
|
602
|
-
* (`@lunora/platform`):
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
655
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
656
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
657
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
658
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
659
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
660
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
661
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
662
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
663
|
+
*
|
|
664
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
665
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
666
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
667
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
668
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
669
|
+
*
|
|
670
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
671
|
+
* for `provision` — which reports, once, which declared features this target
|
|
672
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
673
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
674
|
+
* server, which is why both statements above hold at once. See
|
|
610
675
|
* `plans/234-node-host-findings.md`.
|
|
611
676
|
*/
|
|
612
677
|
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-Cwi33XOk.mjs";import{createNodeShardKvStore as d}from"./packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodePlatform as f}from"./packem_shared/createNodePlatform-
|
|
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-DZ710ybu.mjs";import{createNodeQueueHost as x}from"./packem_shared/createNodeQueueHost--mBLGSxF.mjs";import{createNodeR2Bucket as p}from"./packem_shared/createNodeR2Bucket-BzNH1lBp.mjs";import{createNodeSchedulerHost as l}from"./packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as h}from"./packem_shared/createNodeShardHost-CNoG3Fzy.mjs";import{createNodeShardRegistry as k}from"./packem_shared/createNodeShardRegistry-ZWGlgv0E.mjs";import{createNodeShardState as w}from"./packem_shared/createNodeShardState-CpUzvWwd.mjs";import{createNodeSocketHost as W}from"./packem_shared/createNodeSocketHost-CoE4VZbB.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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as n}from"@lunora/errors";import{NODE_CAPABILITIES as S}from"@lunora/platform";import{createNodeShardKvStore as N}from"./createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeQueueHost as p}from"./createNodeQueueHost--mBLGSxF.mjs";import{createNodeR2Bucket as k}from"./createNodeR2Bucket-BzNH1lBp.mjs";import{createNodeSchedulerHost as v}from"./createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as b}from"./createNodeShardHost-CNoG3Fzy.mjs";import{createNodeShardRegistry as g}from"./createNodeShardRegistry-ZWGlgv0E.mjs";import{createNodeSocketHost as y}from"./createNodeSocketHost-CoE4VZbB.mjs";import{createNodeWorkflowHost as q}from"./createNodeWorkflowHost-CTVBYiuj.mjs";import{createNodeWorkflowStore as B}from"./createNodeWorkflowStore-CL7RYP6H.mjs";const T=(e={})=>{const{database:o,dispose:c,drain:s,host:a}=b(e),d=N(o),r=g(e),{directory:u}=r,{socket:i}=y(o),{dispose:m,scheduler:f}=v(o,e),h=e.queues===void 0?void 0:p(o,{onBatch:e.onQueueBatch??(()=>{throw new n("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}),l=e.objectStorageDirectory===void 0?void 0:k({directory:e.objectStorageDirectory}),w=e.workflows===void 0?void 0:q({store:B(o),workflows:e.workflows}),t=()=>{m(),c(),r.close()};return{capabilities:S,close:t,directory:u,drain:s,kv:d,objectStorage:l,queues:h,scheduler:f,shard:a,sockets:i,workflows:w,[Symbol.dispose]:t}};export{T as createNodePlatform};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUID as k,createHash as H}from"node:crypto";import{mkdir as _,open as j,rename as K,unlink as G,readdir as z}from"node:fs/promises";import{join as m,dirname as L}from"node:path";import{LunoraError as l}from"@lunora/errors";import{t as U}from"./to-array-buffer-DVyXU4Ep.mjs";new TextEncoder;const W=Array.from({length:32},(t,e)=>e),X=new RegExp(`[${W.map(t=>String.fromCodePoint(t)).join("")}]`,"u"),Y=t=>X.test(t),O=".lunora-tmp",V="LNR1",p=8,b=1024,B=255,J=/[%A-Z:]/g,$=t=>{const e=t.replaceAll(J,r=>`%${r.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`),n=e.at(-1);return n==="."||n===" "?`${e.slice(0,-1)}%${n.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`:e},Z=t=>{try{return decodeURIComponent(t)}catch{return}},g=t=>t.split("/").map(e=>$(e)).join("/"),q=t=>{const e=[];for(const n of t.split("/")){const r=Z(n);if(r===void 0)return;e.push(r)}return e.join("/")},Q=new TextEncoder,tt=t=>Q.encode(t).length,E=t=>{if(typeof t!="string"||t.length===0)throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key must be a non-empty string");if(tt(t)>b)throw new l("VALIDATION_ERROR",`@lunora/platform-node: R2 key exceeds ${String(b)}-byte limit`);if(t.includes("\0"))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a NUL byte");if(Y(t))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a control character (including CR/LF)");if(t.includes("\\"))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a backslash (a path separator on Windows)");if(t.startsWith("/"))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key must not start with `/`");for(const e of t.split("/")){if(Buffer.byteLength($(e),"utf8")>B)throw new l("VALIDATION_ERROR",`@lunora/platform-node: R2 key segment "${e}" exceeds ${String(B)} bytes once escaped for the filesystem`);if(e===""||e==="."||e==="..")throw new l("VALIDATION_ERROR",`@lunora/platform-node: R2 key has an empty, \`.\` or \`..\` path segment ("${t}")`);if(e===O)throw new l("VALIDATION_ERROR",`@lunora/platform-node: R2 key is reserved (no segment may be "${O}")`)}},x=t=>t instanceof Error&&"code"in t&&t.code==="ENOENT",D=(t,e)=>{const n=e instanceof Error&&"code"in e?e.code:void 0;return n==="EEXIST"||n==="EISDIR"||n==="ENOTDIR"?new l("VALIDATION_ERROR",`@lunora/platform-node: R2 key "${t}" 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`):e},et=t=>{const e=Buffer.from(JSON.stringify(t),"utf8"),n=Buffer.alloc(p);return n.writeUInt32BE(e.byteLength,0),n.write(V,4,"ascii"),Buffer.concat([e,n])},nt=t=>{if(typeof t!="object"||t===null)return!1;const e=t;return typeof e.sha256Hex=="string"&&typeof e.size=="number"&&typeof e.uploaded=="string"},rt=async t=>{const e=await t.stat();if(!e.isFile()||e.size<p)return;const n=Buffer.alloc(p);if(await t.read(n,0,p,e.size-p),n.toString("ascii",4,p)!==V)return;const r=n.readUInt32BE(0),o=e.size-p-r;if(r===0||o<0)return;const s=Buffer.alloc(r),{bytesRead:i}=await t.read(s,0,r,o);if(i!==r)return;let a;try{a=JSON.parse(s.toString("utf8"))}catch{return}return nt(a)?{bodySize:o,meta:a}:void 0},P=async t=>{let e;try{e=await j(t,"r")}catch(n){if(x(n))return;throw n}try{const n=await rt(e);if(n===void 0){await e.close();return}return{...n,handle:e}}catch(n){throw await e.close(),n}},M=new FinalizationRegistry(t=>{t.close().catch(()=>{})}),C=async t=>{const e=await P(t);if(e!==void 0)return await e.handle.close(),{bodySize:e.bodySize,meta:e.meta}},I=(t,e)=>{const{sha256Hex:n}=e,r=Buffer.from(n,"hex");return{checksums:{sha256:U(r)},customMetadata:e.customMetadata,etag:n,httpEtag:`"${n}"`,httpMetadata:e.httpMetadata,key:t,sha256:n,sha256Base64:r.toString("base64"),size:e.size,uploaded:new Date(e.uploaded)}},v=(t,e,n)=>t===void 0||!Number.isFinite(t)?e:Math.min(Math.max(0,Math.floor(t)),n),ot=(t,e)=>{if(e===void 0)return{end:t,start:0};if("suffix"in e)return{end:t,start:t-v(e.suffix,t,t)};const n=v(e.offset,0,t);return{end:e.length===void 0||!Number.isFinite(e.length)?t:Math.min(n+v(e.length,0,t),t),start:n}},at=(t,e,n,r)=>{const o=t.createReadStream({autoClose:!1,end:n-1,start:e})[Symbol.asyncIterator]();return new ReadableStream({cancel:async()=>{await o.return?.(),await r()},pull:async s=>{let i;try{i=await o.next()}catch(a){throw await r(),a}i.done===!0?(s.close(),await r()):s.enqueue(i.value)}})},st=async(t,e,n)=>{const r=n-e;if(r<=0)return Buffer.alloc(0);const o=Buffer.alloc(r),{bytesRead:s}=await t.read(o,0,r,e);return o.subarray(0,s)},F=async function*(t){if(t!==null){if(typeof t=="string"){yield new TextEncoder().encode(t);return}if(t instanceof ArrayBuffer){yield new Uint8Array(t);return}if(ArrayBuffer.isView(t)){yield new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return}if(t instanceof Blob){yield*F(t.stream());return}if(t instanceof ReadableStream){for await(const e of t)if(e instanceof Uint8Array)yield e;else if(e instanceof ArrayBuffer)yield new Uint8Array(e);else throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 put stream must yield byte chunks");return}throw new l("VALIDATION_ERROR","@lunora/platform-node: unsupported R2 put body")}},it=async t=>{const e=[],n=async(r,o)=>{let s;try{s=await z(r,{withFileTypes:!0})}catch(a){if(x(a))return;throw a}const i=[];for(const a of s){if(a.name===O)continue;const d=o===""?a.name:`${o}/${a.name}`;if(a.isDirectory())i.push(n(m(r,a.name),d));else{const u=q(d);u!==void 0&&e.push(u)}}await Promise.all(i)};return await n(t,""),e.sort((r,o)=>r<o?-1:+(r>o)),e},ct=(t,e)=>{let n=0,r=t.length;for(;n<r;){const o=n+Math.floor((r-n)/2),s=t[o];s===void 0||s<=e?n=o+1:r=o}return n},T=async t=>{try{await G(t)}catch(e){if(!x(e))throw e}},wt=t=>{const{directory:e}=t;return{delete:async n=>{E(n),await T(m(e,g(n)))},get:async(n,r)=>{E(n);const o=await P(m(e,g(n)));if(o===void 0)return null;const{bodySize:s,handle:i,meta:a}=o,{end:d,start:u}=ot(s,r?.range),c={};let h=!1;const w=async()=>{h||(h=!0,M.unregister(c),await i.close())},y=()=>{if(h)throw new l("BAD_REQUEST",`@lunora/platform-node: R2 object "${n}" body has already been consumed`)},N=async()=>{y();try{return await st(i,u,d)}finally{await w()}},A={...I(n,a),arrayBuffer:async()=>U(await N()),get body(){return y(),d<=u?(w().catch(()=>{}),new Blob([]).stream()):at(i,u,d,w)},text:async()=>(await N()).toString("utf8")};return M.register(A,{close:w},c),A},head:async n=>{E(n);const r=await C(m(e,g(n)));return r===void 0?null:I(n,r.meta)},list:async(n={})=>{const r=Math.max(1,v(n.limit,1e3,1e3));if(n.prefix?.includes("\0"))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 list prefix contains a NUL byte");const o=await it(e),s=n.prefix??"",i=o.filter(f=>f.startsWith(s)),{delimiter:a}=n,d=a===void 0?i:i.filter(f=>!f.slice(s.length).includes(a)),u=a===void 0?void 0:[...new Set(i.map(f=>{const S=f.slice(s.length).indexOf(a);return S===-1?void 0:f.slice(0,s.length+S+a.length)}).filter(f=>f!==void 0))].toSorted((f,R)=>f<R?-1:+(f>R));let c=n.cursor;n.startAfter!==void 0&&(c===void 0||n.startAfter>c)&&(c=n.startAfter);const h=c===void 0?0:ct(d,c),w=d.slice(h,h+r),y=h+r<d.length,A=(await Promise.all(w.map(async f=>{const R=await C(m(e,g(f)));return R===void 0?void 0:I(f,R.meta)}))).filter(f=>f!==void 0);return{cursor:y?w.at(-1):void 0,delimitedPrefixes:u,objects:A,truncated:y}},put:async(n,r,o)=>{E(n);const s=m(e,g(n)),i=m(e,O,k());try{await _(L(s),{recursive:!0})}catch(c){throw D(n,c)}await _(L(i),{recursive:!0});const a=H("sha256");let d=0,u;try{const c=await j(i,"w");try{for await(const h of F(r))a.update(h),d+=h.byteLength,await c.write(h);u={customMetadata:o?.customMetadata,httpMetadata:o?.httpMetadata,sha256Hex:a.digest("hex"),size:d,uploaded:new Date().toISOString()},await c.write(et(u))}finally{await c.close()}}catch(c){throw await T(i),c}try{await K(i,s)}catch(c){throw await T(i),D(n,c)}return I(n,u)}}};export{wt 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{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-CNoG3Fzy.mjs";import{createNodeSocketHost as A}from"./createNodeSocketHost-CoE4VZbB.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,2 @@
|
|
|
1
|
+
import{deserialize as w,serialize as l}from"node:v8";import{t as S}from"./to-array-buffer-DVyXU4Ep.mjs";const p=n=>typeof n=="object"&&n!==null||typeof n=="function",A=n=>{n.exec("CREATE TABLE IF NOT EXISTS _lunora_sockets (id TEXT PRIMARY KEY, attachment BLOB, tags TEXT NOT NULL DEFAULT '[]')");const g=n.prepare(`INSERT INTO _lunora_sockets (id, attachment, tags) VALUES (?, ?, ?)
|
|
2
|
+
ON CONFLICT (id) DO UPDATE SET attachment = excluded.attachment, tags = excluded.tags`),h=n.prepare("UPDATE _lunora_sockets SET attachment = ? WHERE id = ?"),E=n.prepare("UPDATE _lunora_sockets SET tags = ? WHERE id = ?"),m=n.prepare("SELECT attachment, tags FROM _lunora_sockets WHERE id = ?"),T=n.prepare("DELETE FROM _lunora_sockets WHERE id = ?"),c=new Map,i=new WeakMap;let a=new WeakMap;const v=(t,e)=>{n.open&&h.run(e===void 0?null:l(e),t)},f=(t,e)=>{n.open&&E.run(JSON.stringify([...e]),t)},u=t=>{const e=t,o={close:(r,s)=>{e.closed=!0,c.delete(e.id),p(e.raw)&&a.delete(e.raw),n.open&&T.run(e.id)},deserializeAttachment:()=>e.attachment,send:r=>{e.received.push(typeof r=="string"?r:S(r))},serializeAttachment:r=>{e.attachment=r,v(e.id,r)}};return e.handle=o,i.set(o,e.id),o};return{readFrames:t=>(c.get(i.get(t)??"")?.received??[]).filter(e=>typeof e=="string"),restoreSocket:(t,e)=>{const o=n.open?m.get(t):void 0,r=o?.attachment===null?void 0:o?.attachment,s={attachment:r===void 0?e:w(r),closed:!1,handle:void 0,id:t,raw:void 0,received:[],tags:new Set(o===void 0?[]:JSON.parse(o.tags))};return c.set(t,s),o===void 0&&n.open&&g.run(t,s.attachment===void 0?null:l(s.attachment),JSON.stringify([...s.tags])),u(s)},simulateRecycle:()=>{c.clear(),a=new WeakMap},socket:{accept:(t,e,o)=>{if(!n.open)throw new Error("platform closed: cannot accept a socket");const r=crypto.randomUUID(),s=new Set(o);g.run(r,e===void 0?null:l(e),JSON.stringify([...s]));const d={attachment:e,closed:!1,handle:void 0,id:r,raw:t,received:[],tags:s};return c.set(r,d),p(t)&&a.set(t,d),u(d)},getSockets:t=>{const e=[...c.values()];return(t===void 0?e:e.filter(r=>r.tags.has(t))).map(r=>r.handle)},handleFor:t=>p(t)?a.get(t)?.handle:void 0,idFor:t=>{const e=i.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(!n.open)throw new Error("platform closed: cannot remove a tag");const o=c.get(i.get(t)??"");o!==void 0&&(e===void 0?o.tags.clear():o.tags.delete(e),f(o.id,o.tags))},setTag:(t,e)=>{if(!n.open)throw new Error("platform closed: cannot set a tag");const o=c.get(i.get(t)??"");o!==void 0&&(o.tags.add(e),f(o.id,o.tags))}}}};export{A as createNodeSocketHost};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/platform-node",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.39",
|
|
4
4
|
"description": "Node implementation of the Lunora platform contracts: ShardHost, SocketHost, ShardDirectory, ShardKvStore, SchedulerHost over better-sqlite3 and an in-process socket registry (dev/test target)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"better-sqlite3",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/d1": "1.0.0-alpha.
|
|
49
|
+
"@lunora/d1": "1.0.0-alpha.99",
|
|
50
50
|
"@lunora/errors": "1.0.0-alpha.26",
|
|
51
|
-
"@lunora/platform": "1.0.0-alpha.
|
|
52
|
-
"@lunora/queue": "1.0.0-alpha.
|
|
53
|
-
"@lunora/sql-store": "1.0.0-alpha.
|
|
54
|
-
"@lunora/workflow": "1.0.0-alpha.
|
|
51
|
+
"@lunora/platform": "1.0.0-alpha.22",
|
|
52
|
+
"@lunora/queue": "1.0.0-alpha.39",
|
|
53
|
+
"@lunora/sql-store": "1.0.0-alpha.101",
|
|
54
|
+
"@lunora/workflow": "1.0.0-alpha.38",
|
|
55
55
|
"@visulima/workflow": "3.0.0",
|
|
56
56
|
"better-sqlite3": "^13.0.3",
|
|
57
57
|
"cron-parser": "5.8.1"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as l}from"@lunora/errors";import{NODE_CAPABILITIES as p}from"@lunora/platform";import{createNodeShardKvStore as f}from"./createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeQueueHost as N}from"./createNodeQueueHost--mBLGSxF.mjs";import{createNodeSchedulerHost as S}from"./createNodeSchedulerHost-vZgv1CEo.mjs";import{createNodeShardHost as q}from"./createNodeShardHost-CNoG3Fzy.mjs";import{createNodeShardRegistry as v}from"./createNodeShardRegistry-BmSAs2lp.mjs";import{createNodeSocketHost as w}from"./createNodeSocketHost-DK3X5PlQ.mjs";const H=(e={})=>{const{database:o,dispose:s,drain:a,host:c}=q(e),d=f(o),r=v(e),{directory:u}=r,{socket:i}=w(o),{dispose:m,scheduler:h}=S(o,e),n=e.queues===void 0?void 0:N(o,{onBatch:e.onQueueBatch??(()=>{throw new l("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}),t=()=>{m(),s(),r.close()};return{capabilities:p,close:t,directory:u,drain:a,kv:d,queues:n,scheduler:h,shard:c,sockets:i,[Symbol.dispose]:t}};export{H as createNodePlatform};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{randomUUID as $,createHash as P}from"node:crypto";import{mkdir as N,open as D,rename as F,unlink as k,readdir as H}from"node:fs/promises";import{join as w,dirname as _}from"node:path";import{LunoraError as f}from"@lunora/errors";import{t as M}from"./to-array-buffer-DVyXU4Ep.mjs";new TextEncoder;const K=Array.from({length:32},(t,e)=>e),G=new RegExp(`[${K.map(t=>String.fromCodePoint(t)).join("")}]`,"u"),z=t=>G.test(t),I=".lunora-tmp",C="LNR1",m=8,S=1024,x=255,W=/[%A-Z:]/g,j=t=>{const e=t.replaceAll(W,n=>`%${n.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`),r=e.at(-1);return r==="."||r===" "?`${e.slice(0,-1)}%${r.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`:e},X=t=>{try{return decodeURIComponent(t)}catch{return}},y=t=>t.split("/").map(e=>j(e)).join("/"),Y=t=>{const e=[];for(const r of t.split("/")){const n=X(r);if(n===void 0)return;e.push(n)}return e.join("/")},J=new TextEncoder,Z=t=>J.encode(t).length,g=t=>{if(typeof t!="string"||t.length===0)throw new f("VALIDATION_ERROR","@lunora/platform-node: R2 key must be a non-empty string");if(Z(t)>S)throw new f("VALIDATION_ERROR",`@lunora/platform-node: R2 key exceeds ${String(S)}-byte limit`);if(t.includes("\0"))throw new f("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a NUL byte");if(z(t))throw new f("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a control character (including CR/LF)");if(t.includes("\\"))throw new f("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a backslash (a path separator on Windows)");if(t.startsWith("/"))throw new f("VALIDATION_ERROR","@lunora/platform-node: R2 key must not start with `/`");for(const e of t.split("/")){if(Buffer.byteLength(j(e),"utf8")>x)throw new f("VALIDATION_ERROR",`@lunora/platform-node: R2 key segment "${e}" exceeds ${String(x)} bytes once escaped for the filesystem`);if(e===""||e==="."||e==="..")throw new f("VALIDATION_ERROR",`@lunora/platform-node: R2 key has an empty, \`.\` or \`..\` path segment ("${t}")`);if(e===I)throw new f("VALIDATION_ERROR",`@lunora/platform-node: R2 key is reserved (no segment may be "${I}")`)}},v=t=>t instanceof Error&&"code"in t&&t.code==="ENOENT",L=(t,e)=>{const r=e instanceof Error&&"code"in e?e.code:void 0;return r==="EEXIST"||r==="EISDIR"||r==="ENOTDIR"?new f("VALIDATION_ERROR",`@lunora/platform-node: R2 key "${t}" 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`):e},q=t=>{const e=Buffer.from(JSON.stringify(t),"utf8"),r=Buffer.alloc(m);return r.writeUInt32BE(e.byteLength,0),r.write(C,4,"ascii"),Buffer.concat([e,r])},Q=t=>{if(typeof t!="object"||t===null)return!1;const e=t;return typeof e.sha256Hex=="string"&&typeof e.size=="number"&&typeof e.uploaded=="string"},tt=async t=>{const e=await t.stat();if(!e.isFile()||e.size<m)return;const r=Buffer.alloc(m);if(await t.read(r,0,m,e.size-m),r.toString("ascii",4,m)!==C)return;const n=r.readUInt32BE(0),o=e.size-m-n;if(n===0||o<0)return;const i=Buffer.alloc(n),{bytesRead:s}=await t.read(i,0,n,o);if(s!==n)return;let a;try{a=JSON.parse(i.toString("utf8"))}catch{return}return Q(a)?{bodySize:o,meta:a}:void 0},U=async t=>{let e;try{e=await D(t,"r")}catch(r){if(v(r))return;throw r}try{const r=await tt(e);if(r===void 0){await e.close();return}return{...r,handle:e}}catch(r){throw await e.close(),r}},b=new FinalizationRegistry(t=>{t.close().catch(()=>{})}),B=async t=>{const e=await U(t);if(e!==void 0)return await e.handle.close(),{bodySize:e.bodySize,meta:e.meta}},A=(t,e)=>{const{sha256Hex:r}=e,n=Buffer.from(r,"hex");return{checksums:{sha256:M(n)},customMetadata:e.customMetadata,etag:r,httpEtag:`"${r}"`,httpMetadata:e.httpMetadata,key:t,sha256:r,sha256Base64:n.toString("base64"),size:e.size,uploaded:new Date(e.uploaded)}},E=(t,e,r)=>t===void 0||!Number.isFinite(t)?e:Math.min(Math.max(0,Math.floor(t)),r),et=(t,e)=>{if(e===void 0)return{end:t,start:0};if("suffix"in e)return{end:t,start:t-E(e.suffix,t,t)};const r=E(e.offset,0,t);return{end:e.length===void 0||!Number.isFinite(e.length)?t:Math.min(r+E(e.length,0,t),t),start:r}},rt=(t,e,r,n)=>{const o=t.createReadStream({autoClose:!1,end:r-1,start:e})[Symbol.asyncIterator]();return new ReadableStream({cancel:async()=>{await o.return?.(),await n()},pull:async i=>{let s;try{s=await o.next()}catch(a){throw await n(),a}s.done===!0?(i.close(),await n()):i.enqueue(s.value)}})},nt=async(t,e,r)=>{const n=r-e;if(n<=0)return Buffer.alloc(0);const o=Buffer.alloc(n),{bytesRead:i}=await t.read(o,0,n,e);return o.subarray(0,i)},V=async function*(t){if(t!==null){if(typeof t=="string"){yield new TextEncoder().encode(t);return}if(t instanceof ArrayBuffer){yield new Uint8Array(t);return}if(ArrayBuffer.isView(t)){yield new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return}if(t instanceof Blob){yield*V(t.stream());return}if(t instanceof ReadableStream){for await(const e of t)if(e instanceof Uint8Array)yield e;else if(e instanceof ArrayBuffer)yield new Uint8Array(e);else throw new f("VALIDATION_ERROR","@lunora/platform-node: R2 put stream must yield byte chunks");return}throw new f("VALIDATION_ERROR","@lunora/platform-node: unsupported R2 put body")}},ot=async t=>{const e=[],r=async(n,o)=>{let i;try{i=await H(n,{withFileTypes:!0})}catch(a){if(v(a))return;throw a}const s=[];for(const a of i){if(a.name===I)continue;const l=o===""?a.name:`${o}/${a.name}`;if(a.isDirectory())s.push(r(w(n,a.name),l));else{const d=Y(l);d!==void 0&&e.push(d)}}await Promise.all(s)};return await r(t,""),e.sort((n,o)=>n<o?-1:+(n>o)),e},at=(t,e)=>{let r=0,n=t.length;for(;r<n;){const o=r+Math.floor((n-r)/2),i=t[o];i===void 0||i<=e?r=o+1:n=o}return r},O=async t=>{try{await k(t)}catch(e){if(!v(e))throw e}},ut=t=>{const{directory:e}=t;return{delete:async r=>{g(r),await O(w(e,y(r)))},get:async(r,n)=>{g(r);const o=await U(w(e,y(r)));if(o===void 0)return null;const{bodySize:i,handle:s,meta:a}=o,{end:l,start:d}=et(i,n?.range),c={};let h=!1;const p=async()=>{h||(h=!0,b.unregister(c),await s.close())},u=()=>{if(h)throw new f("BAD_REQUEST",`@lunora/platform-node: R2 object "${r}" body has already been consumed`)},R=async()=>{u();try{return await nt(s,d,l)}finally{await p()}},T={...A(r,a),arrayBuffer:async()=>M(await R()),get body(){return u(),l<=d?(p().catch(()=>{}),new Blob([]).stream()):rt(s,d,l,p)},text:async()=>(await R()).toString("utf8")};return b.register(T,{close:p},c),T},head:async r=>{g(r);const n=await B(w(e,y(r)));return n===void 0?null:A(r,n.meta)},list:async(r={})=>{const n=Math.max(1,E(r.limit,1e3,1e3));if(r.prefix?.includes("\0"))throw new f("VALIDATION_ERROR","@lunora/platform-node: R2 list prefix contains a NUL byte");const o=await ot(e),i=r.prefix??"",s=o.filter(u=>u.startsWith(i)&&(r.delimiter===void 0||!u.slice(i.length).includes(r.delimiter)));let a=r.cursor;r.startAfter!==void 0&&(a===void 0||r.startAfter>a)&&(a=r.startAfter);const l=a===void 0?0:at(s,a),d=s.slice(l,l+n),c=l+n<s.length,p=(await Promise.all(d.map(async u=>{const R=await B(w(e,y(u)));return R===void 0?void 0:A(u,R.meta)}))).filter(u=>u!==void 0);return{cursor:c?d.at(-1):void 0,objects:p,truncated:c}},put:async(r,n,o)=>{g(r);const i=w(e,y(r)),s=w(e,I,$());try{await N(_(i),{recursive:!0})}catch(c){throw L(r,c)}await N(_(s),{recursive:!0});const a=P("sha256");let l=0,d;try{const c=await D(s,"w");try{for await(const h of V(n))a.update(h),l+=h.byteLength,await c.write(h);d={customMetadata:o?.customMetadata,httpMetadata:o?.httpMetadata,sha256Hex:a.digest("hex"),size:l,uploaded:new Date().toISOString()},await c.write(q(d))}finally{await c.close()}}catch(c){throw await O(s),c}try{await F(s,i)}catch(c){throw await O(s),L(r,c)}return A(r,d)}}};export{ut as createNodeR2Bucket};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import{randomUUID as k}from"node:crypto";import{deserialize as y,serialize as L}from"node:v8";import{CronExpressionParser as D}from"cron-parser";const T={backoffMultiplier:2,initialDelayMs:1e3,maxAttempts:5,maxDelayMs:6e4},x=2147483647,F=(s,i)=>{const f=i?.initialDelayMs??T.initialDelayMs,m=i?.backoffMultiplier??T.backoffMultiplier,u=i?.maxDelayMs??T.maxDelayMs;return Math.min(u,f*m**Math.max(0,s-1))},w=(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 f=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 = ?"),u=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 = ?"),N=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,g=e=>e===null?void 0:y(e);let o=!1;const l=new Map,p=new Map,A=e=>({attempts:e.attempts,functionPath:e.function_path,id:e.id,scheduledFor:e.scheduled_for}),d=e=>{const t=l.get(e);t!==void 0&&(clearTimeout(t),l.delete(e))},E=(e,t)=>{d(e);const r=Math.max(0,t-Date.now());if(r>x){const a=setTimeout(()=>{E(e,t)},x);l.set(e,a);return}const n=setTimeout(()=>{I(e).catch(()=>{})},r);l.set(e,n)},I=async e=>{if(l.delete(e),!c())return;const t=m.get(e);if(t?.state!=="pending")return;const r=t.attempts+1,n=g(t.retry);try{await i.onDispatch?.(t.function_path,y(t.args),{attempts:r,id:e}),c()&&R.run(e)}catch{if(o||!c())return;const a=n?.maxAttempts??T.maxAttempts;if(r>=a){N.run(r,e);return}const h=Date.now()+F(r,n);O.run(r,h,e),E(e,h)}},_=e=>{const t=p.get(e.key);t!==void 0&&clearTimeout(t);let r;try{r=D.parse(e.expression).next().getTime()}catch{return}const n=setTimeout(()=>{p.delete(e.key),(async()=>{try{await i.onDispatch?.(e.function_path,y(e.args),{attempts:0,id:e.key})}catch{}!o&&c()&&_(e)})().catch(()=>{})},Math.max(0,r-Date.now()));p.set(e.key,n)},j={cancel:async e=>o?!1:(d(e),M.run(e).changes>0),cron:async(e,t,r)=>{if(o)throw new Error("platform closed: cannot register a cron");D.parse(e);const n={args:L(r??{}),expression:e,function_path:t,key:`${e} ${t}`};S.run(n.key,n.expression,n.function_path,n.args),_(n)},deadLetter:{list:async()=>o||!c()?[]:u.all("dead").map(e=>A(e)),requeue:async e=>{if(o||!c())return!1;const t=Date.now();return U.run(t,e).changes===0?!1:(E(e,t),!0)}},list:async()=>o||!c()?[]:u.all("pending").map(e=>A(e)),schedule:async(e,t,r)=>{if(o)throw new Error("platform closed: cannot schedule a job");const n=`node-job-${k()}`;let a;return r?.at===void 0?a=Date.now()+(r?.delayMs??0):a=typeof r.at=="number"?r.at:r.at.getTime(),f.run(n,e,L(t),a,r?.retry===void 0?null:L(r.retry)),E(n,a),{id:n,scheduledFor:a}}};for(const e of u.all("pending"))E(e.id,e.scheduled_for);for(const e of b.all())_(e);return{dispose:()=>{for(const e of l.values())clearTimeout(e);for(const e of p.values())clearTimeout(e);l.clear(),p.clear(),o=!0},scheduler:j,simulateDeadLetter:async e=>{const t=m.get(e);if(t?.state!=="pending")return!1;d(e);const r=g(t.retry);return N.run((r?.maxAttempts??T.maxAttempts)+1,e),!0}}};export{w as createNodeSchedulerHost};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{mkdirSync as S,readdirSync as l}from"node:fs";import{join as u}from"node:path";import{createNodeShardKvStore as p}from"./createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeShardHost as y}from"./createNodeShardHost-CNoG3Fzy.mjs";import{createNodeSocketHost as v}from"./createNodeSocketHost-DK3X5PlQ.mjs";const n=".sqlite3",g=r=>r.replaceAll(/[^\w.-]/gu,t=>[...Buffer.from(t,"utf8")].map(o=>`%${o.toString(16).padStart(2,"0").toUpperCase()}`).join("")),F=r=>decodeURIComponent(r),R=(r={})=>{const t=new Map,o=new Set;if(r.directory!==void 0){S(r.directory,{recursive:!0});for(const e of l(r.directory))e.endsWith(n)&&o.add(F(e.slice(0,-n.length)))}const a=e=>{const d=t.get(e);if(d!==void 0)return d.shard;const c=r.directory===void 0?void 0:u(r.directory,`${g(e)}${n}`);let s;const{database:h,dispose:f,host:m}=y({onAlarm:()=>r.onAlarm?.(s),path:c,shardKey:e});return s={kv:p(h),shard:m,shardKey:e,sockets:v(h).socket},t.set(e,{dispose:f,shard:s}),o.add(e),s},i=e=>({fetch:async d=>{const c=a(e);return r.onFetch===void 0?new Response(e):r.onFetch(d,c)}});return{close:()=>{for(const e of t.values())e.dispose();t.clear()},directory:{get:e=>i(String(e)),getByName:e=>i(e),idForName:e=>e},listShardKeys:()=>[...o],shardFor:a}};export{R as createNodeShardRegistry};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{deserialize as w,serialize as l}from"node:v8";import{t as S}from"./to-array-buffer-DVyXU4Ep.mjs";const p=n=>typeof n=="object"&&n!==null||typeof n=="function",_=n=>{n.exec("CREATE TABLE IF NOT EXISTS _lunora_sockets (id TEXT PRIMARY KEY, attachment BLOB, tags TEXT NOT NULL DEFAULT '[]')");const f=n.prepare(`INSERT INTO _lunora_sockets (id, attachment, tags) VALUES (?, ?, ?)
|
|
2
|
-
ON CONFLICT (id) DO UPDATE SET attachment = excluded.attachment, tags = excluded.tags`),m=n.prepare("UPDATE _lunora_sockets SET attachment = ? WHERE id = ?"),h=n.prepare("UPDATE _lunora_sockets SET tags = ? WHERE id = ?"),E=n.prepare("SELECT attachment, tags FROM _lunora_sockets WHERE id = ?"),T=n.prepare("DELETE FROM _lunora_sockets WHERE id = ?"),c=new Map,i=new WeakMap;let a=new WeakMap;const v=(t,e)=>{n.open&&m.run(e===void 0?null:l(e),t)},u=(t,e)=>{n.open&&h.run(JSON.stringify([...e]),t)},g=t=>{const e=t,o={bufferedAmount:e.bufferedAmount,close:(r,s)=>{e.closed=!0,c.delete(e.id),p(e.raw)&&a.delete(e.raw),n.open&&T.run(e.id)},deserializeAttachment:()=>e.attachment,send:r=>{e.received.push(typeof r=="string"?r:S(r))},serializeAttachment:r=>{e.attachment=r,v(e.id,r)}};return e.handle=o,i.set(o,e.id),o};return{readFrames:t=>(c.get(i.get(t)??"")?.received??[]).filter(e=>typeof e=="string"),restoreSocket:(t,e)=>{const o=n.open?E.get(t):void 0,r=o?.attachment===null?void 0:o?.attachment,s={attachment:r===void 0?e:w(r),bufferedAmount:0,closed:!1,handle:void 0,id:t,raw:void 0,received:[],tags:new Set(o===void 0?[]:JSON.parse(o.tags))};return c.set(t,s),o===void 0&&n.open&&f.run(t,s.attachment===void 0?null:l(s.attachment),JSON.stringify([...s.tags])),g(s)},simulateRecycle:()=>{c.clear(),a=new WeakMap},socket:{accept:(t,e,o)=>{if(!n.open)throw new Error("platform closed: cannot accept a socket");const r=crypto.randomUUID(),s=new Set(o);f.run(r,e===void 0?null:l(e),JSON.stringify([...s]));const d={attachment:e,bufferedAmount:0,closed:!1,handle:void 0,id:r,raw:t,received:[],tags:s};return c.set(r,d),p(t)&&a.set(t,d),g(d)},getSockets:t=>{const e=[...c.values()];return(t===void 0?e:e.filter(r=>r.tags.has(t))).map(r=>r.handle)},handleFor:t=>p(t)?a.get(t)?.handle:void 0,idFor:t=>{const e=i.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(!n.open)throw new Error("platform closed: cannot remove a tag");const o=c.get(i.get(t)??"");o!==void 0&&(e===void 0?o.tags.clear():o.tags.delete(e),u(o.id,o.tags))},setTag:(t,e)=>{if(!n.open)throw new Error("platform closed: cannot set a tag");const o=c.get(i.get(t)??"");o!==void 0&&(o.tags.add(e),u(o.id,o.tags))}}}};export{_ as createNodeSocketHost};
|