@lunora/platform-node 1.0.0-alpha.6 → 1.0.0-alpha.60
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 +236 -50
- package/dist/index.d.ts +236 -50
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/createNodeGlobalStore-nAJ4WO7w.mjs +1 -0
- package/dist/packem_shared/createNodePlatform-Inw4xniW.mjs +1 -0
- package/dist/packem_shared/createNodeQueueHost--mBLGSxF.mjs +17 -0
- package/dist/packem_shared/createNodeR2Bucket-BzNH1lBp.mjs +1 -0
- package/dist/packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs +15 -0
- package/dist/packem_shared/createNodeShardHost-B76kaXz-.mjs +1 -0
- package/dist/packem_shared/createNodeShardKvStore-DVi1OsvH.mjs +1 -0
- package/dist/packem_shared/createNodeShardRegistry-Dj1mXStS.mjs +1 -0
- package/dist/packem_shared/createNodeShardState-D6XMWbFs.mjs +1 -0
- package/dist/packem_shared/createNodeSocketHost-DQs85G7W.mjs +2 -0
- package/dist/packem_shared/createNodeWorkflowHost-CTVBYiuj.mjs +1 -0
- package/dist/packem_shared/{createNodeWorkflowStore-jlNR6Y3h.mjs → createNodeWorkflowStore-CL7RYP6H.mjs} +5 -5
- package/dist/packem_shared/{to-array-buffer-CP4NlF-y.mjs → to-array-buffer-DVyXU4Ep.mjs} +1 -1
- package/package.json +13 -10
- package/dist/packem_shared/createNodeGlobalStore-BvteXWRK.mjs +0 -1
- package/dist/packem_shared/createNodePlatform-BQimmgT9.mjs +0 -1
- package/dist/packem_shared/createNodeQueueHost-CLQ5hOBf.mjs +0 -17
- package/dist/packem_shared/createNodeR2Bucket-Wb8pyi2X.mjs +0 -1
- package/dist/packem_shared/createNodeSchedulerHost-DRjZhuxe.mjs +0 -15
- package/dist/packem_shared/createNodeShardHost-DEHe5Vcm.mjs +0 -1
- package/dist/packem_shared/createNodeShardKvStore-BfKCQTjE.mjs +0 -1
- package/dist/packem_shared/createNodeShardRegistry-0iSMdi9-.mjs +0 -1
- package/dist/packem_shared/createNodeShardState-DdOj9FzJ.mjs +0 -1
- package/dist/packem_shared/createNodeSocketHost-CytowrYN.mjs +0 -2
- package/dist/packem_shared/createNodeWorkflowHost-Dht_jgSv.mjs +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createNodeShardKvStore 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-B76kaXz-.mjs";import{createNodeShardRegistry as H}from"../packem_shared/createNodeShardRegistry-Dj1mXStS.mjs";import{createNodeSocketHost as T}from"../packem_shared/createNodeSocketHost-DQs85G7W.mjs";const P=()=>{const{database:o,dispose:m,host:p}=D(),h=N(o),c=H(),{directory:l}=c,{readFrames:w,restoreSocket:u,simulateRecycle:S,socket:y}=T(o),d=new Set,s=new Map,{dispose:f,scheduler:i,simulateDeadLetter:g}=k(o,{onDispatch:(e,a,t)=>{d.add(t.id),s.set(e,(s.get(e)??0)+1)}}),n=()=>{f(),m(),c.close()};return{awaitAlarmFired:async e=>{await new Promise(a=>{setTimeout(a,Math.max(0,e-Date.now())+30)})},awaitJobDispatched:async e=>{const t=(await i.list?.())?.find(r=>r.id===e);return t!==void 0&&await new Promise(r=>{setTimeout(r,Math.max(0,t.scheduledFor-Date.now())+30)}),d.has(e)},cleanup:n,cronTicks:e=>s.get(e)??0,directory:l,disposeTerminally:n,kv:h,readFrames:w,restoreSocket:u,scheduler:i,shard:p,simulateDeadLetter:g,simulateRecycle:S,socket:y}};export{P as createNodeConformanceHost};
|
package/dist/index.d.mts
CHANGED
|
@@ -197,9 +197,12 @@ interface NodeShardHostOptions {
|
|
|
197
197
|
* `_lunora_alarm` when the host is constructed over an existing database,
|
|
198
198
|
* including an alarm whose time elapsed while nothing was running.
|
|
199
199
|
*
|
|
200
|
-
* A handler that throws is isolated to its own delivery
|
|
200
|
+
* A handler that throws is isolated to its own delivery — it never reaches
|
|
201
201
|
* the caller that set the alarm, because by then that call has long
|
|
202
|
-
* returned
|
|
202
|
+
* returned — and the wakeup is then re-delivered with backoff up to
|
|
203
|
+
* {@link ALARM_RETRY_LIMIT} times, the way workerd retries a throwing
|
|
204
|
+
* `alarm()`. Delivery is at-least-once, so the handler must tolerate
|
|
205
|
+
* running twice for one scheduled timestamp.
|
|
203
206
|
*/
|
|
204
207
|
onAlarm?: () => Promise<void> | void;
|
|
205
208
|
/**
|
|
@@ -310,22 +313,64 @@ interface NodeShardRegistry {
|
|
|
310
313
|
}
|
|
311
314
|
/** Build the in-process shard registry. */
|
|
312
315
|
declare const createNodeShardRegistry: (options?: NodeShardRegistryOptions) => NodeShardRegistry;
|
|
316
|
+
/** Options for {@link createNodeWorkflowHost}. */
|
|
317
|
+
interface NodeWorkflowHostOptions<Workflows extends Record<string, {
|
|
318
|
+
isLunoraWorkflow: true;
|
|
319
|
+
}>> {
|
|
320
|
+
/** Base env merged under the derived `WORKFLOW_*` bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
|
|
321
|
+
env?: Record<string, unknown>;
|
|
322
|
+
/** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
|
|
323
|
+
leaseTtlMs?: number;
|
|
324
|
+
/** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
|
|
325
|
+
store: WorkflowStore;
|
|
326
|
+
/** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
|
|
327
|
+
workflows: Workflows;
|
|
328
|
+
}
|
|
329
|
+
/** A fully-wired Node workflow host. */
|
|
330
|
+
interface NodeWorkflowHost<Workflows extends Record<string, {
|
|
331
|
+
isLunoraWorkflow: true;
|
|
332
|
+
}>> {
|
|
333
|
+
/** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
|
|
334
|
+
readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
|
|
335
|
+
/**
|
|
336
|
+
* The caller's `env` plus one `WORKFLOW_<EXPORT>` binding per workflow —
|
|
337
|
+
* merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
|
|
338
|
+
* children through the same runtime.
|
|
339
|
+
*/
|
|
340
|
+
readonly env: Record<string, unknown>;
|
|
341
|
+
/** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
|
|
342
|
+
readonly runtime: WorkflowRuntime;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Create a Node workflow host: compile every declared Lunora workflow onto the
|
|
346
|
+
* visulima engine, derive the `WORKFLOW_*` env, and expose the per-workflow
|
|
347
|
+
* `WorkflowBindingLike` handles.
|
|
348
|
+
*/
|
|
349
|
+
declare const createNodeWorkflowHost: <Workflows extends Record<string, {
|
|
350
|
+
isLunoraWorkflow: true;
|
|
351
|
+
}>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
|
|
313
352
|
/** Every contract this package provides, composed for one Node process. */
|
|
314
353
|
interface NodePlatform<Queues extends Record<string, {
|
|
315
354
|
isLunoraQueue: true;
|
|
355
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
356
|
+
isLunoraWorkflow: true;
|
|
316
357
|
}> = Record<string, never>> {
|
|
317
358
|
/** `using platform = createNodePlatform(...)` support — delegates to `close()`. */
|
|
318
359
|
[Symbol.dispose]: () => void;
|
|
319
360
|
/** What this target supports — see `NODE_CAPABILITIES` in `@lunora/platform`. */
|
|
320
361
|
capabilities: PlatformCapabilities;
|
|
321
362
|
/**
|
|
322
|
-
* Tear this platform instance down:
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
363
|
+
* Tear this platform instance down: every resource this root built, in
|
|
364
|
+
* reverse construction order — the global store's own connection, every
|
|
365
|
+
* armed scheduler job timer, the registry's live shards, and last the shard
|
|
366
|
+
* host's pending alarm timer and its `better-sqlite3` database (and, with
|
|
367
|
+
* it, `kv`'s table — both live on the same connection). Nothing in this
|
|
368
|
+
* package closes these resources on its own — a `NodePlatform` a caller
|
|
369
|
+
* stops using without calling `close()` leaks the open file handles (plus
|
|
370
|
+
* their WAL/SHM sidecar files) and keeps the process alive on outstanding
|
|
371
|
+
* timers. Safe to call more than once. A `createNodePlatform` that throws
|
|
372
|
+
* unwinds the same list itself, so a failed construction leaks nothing and
|
|
373
|
+
* leaves nothing to call this on.
|
|
329
374
|
*
|
|
330
375
|
* **`close()` is a terminal state, not merely a cleanup step.** After it
|
|
331
376
|
* runs: `scheduler.schedule()` throws instead of arming a fresh timer
|
|
@@ -335,8 +380,10 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
335
380
|
* benign); `shard.alarms.set()`/`delete()` throw before mutating any
|
|
336
381
|
* in-memory state (checked against the connection's own open/closed state,
|
|
337
382
|
* the single source of truth); `shard.alarms.get()` keeps answering
|
|
338
|
-
* whatever it last held.
|
|
339
|
-
*
|
|
383
|
+
* whatever it last held; `sockets.accept()`/`setTag()`/`removeTag()` throw
|
|
384
|
+
* the same way, checked against the same connection state, before touching
|
|
385
|
+
* any runtime socket map or durable row. A no-op instead of a throw would
|
|
386
|
+
* be indistinguishable from a working call — exactly the silent-vanishing
|
|
340
387
|
* this lifecycle exists to end.
|
|
341
388
|
*/
|
|
342
389
|
close: () => void;
|
|
@@ -351,8 +398,28 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
351
398
|
* shutdown is `await platform.drain()` then `platform.close()`.
|
|
352
399
|
*/
|
|
353
400
|
drain: () => Promise<void>;
|
|
401
|
+
/**
|
|
402
|
+
* The `.global()` backend, or `undefined` when the caller named no database
|
|
403
|
+
* file for it. There is no default path, because a global store silently
|
|
404
|
+
* rooted at `:memory:` loses every row when the process exits.
|
|
405
|
+
*
|
|
406
|
+
* **A building block, not a wiring.** Unlike its three siblings, nothing
|
|
407
|
+
* downstream of this composition root reads it: a `.global()` read or write
|
|
408
|
+
* reaches its backend through exactly one seam, `createShardCtxDb({ globalDb
|
|
409
|
+
* })`, and the only thing that passes `globalDb` is the generated `shard.ts`,
|
|
410
|
+
* from its `d1` / `hyperdriveGlobal` config thunks. `createNodePlatform`
|
|
411
|
+
* constructs no shard DO, so it cannot make that hop itself. A caller that
|
|
412
|
+
* wants `.global()` on this host makes it: after `migrate(schema)` has
|
|
413
|
+
* provisioned the tables, give the generated `createShardDO` a `d1` thunk
|
|
414
|
+
* returning `platform.globalTables.writer({ schema, … })` — `writer` builds
|
|
415
|
+
* the `createSqlCtxDb` facade that seam expects, and `node-platform.test.ts`
|
|
416
|
+
* round-trips a row through exactly that object.
|
|
417
|
+
*/
|
|
418
|
+
globalTables?: NodeGlobalStore;
|
|
354
419
|
/** Durable key-value storage backed by the same `better-sqlite3` database as `shard`. */
|
|
355
420
|
kv: ShardKvStore;
|
|
421
|
+
/** The local-filesystem bucket, or `undefined` when the caller named no bucket directory. */
|
|
422
|
+
objectStorage?: R2BucketLike;
|
|
356
423
|
/**
|
|
357
424
|
* The declared queues, or `undefined` when the caller declared none.
|
|
358
425
|
*
|
|
@@ -367,6 +434,12 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
367
434
|
shard: ShardHost;
|
|
368
435
|
/** Socket registry with mutable tags and SQLite-persisted attachments. */
|
|
369
436
|
sockets: SocketHost;
|
|
437
|
+
/**
|
|
438
|
+
* The declared workflows, or `undefined` when the caller declared none.
|
|
439
|
+
* Runs are persisted to the same `better-sqlite3` database as the shard, so
|
|
440
|
+
* they survive a restart without a second store to configure.
|
|
441
|
+
*/
|
|
442
|
+
workflows?: NodeWorkflowHost<Workflows>;
|
|
370
443
|
}
|
|
371
444
|
/**
|
|
372
445
|
* Options for {@link createNodePlatform} — the shard host's (`path`,
|
|
@@ -376,10 +449,29 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
376
449
|
* or job with nowhere to land is bookkeeping. `directory` makes the shards the
|
|
377
450
|
* directory resolves for fan-out file-backed too, and `onAlarm` gives their
|
|
378
451
|
* durable alarms somewhere to land.
|
|
452
|
+
*
|
|
453
|
+
* `queues`, `workflows`, `objectStorageDirectory` and `globalTablesPath` are the
|
|
454
|
+
* four declarations — each is absent from the returned platform when omitted.
|
|
379
455
|
*/
|
|
380
456
|
type NodePlatformOptions<Queues extends Record<string, {
|
|
381
457
|
isLunoraQueue: true;
|
|
458
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
459
|
+
isLunoraWorkflow: true;
|
|
382
460
|
}> = Record<string, never>> = {
|
|
461
|
+
/**
|
|
462
|
+
* Database file the `.global()` tables live in — its own file, never a
|
|
463
|
+
* shard's, because a table every shard reads must not be inside any one of
|
|
464
|
+
* them. Omit when the app declares no `.global()` table; there is no
|
|
465
|
+
* default, for the same reason the bucket has none.
|
|
466
|
+
*/
|
|
467
|
+
globalTablesPath?: string;
|
|
468
|
+
/**
|
|
469
|
+
* Directory the object-storage bucket keeps its objects in, one file per
|
|
470
|
+
* key. Omit when the app declares no buckets — there is no default,
|
|
471
|
+
* because a bucket silently rooted at the process's working directory is
|
|
472
|
+
* worse than an absent one.
|
|
473
|
+
*/
|
|
474
|
+
objectStorageDirectory?: string;
|
|
383
475
|
/**
|
|
384
476
|
* Deliver one assembled queue batch — wire this to `dispatchQueueBatch`.
|
|
385
477
|
* Required alongside `queues`; without it the messages would be stored
|
|
@@ -388,11 +480,15 @@ type NodePlatformOptions<Queues extends Record<string, {
|
|
|
388
480
|
onQueueBatch?: NodeQueueHostOptions<Queues>["onBatch"];
|
|
389
481
|
/** The app's `defineQueue` results, keyed by export name. Omit when the app declares no queues. */
|
|
390
482
|
queues?: Queues;
|
|
483
|
+
/** The app's `defineWorkflow` results, keyed by export name. Omit when the app declares no workflows. */
|
|
484
|
+
workflows?: Workflows;
|
|
391
485
|
} & NodeSchedulerHostOptions & NodeShardHostOptions & NodeShardRegistryOptions;
|
|
392
486
|
/** Compose every contract this package provides over one `better-sqlite3` database. */
|
|
393
487
|
declare const createNodePlatform: <Queues extends Record<string, {
|
|
394
488
|
isLunoraQueue: true;
|
|
395
|
-
}> = Record<string, never
|
|
489
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
490
|
+
isLunoraWorkflow: true;
|
|
491
|
+
}> = Record<string, never>>(options?: NodePlatformOptions<Queues, Workflows>) => NodePlatform<Queues, Workflows>;
|
|
396
492
|
/** Options for {@link createNodeR2Bucket}. */
|
|
397
493
|
interface NodeR2BucketOptions {
|
|
398
494
|
/** The bucket directory — created on first write. Objects live here, one file per key. */
|
|
@@ -400,7 +496,7 @@ interface NodeR2BucketOptions {
|
|
|
400
496
|
}
|
|
401
497
|
/**
|
|
402
498
|
* Create an `R2BucketLike` over the local filesystem. Any object shape
|
|
403
|
-
* `createStorage({ bucket })` accepts — `put`/`get`/`head`/`delete`/`list` —
|
|
499
|
+
* `createStorage({ bucket, bucketName })` accepts — `put`/`get`/`head`/`delete`/`list` —
|
|
404
500
|
* maps directly onto a file operation.
|
|
405
501
|
*/
|
|
406
502
|
declare const createNodeR2Bucket: (options: NodeR2BucketOptions) => R2BucketLike;
|
|
@@ -473,45 +569,135 @@ interface NodeSocketHost {
|
|
|
473
569
|
}
|
|
474
570
|
/** Build the socket registry, persisting attachments and tags to `database`. */
|
|
475
571
|
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_<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
572
|
/**
|
|
513
573
|
* Build a durable {@link WorkflowStore} over a `better-sqlite3` connection.
|
|
514
574
|
* Pass the result as `createNodeWorkflowHost({ store })`.
|
|
515
575
|
*/
|
|
516
576
|
declare const createNodeWorkflowStore: (database: Database.Database) => WorkflowStore;
|
|
517
|
-
export {
|
|
577
|
+
export {
|
|
578
|
+
/**
|
|
579
|
+
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
580
|
+
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
581
|
+
* `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
|
|
582
|
+
* scheduler registry.
|
|
583
|
+
*
|
|
584
|
+
* Promoted from `@lunora/platform`'s `node:sqlite` reference host
|
|
585
|
+
* (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
|
|
586
|
+
* durability half of each contract actually holds: alarms and scheduler jobs
|
|
587
|
+
* are persisted **and re-armed on construction**, socket attachments and tags
|
|
588
|
+
* live in SQLite rather than a `Map`, and `.global()` tables run the real
|
|
589
|
+
* `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
|
|
590
|
+
* a restart test — a second host over the same database file — not only by the
|
|
591
|
+
* TCK's simulated recycle.
|
|
592
|
+
*
|
|
593
|
+
* Three suites run against this package: `@lunora/platform/conformance`'s host
|
|
594
|
+
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
595
|
+
* lifecycle/global-store tests.
|
|
596
|
+
*
|
|
597
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
598
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
599
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
600
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
601
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
602
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
603
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
604
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
605
|
+
*
|
|
606
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
607
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
608
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
609
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
610
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
611
|
+
*
|
|
612
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
613
|
+
* for `provision` — which reports, once, which declared features this target
|
|
614
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
615
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
616
|
+
* server, which is why both statements above hold at once. See
|
|
617
|
+
* `plans/234-node-host-findings.md`.
|
|
618
|
+
*/
|
|
619
|
+
type NodeGlobalContextDatabaseOptions,
|
|
620
|
+
/**
|
|
621
|
+
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
622
|
+
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
623
|
+
* `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
|
|
624
|
+
* scheduler registry.
|
|
625
|
+
*
|
|
626
|
+
* Promoted from `@lunora/platform`'s `node:sqlite` reference host
|
|
627
|
+
* (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
|
|
628
|
+
* durability half of each contract actually holds: alarms and scheduler jobs
|
|
629
|
+
* are persisted **and re-armed on construction**, socket attachments and tags
|
|
630
|
+
* live in SQLite rather than a `Map`, and `.global()` tables run the real
|
|
631
|
+
* `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
|
|
632
|
+
* a restart test — a second host over the same database file — not only by the
|
|
633
|
+
* TCK's simulated recycle.
|
|
634
|
+
*
|
|
635
|
+
* Three suites run against this package: `@lunora/platform/conformance`'s host
|
|
636
|
+
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
637
|
+
* lifecycle/global-store tests.
|
|
638
|
+
*
|
|
639
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
640
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
641
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
642
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
643
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
644
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
645
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
646
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
647
|
+
*
|
|
648
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
649
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
650
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
651
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
652
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
653
|
+
*
|
|
654
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
655
|
+
* for `provision` — which reports, once, which declared features this target
|
|
656
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
657
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
658
|
+
* server, which is why both statements above hold at once. See
|
|
659
|
+
* `plans/234-node-host-findings.md`.
|
|
660
|
+
*/
|
|
661
|
+
type NodeGlobalStore,
|
|
662
|
+
/**
|
|
663
|
+
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
664
|
+
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
665
|
+
* `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
|
|
666
|
+
* scheduler registry.
|
|
667
|
+
*
|
|
668
|
+
* Promoted from `@lunora/platform`'s `node:sqlite` reference host
|
|
669
|
+
* (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
|
|
670
|
+
* durability half of each contract actually holds: alarms and scheduler jobs
|
|
671
|
+
* are persisted **and re-armed on construction**, socket attachments and tags
|
|
672
|
+
* live in SQLite rather than a `Map`, and `.global()` tables run the real
|
|
673
|
+
* `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
|
|
674
|
+
* a restart test — a second host over the same database file — not only by the
|
|
675
|
+
* TCK's simulated recycle.
|
|
676
|
+
*
|
|
677
|
+
* Three suites run against this package: `@lunora/platform/conformance`'s host
|
|
678
|
+
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
679
|
+
* lifecycle/global-store tests.
|
|
680
|
+
*
|
|
681
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
682
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
683
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
684
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
685
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
686
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
687
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
688
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
689
|
+
*
|
|
690
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
691
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
692
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
693
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
694
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
695
|
+
*
|
|
696
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
697
|
+
* for `provision` — which reports, once, which declared features this target
|
|
698
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
699
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
700
|
+
* server, which is why both statements above hold at once. See
|
|
701
|
+
* `plans/234-node-host-findings.md`.
|
|
702
|
+
*/
|
|
703
|
+
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
|
@@ -197,9 +197,12 @@ interface NodeShardHostOptions {
|
|
|
197
197
|
* `_lunora_alarm` when the host is constructed over an existing database,
|
|
198
198
|
* including an alarm whose time elapsed while nothing was running.
|
|
199
199
|
*
|
|
200
|
-
* A handler that throws is isolated to its own delivery
|
|
200
|
+
* A handler that throws is isolated to its own delivery — it never reaches
|
|
201
201
|
* the caller that set the alarm, because by then that call has long
|
|
202
|
-
* returned
|
|
202
|
+
* returned — and the wakeup is then re-delivered with backoff up to
|
|
203
|
+
* {@link ALARM_RETRY_LIMIT} times, the way workerd retries a throwing
|
|
204
|
+
* `alarm()`. Delivery is at-least-once, so the handler must tolerate
|
|
205
|
+
* running twice for one scheduled timestamp.
|
|
203
206
|
*/
|
|
204
207
|
onAlarm?: () => Promise<void> | void;
|
|
205
208
|
/**
|
|
@@ -310,22 +313,64 @@ interface NodeShardRegistry {
|
|
|
310
313
|
}
|
|
311
314
|
/** Build the in-process shard registry. */
|
|
312
315
|
declare const createNodeShardRegistry: (options?: NodeShardRegistryOptions) => NodeShardRegistry;
|
|
316
|
+
/** Options for {@link createNodeWorkflowHost}. */
|
|
317
|
+
interface NodeWorkflowHostOptions<Workflows extends Record<string, {
|
|
318
|
+
isLunoraWorkflow: true;
|
|
319
|
+
}>> {
|
|
320
|
+
/** Base env merged under the derived `WORKFLOW_*` bindings — surfaced to workflow bodies as `ctx.env` and used to resolve spawned children. */
|
|
321
|
+
env?: Record<string, unknown>;
|
|
322
|
+
/** How long (ms) the engine holds a cross-process lease while an activation runs, for stores that implement `acquire`. Defaults to 30000. */
|
|
323
|
+
leaseTtlMs?: number;
|
|
324
|
+
/** Where runs are persisted. Required — see the header for why there is no default. `createNodeWorkflowStore(database)` is the durable one. */
|
|
325
|
+
store: WorkflowStore;
|
|
326
|
+
/** The declared workflows keyed by their `lunora/workflows.ts` export name (e.g. `{ orderPipeline: orderPipeline }`). Values must be `defineWorkflow` results. */
|
|
327
|
+
workflows: Workflows;
|
|
328
|
+
}
|
|
329
|
+
/** A fully-wired Node workflow host. */
|
|
330
|
+
interface NodeWorkflowHost<Workflows extends Record<string, {
|
|
331
|
+
isLunoraWorkflow: true;
|
|
332
|
+
}>> {
|
|
333
|
+
/** Per-export-name `WorkflowBindingLike` — the map `ctx.workflows` consumes. */
|
|
334
|
+
readonly bindings: { [K in keyof Workflows]: WorkflowBindingLike; };
|
|
335
|
+
/**
|
|
336
|
+
* The caller's `env` plus one `WORKFLOW_<EXPORT>` binding per workflow —
|
|
337
|
+
* merge this into a worker env so `ctx.spawn`/`ctx.parallel` resolve
|
|
338
|
+
* children through the same runtime.
|
|
339
|
+
*/
|
|
340
|
+
readonly env: Record<string, unknown>;
|
|
341
|
+
/** The underlying visulima runtime — `sweep`/`signal` for a dev loop or tests. */
|
|
342
|
+
readonly runtime: WorkflowRuntime;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Create a Node workflow host: compile every declared Lunora workflow onto the
|
|
346
|
+
* visulima engine, derive the `WORKFLOW_*` env, and expose the per-workflow
|
|
347
|
+
* `WorkflowBindingLike` handles.
|
|
348
|
+
*/
|
|
349
|
+
declare const createNodeWorkflowHost: <Workflows extends Record<string, {
|
|
350
|
+
isLunoraWorkflow: true;
|
|
351
|
+
}>>(options: NodeWorkflowHostOptions<Workflows>) => NodeWorkflowHost<Workflows>;
|
|
313
352
|
/** Every contract this package provides, composed for one Node process. */
|
|
314
353
|
interface NodePlatform<Queues extends Record<string, {
|
|
315
354
|
isLunoraQueue: true;
|
|
355
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
356
|
+
isLunoraWorkflow: true;
|
|
316
357
|
}> = Record<string, never>> {
|
|
317
358
|
/** `using platform = createNodePlatform(...)` support — delegates to `close()`. */
|
|
318
359
|
[Symbol.dispose]: () => void;
|
|
319
360
|
/** What this target supports — see `NODE_CAPABILITIES` in `@lunora/platform`. */
|
|
320
361
|
capabilities: PlatformCapabilities;
|
|
321
362
|
/**
|
|
322
|
-
* Tear this platform instance down:
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
363
|
+
* Tear this platform instance down: every resource this root built, in
|
|
364
|
+
* reverse construction order — the global store's own connection, every
|
|
365
|
+
* armed scheduler job timer, the registry's live shards, and last the shard
|
|
366
|
+
* host's pending alarm timer and its `better-sqlite3` database (and, with
|
|
367
|
+
* it, `kv`'s table — both live on the same connection). Nothing in this
|
|
368
|
+
* package closes these resources on its own — a `NodePlatform` a caller
|
|
369
|
+
* stops using without calling `close()` leaks the open file handles (plus
|
|
370
|
+
* their WAL/SHM sidecar files) and keeps the process alive on outstanding
|
|
371
|
+
* timers. Safe to call more than once. A `createNodePlatform` that throws
|
|
372
|
+
* unwinds the same list itself, so a failed construction leaks nothing and
|
|
373
|
+
* leaves nothing to call this on.
|
|
329
374
|
*
|
|
330
375
|
* **`close()` is a terminal state, not merely a cleanup step.** After it
|
|
331
376
|
* runs: `scheduler.schedule()` throws instead of arming a fresh timer
|
|
@@ -335,8 +380,10 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
335
380
|
* benign); `shard.alarms.set()`/`delete()` throw before mutating any
|
|
336
381
|
* in-memory state (checked against the connection's own open/closed state,
|
|
337
382
|
* the single source of truth); `shard.alarms.get()` keeps answering
|
|
338
|
-
* whatever it last held.
|
|
339
|
-
*
|
|
383
|
+
* whatever it last held; `sockets.accept()`/`setTag()`/`removeTag()` throw
|
|
384
|
+
* the same way, checked against the same connection state, before touching
|
|
385
|
+
* any runtime socket map or durable row. A no-op instead of a throw would
|
|
386
|
+
* be indistinguishable from a working call — exactly the silent-vanishing
|
|
340
387
|
* this lifecycle exists to end.
|
|
341
388
|
*/
|
|
342
389
|
close: () => void;
|
|
@@ -351,8 +398,28 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
351
398
|
* shutdown is `await platform.drain()` then `platform.close()`.
|
|
352
399
|
*/
|
|
353
400
|
drain: () => Promise<void>;
|
|
401
|
+
/**
|
|
402
|
+
* The `.global()` backend, or `undefined` when the caller named no database
|
|
403
|
+
* file for it. There is no default path, because a global store silently
|
|
404
|
+
* rooted at `:memory:` loses every row when the process exits.
|
|
405
|
+
*
|
|
406
|
+
* **A building block, not a wiring.** Unlike its three siblings, nothing
|
|
407
|
+
* downstream of this composition root reads it: a `.global()` read or write
|
|
408
|
+
* reaches its backend through exactly one seam, `createShardCtxDb({ globalDb
|
|
409
|
+
* })`, and the only thing that passes `globalDb` is the generated `shard.ts`,
|
|
410
|
+
* from its `d1` / `hyperdriveGlobal` config thunks. `createNodePlatform`
|
|
411
|
+
* constructs no shard DO, so it cannot make that hop itself. A caller that
|
|
412
|
+
* wants `.global()` on this host makes it: after `migrate(schema)` has
|
|
413
|
+
* provisioned the tables, give the generated `createShardDO` a `d1` thunk
|
|
414
|
+
* returning `platform.globalTables.writer({ schema, … })` — `writer` builds
|
|
415
|
+
* the `createSqlCtxDb` facade that seam expects, and `node-platform.test.ts`
|
|
416
|
+
* round-trips a row through exactly that object.
|
|
417
|
+
*/
|
|
418
|
+
globalTables?: NodeGlobalStore;
|
|
354
419
|
/** Durable key-value storage backed by the same `better-sqlite3` database as `shard`. */
|
|
355
420
|
kv: ShardKvStore;
|
|
421
|
+
/** The local-filesystem bucket, or `undefined` when the caller named no bucket directory. */
|
|
422
|
+
objectStorage?: R2BucketLike;
|
|
356
423
|
/**
|
|
357
424
|
* The declared queues, or `undefined` when the caller declared none.
|
|
358
425
|
*
|
|
@@ -367,6 +434,12 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
367
434
|
shard: ShardHost;
|
|
368
435
|
/** Socket registry with mutable tags and SQLite-persisted attachments. */
|
|
369
436
|
sockets: SocketHost;
|
|
437
|
+
/**
|
|
438
|
+
* The declared workflows, or `undefined` when the caller declared none.
|
|
439
|
+
* Runs are persisted to the same `better-sqlite3` database as the shard, so
|
|
440
|
+
* they survive a restart without a second store to configure.
|
|
441
|
+
*/
|
|
442
|
+
workflows?: NodeWorkflowHost<Workflows>;
|
|
370
443
|
}
|
|
371
444
|
/**
|
|
372
445
|
* Options for {@link createNodePlatform} — the shard host's (`path`,
|
|
@@ -376,10 +449,29 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
376
449
|
* or job with nowhere to land is bookkeeping. `directory` makes the shards the
|
|
377
450
|
* directory resolves for fan-out file-backed too, and `onAlarm` gives their
|
|
378
451
|
* durable alarms somewhere to land.
|
|
452
|
+
*
|
|
453
|
+
* `queues`, `workflows`, `objectStorageDirectory` and `globalTablesPath` are the
|
|
454
|
+
* four declarations — each is absent from the returned platform when omitted.
|
|
379
455
|
*/
|
|
380
456
|
type NodePlatformOptions<Queues extends Record<string, {
|
|
381
457
|
isLunoraQueue: true;
|
|
458
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
459
|
+
isLunoraWorkflow: true;
|
|
382
460
|
}> = Record<string, never>> = {
|
|
461
|
+
/**
|
|
462
|
+
* Database file the `.global()` tables live in — its own file, never a
|
|
463
|
+
* shard's, because a table every shard reads must not be inside any one of
|
|
464
|
+
* them. Omit when the app declares no `.global()` table; there is no
|
|
465
|
+
* default, for the same reason the bucket has none.
|
|
466
|
+
*/
|
|
467
|
+
globalTablesPath?: string;
|
|
468
|
+
/**
|
|
469
|
+
* Directory the object-storage bucket keeps its objects in, one file per
|
|
470
|
+
* key. Omit when the app declares no buckets — there is no default,
|
|
471
|
+
* because a bucket silently rooted at the process's working directory is
|
|
472
|
+
* worse than an absent one.
|
|
473
|
+
*/
|
|
474
|
+
objectStorageDirectory?: string;
|
|
383
475
|
/**
|
|
384
476
|
* Deliver one assembled queue batch — wire this to `dispatchQueueBatch`.
|
|
385
477
|
* Required alongside `queues`; without it the messages would be stored
|
|
@@ -388,11 +480,15 @@ type NodePlatformOptions<Queues extends Record<string, {
|
|
|
388
480
|
onQueueBatch?: NodeQueueHostOptions<Queues>["onBatch"];
|
|
389
481
|
/** The app's `defineQueue` results, keyed by export name. Omit when the app declares no queues. */
|
|
390
482
|
queues?: Queues;
|
|
483
|
+
/** The app's `defineWorkflow` results, keyed by export name. Omit when the app declares no workflows. */
|
|
484
|
+
workflows?: Workflows;
|
|
391
485
|
} & NodeSchedulerHostOptions & NodeShardHostOptions & NodeShardRegistryOptions;
|
|
392
486
|
/** Compose every contract this package provides over one `better-sqlite3` database. */
|
|
393
487
|
declare const createNodePlatform: <Queues extends Record<string, {
|
|
394
488
|
isLunoraQueue: true;
|
|
395
|
-
}> = Record<string, never
|
|
489
|
+
}> = Record<string, never>, Workflows extends Record<string, {
|
|
490
|
+
isLunoraWorkflow: true;
|
|
491
|
+
}> = Record<string, never>>(options?: NodePlatformOptions<Queues, Workflows>) => NodePlatform<Queues, Workflows>;
|
|
396
492
|
/** Options for {@link createNodeR2Bucket}. */
|
|
397
493
|
interface NodeR2BucketOptions {
|
|
398
494
|
/** The bucket directory — created on first write. Objects live here, one file per key. */
|
|
@@ -400,7 +496,7 @@ interface NodeR2BucketOptions {
|
|
|
400
496
|
}
|
|
401
497
|
/**
|
|
402
498
|
* Create an `R2BucketLike` over the local filesystem. Any object shape
|
|
403
|
-
* `createStorage({ bucket })` accepts — `put`/`get`/`head`/`delete`/`list` —
|
|
499
|
+
* `createStorage({ bucket, bucketName })` accepts — `put`/`get`/`head`/`delete`/`list` —
|
|
404
500
|
* maps directly onto a file operation.
|
|
405
501
|
*/
|
|
406
502
|
declare const createNodeR2Bucket: (options: NodeR2BucketOptions) => R2BucketLike;
|
|
@@ -473,45 +569,135 @@ interface NodeSocketHost {
|
|
|
473
569
|
}
|
|
474
570
|
/** Build the socket registry, persisting attachments and tags to `database`. */
|
|
475
571
|
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_<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
572
|
/**
|
|
513
573
|
* Build a durable {@link WorkflowStore} over a `better-sqlite3` connection.
|
|
514
574
|
* Pass the result as `createNodeWorkflowHost({ store })`.
|
|
515
575
|
*/
|
|
516
576
|
declare const createNodeWorkflowStore: (database: Database.Database) => WorkflowStore;
|
|
517
|
-
export {
|
|
577
|
+
export {
|
|
578
|
+
/**
|
|
579
|
+
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
580
|
+
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
581
|
+
* `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
|
|
582
|
+
* scheduler registry.
|
|
583
|
+
*
|
|
584
|
+
* Promoted from `@lunora/platform`'s `node:sqlite` reference host
|
|
585
|
+
* (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
|
|
586
|
+
* durability half of each contract actually holds: alarms and scheduler jobs
|
|
587
|
+
* are persisted **and re-armed on construction**, socket attachments and tags
|
|
588
|
+
* live in SQLite rather than a `Map`, and `.global()` tables run the real
|
|
589
|
+
* `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
|
|
590
|
+
* a restart test — a second host over the same database file — not only by the
|
|
591
|
+
* TCK's simulated recycle.
|
|
592
|
+
*
|
|
593
|
+
* Three suites run against this package: `@lunora/platform/conformance`'s host
|
|
594
|
+
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
595
|
+
* lifecycle/global-store tests.
|
|
596
|
+
*
|
|
597
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
598
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
599
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
600
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
601
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
602
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
603
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
604
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
605
|
+
*
|
|
606
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
607
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
608
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
609
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
610
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
611
|
+
*
|
|
612
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
613
|
+
* for `provision` — which reports, once, which declared features this target
|
|
614
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
615
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
616
|
+
* server, which is why both statements above hold at once. See
|
|
617
|
+
* `plans/234-node-host-findings.md`.
|
|
618
|
+
*/
|
|
619
|
+
type NodeGlobalContextDatabaseOptions,
|
|
620
|
+
/**
|
|
621
|
+
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
622
|
+
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
623
|
+
* `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
|
|
624
|
+
* scheduler registry.
|
|
625
|
+
*
|
|
626
|
+
* Promoted from `@lunora/platform`'s `node:sqlite` reference host
|
|
627
|
+
* (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
|
|
628
|
+
* durability half of each contract actually holds: alarms and scheduler jobs
|
|
629
|
+
* are persisted **and re-armed on construction**, socket attachments and tags
|
|
630
|
+
* live in SQLite rather than a `Map`, and `.global()` tables run the real
|
|
631
|
+
* `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
|
|
632
|
+
* a restart test — a second host over the same database file — not only by the
|
|
633
|
+
* TCK's simulated recycle.
|
|
634
|
+
*
|
|
635
|
+
* Three suites run against this package: `@lunora/platform/conformance`'s host
|
|
636
|
+
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
637
|
+
* lifecycle/global-store tests.
|
|
638
|
+
*
|
|
639
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
640
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
641
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
642
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
643
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
644
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
645
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
646
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
647
|
+
*
|
|
648
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
649
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
650
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
651
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
652
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
653
|
+
*
|
|
654
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
655
|
+
* for `provision` — which reports, once, which declared features this target
|
|
656
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
657
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
658
|
+
* server, which is why both statements above hold at once. See
|
|
659
|
+
* `plans/234-node-host-findings.md`.
|
|
660
|
+
*/
|
|
661
|
+
type NodeGlobalStore,
|
|
662
|
+
/**
|
|
663
|
+
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
664
|
+
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
665
|
+
* `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
|
|
666
|
+
* scheduler registry.
|
|
667
|
+
*
|
|
668
|
+
* Promoted from `@lunora/platform`'s `node:sqlite` reference host
|
|
669
|
+
* (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
|
|
670
|
+
* durability half of each contract actually holds: alarms and scheduler jobs
|
|
671
|
+
* are persisted **and re-armed on construction**, socket attachments and tags
|
|
672
|
+
* live in SQLite rather than a `Map`, and `.global()` tables run the real
|
|
673
|
+
* `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
|
|
674
|
+
* a restart test — a second host over the same database file — not only by the
|
|
675
|
+
* TCK's simulated recycle.
|
|
676
|
+
*
|
|
677
|
+
* Three suites run against this package: `@lunora/platform/conformance`'s host
|
|
678
|
+
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
679
|
+
* lifecycle/global-store tests.
|
|
680
|
+
*
|
|
681
|
+
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
682
|
+
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
683
|
+
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
684
|
+
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
685
|
+
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
686
|
+
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
687
|
+
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
688
|
+
* is the one combination that fails at runtime with no diagnostic before it.
|
|
689
|
+
*
|
|
690
|
+
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
691
|
+
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
692
|
+
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
693
|
+
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
694
|
+
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
695
|
+
*
|
|
696
|
+
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
697
|
+
* for `provision` — which reports, once, which declared features this target
|
|
698
|
+
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
699
|
+
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
700
|
+
* server, which is why both statements above hold at once. See
|
|
701
|
+
* `plans/234-node-host-findings.md`.
|
|
702
|
+
*/
|
|
703
|
+
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-
|
|
1
|
+
import{createNodeGlobalStore as r,createNodeSqlExec as t}from"./packem_shared/createNodeGlobalStore-nAJ4WO7w.mjs";import{createNodeShardKvStore as d}from"./packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodePlatform as f}from"./packem_shared/createNodePlatform-Inw4xniW.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-B76kaXz-.mjs";import{createNodeShardRegistry as k}from"./packem_shared/createNodeShardRegistry-Dj1mXStS.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-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{sqliteDialect as o}from"@lunora/d1";import{createSqlCtxDb as c,runSqlGlobalTableMigrations as s,runSqlAggregateMigrations as p,runSqlRankMigrations as u,runSqlSearchMigrations as g,backfillSqlSearchIndexes as S,runSqlCdcMigration as d}from"@lunora/sql-store";import w from"better-sqlite3";const n=new WeakMap,i=(a,r)=>{let e=n.get(a);e===void 0&&(e=new Map,n.set(a,e));let t=e.get(r);return t===void 0&&(t=a.prepare(r),e.set(r,t)),t},f=a=>({all:async(r,e)=>i(a,r).all(...e),run:async(r,e)=>({rowsAffected:i(a,r).run(...e).changes})}),x=(a={})=>{const r=new w(a.path??":memory:");r.pragma("journal_mode = WAL");const e=f(r);return{database:r,dispose:()=>{r.open&&r.close()},exec:e,migrate:async(t,l={})=>{await s(e,t,o),await p(e,t,o),await u(e,t,o),await g(e,t,o),await S(e,t,o),l.cdc===!0&&await d(e,o)},writer:t=>c({...t,dialect:o,exec:e,provisionScope:e})}};export{x as createNodeGlobalStore,f 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-nAJ4WO7w.mjs";import{createNodeShardKvStore as k}from"./createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeQueueHost as g}from"./createNodeQueueHost--mBLGSxF.mjs";import{createNodeR2Bucket as y}from"./createNodeR2Bucket-BzNH1lBp.mjs";import{createNodeSchedulerHost as R}from"./createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as q}from"./createNodeShardHost-B76kaXz-.mjs";import{createNodeShardRegistry as B}from"./createNodeShardRegistry-Dj1mXStS.mjs";import{createNodeSocketHost as H}from"./createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as I}from"./createNodeWorkflowHost-CTVBYiuj.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 b}from"node:crypto";import{deserialize as F,serialize as H}from"node:v8";import{LunoraError as f}from"@lunora/errors";import{isQueueDefinition as W,queueDefaultName as k,queueBindingName as C}from"@lunora/queue";const l={maxBatchSize:10,maxBatchTimeoutSeconds:5,maxRetries:3,retryDelaySeconds:0,visibilityTimeoutMs:3e4},X=43200,w=(e,o)=>{switch(o){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 f("VALIDATION_ERROR",'@lunora/platform-node: queue contentType "bytes" needs an ArrayBuffer or a typed array')}case"text":{if(typeof e!="string")throw new f("VALIDATION_ERROR",'@lunora/platform-node: queue contentType "text" needs a string');return Buffer.from(e,"utf8")}case"v8":return H(e);default:return Buffer.from(JSON.stringify(e??null),"utf8")}},y=(e,o)=>{switch(o){case"bytes":return e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);case"text":return e.toString("utf8");case"v8":return F(e);default:return JSON.parse(e.toString("utf8"))}},P=e=>{try{return y(e.body,e.content_type)}catch{return}},p=e=>e===void 0||!Number.isFinite(e)?0:Math.min(Math.max(0,Math.floor(e)),X)*1e3,j=(e,o)=>{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 h=e.prepare("INSERT INTO _lunora_queue_messages (id, queue, body, content_type, visible_at, enqueued_at) VALUES (?, ?, ?, ?, ?, ?)"),A=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 *`),D=e.prepare("UPDATE _lunora_queue_messages SET visible_at = ? WHERE id = ?"),R=e.prepare("DELETE FROM _lunora_queue_messages WHERE id = ?"),O=e.prepare("UPDATE _lunora_queue_messages SET state = 'dead' WHERE id = ?"),S=e.prepare("SELECT enqueued_at FROM _lunora_queue_messages WHERE queue = ? AND state = 'pending' AND visible_at <= ? ORDER BY enqueued_at LIMIT 1"),q=e.prepare("SELECT COUNT(*) AS n FROM _lunora_queue_messages WHERE queue = ? AND state = 'pending' AND visible_at <= ?"),v=e.prepare("SELECT * FROM _lunora_queue_messages WHERE queue = ? AND state = 'dead' ORDER BY enqueued_at"),I=e.prepare("UPDATE _lunora_queue_messages SET state = 'pending', attempts = 0, visible_at = ? WHERE id = ? AND state = 'dead'"),E=Object.entries(o.queues).map(([t,n])=>{if(!W(n))throw new f("VALIDATION_ERROR",`@lunora/platform-node: "${t}" is not a defineQueue result`);return{definition:n,exportName:t,name:n.name??k(t)}}),B=new Set(E.map(t=>t.name));for(const t of E){const n=t.definition.deadLetterQueue;if(n!==void 0){if(n===t.name)throw new f("VALIDATION_ERROR",`@lunora/platform-node: queue "${t.name}" names itself as its own deadLetterQueue, which would redeliver forever`);if(!B.has(n))throw new f("VALIDATION_ERROR",`@lunora/platform-node: queue "${t.name}" names deadLetterQueue "${n}", which no declared queue provides — its dead letters would be unreachable`)}}const M=o.visibilityTimeoutMs??l.visibilityTimeoutMs,_=o.now??Date.now,T=(t,n,s,a,i)=>{h.run(b(),t,w(n,s),s,i+a,i)},x=(t,n,s,a,i)=>{if(s==="ack"){R.run(t.id);return}const u=n.definition.maxRetries??l.maxRetries;if(t.attempts>u){const m=n.definition.deadLetterQueue;m===void 0?O.run(t.id):(T(m,y(t.body,t.content_type),t.content_type,0,i),R.run(t.id));return}const c=p(a??n.definition.retryDelay??l.retryDelaySeconds);D.run(i+c,t.id)},N={},L={...o.env};for(const t of E){const n={send:async(s,a)=>{T(t.name,s,a?.contentType??"json",p(a?.delaySeconds),_())},sendBatch:async(s,a)=>{const i=_(),u=p(a?.delaySeconds);e.transaction(()=>{for(const c of s){const m=c.delaySeconds===void 0?u:p(c.delaySeconds);T(t.name,c.body,c.contentType??"json",m,i)}})()}};N[t.exportName]=n,L[C(t.exportName)]=n}const U=async(t,n)=>{const s=Math.min(Math.max(1,t.definition.maxBatchSize??l.maxBatchSize),100),a=q.get(t.name,n)?.n??0;if(a===0)return!1;if(a<s){const r=S.get(t.name,n)?.enqueued_at,d=Math.min(Math.max(0,t.definition.maxBatchTimeout??l.maxBatchTimeoutSeconds),60)*1e3;if(r!==void 0&&n-r<d)return!1}const i=e.transaction(()=>A.all(n+M,t.name,n,s)).immediate();if(i.length===0)return!1;const u=new Map,c=i.map(r=>({ack:()=>{u.set(r.id,{outcome:"ack"})},attempts:r.attempts,body:y(r.body,r.content_type),id:r.id,retry:d=>{u.set(r.id,{delaySeconds:d?.delaySeconds,outcome:"retry"})},timestamp:new Date(r.enqueued_at)})),m={ackAll:()=>{for(const r of i)u.set(r.id,{outcome:"ack"})},messages:c,queue:t.name,retryAll:r=>{for(const d of i)u.set(d.id,{delaySeconds:r?.delaySeconds,outcome:"retry"})}};let g="ack";try{await o.onBatch(m)}catch{g="retry"}return e.transaction(()=>{for(const r of i){const d=u.get(r.id);x(r,t,d?.outcome??g,d?.delaySeconds,n)}})(),!0};return{bindings:N,deadLetters:{list:t=>v.all(t).map(n=>({attempts:n.attempts,body:P(n),id:n.id})),requeue:t=>I.run(_(),t).changes>0},env:L,poll:async(t=_())=>{let n=0;for(const s of E)s.definition.mode!=="pull"&&await U(s,t)&&(n+=1);return n}}};export{j as createNodeQueueHost};
|
|
@@ -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{AsyncLocalStorage as S}from"node:async_hooks";import{LunoraError as _}from"@lunora/errors";import v from"better-sqlite3";const w=2147483647,g=6,y=100,L=e=>e===void 0?null:e,R=(e,t)=>({get databaseSize(){const c=e.pragma("page_count",{simple:!0}),r=e.pragma("page_size",{simple:!0});return c*r},exec:(c,...r)=>{t("run SQL");const a=e.prepare(c),s=r.map(u=>L(u)),o=a.reader?a.all(...s):(a.run(...s),[]);return{[Symbol.iterator]:()=>o[Symbol.iterator](),one:()=>{if(o.length!==1)throw new Error(`expected exactly one row, got ${String(o.length)}`);return o[0]},toArray:()=>[...o]}}}),O=(e,t,c)=>{let r,a,s=0,o;e.exec("CREATE TABLE IF NOT EXISTS _lunora_alarm (id INTEGER PRIMARY KEY CHECK (id = 0), scheduled_for INTEGER NOT NULL)");const u=n=>{n===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(n)},p=()=>{a!==void 0&&(clearTimeout(a),a=void 0)},m=n=>{p();const d=Math.max(0,n-Date.now());if(d>w){a=setTimeout(()=>{m(n)},w);return}a=setTimeout(()=>{r=void 0,a=void 0,e.open&&(u(void 0),(async()=>{await c?.(),s=0})().catch(()=>{if(!e.open||r!==void 0)return;if(s+=1,s>g){s=0;return}const f=Date.now()+y*2**(s-1);r=f,u(f),m(f)}))},d)},h={delete:()=>{if(!e.open)throw new Error("platform closed: cannot delete an alarm");if(t.assertOwnTurn("delete an alarm"),s=0,u(void 0),t.inside()){o={at:void 0};return}r=void 0,p()},get:()=>(o===void 0?r:o.at)??null,set:n=>{if(!e.open)throw new Error("platform closed: cannot set an alarm");t.assertOwnTurn("set an alarm");const d=typeof n=="number"?n:n.getTime();if(s=0,u(d),t.inside()){o={at:d};return}r=d,m(d)}},T=()=>{if(o===void 0)return;const{at:n}=o;if(o=void 0,n===void 0){r=void 0,p();return}r=n,m(n)},A=()=>{o=void 0},E=e.prepare("SELECT scheduled_for FROM _lunora_alarm WHERE id = 0").get();return E!==void 0&&(r=E.scheduled_for,m(E.scheduled_for)),{alarms:h,commit:T,dispose:p,rollback:A}},z=(e={})=>{const t=new v(e.path??":memory:");t.pragma("journal_mode = WAL");const c=new S;let r=!1;const a=l=>{if(r&&c.getStore()!==!0)throw new _("SHARD_UNAVAILABLE",`shard busy: cannot ${l} while another task holds this shard's transaction`)},s=R(t,a),{alarms:o,commit:u,dispose:p,rollback:m}=O(t,{assertOwnTurn:a,inside:()=>c.getStore()===!0},e.onAlarm),h=new Set;let T=Promise.resolve();const A=l=>{const i=T.then(l,l);return T=i.then(()=>{},()=>{}),i};let E=Promise.resolve();const n=async l=>{t.exec("BEGIN"),r=!0;try{const i=await c.run(!0,l);return t.exec("COMMIT"),r=!1,u(),i}catch(i){r=!1,m();try{t.exec("ROLLBACK")}catch{}throw i}},d=l=>{const i=E.then(()=>n(l),()=>n(l));return E=i.then(()=>{},()=>{}),i},f={alarms:o,runSerialized:A,shardKey:e.shardKey,sql:s,transaction:d,waitUntil:l=>{const i=l.catch(()=>{}).finally(()=>{h.delete(i)});h.add(i)}};return{database:t,dispose:()=>{p(),t.open&&t.close()},drain:async()=>{for(;h.size>0;)await Promise.all(h)},host:f}};export{z 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-B76kaXz-.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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as u}from"@lunora/errors";import{isWorkflowDefinition as E,workflowDefaultName as k,createWorkflowRunContext as A,workflowBindingName as N}from"@lunora/workflow";import{defineWorkflow as D,createRuntime as T}from"@visulima/workflow";const m="@lunora/platform-node:terminated",_={d:864e5,day:864e5,days:864e5,h:36e5,hour:36e5,hours:36e5,m:6e4,millisecond:1,milliseconds:1,minute:6e4,minutes:6e4,ms:1,s:1e3,second:1e3,seconds:1e3,w:6048e5,week:6048e5,weeks:6048e5,month:2592e6,months:2592e6},O=/^(\d+(?:\.\d+)?)\s*([a-z]+)$/i,v=o=>{if(typeof o=="number"){if(!Number.isFinite(o))throw new u("VALIDATION_ERROR",`@lunora/platform-node: workflow duration must be finite, got ${String(o)}`);return Math.max(0,o)}const r=O.exec(o.trim());if(r===null)throw new u("VALIDATION_ERROR",`@lunora/platform-node: unsupported workflow duration "${o}"`);const n=Number(r[1]),s=(r[2]??"").toLowerCase(),d=_[s];if(d===void 0)throw new u("VALIDATION_ERROR",`@lunora/platform-node: unsupported workflow duration "${o}"`);const l=n*d;if(!Number.isFinite(l))throw new u("VALIDATION_ERROR",`@lunora/platform-node: workflow duration "${o}" overflows`);return Math.max(0,l)},p="@lunora/platform-node:alias",b=o=>({attempt:1,config:{retries:{limit:1}},step:{count:1,name:o}}),$=o=>{switch(o){case"completed":return"complete";case"failed":return"errored";default:return"waiting"}},L=o=>({do:(async(r,n,s,d)=>{const l=typeof n=="function"?n:s;if(typeof l!="function")throw new u("VALIDATION_ERROR",`@lunora/platform-node: step.do("${r}") requires a callback`);return o.step(r,async()=>l(b(r)))}),sleep:async(r,n)=>{const s=v(n);s!==0&&await o.sleep(r,s)},sleepUntil:async(r,n)=>{const s=typeof n=="number"?n:n.getTime();if(!Number.isFinite(s))throw new u("VALIDATION_ERROR",`@lunora/platform-node: step.sleepUntil("${r}") needs a valid timestamp`);const d=Math.max(0,s-Date.now());d!==0&&await o.sleep(r,d)},waitForEvent:(async(r,n)=>({payload:await o.waitForEvent(r,n.type,{timeout:n.timeout===void 0?void 0:v(n.timeout)}),type:n.type}))}),U=o=>{const r=Object.entries(o.workflows).map(([e,t])=>{if(!E(t))throw new u("VALIDATION_ERROR",`@lunora/platform-node: "${e}" is not a defineWorkflow result`);return{definition:t,exportName:e,id:t.name??k(e)}}),{store:n}=o,s={...o.env},d=r.map(({definition:e,exportName:t,id:a})=>D({id:a,tags:[t],run:async i=>{const w=L(i),I=A({env:s,event:{instanceId:i.runId,payload:i.payload,timestamp:new Date,workflowName:a},exportName:t,step:w});return await e.handler(I)}})),l=new Set,R={acquire:n.acquire===void 0?void 0:async(e,t,a)=>n.acquire?.(e,t,a)??!1,delete:async e=>n.delete(e),due:async(e,t)=>n.due(e,t),load:async e=>{const t=await n.load(e);return t===void 0||t.definitionId===m||t.definitionId===p?void 0:t},release:n.release===void 0?void 0:async(e,t)=>n.release?.(e,t),save:async e=>{l.has(e.runId)||await n.save(e)}},c=T({leaseTtlMs:o.leaseTtlMs,store:R,workflows:d}),f=e=>({id:e,pause:()=>Promise.reject(new u("NOT_IMPLEMENTED",`@lunora/platform-node: workflow instance "${e}" cannot be paused — the visulima engine has no pause equivalent`)),restart:()=>Promise.reject(new u("NOT_IMPLEMENTED",`@lunora/platform-node: workflow instance "${e}" cannot be restarted — the visulima engine has no restart equivalent`)),resume:async()=>{if((await n.load(e))?.definitionId===m)throw new u("BAD_REQUEST",`@lunora/platform-node: workflow instance "${e}" was terminated and cannot be resumed`);await c.resume(e)},sendEvent:async t=>{const a=await c.getRun(e);if(a?.status!=="waiting"||a.pending?.kind!=="event"||a.pending.eventName!==t.type)throw new u("BAD_REQUEST",`@lunora/platform-node: workflow instance "${e}" is not waiting for event "${t.type}"`);await c.signal(e,t.type,t.payload)},status:async()=>{const t=await n.load(e);if(t===void 0)return{status:"unknown"};if(t.definitionId===m)return{status:"terminated"};const a=await c.getRun(e);return a===void 0?{status:"unknown"}:{error:a.error===void 0?void 0:{message:a.error.message,name:a.error.name},output:a.output,status:$(a.status)}},terminate:async()=>{await n.load(e)!==void 0&&(l.add(e),await n.delete(e),await n.save({definitionId:m,runId:e,snapshot:void 0,status:"failed",updatedAt:Date.now()}))}}),g=async e=>{const t=await n.load(e);return t?.definitionId===p?t.snapshot:e},y=async(e,t)=>{if(t?.id===void 0){const w=await c.trigger(e,t?.params);return f(w.runId)}const a=await n.load(t.id);if(a?.definitionId===p)return f(a.snapshot);if(a!==void 0)throw new u("BAD_REQUEST",`@lunora/platform-node: workflow create({ id: "${t.id}" }) names an existing run — pass the id a previous create returned, or a fresh one`);const i=await c.trigger(e,t.params);return await n.save({definitionId:p,runId:t.id,snapshot:i.runId,status:"failed",updatedAt:Date.now()}),f(i.runId)},h={};for(const{exportName:e,id:t}of r){const a={create:async i=>y(t,i),createBatch:async i=>{const w=[];for(const I of i)w.push(await y(t,I));return w},get:async i=>f(await g(i))};h[e]=a,s[N(e)]=a}return{bindings:h,env:s,runtime:c}};export{U as createNodeWorkflowHost};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const i=
|
|
1
|
+
const i=t=>{t.pragma("journal_mode = WAL"),t.exec(`CREATE TABLE IF NOT EXISTS _lunora_workflow_runs (
|
|
2
2
|
run_id TEXT PRIMARY KEY,
|
|
3
3
|
definition_id TEXT NOT NULL,
|
|
4
4
|
status TEXT NOT NULL,
|
|
@@ -6,7 +6,7 @@ const i=n=>{n.pragma("journal_mode = WAL"),n.exec(`CREATE TABLE IF NOT EXISTS _l
|
|
|
6
6
|
wake_at INTEGER,
|
|
7
7
|
updated_at INTEGER NOT NULL,
|
|
8
8
|
snapshot TEXT NOT NULL
|
|
9
|
-
)`),
|
|
9
|
+
)`),t.exec("CREATE INDEX IF NOT EXISTS _lunora_workflow_runs_due ON _lunora_workflow_runs (wake_at)"),t.exec("CREATE TABLE IF NOT EXISTS _lunora_workflow_leases (run_id TEXT PRIMARY KEY, token TEXT NOT NULL, expires_at INTEGER NOT NULL)");const r=t.prepare(`INSERT INTO _lunora_workflow_runs (run_id, definition_id, status, event_name, wake_at, updated_at, snapshot)
|
|
10
10
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
11
11
|
ON CONFLICT (run_id) DO UPDATE SET
|
|
12
12
|
definition_id = excluded.definition_id,
|
|
@@ -14,8 +14,8 @@ const i=n=>{n.pragma("journal_mode = WAL"),n.exec(`CREATE TABLE IF NOT EXISTS _l
|
|
|
14
14
|
event_name = excluded.event_name,
|
|
15
15
|
wake_at = excluded.wake_at,
|
|
16
16
|
updated_at = excluded.updated_at,
|
|
17
|
-
snapshot = excluded.snapshot`),
|
|
17
|
+
snapshot = excluded.snapshot`),_=t.prepare("SELECT * FROM _lunora_workflow_runs WHERE run_id = ?"),u=t.prepare("DELETE FROM _lunora_workflow_runs WHERE run_id = ?"),s=t.prepare("DELETE FROM _lunora_workflow_leases WHERE run_id = ?"),d=t.prepare(`SELECT run_id FROM _lunora_workflow_runs
|
|
18
18
|
WHERE status IN ('suspended', 'waiting') AND wake_at IS NOT NULL AND wake_at <= ?
|
|
19
|
-
ORDER BY wake_at LIMIT ?`),E=
|
|
19
|
+
ORDER BY wake_at LIMIT ?`),E=t.prepare(`INSERT INTO _lunora_workflow_leases (run_id, token, expires_at) VALUES (?, ?, ?)
|
|
20
20
|
ON CONFLICT (run_id) DO UPDATE SET token = excluded.token, expires_at = excluded.expires_at
|
|
21
|
-
WHERE _lunora_workflow_leases.expires_at <= ? OR _lunora_workflow_leases.token = excluded.token`),l=
|
|
21
|
+
WHERE _lunora_workflow_leases.expires_at <= ? OR _lunora_workflow_leases.token = excluded.token`),l=t.prepare("DELETE FROM _lunora_workflow_leases WHERE run_id = ? AND token = ?");return{acquire:async(e,n,a)=>{const o=Date.now();return E.run(e,n,o+a,o).changes>0},delete:async e=>{u.run(e),s.run(e)},due:async(e,n)=>d.all(e,n).map(a=>a.run_id),load:async e=>{const n=_.get(e);if(n!==void 0)return{definitionId:n.definition_id,eventName:n.event_name??void 0,runId:n.run_id,snapshot:JSON.parse(n.snapshot),status:n.status,updatedAt:n.updated_at,wakeAt:n.wake_at??void 0}},release:async(e,n)=>{l.run(e,n)},save:async e=>{r.run(e.runId,e.definitionId,e.status,e.eventName??null,e.wakeAt??null,e.updatedAt,JSON.stringify(e.snapshot??null))}}};export{i as createNodeWorkflowStore};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const r=e=>typeof e=="string"?new TextEncoder().encode(e).buffer:e instanceof ArrayBuffer?e:ArrayBuffer.isView(e)?e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength):new ArrayBuffer(0);export{r as t};
|
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.60",
|
|
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,15 +46,18 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/d1": "1.0.0-alpha.
|
|
50
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
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.
|
|
55
|
-
"@visulima/workflow": "
|
|
56
|
-
"better-sqlite3": "^
|
|
57
|
-
"cron-parser": "5.
|
|
49
|
+
"@lunora/d1": "1.0.0-alpha.114",
|
|
50
|
+
"@lunora/errors": "1.0.0-alpha.35",
|
|
51
|
+
"@lunora/platform": "1.0.0-alpha.28",
|
|
52
|
+
"@lunora/queue": "1.0.0-alpha.52",
|
|
53
|
+
"@lunora/sql-store": "1.0.0-alpha.114",
|
|
54
|
+
"@lunora/workflow": "1.0.0-alpha.49",
|
|
55
|
+
"@visulima/workflow": "3.0.0",
|
|
56
|
+
"better-sqlite3": "^13.0.3",
|
|
57
|
+
"cron-parser": "5.10.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@lunora/shard-engine": ">=1.0.0-alpha.45 <2.0.0-0"
|
|
58
61
|
},
|
|
59
62
|
"engines": {
|
|
60
63
|
"node": "^22.15.0 || >=24.11.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{sqliteDialect as n}from"@lunora/d1";import{createSqlCtxDb as c,runSqlGlobalTableMigrations as s,runSqlAggregateMigrations as g,runSqlRankMigrations as d,runSqlSearchMigrations as p,backfillSqlSearchIndexes as m,runSqlCdcMigration as u}from"@lunora/sql-store";import w from"better-sqlite3";const i=new WeakMap,o=(r,a)=>{let e=i.get(r);e===void 0&&(e=new Map,i.set(r,e));let t=e.get(a);return t===void 0&&(t=r.prepare(a),e.set(a,t)),t},S=r=>({all:async(a,e)=>o(r,a).all(...e),run:async(a,e)=>({rowsAffected:o(r,a).run(...e).changes})}),f=(r={})=>{const a=new w(r.path??":memory:");a.pragma("journal_mode = WAL");const e=S(a);return{database:a,dispose:()=>{a.open&&a.close()},exec:e,migrate:async(t,l={})=>{await s(e,t,n),await g(e,t,n),await d(e,t,n),await p(e,t,n),await m(e,t,n),l.cdc===!0&&await u(e,n)},writer:t=>c({...t,dialect:n,exec:e})}};export{f as createNodeGlobalStore,S as createNodeSqlExec};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as n}from"@lunora/errors";import{NODE_CAPABILITIES as l}from"@lunora/platform";import{createNodeShardKvStore as f}from"./createNodeShardKvStore-BfKCQTjE.mjs";import{createNodeQueueHost as N}from"./createNodeQueueHost-CLQ5hOBf.mjs";import{createNodeSchedulerHost as S}from"./createNodeSchedulerHost-DRjZhuxe.mjs";import{createNodeShardHost as q}from"./createNodeShardHost-DEHe5Vcm.mjs";import{createNodeShardRegistry as v}from"./createNodeShardRegistry-0iSMdi9-.mjs";import{createNodeSocketHost as w}from"./createNodeSocketHost-CytowrYN.mjs";const H=(e={})=>{const{database:o,dispose:a,drain:s,host:d}=q(e),u=f(o),r=v(e),{directory:c}=r,{socket:i}=w(o),{dispose:m,scheduler:h}=S(o,e),p=e.queues===void 0?void 0:N(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}),t=()=>{m(),a(),r.close()};return{capabilities:l,close:t,directory:c,drain:s,kv:u,queues:p,scheduler:h,shard:d,sockets:i,[Symbol.dispose]:t}};export{H as createNodePlatform};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import{randomUUID as M}from"node:crypto";import{deserialize as U,serialize as F}from"node:v8";import{LunoraError as l}from"@lunora/errors";import{isQueueDefinition as W,queueDefaultName as H,queueBindingName as Q}from"@lunora/queue";const f={maxBatchSize:10,maxBatchTimeoutSeconds:5,maxRetries:3,retryDelaySeconds:0,visibilityTimeoutMs:3e4},k=43200,C=(e,u)=>{switch(u){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 l("VALIDATION_ERROR",'@lunora/platform-node: queue contentType "bytes" needs an ArrayBuffer or a typed array')}case"text":{if(typeof e!="string")throw new l("VALIDATION_ERROR",'@lunora/platform-node: queue contentType "text" needs a string');return Buffer.from(e,"utf8")}case"v8":return F(e);default:return Buffer.from(JSON.stringify(e??null),"utf8")}},T=(e,u)=>{switch(u){case"bytes":return e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);case"text":return e.toString("utf8");case"v8":return U(e);default:return JSON.parse(e.toString("utf8"))}},V=e=>{try{return T(e.body,e.content_type)}catch{return}},p=e=>e===void 0||!Number.isFinite(e)?0:Math.min(Math.max(0,Math.floor(e)),k)*1e3,j=(e,u)=>{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 O=e.prepare("INSERT INTO _lunora_queue_messages (id, queue, body, content_type, visible_at, enqueued_at) VALUES (?, ?, ?, ?, ?, ?)"),b=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 *`),A=e.prepare("UPDATE _lunora_queue_messages SET visible_at = ? WHERE id = ?"),N=e.prepare("DELETE FROM _lunora_queue_messages WHERE id = ?"),S=e.prepare("UPDATE _lunora_queue_messages SET state = 'dead' WHERE id = ?"),g=e.prepare("SELECT enqueued_at FROM _lunora_queue_messages WHERE queue = ? AND state = 'pending' AND visible_at <= ? ORDER BY enqueued_at LIMIT 1"),h=e.prepare("SELECT COUNT(*) AS n FROM _lunora_queue_messages WHERE queue = ? AND state = 'pending' AND visible_at <= ?"),D=e.prepare("SELECT * FROM _lunora_queue_messages WHERE queue = ? AND state = 'dead' ORDER BY enqueued_at"),I=e.prepare("UPDATE _lunora_queue_messages SET state = 'pending', attempts = 0, visible_at = ? WHERE id = ? AND state = 'dead'"),E=Object.entries(u.queues).map(([t,a])=>{if(!W(a))throw new l("VALIDATION_ERROR",`@lunora/platform-node: "${t}" is not a defineQueue result`);return{definition:a,exportName:t,name:a.name??H(t)}}),v=new Set(E.map(t=>t.name));for(const t of E){const a=t.definition.deadLetterQueue;if(a!==void 0){if(a===t.name)throw new l("VALIDATION_ERROR",`@lunora/platform-node: queue "${t.name}" names itself as its own deadLetterQueue, which would redeliver forever`);if(!v.has(a))throw new l("VALIDATION_ERROR",`@lunora/platform-node: queue "${t.name}" names deadLetterQueue "${a}", which no declared queue provides — its dead letters would be unreachable`)}}const w=u.visibilityTimeoutMs??f.visibilityTimeoutMs,_=u.now??Date.now,y=(t,a,r,o,s)=>{O.run(M(),t,C(a,r),r,s+o,s)},B=(t,a,r,o,s)=>{if(r==="ack"){N.run(t.id);return}const i=a.definition.maxRetries??f.maxRetries;if(t.attempts>i){const c=a.definition.deadLetterQueue;c===void 0?S.run(t.id):(y(c,T(t.body,t.content_type),t.content_type,0,s),N.run(t.id));return}const d=p(o??a.definition.retryDelay??f.retryDelaySeconds);A.run(s+d,t.id)},R={},L={...u.env};for(const t of E){const a={send:async(r,o)=>{y(t.name,r,o?.contentType??"json",p(o?.delaySeconds),_())},sendBatch:async(r,o)=>{const s=_(),i=p(o?.delaySeconds);e.transaction(()=>{for(const d of r){const c=d.delaySeconds===void 0?i:p(d.delaySeconds);y(t.name,d.body,d.contentType??"json",c,s)}})()}};R[t.exportName]=a,L[Q(t.exportName)]=a}const x=async(t,a)=>{const r=Math.min(Math.max(1,t.definition.maxBatchSize??f.maxBatchSize),100),o=h.get(t.name,a)?.n??0;if(o===0)return!1;if(o<r){const n=g.get(t.name,a)?.enqueued_at,m=Math.min(Math.max(0,t.definition.maxBatchTimeout??f.maxBatchTimeoutSeconds),60)*1e3;if(n!==void 0&&a-n<m)return!1}const s=e.transaction(()=>b.all(a+w,t.name,a,r)).immediate();if(s.length===0)return!1;const i=new Map,d=s.map(n=>({ack:()=>{i.set(n.id,{outcome:"ack"})},attempts:n.attempts,body:T(n.body,n.content_type),id:n.id,retry:m=>{i.set(n.id,{delaySeconds:m?.delaySeconds,outcome:"retry"})},timestamp:new Date(n.enqueued_at)})),c={ackAll:()=>{for(const n of s)i.set(n.id,{outcome:"ack"})},messages:d,queue:t.name,retryAll:n=>{for(const m of s)i.set(m.id,{delaySeconds:n?.delaySeconds,outcome:"retry"})}};let q="ack";try{await u.onBatch(c)}catch{q="retry"}return e.transaction(()=>{for(const n of s){const m=i.get(n.id);B(n,t,m?.outcome??q,m?.delaySeconds,a)}})(),!0};return{bindings:R,deadLetters:{list:t=>D.all(t).map(a=>({attempts:a.attempts,body:V(a),id:a.id})),requeue:t=>I.run(_(),t).changes>0},env:L,poll:async(t=_())=>{let a=0;for(const r of E)r.definition.mode!=="pull"&&await x(r,t)&&(a+=1);return a}}};export{j as createNodeQueueHost};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{randomUUID as z,createHash as j}from"node:crypto";import{mkdir as N,open as k,rename as C,unlink as F,readdir as P}from"node:fs/promises";import{join as w,dirname as x}from"node:path";import{LunoraError as l}from"@lunora/errors";import{t as M}from"./to-array-buffer-CP4NlF-y.mjs";new TextEncoder;const H=Array.from({length:32},(e,t)=>t),W=new RegExp(`[${H.map(e=>String.fromCodePoint(e)).join("")}]`,"u"),J=e=>W.test(e),b=".lunora-tmp",U="LNR1",h=8,L=1024,v=255,q=/[%A-Z:]/g,V=e=>{const t=e.replaceAll(q,r=>`%${r.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`),a=t.at(-1);return a==="."||a===" "?`${t.slice(0,-1)}%${a.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`:t},Q=e=>{try{return decodeURIComponent(e)}catch{return}},m=e=>e.split("/").map(t=>V(t)).join("/"),X=e=>{const t=[];for(const a of e.split("/")){const r=Q(a);if(r===void 0)return;t.push(r)}return t.join("/")},p=e=>{if(typeof e!="string"||e.length===0)throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key must be a non-empty string");if(e.length>L)throw new l("VALIDATION_ERROR",`@lunora/platform-node: R2 key exceeds ${String(L)}-byte limit`);if(e.includes("\0"))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a NUL byte");if(J(e))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a control character (including CR/LF)");if(e.includes("\\"))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a backslash (a path separator on Windows)");if(e.startsWith("/"))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 key must not start with `/`");for(const t of e.split("/")){if(Buffer.byteLength(V(t),"utf8")>v)throw new l("VALIDATION_ERROR",`@lunora/platform-node: R2 key segment "${t}" exceeds ${String(v)} bytes once escaped for the filesystem`);if(t===""||t==="."||t==="..")throw new l("VALIDATION_ERROR",`@lunora/platform-node: R2 key has an empty, \`.\` or \`..\` path segment ("${e}")`);if(t===b)throw new l("VALIDATION_ERROR",`@lunora/platform-node: R2 key is reserved (no segment may be "${b}")`)}},I=e=>e instanceof Error&&"code"in e&&e.code==="ENOENT",B=(e,t)=>{const a=t instanceof Error&&"code"in t?t.code:void 0;return a==="EEXIST"||a==="EISDIR"||a==="ENOTDIR"?new l("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},Z=e=>{const t=Buffer.from(JSON.stringify(e),"utf8"),a=Buffer.alloc(h);return a.writeUInt32BE(t.byteLength,0),a.write(U,4,"ascii"),Buffer.concat([t,a])},G=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"},K=async e=>{const t=await e.stat();if(!t.isFile()||t.size<h)return;const a=Buffer.alloc(h);if(await e.read(a,0,h,t.size-h),a.toString("ascii",4,h)!==U)return;const r=a.readUInt32BE(0),n=t.size-h-r;if(r===0||n<0)return;const s=Buffer.alloc(r),{bytesRead:i}=await e.read(s,0,r,n);if(i!==r)return;let o;try{o=JSON.parse(s.toString("utf8"))}catch{return}return G(o)?{bodySize:n,meta:o}:void 0},_=async e=>{let t;try{t=await k(e,"r")}catch(a){if(I(a))return;throw a}try{const a=await K(t);if(a===void 0){await t.close();return}return{...a,handle:t}}catch(a){throw await t.close(),a}},D=new FinalizationRegistry(e=>{e.close().catch(()=>{})}),T=async e=>{const t=await _(e);if(t!==void 0)return await t.handle.close(),{bodySize:t.bodySize,meta:t.meta}},R=(e,t)=>{const{sha256Hex:a}=t,r=Buffer.from(a,"hex");return{checksums:{sha256:M(r)},customMetadata:t.customMetadata,etag:a,httpEtag:`"${a}"`,httpMetadata:t.httpMetadata,key:e,sha256:a,sha256Base64:r.toString("base64"),size:t.size,uploaded:new Date(t.uploaded)}},g=(e,t,a)=>e===void 0||!Number.isFinite(e)?t:Math.min(Math.max(0,Math.floor(e)),a),Y=(e,t)=>{if(t===void 0)return{end:e,start:0};if("suffix"in t)return{end:e,start:e-g(t.suffix,e,e)};const a=g(t.offset,0,e);return{end:t.length===void 0||!Number.isFinite(t.length)?e:Math.min(a+g(t.length,0,e),e),start:a}},tt=(e,t,a,r)=>{const n=e.createReadStream({autoClose:!1,end:a-1,start:t})[Symbol.asyncIterator]();return new ReadableStream({cancel:async()=>{await n.return?.(),await r()},pull:async s=>{let i;try{i=await n.next()}catch(o){throw await r(),o}i.done===!0?(s.close(),await r()):s.enqueue(i.value)}})},et=async(e,t,a)=>{const r=a-t;if(r<=0)return Buffer.alloc(0);const n=Buffer.alloc(r),{bytesRead:s}=await e.read(n,0,r,t);return n.subarray(0,s)},$=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*$(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 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")}},at=async e=>{const t=[],a=async(r,n)=>{let s;try{s=await P(r,{withFileTypes:!0})}catch(o){if(I(o))return;throw o}const i=[];for(const o of s){if(o.name===b)continue;const d=n===""?o.name:`${n}/${o.name}`;if(o.isDirectory())i.push(a(w(r,o.name),d));else{const u=X(d);u!==void 0&&t.push(u)}}await Promise.all(i)};return await a(e,""),t.sort((r,n)=>r<n?-1:+(r>n)),t},rt=(e,t)=>{let a=0,r=e.length;for(;a<r;){const n=a+Math.floor((r-a)/2),s=e[n];s===void 0||s<=t?a=n+1:r=n}return a},A=async e=>{try{await F(e)}catch(t){if(!I(t))throw t}},ft=e=>{const{directory:t}=e;return{delete:async a=>{p(a),await A(w(t,m(a)))},get:async(a,r)=>{p(a);const n=await _(w(t,m(a)));if(n===void 0)return null;const{bodySize:s,handle:i,meta:o}=n,{end:d,start:u}=Y(s,r?.range),f={};let c=!1;const y=async()=>{c||(c=!0,D.unregister(f),await i.close())},O=()=>{if(c)throw new l("BAD_REQUEST",`@lunora/platform-node: R2 object "${a}" body has already been consumed`)},E=async()=>{O();try{return await et(i,u,d)}finally{await y()}},S={...R(a,o),arrayBuffer:async()=>M(await E()),get body(){return O(),d<=u?(y().catch(()=>{}),new Blob([]).stream()):tt(i,u,d,y)},text:async()=>(await E()).toString("utf8")};return D.register(S,{close:y},f),S},head:async a=>{p(a);const r=await T(w(t,m(a)));return r===void 0?null:R(a,r.meta)},list:async(a={})=>{const r=Math.max(1,g(a.limit,1e3,1e3));if(a.prefix?.includes("\0"))throw new l("VALIDATION_ERROR","@lunora/platform-node: R2 list prefix contains a NUL byte");const n=await at(t),s=a.prefix??"",i=n.filter(c=>c.startsWith(s)&&(a.delimiter===void 0||!c.slice(s.length).includes(a.delimiter))),o=a.cursor===void 0?0:rt(i,a.cursor),d=i.slice(o,o+r),u=o+r<i.length,f=(await Promise.all(d.map(async c=>{const y=await T(w(t,m(c)));return y===void 0?void 0:R(c,y.meta)}))).filter(c=>c!==void 0);return{cursor:u?d.at(-1):void 0,objects:f,truncated:u}},put:async(a,r,n)=>{p(a);const s=w(t,m(a)),i=w(t,b,z());try{await N(x(s),{recursive:!0})}catch(f){throw B(a,f)}await N(x(i),{recursive:!0});const o=j("sha256");let d=0,u;try{const f=await k(i,"w");try{for await(const c of $(r))o.update(c),d+=c.byteLength,await f.write(c);u={customMetadata:n?.customMetadata,httpMetadata:n?.httpMetadata,sha256Hex:o.digest("hex"),size:d,uploaded:new Date().toISOString()},await f.write(Z(u))}finally{await f.close()}}catch(f){throw await A(i),f}try{await C(i,s)}catch(f){throw await A(i),B(a,f)}return R(a,u)}}};export{ft as createNodeR2Bucket};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import{randomUUID as k}from"node:crypto";import{deserialize as m,serialize as y}from"node:v8";import{CronExpressionParser as x}from"cron-parser";const E={backoffMultiplier:2,initialDelayMs:1e3,maxAttempts:5,maxDelayMs:6e4},O=2147483647,F=(s,i)=>{const _=i?.initialDelayMs??E.initialDelayMs,T=i?.backoffMultiplier??E.backoffMultiplier,u=i?.maxDelayMs??E.maxDelayMs;return Math.min(u,_*T**Math.max(0,s-1))},C=(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 (?, ?, ?, ?, ?)"),T=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'"),A=s.prepare("UPDATE _lunora_scheduler_jobs SET attempts = ?, scheduled_for = ? WHERE id = ?"),L=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,N=e=>e===null?void 0:m(e);let n=!1;const l=new Map,d=new Map,D=e=>({attempts:e.attempts,functionPath:e.function_path,id:e.id,scheduledFor:e.scheduled_for}),f=e=>{const t=l.get(e);t!==void 0&&(clearTimeout(t),l.delete(e))},p=(e,t)=>{f(e);const r=Math.max(0,t-Date.now());if(r>O){const o=setTimeout(()=>{p(e,t)},O);l.set(e,o);return}const a=setTimeout(()=>{w(e).catch(()=>{})},r);l.set(e,a)},w=async e=>{if(l.delete(e),!c())return;const t=T.get(e);if(t?.state!=="pending")return;const r=t.attempts+1,a=N(t.retry);try{await i.onDispatch?.(t.function_path,m(t.args),{attempts:r,id:e}),c()&&R.run(e)}catch{if(n||!c())return;const o=a?.maxAttempts??E.maxAttempts;if(r>=o){L.run(r,e);return}const g=Date.now()+F(r,a);A.run(r,g,e),p(e,g)}},h=e=>{const t=d.get(e.key);t!==void 0&&clearTimeout(t);let r;try{r=x.parse(e.expression).next().getTime()}catch{return}const a=setTimeout(()=>{d.delete(e.key),(async()=>{try{await i.onDispatch?.(e.function_path,m(e.args),{attempts:0,id:e.key})}catch{}!n&&c()&&h(e)})().catch(()=>{})},Math.max(0,r-Date.now()));d.set(e.key,a)},I={cancel:async e=>n?!1:(f(e),M.run(e).changes>0),cron:async(e,t,r)=>{if(n)throw new Error("platform closed: cannot register a cron");x.parse(e);const a={args:y(r??{}),expression:e,function_path:t,key:`${e} ${t}`};S.run(a.key,a.expression,a.function_path,a.args),h(a)},deadLetter:{list:async()=>n||!c()?[]:u.all("dead").map(e=>D(e)),requeue:async e=>{if(n||!c())return!1;const t=Date.now();return U.run(t,e).changes===0?!1:(p(e,t),!0)}},list:async()=>n||!c()?[]:u.all("pending").map(e=>D(e)),schedule:async(e,t,r)=>{if(n)throw new Error("platform closed: cannot schedule a job");const a=`node-job-${k()}`;let o;return r?.at===void 0?o=Date.now()+(r?.delayMs??0):o=typeof r.at=="number"?r.at:r.at.getTime(),_.run(a,e,y(t),o,r?.retry===void 0?null:y(r.retry)),p(a,o),{id:a,scheduledFor:o}}};for(const e of u.all("pending"))p(e.id,e.scheduled_for);for(const e of b.all())h(e);return{dispose:()=>{for(const e of l.values())clearTimeout(e);for(const e of d.values())clearTimeout(e);l.clear(),d.clear(),n=!0},scheduler:I,simulateDeadLetter:async e=>{const t=T.get(e);if(t?.state!=="pending")return!1;f(e);const r=N(t.retry);return L.run((r?.maxAttempts??E.maxAttempts)+1,e),!0}}};export{C as createNodeSchedulerHost};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import T from"better-sqlite3";const h=2147483647,p=e=>e===void 0?null:e,_=e=>({get databaseSize(){const r=e.pragma("page_count",{simple:!0}),a=e.pragma("page_size",{simple:!0});return r*a},exec:(r,...a)=>{const t=e.prepare(r),c=a.map(l=>p(l)),s=t.reader?t.all(...c):(t.run(...c),[]);return{[Symbol.iterator]:()=>s[Symbol.iterator](),one:()=>{if(s.length!==1)throw new Error(`expected exactly one row, got ${String(s.length)}`);return s[0]},toArray:()=>[...s]}}}),w=e=>({all:async(r,a)=>e.prepare(r).all(...a.map(t=>p(t))),run:async(r,a)=>({rowsAffected:e.prepare(r).run(...a.map(t=>p(t))).changes})}),y=(e,r)=>{let a,t;e.exec("CREATE TABLE IF NOT EXISTS _lunora_alarm (id INTEGER PRIMARY KEY CHECK (id = 0), scheduled_for INTEGER NOT NULL)");const c=o=>{o===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(o)},s=()=>{t!==void 0&&(clearTimeout(t),t=void 0)},l=o=>{s();const d=Math.max(0,o-Date.now());if(d>h){t=setTimeout(()=>{l(o)},h);return}t=setTimeout(()=>{a=void 0,t=void 0,e.open&&(c(void 0),(async()=>{await r?.()})().catch(()=>{}))},d)},m={delete:()=>{if(!e.open)throw new Error("platform closed: cannot delete an alarm");a=void 0,s(),c(void 0)},get:()=>a??null,set:o=>{if(!e.open)throw new Error("platform closed: cannot set an alarm");const d=typeof o=="number"?o:o.getTime();a=d,c(d),l(d)}},u=e.prepare("SELECT scheduled_for FROM _lunora_alarm WHERE id = 0").get();return u!==void 0&&(a=u.scheduled_for,l(u.scheduled_for)),{alarms:m,dispose:s}},S=(e={})=>{const r=new T(e.path??":memory:");r.pragma("journal_mode = WAL");const a=_(r),t=w(r),{alarms:c,dispose:s}=y(r,e.onAlarm),l=new Set;let m=Promise.resolve();const u=i=>{const n=m.then(i,i);return m=n.then(()=>{},()=>{}),n};let o=Promise.resolve();const d=async i=>{r.exec("BEGIN");try{const n=await i();return r.exec("COMMIT"),n}catch(n){throw r.exec("ROLLBACK"),n}},E=i=>{const n=o.then(()=>d(i),()=>d(i));return o=n.then(()=>{},()=>{}),n},f={alarms:c,asyncSql:t,runSerialized:u,shardKey:e.shardKey,sql:a,transaction:E,waitUntil:i=>{const n=i.catch(()=>{}).finally(()=>{l.delete(n)});l.add(n)}};return{database:r,dispose:()=>{s(),r.open&&r.close()},drain:async()=>{for(;l.size>0;)await Promise.all(l)},host:f}};export{S as createNodeShardHost};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{serialize as T,deserialize as u}from"node:v8";const c=a=>{a.exec("CREATE TABLE IF NOT EXISTS _lunora_kv (key TEXT PRIMARY KEY, value BLOB NOT NULL)");const E=a.prepare("SELECT value FROM _lunora_kv WHERE key = ?"),o=a.prepare("INSERT INTO _lunora_kv (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value"),t=a.prepare("DELETE FROM _lunora_kv WHERE key = ?"),s=a.prepare("SELECT key, value FROM _lunora_kv");return{delete:async e=>t.run(e).changes>0,get:async e=>{const r=E.get(e);return r===void 0?void 0:u(r.value)},list:async e=>{const r=e?.prefix??"",v=s.all(),l=new Map;for(const n of v)n.key.startsWith(r)&&l.set(n.key,u(n.value));return l},put:async(e,r)=>{o.run(e,T(r))}}};export{c as createNodeShardKvStore};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{mkdirSync as f,readdirSync as y}from"node:fs";import{join as l}from"node:path";import{createNodeShardKvStore as S}from"./createNodeShardKvStore-BfKCQTjE.mjs";import{createNodeShardHost as u}from"./createNodeShardHost-DEHe5Vcm.mjs";import{createNodeSocketHost as v}from"./createNodeSocketHost-CytowrYN.mjs";const c=".sqlite3",g=e=>e.replaceAll(/[^\w.-]/gu,r=>[...Buffer.from(r,"utf8")].map(t=>`%${t.toString(16).padStart(2,"0").toUpperCase()}`).join("")),N=e=>decodeURIComponent(e),R=(e={})=>{const r=new Map,t=new Set;if(e.directory!==void 0){f(e.directory,{recursive:!0});for(const o of y(e.directory))o.endsWith(c)&&t.add(N(o.slice(0,-c.length)))}const i=o=>{const s=r.get(o);if(s!==void 0)return s.shard;const a=e.directory===void 0?void 0:l(e.directory,`${g(o)}${c}`);let d;const{database:h,dispose:m,host:p}=u({onAlarm:()=>e.onAlarm?.(d),path:a,shardKey:o});return d={kv:S(h),shard:p,shardKey:o,sockets:v(h).socket},r.set(o,{dispose:m,shard:d}),t.add(o),d},n=o=>({fetch:async s=>{const a=i(o);return e.onFetch===void 0?new Response(o):e.onFetch(s,a)}});return{close:()=>{for(const o of r.values())o.dispose();r.clear()},directory:{get:o=>n(String(o)),getByName:o=>n(o),idForName:o=>o},listShardKeys:()=>[...t],shardFor:i}};export{R as createNodeShardRegistry};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const i=a=>({acceptWebSocket:(e,t)=>{const s=a.sockets.accept(e,void 0,t),r=e;r.serializeAttachment=c=>{s.serializeAttachment(c)},r.deserializeAttachment=()=>s.deserializeAttachment()},blockConcurrencyWhile:e=>a.shard.runSerialized(e),getWebSockets:e=>a.sockets.getSockets(e),id:{name:a.shardKey},storage:{delete:e=>a.kv.delete(e),deleteAlarm:async()=>{await a.shard.alarms.delete()},get:e=>a.kv.get(e),getAlarm:async()=>a.shard.alarms.get(),list:e=>a.kv.list(e),put:(e,t)=>a.kv.put(e,t),setAlarm:async e=>{await a.shard.alarms.set(e)},sql:{get databaseSize(){return a.shard.sql.databaseSize},exec:(e,...t)=>a.shard.sql.exec(e,...t)},transaction:e=>a.shard.transaction(e)},waitUntil:e=>{a.shard.waitUntil?.(e)}});export{i as createNodeShardState};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{serialize as u,deserialize as T}from"node:v8";import{t as v}from"./to-array-buffer-CP4NlF-y.mjs";const k=o=>{o.exec("CREATE TABLE IF NOT EXISTS _lunora_sockets (id TEXT PRIMARY KEY, attachment BLOB, tags TEXT NOT NULL DEFAULT '[]')");const g=o.prepare(`INSERT INTO _lunora_sockets (id, attachment, tags) VALUES (?, ?, ?)
|
|
2
|
-
ON CONFLICT (id) DO UPDATE SET attachment = excluded.attachment, tags = excluded.tags`),E=o.prepare("UPDATE _lunora_sockets SET attachment = ? WHERE id = ?"),m=o.prepare("UPDATE _lunora_sockets SET tags = ? WHERE id = ?"),p=o.prepare("SELECT attachment, tags FROM _lunora_sockets WHERE id = ?"),h=o.prepare("DELETE FROM _lunora_sockets WHERE id = ?"),r=new Map,d=new WeakMap,f=(a,e)=>{o.open&&E.run(e===void 0?null:u(e),a)},i=(a,e)=>{o.open&&m.run(JSON.stringify([...e]),a)},c=a=>{const e=a,t={bufferedAmount:e.bufferedAmount,close:(n,s)=>{e.closed=!0,r.delete(e.id),o.open&&h.run(e.id)},deserializeAttachment:()=>e.attachment,send:n=>{e.received.push(typeof n=="string"?n:v(n))},serializeAttachment:n=>{e.attachment=n,f(e.id,n)}};return e.handle=t,d.set(t,e.id),t};return{readFrames:a=>(r.get(d.get(a)??"")?.received??[]).filter(e=>typeof e=="string"),restoreSocket:(a,e)=>{const t=o.open?p.get(a):void 0,n=t?.attachment===null?void 0:t?.attachment,s={attachment:n===void 0?e:T(n),bufferedAmount:0,closed:!1,handle:void 0,id:a,raw:void 0,received:[],tags:new Set(t===void 0?[]:JSON.parse(t.tags))};return r.set(a,s),c(s)},simulateRecycle:()=>{r.clear()},socket:{accept:(a,e,t)=>{const n=crypto.randomUUID(),s=new Set(t),l={attachment:e,bufferedAmount:0,closed:!1,handle:void 0,id:n,raw:a,received:[],tags:s};return r.set(n,l),o.open&&g.run(n,e===void 0?null:u(e),JSON.stringify([...s])),c(l)},getSockets:a=>{const e=[...r.values()];return(a===void 0?e:e.filter(t=>t.tags.has(a))).map(t=>t.handle)},handleFor:a=>[...r.values()].find(e=>e.raw===a)?.handle,idFor:a=>{const e=d.get(a);if(e===void 0)throw new Error("@lunora/platform-node: idFor called with a handle this host never issued");return e},removeTag:(a,e)=>{const t=r.get(d.get(a)??"");t!==void 0&&(e===void 0?t.tags.clear():t.tags.delete(e),i(t.id,t.tags))},setTag:(a,e)=>{const t=r.get(d.get(a)??"");t!==void 0&&(t.tags.add(e),i(t.id,t.tags))}}}};export{k as createNodeSocketHost};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as d}from"@lunora/errors";import{isWorkflowDefinition as k,workflowDefaultName as N,createWorkflowRunContext as E,workflowBindingName as A}from"@lunora/workflow";import{defineWorkflow as D,createRuntime as O}from"@visulima/workflow";const c="@lunora/platform-node:terminated",T={d:864e5,day:864e5,days:864e5,h:36e5,hour:36e5,hours:36e5,m:6e4,millisecond:1,milliseconds:1,minute:6e4,minutes:6e4,ms:1,s:1e3,second:1e3,seconds:1e3,w:6048e5,week:6048e5,weeks:6048e5,month:2592e6,months:2592e6},b=/^(\d+(?:\.\d+)?)\s*([a-z]+)$/i,I=n=>{if(typeof n=="number"){if(!Number.isFinite(n))throw new d("VALIDATION_ERROR",`@lunora/platform-node: workflow duration must be finite, got ${String(n)}`);return Math.max(0,n)}const r=b.exec(n.trim());if(r===null)throw new d("VALIDATION_ERROR",`@lunora/platform-node: unsupported workflow duration "${n}"`);const a=Number(r[1]),i=(r[2]??"").toLowerCase(),u=T[i];if(u===void 0)throw new d("VALIDATION_ERROR",`@lunora/platform-node: unsupported workflow duration "${n}"`);const l=a*u;if(!Number.isFinite(l))throw new d("VALIDATION_ERROR",`@lunora/platform-node: workflow duration "${n}" overflows`);return Math.max(0,l)},p="@lunora/platform-node:alias",$=n=>({attempt:1,config:{retries:{limit:1}},step:{count:1,name:n}}),_=n=>{switch(n){case"completed":return"complete";case"failed":return"errored";default:return"waiting"}},x=n=>({do:(async(r,a,i,u)=>{const l=typeof a=="function"?a:i;if(typeof l!="function")throw new d("VALIDATION_ERROR",`@lunora/platform-node: step.do("${r}") requires a callback`);return n.step(r,async()=>l($(r)))}),sleep:async(r,a)=>{const i=I(a);i!==0&&await n.sleep(r,i)},sleepUntil:async(r,a)=>{const i=typeof a=="number"?a:a.getTime();if(!Number.isFinite(i))throw new d("VALIDATION_ERROR",`@lunora/platform-node: step.sleepUntil("${r}") needs a valid timestamp`);const u=Math.max(0,i-Date.now());u!==0&&await n.sleep(r,u)},waitForEvent:(async(r,a)=>({payload:await n.waitForEvent(r,a.type,{timeout:a.timeout===void 0?void 0:I(a.timeout)}),type:a.type}))}),q=n=>{const r=Object.entries(n.workflows).map(([e,t])=>{if(!k(t))throw new d("VALIDATION_ERROR",`@lunora/platform-node: "${e}" is not a defineWorkflow result`);return{definition:t,exportName:e,id:t.name??N(e)}}),{store:a}=n,i={...n.env},u=r.map(({definition:e,exportName:t,id:o})=>D({id:o,tags:[t],run:async s=>{const f=x(s),v=E({env:i,event:{instanceId:s.runId,payload:s.payload,timestamp:new Date,workflowName:o},exportName:t,step:f});return await e.handler(v)}})),l=new Set,R={acquire:a.acquire===void 0?void 0:async(e,t,o)=>a.acquire?.(e,t,o)??!1,delete:async e=>a.delete(e),due:async(e,t)=>a.due(e,t),load:async e=>{const t=await a.load(e);return t===void 0||t.definitionId===c||t.definitionId===p?void 0:t},release:a.release===void 0?void 0:async(e,t)=>a.release?.(e,t),save:async e=>{l.has(e.runId)||await a.save(e)}},w=O({leaseTtlMs:n.leaseTtlMs,store:R,workflows:u}),m=e=>({id:e,pause:()=>Promise.reject(new d("NOT_IMPLEMENTED",`@lunora/platform-node: workflow instance "${e}" cannot be paused — the visulima engine has no pause equivalent`)),restart:()=>Promise.reject(new d("NOT_IMPLEMENTED",`@lunora/platform-node: workflow instance "${e}" cannot be restarted — the visulima engine has no restart equivalent`)),resume:async()=>{if((await a.load(e))?.definitionId===c)throw new d("BAD_REQUEST",`@lunora/platform-node: workflow instance "${e}" was terminated and cannot be resumed`);await w.resume(e)},sendEvent:async t=>{const o=await w.getRun(e);if(o?.status!=="waiting"||o.pending?.kind!=="event"||o.pending.eventName!==t.type)throw new d("BAD_REQUEST",`@lunora/platform-node: workflow instance "${e}" is not waiting for event "${t.type}"`);await w.signal(e,t.type,t.payload)},status:async()=>{const t=await a.load(e);if(t===void 0)return{status:"unknown"};if(t.definitionId===c)return{status:"terminated"};const o=await w.getRun(e);return o===void 0?{status:"unknown"}:{error:o.error===void 0?void 0:{message:o.error.message,name:o.error.name},output:o.output,status:_(o.status)}},terminate:async()=>{await a.load(e)!==void 0&&(l.add(e),await a.delete(e),await a.save({definitionId:c,runId:e,snapshot:void 0,status:"failed",updatedAt:Date.now()}))}}),g=async e=>{const t=await a.load(e);return t?.definitionId===p?t.snapshot:e},y=async(e,t)=>{if(t?.id===void 0){const f=await w.trigger(e,t?.params);return m(f.runId)}const o=await a.load(t.id);if(o?.definitionId===p)return m(o.snapshot);if(o!==void 0)throw new d("BAD_REQUEST",`@lunora/platform-node: workflow create({ id: "${t.id}" }) names an existing run — pass the id a previous create returned, or a fresh one`);const s=await w.trigger(e,t.params);return await a.save({definitionId:p,runId:t.id,snapshot:s.runId,status:"failed",updatedAt:Date.now()}),m(s.runId)},h={};for(const{exportName:e,id:t}of r){const o={create:async s=>y(t,s),createBatch:async s=>{const f=[];for(const v of s)f.push(await y(t,v));return f},get:async s=>m(await g(s))};h[e]=o,i[A(e)]=o}return{bindings:h,env:i,runtime:w}};export{q as createNodeWorkflowHost};
|