@lunora/platform-node 1.0.0-alpha.64 → 1.0.0-alpha.65

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.
@@ -1 +1 @@
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};
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-CfX2IrZ8.mjs";import{createNodeShardRegistry as H}from"../packem_shared/createNodeShardRegistry-Dzqi0Q8l.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
@@ -221,25 +221,59 @@ interface NodeShardHostOptions {
221
221
  /**
222
222
  * Build a `ShardHost` over a real `better-sqlite3` database.
223
223
  *
224
- * `runSerialized` chains onto a single `tail` promise rather than the
225
- * reference host's explicit job-array-plus-drain-loop: every queued closure
226
- * runs once `tail` settles, and `tail` is reset to a version that always
227
- * resolves (never rejects) so one job's failure cannot wedge the queue for
228
- * every job after it — the same "no two closures interleave" guarantee,
229
- * fewer moving parts to get wrong.
224
+ * `runSerialized` and `transaction` share **one** boundary lock, because they
225
+ * write **one** `better-sqlite3` connection. `transaction` issues raw
226
+ * `BEGIN`/`COMMIT`/`ROLLBACK` — legal here (unlike inside a Cloudflare Durable
227
+ * Object, where the runtime forbids it and callers must use
228
+ * `storage.transaction`) because better-sqlite3 is a plain embedded database
229
+ * with no platform-level transaction primitive layered over it.
230
230
  *
231
- * `transaction` issues raw `BEGIN`/`COMMIT`/`ROLLBACK` — legal here (unlike
232
- * inside a Cloudflare Durable Object, where the runtime forbids it and
233
- * callers must use `storage.transaction`) because better-sqlite3 is a plain
234
- * embedded database with no platform-level transaction primitive layered over
235
- * it. It runs on its own private `transactionTail` chain, the same shape as
236
- * `runSerialized`'s but never shared with it: two bare, overlapping
237
- * `transaction()` calls on this host serialize against each other rather than
238
- * corrupting each other's commits (raw `BEGIN` on a connection already inside
239
- * a transaction throws, or worse, interleaves). Routing `transaction` through
240
- * `runSerialized` itself would deadlock, since the engine already composes
241
- * `runSerialized(() => transaction(work))` — the inner enqueue would then wait
242
- * on the outer closure awaiting it.
231
+ * ## Why one lane and not two
232
+ *
233
+ * This host shipped with two private tail chains, one per entry point, so each
234
+ * serialized against itself and neither against the other. `runSerialized`
235
+ * issues no `BEGIN` of its own, so one of its closures overlapping a bare
236
+ * `transaction()` wrote straight into that transaction's span. `assertOwnTurn`
237
+ * below catches the write — it is the only reason this host never silently lost
238
+ * one the way an unguarded connection would — but catching it is not the same
239
+ * as preventing it, and the refusal lands in the wrong place:
240
+ *
241
+ * 1. A `runSerialized` closure writes a row. Nothing is open, so it commits.
242
+ * 2. It awaits (a scheduler hop, an `onShardInit` read, any real I/O).
243
+ * 3. A bare `transaction()` runs `BEGIN` while it is parked. `transaction` is
244
+ * a public contract surface, and `./node-shard-state` re-exports it as the
245
+ * `storage.transaction` member `ShardDO` documents as "the platform
246
+ * primitive", so a subclass reaches one without going through the engine.
247
+ * 4. The closure resumes and its next write is refused with
248
+ * `SHARD_UNAVAILABLE`, so `runSerialized` **rejects** — with step 1 already
249
+ * durable.
250
+ *
251
+ * The boundary the contract promises is atomic-or-nothing to its caller tore in
252
+ * half: a rejection the caller will retry, on top of a write the retry now
253
+ * re-applies. That is `runSerialized`'s "no two closures interleave" guarantee
254
+ * broken by a `transaction` it was never serialized against.
255
+ *
256
+ * ## Why the lock is re-entrant, and why `AsyncLocalStorage` is what makes it so
257
+ *
258
+ * The engine composes the two, and stacks them: `ShardRunner.runInTransaction`
259
+ * is `runSerialized(() => transaction(work))` (`@lunora/shard-engine`), and
260
+ * `ShardDO.fetch` widens a mutation dispatch carrying `x-lunora-mutation-id`
261
+ * into a *further* `runSerialized` span around it (`@lunora/do`). Under two
262
+ * plain FIFO chains that outer span is already fatal on this host — the inner
263
+ * `runSerialized` waits on a `tail` that only settles when the outer closure it
264
+ * is running inside resolves, and because nothing ever resets that `tail`, the
265
+ * shard's write lane is wedged for the life of the process rather than for the
266
+ * life of the request.
267
+ *
268
+ * So the lock is skipped for a boundary opened from inside a boundary, and
269
+ * `AsyncLocalStorage` is the only thing that can tell that case from the one
270
+ * that must queue: its store follows a single call's own await chain without
271
+ * leaking into a sibling chain. A held boolean cannot — it reads `true` for an
272
+ * unrelated *second top-level* `transaction()` too, which would then nest a raw
273
+ * `BEGIN` on a connection already inside one. This is the same distinction
274
+ * workerd's `blockConcurrencyWhile` draws natively with "does not queue events
275
+ * initiated as part of the callback itself", which is what
276
+ * `@lunora/platform-cloudflare` leans on for the identical composition.
243
277
  *
244
278
  * The returned `dispose()` is this host's lifecycle owner: it clears the
245
279
  * pending alarm `setTimeout` (so it can never fire against a connection this
package/dist/index.d.ts CHANGED
@@ -221,25 +221,59 @@ interface NodeShardHostOptions {
221
221
  /**
222
222
  * Build a `ShardHost` over a real `better-sqlite3` database.
223
223
  *
224
- * `runSerialized` chains onto a single `tail` promise rather than the
225
- * reference host's explicit job-array-plus-drain-loop: every queued closure
226
- * runs once `tail` settles, and `tail` is reset to a version that always
227
- * resolves (never rejects) so one job's failure cannot wedge the queue for
228
- * every job after it — the same "no two closures interleave" guarantee,
229
- * fewer moving parts to get wrong.
224
+ * `runSerialized` and `transaction` share **one** boundary lock, because they
225
+ * write **one** `better-sqlite3` connection. `transaction` issues raw
226
+ * `BEGIN`/`COMMIT`/`ROLLBACK` — legal here (unlike inside a Cloudflare Durable
227
+ * Object, where the runtime forbids it and callers must use
228
+ * `storage.transaction`) because better-sqlite3 is a plain embedded database
229
+ * with no platform-level transaction primitive layered over it.
230
230
  *
231
- * `transaction` issues raw `BEGIN`/`COMMIT`/`ROLLBACK` — legal here (unlike
232
- * inside a Cloudflare Durable Object, where the runtime forbids it and
233
- * callers must use `storage.transaction`) because better-sqlite3 is a plain
234
- * embedded database with no platform-level transaction primitive layered over
235
- * it. It runs on its own private `transactionTail` chain, the same shape as
236
- * `runSerialized`'s but never shared with it: two bare, overlapping
237
- * `transaction()` calls on this host serialize against each other rather than
238
- * corrupting each other's commits (raw `BEGIN` on a connection already inside
239
- * a transaction throws, or worse, interleaves). Routing `transaction` through
240
- * `runSerialized` itself would deadlock, since the engine already composes
241
- * `runSerialized(() => transaction(work))` — the inner enqueue would then wait
242
- * on the outer closure awaiting it.
231
+ * ## Why one lane and not two
232
+ *
233
+ * This host shipped with two private tail chains, one per entry point, so each
234
+ * serialized against itself and neither against the other. `runSerialized`
235
+ * issues no `BEGIN` of its own, so one of its closures overlapping a bare
236
+ * `transaction()` wrote straight into that transaction's span. `assertOwnTurn`
237
+ * below catches the write — it is the only reason this host never silently lost
238
+ * one the way an unguarded connection would — but catching it is not the same
239
+ * as preventing it, and the refusal lands in the wrong place:
240
+ *
241
+ * 1. A `runSerialized` closure writes a row. Nothing is open, so it commits.
242
+ * 2. It awaits (a scheduler hop, an `onShardInit` read, any real I/O).
243
+ * 3. A bare `transaction()` runs `BEGIN` while it is parked. `transaction` is
244
+ * a public contract surface, and `./node-shard-state` re-exports it as the
245
+ * `storage.transaction` member `ShardDO` documents as "the platform
246
+ * primitive", so a subclass reaches one without going through the engine.
247
+ * 4. The closure resumes and its next write is refused with
248
+ * `SHARD_UNAVAILABLE`, so `runSerialized` **rejects** — with step 1 already
249
+ * durable.
250
+ *
251
+ * The boundary the contract promises is atomic-or-nothing to its caller tore in
252
+ * half: a rejection the caller will retry, on top of a write the retry now
253
+ * re-applies. That is `runSerialized`'s "no two closures interleave" guarantee
254
+ * broken by a `transaction` it was never serialized against.
255
+ *
256
+ * ## Why the lock is re-entrant, and why `AsyncLocalStorage` is what makes it so
257
+ *
258
+ * The engine composes the two, and stacks them: `ShardRunner.runInTransaction`
259
+ * is `runSerialized(() => transaction(work))` (`@lunora/shard-engine`), and
260
+ * `ShardDO.fetch` widens a mutation dispatch carrying `x-lunora-mutation-id`
261
+ * into a *further* `runSerialized` span around it (`@lunora/do`). Under two
262
+ * plain FIFO chains that outer span is already fatal on this host — the inner
263
+ * `runSerialized` waits on a `tail` that only settles when the outer closure it
264
+ * is running inside resolves, and because nothing ever resets that `tail`, the
265
+ * shard's write lane is wedged for the life of the process rather than for the
266
+ * life of the request.
267
+ *
268
+ * So the lock is skipped for a boundary opened from inside a boundary, and
269
+ * `AsyncLocalStorage` is the only thing that can tell that case from the one
270
+ * that must queue: its store follows a single call's own await chain without
271
+ * leaking into a sibling chain. A held boolean cannot — it reads `true` for an
272
+ * unrelated *second top-level* `transaction()` too, which would then nest a raw
273
+ * `BEGIN` on a connection already inside one. This is the same distinction
274
+ * workerd's `blockConcurrencyWhile` draws natively with "does not queue events
275
+ * initiated as part of the callback itself", which is what
276
+ * `@lunora/platform-cloudflare` leans on for the identical composition.
243
277
  *
244
278
  * The returned `dispose()` is this host's lifecycle owner: it clears the
245
279
  * pending alarm `setTimeout` (so it can never fire against a connection this
package/dist/index.mjs CHANGED
@@ -1 +1 @@
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};
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-BnplJiAw.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-CfX2IrZ8.mjs";import{createNodeShardRegistry as k}from"./packem_shared/createNodeShardRegistry-Dzqi0Q8l.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};
@@ -1 +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};
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-CfX2IrZ8.mjs";import{createNodeShardRegistry as B}from"./createNodeShardRegistry-Dzqi0Q8l.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 @@
1
+ import{AsyncLocalStorage as y}from"node:async_hooks";import{LunoraError as v}from"@lunora/errors";import L from"better-sqlite3";const S=2147483647,R=6,O=100,I=e=>e===void 0?null:e,M=(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(d=>I(d)),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]}}}),N=(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 d=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 l=Math.max(0,n-Date.now());if(l>S){a=setTimeout(()=>{m(n)},S);return}a=setTimeout(()=>{r=void 0,a=void 0,e.open&&(d(void 0),(async()=>{await c?.(),s=0})().catch(()=>{if(!e.open||r!==void 0)return;if(s+=1,s>R){s=0;return}const h=Date.now()+O*2**(s-1);r=h,d(h),m(h)}))},l)},f={delete:()=>{if(!e.open)throw new Error("platform closed: cannot delete an alarm");if(t.assertOwnTurn("delete an alarm"),s=0,d(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 l=typeof n=="number"?n:n.getTime();if(s=0,d(l),t.inside()){o={at:l};return}r=l,m(l)}},w=()=>{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)},T=()=>{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:f,commit:w,dispose:p,rollback:T}},k=(e={})=>{const t=new L(e.path??":memory:");t.pragma("journal_mode = WAL");const c=new y;let r=!1;const a=i=>{if(r&&c.getStore()!==!0)throw new v("SHARD_UNAVAILABLE",`shard busy: cannot ${i} while another task holds this shard's transaction`)},s=M(t,a),{alarms:o,commit:d,dispose:p,rollback:m}=N(t,{assertOwnTurn:a,inside:()=>c.getStore()===!0},e.onAlarm),f=new Set,w=new y;let T=Promise.resolve();const E=async i=>{if(w.getStore()===!0)return await i();const u=T;let A=()=>{};T=new Promise(g=>{A=g}),await u;try{return await w.run(!0,i)}finally{A()}},n=i=>E(i),l=async i=>{t.exec("BEGIN"),r=!0;try{const u=await c.run(!0,i);return t.exec("COMMIT"),r=!1,d(),u}catch(u){r=!1,m();try{t.exec("ROLLBACK")}catch{}throw u}},h=i=>E(async()=>l(i)),_={alarms:o,runSerialized:n,shardKey:e.shardKey,sql:s,transaction:h,waitUntil:i=>{const u=i.catch(()=>{}).finally(()=>{f.delete(u)});f.add(u)}};return{database:t,dispose:()=>{p(),t.open&&t.close()},drain:async()=>{for(;f.size>0;)await Promise.all(f)},host:_}};export{k as createNodeShardHost};
@@ -1 +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};
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-CfX2IrZ8.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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/platform-node",
3
- "version": "1.0.0-alpha.64",
3
+ "version": "1.0.0-alpha.65",
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",
@@ -1 +0,0 @@
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};