@lunora/platform 1.0.0-alpha.3 → 1.0.0-alpha.30

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/index.mjs CHANGED
@@ -1 +1 @@
1
- import{NOOP_EXECUTION_CONTEXT as E}from"./packem_shared/NOOP_EXECUTION_CONTEXT-YmXqH-jH.mjs";import{CLOUDFLARE_CAPABILITIES as O,NODE_CAPABILITIES as e}from"./packem_shared/CLOUDFLARE_CAPABILITIES-DMLgo_TI.mjs";import{resolveShard as C}from"./packem_shared/resolveShard-uGhAKuTB.mjs";export{O as CLOUDFLARE_CAPABILITIES,e as NODE_CAPABILITIES,E as NOOP_EXECUTION_CONTEXT,C as resolveShard};
1
+ import{NOOP_EXECUTION_CONTEXT as E}from"./packem_shared/NOOP_EXECUTION_CONTEXT-YmXqH-jH.mjs";import{CLOUDFLARE_CAPABILITIES as O,NODE_CAPABILITIES as e}from"./packem_shared/CLOUDFLARE_CAPABILITIES-DsJnO_SH.mjs";import{resolveShard as C}from"./packem_shared/resolveShard-BzKOUEO4.mjs";export{O as CLOUDFLARE_CAPABILITIES,e as NODE_CAPABILITIES,E as NOOP_EXECUTION_CONTEXT,C as resolveShard};
@@ -0,0 +1 @@
1
+ const e={id:"cloudflare",name:"Cloudflare",features:{shardedState:{level:"native",note:"Durable Objects with SQLite"},globalTables:{level:"native",note:"D1 with Sessions API. D1 has a documented, expected baseline error rate — Cloudflare's own team calls a handful of transient errors every few hours 'not unexpected' on a healthy database — so read-only statements are retried automatically; writes are not, because every one of those errors is ambiguous about whether the statement applied and D1 has no interactive transactions to resolve it"},websocketHibernation:{level:"native",note:"DO WebSocket hibernation"},durableStreams:{level:"emulated",note:"Lunora persists each chunk to the shard's SQLite under a monotonic seq and keeps the producer alive past the socket via waitUntil; the platform has no streaming primitive of its own, and a run whose DO is evicted mid-flight ends as STREAM_INTERRUPTED rather than resuming"},commitOrderedTables:{level:"native",note:"`state.storage.transaction` makes the `__commit_seq` bump atomic with the rows it stamps, and a Durable Object executes one event at a time — so the allocation order IS the commit order, with no lock of ours in the path"},localSql:{level:"native",note:"state.storage.sql (SQLite)"},serverReactors:{level:"emulated",note:"The wake-up is Lunora's, not the platform's: reactors ride the existing post-write refresh drain, which already exists to push subscription frames. Cloudflare supplies the two properties that make it correct — one event at a time per Durable Object, and `waitUntil` to keep the drain alive past the response — but has no notion of a server-side subscription of its own"},memoryTables:{level:"emulated",note:"The lifetime is real — an eviction drops the DO's heap and the framework clears every `.memory()` table on reconstruction, so the rows behave exactly like heap state, and their writes stay out of the CDC changelog. The STORAGE is not: workerd exposes one SQL handle and no memory-backed database, so a memory row is still written to the DO's SQLite and then deleted. `.memory()` buys the semantics, not the write"},shardAlarms:{level:"native",note:"state.storage.setAlarm"},shardPlacement:{level:"native",note:"DurableObjectNamespace.get/getByName locationHint — best-effort, and honoured only by the resolution that creates the object"},shardReadReplicas:{level:"emulated",note:"Lunora follows the shard's CDC changelog into a replica DO placed in the reader's region; the platform replicates for durability, not for reads, so the follow loop is ours"},crossShardFanout:{level:"emulated",note:"Lunora query coordinator + relay tier over Durable Objects"},queues:{level:"native",note:"Cloudflare Queues"},relationGraph:{level:"emulated",note:"The graph is Lunora's, built on reads Cloudflare already serves: the edge set is derived from the schema's v.id(...) columns, and each hop is one batched WHERE ... IN (...) against the shard's SQLite, all inside the Durable Object's single-threaded request. There is no graph engine being consumed — workerd offers none — so native would misreport who does the work"},workflows:{level:"native",note:"Cloudflare Workflows"},scheduler:{level:"emulated",note:"SchedulerDO (Lunora, on DO alarms) + declarative Cron Triggers; no runtime cron registration"},cronTriggers:{level:"native",note:"wrangler triggers.crons, reconciled from the declared crons at build time, delivered to the worker's scheduled() handler — which is the one cron dispatch that ships: it walks the generated LUNORA_CRONS map itself"},agents:{level:"emulated",note:"The durable agent loop is Lunora's: each defineAgent compiles onto a Cloudflare Workflow under an AGENT_* binding (a voice-enabled agent additionally gets a VoiceSessionDO), and the loop drives Workers AI. Cloudflare supplies the workflow engine, the Durable Object and the inference; the agent is built on them, not consumed as a product"},objectStorage:{level:"native",note:"R2"},objectStorageBackups:{level:"emulated",note:"`lunora backup create|list|restore --bucket` writes NDJSON snapshots + a manifest sidecar per snapshot through the admin storage routes (checksum-verified upload, admin-gated object read), and `backupCron`/`backupStore` runs the same layout unattended on a Cron Trigger. Both are bounded by what a single request body / a Worker isolate can hold, not by R2. `emulated` because every part of that is Lunora's — R2 supplies a bucket, and Cloudflare has no backup product being consumed here; the snapshot format, the manifest, the checksum gate and the retention report are all ours"},objectStorageCdcArchive:{level:"emulated",note:"R2 supplies the bucket and the `startAfter` listing the segment keys are indexed on; everything above that is Lunora's — the segment format, the archive-before-trim ordering the sweep defers behind `waitUntil`, and the de-overlapping read-back. The platform has no notion of a changelog to tier, so this is not a product being consumed"},keyValueStore:{level:"native",note:"Workers KV"},vectorStore:{level:"native",note:"Vectorize; query/upsert namespace scoping is native (remote filter), but getByIds/deleteByIds id-path tenant isolation is facade-enforced (client-side verification) since Vectorize's id operations take no namespace option"},ai:{level:"native",note:"Workers AI"},browser:{level:"native",note:"Browser Rendering"},images:{level:"native",note:"Cloudflare Images binding"},containers:{level:"native",note:"Cloudflare Containers; ctx.containers.<name>.exec rides the same binding over the /__lunora/exec contract, which the container image serves"},analytics:{level:"native",note:"Analytics Engine"},pipelines:{level:"native",note:"Cloudflare Pipelines"},mail:{level:"emulated",note:"Resend (third-party) via Cloudflare Queues"},secrets:{level:"native",note:"Secrets Store"},hyperdrive:{level:"native",note:"Cloudflare Hyperdrive"},httpCache:{level:"native",note:"The colo cache via caches.default. Worker-generated responses are NOT stored by it automatically — the runtime has to caches.default.put() them — and it honours Vary for Accept-Encoding only, so a varying response has to fold those header values into the cache key itself. A 206, a Vary: *, or a Set-Cookie-bearing response is refused by put()"},identityProxy:{level:"native",note:"Cloudflare Access. A policy attached to the Worker covers its custom domains, routes, workers.dev and preview URLs at once, and the authenticated identity arrives on the execution context as ctx.access — no header to verify, and nothing a request can forge to manufacture one. A hostname-scoped Access application instead stamps the Cf-Access-Jwt-Assertion header, which needs no host support at all"}}},t={id:"node",name:"Node",features:{shardedState:{level:"emulated",note:"One better-sqlite3 database per shard key, one process — no distributed placement or failover. Shard keys are percent-encoded into basenames with A-Z escaped, so `Tenant` and `tenant` stay two databases on a case-insensitive volume (APFS, NTFS) rather than folding into one. There is also no input gate: Cloudflare defers every other dispatch for the span of a mutation, whereas this host can only refuse — SQL issued from another task while a transaction is open throws a retryable `SHARD_UNAVAILABLE` (503) rather than reading rows that are about to roll back, so a read that merely arrived mid-mutation is retried instead of failing the request"},globalTables:{level:"emulated",note:"The @lunora/sql-store core on its own SQLite file via the reference sqliteDialect — full store semantics, but one node with no replication"},websocketHibernation:{level:"emulated",note:"Socket registry with attachments/tags persisted to SQLite, so subscription state survives a process restart; nothing is ever actually evicted from memory, so this is durability without hibernation's memory saving"},durableStreams:{level:"unsupported",note:"The transcript store is host-neutral (@lunora/shard-engine), but the attach/produce state machine lives in @lunora/do and nothing in this host mounts it. Gate-bearing: codegen refuses an app that declares a durable stream on this target, rather than emitting one that silently behaves as an ephemeral stream"},commitOrderedTables:{level:"emulated",note:"The sequence orders commits correctly, but the serialization it depends on is Lunora's per-shard write gate rather than a platform property — one process, one better-sqlite3 handle per shard key. Correct here; not something the host guarantees the way a Durable Object does"},localSql:{level:"native",note:"better-sqlite3 (synchronous, embedded)"},serverReactors:{level:"emulated",note:"Same engine-level implementation as Cloudflare; the per-shard serialization it depends on is the host's own write gate rather than a platform guarantee"},memoryTables:{level:"emulated",note:"Same shape as Cloudflare and for a different reason: better-sqlite3 CAN open `:memory:`, but a shard's memory tables share the one handle its durable tables use, so they are cleared rather than never written. A host process also outlives far more than a Durable Object does, so cold starts — and therefore `onShardInit` — are much rarer here than in production on Cloudflare; do not use this target to judge how often a memory table is actually empty"},shardAlarms:{level:"emulated",note:"setTimeout over a durable row, dispatched to onAlarm and re-armed on construction, so an alarm survives a restart and one whose time elapsed while the process was down fires late rather than never. Delivery is at-least-once as it is on workerd: a handler that throws is re-delivered with exponential backoff (6 attempts, from 100ms) and then abandoned, and an alarm set or deleted inside a transaction is armed only if that transaction commits"},shardPlacement:{level:"unsupported",note:"One process — every shard lives where the process does, so a location hint has nowhere to place it"},shardReadReplicas:{level:"unsupported",note:"One process and one region: a replica here would be a second copy of a database already on the same disk"},crossShardFanout:{level:"emulated",note:"@lunora/runtime's query coordinator over the in-process shard registry; listShardKeys is seeded from the shard files on disk, and answers every shard rather than only those holding the table (a correct superset, at the cost of visiting shards with nothing to say)"},queues:{level:"emulated",note:`createNodeQueueHost (@lunora/platform-node) — a QueueBindingLike producer per declared queue over a durable _lunora_queue_messages table, and a batched consumer feeding the same dispatchQueueBatch the Cloudflare host uses. delaySeconds (capped at 12h), all four content types, maxBatchSize/maxBatchTimeout assembly, per-message ack/retry with workerd's implicit-ack-on-return and retry-on-throw, maxRetries into a declared deadLetterQueue (or parked in place, never dropped), and a visibility window so a crash mid-handler redelivers. Delivery is driven by poll(); there is no timer, because this host has no dev server to own one. mode: "pull" queues are written but not consumed — nothing here serves the HTTP pull endpoint`},workflows:{level:"emulated",note:"createNodeWorkflowHost (@lunora/platform-node) compiles defineWorkflow handlers onto the @visulima/workflow engine (createRuntime): step/sleep/waitForEvent are durable + replay-safe, status maps to complete/errored/waiting/terminated, create({ id }) is honoured through a durable alias row (so ctx.spawn resolves and a retried create is one run), and runs survive a restart when backed by createNodeWorkflowStore (a SQLite WorkflowStore; the store is required, so no caller silently gets in-process-only state). terminate is a barrier within the process: a terminated run's writes are dropped, so an activation already in flight cannot overwrite the tombstone — it is not a barrier across processes, which would need the lease rather than a set. Gaps: no pause/restart; ctx.run dispatches to an endpoint no Node HTTP server serves; ctx.parallel's synchronous join cannot interleave within one trigger activation"},scheduler:{level:"emulated",note:"SQLite job table dispatched to onDispatch and re-armed on construction, with retry backoff and a dead-letter queue. It is also the only host implementing runtime cron registration (SchedulerHost.cron), which Cloudflare cannot offer — but nothing walks an app's DECLARED crons into that method, which is why cronTriggers is rated separately and unsupported here. This rating covers the imperative surface only: ctx.scheduler.runAfter/runAt do dispatch on this host"},relationGraph:{level:"emulated",note:"Identical to Cloudflare: the same engine-level traversal over the same ctx.db reads, served here by better-sqlite3 through this host's per-shard handle. One process and one disk, so a deep expansion is if anything cheaper than on workerd; what it is not is a platform feature"},cronTriggers:{level:"unsupported",note:"No runtime walks the generated LUNORA_CRONS map into SchedulerHost.cron, so the conformance suite is that method's only caller and a declared cron does not fire on this host. Gate-bearing: codegen refuses an app that declares one here rather than letting it deploy green and never run. Schedule the work explicitly with ctx.scheduler.runAfter/runAt instead"},agents:{level:"unsupported",note:"Nothing here mounts the generated agent classes: createNodeWorkflowHost compiles defineWorkflow handlers onto the @visulima/workflow engine, and an agent is a generated WorkflowEntrypoint resolved off an AGENT_ prefixed env binding this host never provides. The loop's inference has no home either — ai is unsupported on this target"},objectStorageBackups:{level:"emulated",note:"The commands work unchanged, but the bucket underneath is createNodeR2Bucket — a directory on the same machine the CLI runs on, so a bucket-backed backup here is not the separate failure domain it is on Cloudflare. The scheduled half additionally needs this host's scheduler, which exists but is not a shipping target"},objectStorageCdcArchive:{level:"emulated",note:"createNodeR2Bucket implements the `startAfter` seek the segment index needs, so the read-back behaves as it does on R2. Same caveat as the backups above: the bucket is a directory on the machine running the host, so archiving the changelog here moves it off SQLite but not off the disk that would take the shard with it"},objectStorage:{level:"emulated",note:"createNodeR2Bucket (@lunora/platform-node) — an R2BucketLike over the local filesystem (fs/promises, head/list/range). One file per object with the metadata in a trailer, so the single rename that publishes the bytes publishes their checksum and content-type with them, and a get reads body and metadata through one handle rather than reopening the path. put streams into the staged file and .body streams the requested range; .arrayBuffer()/.text() still allocate the range they return. The body is single-use, as R2's is. Keys are percent-escaped per path segment (`%`, `A-Z`, `:`, and a trailing `.` or space), so `A` and `a` stay two objects on a case-insensitive volume exactly as they are on R2, and a lowercase key containing no `%` or `:` and no segment ending in `.` or a space still maps to a byte-identical filename. No multipart uploads, no presigned URLs"},keyValueStore:{level:"emulated",note:"better-sqlite3 table behind the ShardKvStore API — not a dedicated KV product"},vectorStore:{level:"unsupported",note:"No Vectorize-equivalent binding implemented"},ai:{level:"unsupported",note:"No Workers AI-equivalent binding implemented"},browser:{level:"unsupported",note:"No headless-browser binding implemented"},images:{level:"unsupported",note:"No Images-equivalent binding implemented"},containers:{level:"unsupported",note:"No container orchestration implemented, so there is nothing for ctx.containers.<name>.exec to run a command in either"},analytics:{level:"unsupported",note:"No Analytics Engine-equivalent binding implemented"},pipelines:{level:"unsupported",note:"No Pipelines-equivalent binding implemented"},mail:{level:"unsupported",note:"The queue tier this host lacked when the rating was written now exists (createNodeQueueHost), but nothing here composes a @lunora/mail transport or the queued-send consumer, so a send would be accepted and never delivered"},secrets:{level:"unsupported",note:"No Secrets Store-equivalent binding implemented (a real host would likely map this to env vars). Gate-bearing, and it has to be: ctx.secrets is a core built-in spliced into every context, so codegen refuses an app that reads it on this target instead of emitting a surface that throws on first use"},hyperdrive:{level:"unsupported",note:"No connection-pooling binding implemented"},httpCache:{level:"unsupported",note:"Nothing sits in front of this host to cache its responses, and Node exposes no Web Cache API global — the runtime's REST edge cache finds no HttpCacheLike here and degrades to emitting Cache-Control alone, which browsers and any CDN in front still honour"},identityProxy:{level:"unsupported",note:"Nothing sits in front of this host to authenticate callers, so it never populates the execution context's access identity. @lunora/cloudflare-access still works here through its Cf-Access-Jwt-Assertion fallback, which is a plain header check and needs no host support"}}};export{e as CLOUDFLARE_CAPABILITIES,t as NODE_CAPABILITIES};
@@ -0,0 +1 @@
1
+ import{AsyncLocalStorage as $}from"node:async_hooks";import{DatabaseSync as N}from"node:sqlite";let D=0,F=0;const W=()=>(D+=1,`socket-${D}`),_=()=>(F+=1,`job-${F}`),H=a=>a===void 0?null:a,V=a=>typeof a=="string"?new TextEncoder().encode(a).buffer:a instanceof ArrayBuffer?a:ArrayBuffer.isView(a)?a.buffer.slice(a.byteOffset,a.byteOffset+a.byteLength):new ArrayBuffer(0),G=()=>{const a=new N(":memory:");let m=!1;const u=e=>{if(m)throw new Error(`platform closed: cannot ${e}`)},s={alarmAt:null,alarmTimeout:null,pending:[],running:!1},i=new Map,p=new Map,f=new Map,y=new $;let h=!1;const L=()=>{if(h&&y.getStore()!==!0)throw Object.assign(new Error("shard busy: cannot run SQL while another task holds this shard's transaction"),{code:"SHARD_UNAVAILABLE",status:503,type:"VisulimaError"})},B={exec:(e,...t)=>{L();const r=a.prepare(e),n=t.map(H),o=e.trim().toLowerCase(),l=o.startsWith("select")||o.startsWith("pragma")?r.all(...n):(r.run(...n),[]);return{[Symbol.iterator]:()=>l[Symbol.iterator](),one:()=>{if(l.length!==1)throw new Error(`expected exactly one row, got ${String(l.length)}`);return l[0]},toArray:()=>[...l]}}},b=()=>{if(s.running||s.pending.length===0)return;const e=s.pending.shift();e!==void 0&&(s.running=!0,e.function_().then(e.resolve,e.reject).finally(()=>{s.running=!1,b()}))},E=e=>new Promise((t,r)=>{s.pending.push({function_:e,reject:n=>{r(n)},resolve:n=>{t(n)}}),b()});let v=Promise.resolve();const T=async e=>{a.exec("BEGIN"),h=!0;try{const t=await y.run(!0,e);return a.exec("COMMIT"),h=!1,t}catch(t){h=!1;try{a.exec("ROLLBACK")}catch{}throw t}},O=e=>{const t=v.then(()=>T(e),()=>T(e));return v=t.then(()=>{},()=>{}),t},C=e=>{const t=typeof e=="number"?e:e.getTime();s.alarmAt=t,s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);const r=Math.max(0,t-Date.now());s.alarmTimeout=setTimeout(()=>{s.alarmAt=null,s.alarmTimeout=null},r)},I={alarms:{delete:()=>{u("delete an alarm"),s.alarmAt=null,s.alarmTimeout!==null&&(clearTimeout(s.alarmTimeout),s.alarmTimeout=null)},get:()=>s.alarmAt,set:e=>{u("set an alarm"),C(e)}},runSerialized:E,sql:B,transaction:O,waitUntil:()=>{}},d=new WeakMap,S=e=>{const t={bufferedAmount:e.bufferedAmount,close:(r,n)=>{e.closed=!0},deserializeAttachment:()=>e.attachment,send:r=>{e.received.push(typeof r=="string"?r:V(r))},serializeAttachment:r=>{e.attachment=r,p.set(e.id,r)}};return e.handle=t,d.set(t,e.id),t},J={accept:(e,t,r)=>{u("accept a socket");const n=W(),o={attachment:t,bufferedAmount:0,closed:!1,raw:e,handle:null,id:n,received:[],tags:new Set(r)};return i.set(n,o),f.set(n,new Set(r)),t!==void 0&&p.set(n,t),S(o)},getSockets:e=>{const t=[...i.values()];return(e===void 0?t:t.filter(n=>n.tags.has(e))).map(n=>n.handle)},handleFor:e=>[...i.values()].find(t=>t.raw===e)?.handle,idFor:e=>{const t=d.get(e);if(t===void 0)throw new Error("reference host: idFor called with a handle this host never issued");return t},removeTag:(e,t)=>{u("remove a socket tag");const r=d.get(e)??"",n=i.get(r);n!==void 0&&(t===void 0?n.tags.clear():n.tags.delete(t),f.set(r,new Set(n.tags)))},setTag:(e,t)=>{u("set a socket tag");const r=d.get(e)??"",n=i.get(r);n!==void 0&&(n.tags.add(t),f.set(r,new Set(n.tags)))}},A={get:e=>({fetch:async()=>new Response(String(e))}),getByName:e=>({fetch:async()=>new Response(e)}),idForName:e=>`shard:${e}`,jurisdiction:e=>A},g=new Map,P={delete:async e=>g.delete(e),get:async e=>g.get(e),list:async e=>{const t=e?.prefix??"",r=new Map;for(const[n,o]of g)n.startsWith(t)&&r.set(n,o);return r},put:async(e,t)=>{g.set(e,structuredClone(t))}},c=new Map,w=new Map,x=new Set,M=(e,t)=>({attempts:t.attempts,functionPath:t.functionPath,id:e,scheduledFor:t.scheduledFor}),R={cancel:async e=>{const t=c.get(e);return t===void 0?!1:(clearTimeout(t.timer),c.delete(e),!0)},deadLetter:{list:async()=>[...w].map(([e,t])=>M(e,t)),requeue:async e=>{const t=w.get(e);return t===void 0?!1:(w.delete(e),c.set(e,{...t,attempts:0,timer:void 0}),!0)}},list:async()=>[...c].map(([e,t])=>M(e,t)),schedule:async(e,t,r)=>{u("schedule a job");const n=_();let o;r?.at===void 0?o=Date.now()+(r?.delayMs??0):o=typeof r.at=="number"?r.at:r.at.getTime();const l=Math.max(0,o-Date.now()),z=setTimeout(()=>{const j=c.get(n);j!==void 0&&(j.attempts+=1),x.add(n),c.delete(n)},l);return c.set(n,{args:t,attempts:0,functionPath:e,options:r??{},scheduledFor:o,timer:z}),{id:n,scheduledFor:o}}},k=()=>{if(!m){m=!0,a.close(),s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);for(const e of c.values())clearTimeout(e.timer)}};return{awaitAlarmFired:async e=>{await new Promise(t=>{setTimeout(t,Math.max(0,e-Date.now())+30)})},awaitJobDispatched:async e=>{const t=c.get(e);return t!==void 0&&await new Promise(r=>{setTimeout(r,Math.max(0,t.scheduledFor-Date.now())+30)}),x.has(e)},cleanup:k,directory:A,disposeTerminally:k,kv:P,readFrames:e=>(i.get(d.get(e)??"")?.received??[]).filter(t=>typeof t=="string"),restoreSocket:(e,t)=>{const r={attachment:t,bufferedAmount:0,closed:!1,handle:null,id:e,raw:void 0,received:[],tags:new Set(f.get(e))};return i.set(e,r),S(r)},scheduler:R,simulateDeadLetter:async e=>{const t=c.get(e);return t===void 0?!1:(clearTimeout(t.timer),c.delete(e),w.set(e,{...t,attempts:(t.options.retry?.maxAttempts??5)+1,timer:void 0}),!0)},shard:I,simulateRecycle:()=>{i.clear()},socket:J}};export{G as createReferenceHost};
@@ -0,0 +1 @@
1
+ const a=(e,g,N)=>e.getByName!==void 0?e.getByName(g,N):e.get(e.idForName(g),N);export{a as resolveShard};
@@ -141,6 +141,16 @@ interface SchedulerHost {
141
141
  * presence is the host's declaration that dynamic cron works, exactly as
142
142
  * with `SocketHost.setTag`. A caller that finds it absent must fall
143
143
  * back to the target's declarative configuration.
144
+ *
145
+ * **Nothing in the framework calls this today.** `@lunora/platform-node`
146
+ * implements it (parser, persistence, timer re-arm) and the conformance
147
+ * suite is its only caller: no runtime walks an app's declared crons — the
148
+ * generated `LUNORA_CRONS` map — into it. The only cron dispatch that ships
149
+ * is `@lunora/runtime`'s, reached from Cloudflare's `scheduled()` handler,
150
+ * which is declarative and never touches this method. So a host that
151
+ * implements `cron` gains nothing until that wiring exists, and an app
152
+ * declaring crons on such a host has none of them fire. Wire it before
153
+ * treating a `cron`-bearing host as one that runs declared schedules.
144
154
  */
145
155
  cron?: (cron: string, functionPath: string, args?: Record<string, unknown>) => Promise<void>;
146
156
  /**
@@ -180,20 +190,32 @@ interface SchedulerHost {
180
190
  schedule: (functionPath: string, args: Record<string, unknown>, options?: ScheduleOptions) => Promise<ScheduledJob>;
181
191
  }
182
192
  /**
183
- * `ShardDirectory` the provider-neutral contract for resolving shard keys to
184
- * callable stubs. On Cloudflare this is backed by `DurableObjectNamespace`
185
- * (`idFromName` + `get` + `jurisdiction`). On another provider it may be an
186
- * actor registry, a consistent-hash router, or a local in-process map.
193
+ * Edge geography placement region, shared by `@lunora/runtime` (which reads
194
+ * `request.cf` to pick where a shard, replica, or region-local socket should
195
+ * live) and `@lunora/do` (which parses a region out of its own DO name). Kept
196
+ * here inlined into each consumer's bundle so the two sides can never drift
197
+ * on the region vocabulary without creating a runtime dependency edge between
198
+ * the packages.
187
199
  *
188
- * The engine relies on two capabilities:
189
- * 1. **Deterministic placement** a shard key always resolves to the same
190
- * logical shard (`idForName`).
191
- * 2. **RPC dispatch** — a resolved stub can receive a `fetch` request (or
192
- * equivalent RPC call) that the shard handles.
200
+ * The values are Cloudflare's Durable Object location hints, which is also the
201
+ * only vocabulary a Lunora deployment needs today: a region is *only* ever used
202
+ * as a placement hint and as a name segment, never as data. Wrong-but-close is
203
+ * fine by construction — a misrouted read is one longer hop, never a wrong
204
+ * answer so this maps coarsely and returns `undefined` rather than guessing
205
+ * when the request carries no usable geography.
193
206
  *
194
- * Placement hints (jurisdiction, region) are provider-mapped and may be
195
- * unsupported per the capability matrix.
207
+ * Zero-dependency by design (see the repo's `shared/` rules): only relative /
208
+ * builtin imports, named exports, no `.js` extensions.
209
+ */
210
+ /**
211
+ * The placement regions a name may carry and a hint may request — Cloudflare's
212
+ * `DurableObjectLocationHint` values, listed so the set can be validated at a
213
+ * trust boundary (a region parsed out of a DO name is attacker-influenced input
214
+ * on any route that mints names from a client-supplied shard key).
196
215
  */
216
+ declare const REGION_HINTS: readonly ["wnam", "enam", "sam", "weur", "eeur", "apac", "apac-ne", "apac-se", "oc", "afr", "me"];
217
+ /** One placement region. Structurally identical to Cloudflare's `DurableObjectLocationHint`. */
218
+ type RegionHint = (typeof REGION_HINTS)[number];
197
219
  /**
198
220
  * Cloudflare Durable Object jurisdictions restrict where a DO runs and
199
221
  * persists data, for data-residency / compliance regimes (GDPR, FedRAMP, US
@@ -206,6 +228,23 @@ interface SchedulerHost {
206
228
  * leave them unsupported.
207
229
  */
208
230
  type ShardJurisdiction = "eu" | "fedramp" | "us" | (Record<never, never> & string);
231
+ /**
232
+ * A geographic placement region — where a shard should be created, when the
233
+ * caller has an opinion.
234
+ *
235
+ * One vocabulary, defined once: `shared/region-hint.ts` owns the region list
236
+ * (it is also what derives a region from edge geography), and this contract
237
+ * re-exports it rather than restating the strings. A second list is how the two
238
+ * ends of a placement request drift apart.
239
+ *
240
+ * Unlike a jurisdiction — a hard constraint the caller must fail closed on — a
241
+ * region is **best effort and advisory**: a provider may ignore it, and on
242
+ * Cloudflare it is honoured only by the call that first creates the object.
243
+ * Everything downstream must work identically whether the hint was honoured,
244
+ * ignored, or never supplied, and no caller may treat a resolved stub's
245
+ * location as known.
246
+ */
247
+ type ShardRegionHint = RegionHint;
209
248
  /**
210
249
  * A resolved shard stub. The engine calls `fetch` (or an equivalent RPC
211
250
  * method) to dispatch work to the shard.
@@ -222,9 +261,9 @@ interface ShardStub {
222
261
  */
223
262
  interface DirectShardDirectory {
224
263
  /** Resolve an opaque id (from `idForName`) to a stub, when the provider has ids. */
225
- get?: (id: unknown) => ShardStub;
264
+ get?: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
226
265
  /** Resolve a shard key to a stub. */
227
- getByName: (name: string) => ShardStub;
266
+ getByName: (name: string, locationHint?: ShardRegionHint) => ShardStub;
228
267
  /** Derive a stable, opaque shard id from a shard key, when the provider has ids. */
229
268
  idForName?: (name: string) => unknown;
230
269
  /** See {@link ShardDirectory}. */
@@ -237,7 +276,7 @@ interface DirectShardDirectory {
237
276
  */
238
277
  interface TwoStepShardDirectory {
239
278
  /** Resolve an opaque id (from `idForName`) to a stub. */
240
- get: (id: unknown) => ShardStub;
279
+ get: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
241
280
  /**
242
281
  * Absent — the discriminant that selects the two-step branch.
243
282
  */
@@ -269,8 +308,13 @@ type ShardDirectory = DirectShardDirectory | TwoStepShardDirectory;
269
308
  * Resolve a shard key to a stub against either directory shape. Uses direct
270
309
  * name lookup when the provider has it, and falls back to the two-step
271
310
  * `idForName` + `get` dance otherwise.
311
+ *
312
+ * `locationHint` is forwarded to whichever branch runs. It is advisory in
313
+ * both: a provider with no placement concept ignores the extra argument, which
314
+ * is exactly what an implementation written against the pre-placement
315
+ * signature does.
272
316
  */
273
- declare const resolveShard: (directory: ShardDirectory, name: string) => ShardStub;
317
+ declare const resolveShard: (directory: ShardDirectory, name: string, locationHint?: ShardRegionHint) => ShardStub;
274
318
  /**
275
319
  * `ShardHost` — the provider-neutral contract for a single-writer, durable
276
320
  * shard execution slot. On Cloudflare this is backed by one Durable Object
@@ -340,17 +384,6 @@ interface ShardSqlExec {
340
384
  /** Execute a SQL statement with optional bound parameters. */
341
385
  exec: <Row = SqlRow>(query: string, ...bindings: ReadonlyArray<unknown>) => ShardSqlCursor<Row>;
342
386
  }
343
- /**
344
- * Async SQL executor used by the engine's higher-level paths (global tables,
345
- * metrics, auth). Already defined in `@lunora/sql-store` as `SqlExec`; this
346
- * alias keeps the platform contract self-contained.
347
- */
348
- interface ShardAsyncSqlExec {
349
- all: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRow[]>;
350
- run: (sql: string, params: ReadonlyArray<unknown>) => Promise<{
351
- rowsAffected: number;
352
- }>;
353
- }
354
387
  /**
355
388
  * Alarm scheduling for a shard. Alarms are durable: they survive host
356
389
  * recycling and fire at the requested timestamp.
@@ -372,16 +405,17 @@ interface ShardAlarms {
372
405
  interface ShardHost {
373
406
  /** Durable alarm scheduling for the shard. */
374
407
  alarms: ShardAlarms;
375
- /**
376
- * Async SQL executor for engine paths that need promise-based row access
377
- * (global tables, metrics, auth). Hosts may implement this over the same
378
- * underlying storage as `sql`.
379
- */
380
- asyncSql?: ShardAsyncSqlExec;
381
408
  /**
382
409
  * Run `fn` with exclusive ownership of the shard. Concurrent calls are
383
410
  * queued; no two closures run at once for the same shard key. On
384
411
  * Cloudflare this maps to `state.blockConcurrencyWhile`.
412
+ *
413
+ * A closure that throws must reject with **the value it threw**, and must
414
+ * leave the host usable for the next call. Engine errors carry a `code` and
415
+ * `status` the RPC edge renders from, so a host that lets its platform
416
+ * substitute a copy silently downgrades every coded error to an internal
417
+ * fault — and a host that tears itself down on a throw makes an ordinary
418
+ * application error cost every other caller on that shard.
385
419
  */
386
420
  runSerialized: <T>(function_: () => Promise<T>) => Promise<T>;
387
421
  /**
@@ -401,8 +435,10 @@ interface ShardHost {
401
435
  sql: ShardSqlExec;
402
436
  /**
403
437
  * Run `fn` inside a durable transaction. If `fn` throws, all writes roll
404
- * back. Raw `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure;
405
- * the host manages the transaction boundary.
438
+ * back and the call rejects with **the value `fn` threw** — see
439
+ * {@link ShardHost.runSerialized} for why the identity matters. Raw
440
+ * `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure; the host
441
+ * manages the transaction boundary.
406
442
  */
407
443
  transaction: <T>(function_: () => Promise<T>) => Promise<T>;
408
444
  /**
@@ -551,9 +587,26 @@ interface SocketHost {
551
587
  * durable tag minted at accept (Cloudflare), a registry key, a `WeakMap`.
552
588
  *
553
589
  * Must answer consistently for the same socket within a wake AND across a
554
- * recycle, since the engine uses it to reassociate a rehydrated socket with
555
- * its subscription state. Callers outside the O(subscribers) loops are the
556
- * intended consumers; do not reach for this per socket per frame.
590
+ * recycle. Callers outside the O(subscribers) loops are the intended
591
+ * consumers; do not reach for this per socket per frame.
592
+ *
593
+ * **Who actually calls it, since the previous wording named the wrong
594
+ * caller.** It said "the engine uses it to reassociate a rehydrated socket
595
+ * with its subscription state"; the engine does no such thing. Per-socket
596
+ * state is keyed on the handle object itself (`ShardRunner.socketFor`, over
597
+ * {@link SocketHost.handleFor}), and durable connection identity is the
598
+ * engine's own `connectionId` — a UUID minted at upgrade and stamped onto the
599
+ * socket attachment, which is what survives hibernation. `@lunora/shard-engine`
600
+ * and `@lunora/do` dispatch to `handleFor`, `getSockets` and `accept`, and to
601
+ * nothing else on this interface.
602
+ *
603
+ * `idFor` is the portable **identity oracle** instead: the conformance suite
604
+ * compares sockets through it (a host is allowed to hand back a different
605
+ * wrapper object for the same socket, so object identity is not a legal
606
+ * assertion), and it is how a host's own recycle plumbing addresses a socket
607
+ * by id. That is a real job and it is why this stays required — every host
608
+ * has to answer it for the TCK regardless — but implement it as a test and
609
+ * tooling contract, not as something on the frame path.
557
610
  *
558
611
  * **A socket this host never {@link SocketHost.accept}ed** (a foreign socket
559
612
  * the runtime hands back — a whisper sender in another pool, a relay peer)
@@ -596,4 +649,4 @@ interface SocketHost {
596
649
  */
597
650
  setTag?: (socket: SocketHandle, tag: string) => void;
598
651
  }
599
- export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d, ShardAsyncSqlExec as e, ShardDirectory as f, ShardHost as g, ShardJurisdiction as h, ShardKvListOptions as i, ShardKvStore as j, ShardSqlCursor as k, ShardSqlExec as l, ShardStub as m, SocketHandle as n, SocketHost as o, SqlRow as p, resolveShard as r };
652
+ export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d, ShardDirectory as e, ShardHost as f, ShardJurisdiction as g, ShardKvListOptions as h, ShardKvStore as i, ShardRegionHint as j, ShardSqlCursor as k, ShardSqlExec as l, ShardStub as m, SocketHandle as n, SocketHost as o, SqlRow as p, resolveShard as r };
@@ -141,6 +141,16 @@ interface SchedulerHost {
141
141
  * presence is the host's declaration that dynamic cron works, exactly as
142
142
  * with `SocketHost.setTag`. A caller that finds it absent must fall
143
143
  * back to the target's declarative configuration.
144
+ *
145
+ * **Nothing in the framework calls this today.** `@lunora/platform-node`
146
+ * implements it (parser, persistence, timer re-arm) and the conformance
147
+ * suite is its only caller: no runtime walks an app's declared crons — the
148
+ * generated `LUNORA_CRONS` map — into it. The only cron dispatch that ships
149
+ * is `@lunora/runtime`'s, reached from Cloudflare's `scheduled()` handler,
150
+ * which is declarative and never touches this method. So a host that
151
+ * implements `cron` gains nothing until that wiring exists, and an app
152
+ * declaring crons on such a host has none of them fire. Wire it before
153
+ * treating a `cron`-bearing host as one that runs declared schedules.
144
154
  */
145
155
  cron?: (cron: string, functionPath: string, args?: Record<string, unknown>) => Promise<void>;
146
156
  /**
@@ -180,20 +190,32 @@ interface SchedulerHost {
180
190
  schedule: (functionPath: string, args: Record<string, unknown>, options?: ScheduleOptions) => Promise<ScheduledJob>;
181
191
  }
182
192
  /**
183
- * `ShardDirectory` the provider-neutral contract for resolving shard keys to
184
- * callable stubs. On Cloudflare this is backed by `DurableObjectNamespace`
185
- * (`idFromName` + `get` + `jurisdiction`). On another provider it may be an
186
- * actor registry, a consistent-hash router, or a local in-process map.
193
+ * Edge geography placement region, shared by `@lunora/runtime` (which reads
194
+ * `request.cf` to pick where a shard, replica, or region-local socket should
195
+ * live) and `@lunora/do` (which parses a region out of its own DO name). Kept
196
+ * here inlined into each consumer's bundle so the two sides can never drift
197
+ * on the region vocabulary without creating a runtime dependency edge between
198
+ * the packages.
187
199
  *
188
- * The engine relies on two capabilities:
189
- * 1. **Deterministic placement** a shard key always resolves to the same
190
- * logical shard (`idForName`).
191
- * 2. **RPC dispatch** — a resolved stub can receive a `fetch` request (or
192
- * equivalent RPC call) that the shard handles.
200
+ * The values are Cloudflare's Durable Object location hints, which is also the
201
+ * only vocabulary a Lunora deployment needs today: a region is *only* ever used
202
+ * as a placement hint and as a name segment, never as data. Wrong-but-close is
203
+ * fine by construction — a misrouted read is one longer hop, never a wrong
204
+ * answer so this maps coarsely and returns `undefined` rather than guessing
205
+ * when the request carries no usable geography.
193
206
  *
194
- * Placement hints (jurisdiction, region) are provider-mapped and may be
195
- * unsupported per the capability matrix.
207
+ * Zero-dependency by design (see the repo's `shared/` rules): only relative /
208
+ * builtin imports, named exports, no `.js` extensions.
209
+ */
210
+ /**
211
+ * The placement regions a name may carry and a hint may request — Cloudflare's
212
+ * `DurableObjectLocationHint` values, listed so the set can be validated at a
213
+ * trust boundary (a region parsed out of a DO name is attacker-influenced input
214
+ * on any route that mints names from a client-supplied shard key).
196
215
  */
216
+ declare const REGION_HINTS: readonly ["wnam", "enam", "sam", "weur", "eeur", "apac", "apac-ne", "apac-se", "oc", "afr", "me"];
217
+ /** One placement region. Structurally identical to Cloudflare's `DurableObjectLocationHint`. */
218
+ type RegionHint = (typeof REGION_HINTS)[number];
197
219
  /**
198
220
  * Cloudflare Durable Object jurisdictions restrict where a DO runs and
199
221
  * persists data, for data-residency / compliance regimes (GDPR, FedRAMP, US
@@ -206,6 +228,23 @@ interface SchedulerHost {
206
228
  * leave them unsupported.
207
229
  */
208
230
  type ShardJurisdiction = "eu" | "fedramp" | "us" | (Record<never, never> & string);
231
+ /**
232
+ * A geographic placement region — where a shard should be created, when the
233
+ * caller has an opinion.
234
+ *
235
+ * One vocabulary, defined once: `shared/region-hint.ts` owns the region list
236
+ * (it is also what derives a region from edge geography), and this contract
237
+ * re-exports it rather than restating the strings. A second list is how the two
238
+ * ends of a placement request drift apart.
239
+ *
240
+ * Unlike a jurisdiction — a hard constraint the caller must fail closed on — a
241
+ * region is **best effort and advisory**: a provider may ignore it, and on
242
+ * Cloudflare it is honoured only by the call that first creates the object.
243
+ * Everything downstream must work identically whether the hint was honoured,
244
+ * ignored, or never supplied, and no caller may treat a resolved stub's
245
+ * location as known.
246
+ */
247
+ type ShardRegionHint = RegionHint;
209
248
  /**
210
249
  * A resolved shard stub. The engine calls `fetch` (or an equivalent RPC
211
250
  * method) to dispatch work to the shard.
@@ -222,9 +261,9 @@ interface ShardStub {
222
261
  */
223
262
  interface DirectShardDirectory {
224
263
  /** Resolve an opaque id (from `idForName`) to a stub, when the provider has ids. */
225
- get?: (id: unknown) => ShardStub;
264
+ get?: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
226
265
  /** Resolve a shard key to a stub. */
227
- getByName: (name: string) => ShardStub;
266
+ getByName: (name: string, locationHint?: ShardRegionHint) => ShardStub;
228
267
  /** Derive a stable, opaque shard id from a shard key, when the provider has ids. */
229
268
  idForName?: (name: string) => unknown;
230
269
  /** See {@link ShardDirectory}. */
@@ -237,7 +276,7 @@ interface DirectShardDirectory {
237
276
  */
238
277
  interface TwoStepShardDirectory {
239
278
  /** Resolve an opaque id (from `idForName`) to a stub. */
240
- get: (id: unknown) => ShardStub;
279
+ get: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
241
280
  /**
242
281
  * Absent — the discriminant that selects the two-step branch.
243
282
  */
@@ -269,8 +308,13 @@ type ShardDirectory = DirectShardDirectory | TwoStepShardDirectory;
269
308
  * Resolve a shard key to a stub against either directory shape. Uses direct
270
309
  * name lookup when the provider has it, and falls back to the two-step
271
310
  * `idForName` + `get` dance otherwise.
311
+ *
312
+ * `locationHint` is forwarded to whichever branch runs. It is advisory in
313
+ * both: a provider with no placement concept ignores the extra argument, which
314
+ * is exactly what an implementation written against the pre-placement
315
+ * signature does.
272
316
  */
273
- declare const resolveShard: (directory: ShardDirectory, name: string) => ShardStub;
317
+ declare const resolveShard: (directory: ShardDirectory, name: string, locationHint?: ShardRegionHint) => ShardStub;
274
318
  /**
275
319
  * `ShardHost` — the provider-neutral contract for a single-writer, durable
276
320
  * shard execution slot. On Cloudflare this is backed by one Durable Object
@@ -340,17 +384,6 @@ interface ShardSqlExec {
340
384
  /** Execute a SQL statement with optional bound parameters. */
341
385
  exec: <Row = SqlRow>(query: string, ...bindings: ReadonlyArray<unknown>) => ShardSqlCursor<Row>;
342
386
  }
343
- /**
344
- * Async SQL executor used by the engine's higher-level paths (global tables,
345
- * metrics, auth). Already defined in `@lunora/sql-store` as `SqlExec`; this
346
- * alias keeps the platform contract self-contained.
347
- */
348
- interface ShardAsyncSqlExec {
349
- all: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRow[]>;
350
- run: (sql: string, params: ReadonlyArray<unknown>) => Promise<{
351
- rowsAffected: number;
352
- }>;
353
- }
354
387
  /**
355
388
  * Alarm scheduling for a shard. Alarms are durable: they survive host
356
389
  * recycling and fire at the requested timestamp.
@@ -372,16 +405,17 @@ interface ShardAlarms {
372
405
  interface ShardHost {
373
406
  /** Durable alarm scheduling for the shard. */
374
407
  alarms: ShardAlarms;
375
- /**
376
- * Async SQL executor for engine paths that need promise-based row access
377
- * (global tables, metrics, auth). Hosts may implement this over the same
378
- * underlying storage as `sql`.
379
- */
380
- asyncSql?: ShardAsyncSqlExec;
381
408
  /**
382
409
  * Run `fn` with exclusive ownership of the shard. Concurrent calls are
383
410
  * queued; no two closures run at once for the same shard key. On
384
411
  * Cloudflare this maps to `state.blockConcurrencyWhile`.
412
+ *
413
+ * A closure that throws must reject with **the value it threw**, and must
414
+ * leave the host usable for the next call. Engine errors carry a `code` and
415
+ * `status` the RPC edge renders from, so a host that lets its platform
416
+ * substitute a copy silently downgrades every coded error to an internal
417
+ * fault — and a host that tears itself down on a throw makes an ordinary
418
+ * application error cost every other caller on that shard.
385
419
  */
386
420
  runSerialized: <T>(function_: () => Promise<T>) => Promise<T>;
387
421
  /**
@@ -401,8 +435,10 @@ interface ShardHost {
401
435
  sql: ShardSqlExec;
402
436
  /**
403
437
  * Run `fn` inside a durable transaction. If `fn` throws, all writes roll
404
- * back. Raw `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure;
405
- * the host manages the transaction boundary.
438
+ * back and the call rejects with **the value `fn` threw** — see
439
+ * {@link ShardHost.runSerialized} for why the identity matters. Raw
440
+ * `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure; the host
441
+ * manages the transaction boundary.
406
442
  */
407
443
  transaction: <T>(function_: () => Promise<T>) => Promise<T>;
408
444
  /**
@@ -551,9 +587,26 @@ interface SocketHost {
551
587
  * durable tag minted at accept (Cloudflare), a registry key, a `WeakMap`.
552
588
  *
553
589
  * Must answer consistently for the same socket within a wake AND across a
554
- * recycle, since the engine uses it to reassociate a rehydrated socket with
555
- * its subscription state. Callers outside the O(subscribers) loops are the
556
- * intended consumers; do not reach for this per socket per frame.
590
+ * recycle. Callers outside the O(subscribers) loops are the intended
591
+ * consumers; do not reach for this per socket per frame.
592
+ *
593
+ * **Who actually calls it, since the previous wording named the wrong
594
+ * caller.** It said "the engine uses it to reassociate a rehydrated socket
595
+ * with its subscription state"; the engine does no such thing. Per-socket
596
+ * state is keyed on the handle object itself (`ShardRunner.socketFor`, over
597
+ * {@link SocketHost.handleFor}), and durable connection identity is the
598
+ * engine's own `connectionId` — a UUID minted at upgrade and stamped onto the
599
+ * socket attachment, which is what survives hibernation. `@lunora/shard-engine`
600
+ * and `@lunora/do` dispatch to `handleFor`, `getSockets` and `accept`, and to
601
+ * nothing else on this interface.
602
+ *
603
+ * `idFor` is the portable **identity oracle** instead: the conformance suite
604
+ * compares sockets through it (a host is allowed to hand back a different
605
+ * wrapper object for the same socket, so object identity is not a legal
606
+ * assertion), and it is how a host's own recycle plumbing addresses a socket
607
+ * by id. That is a real job and it is why this stays required — every host
608
+ * has to answer it for the TCK regardless — but implement it as a test and
609
+ * tooling contract, not as something on the frame path.
557
610
  *
558
611
  * **A socket this host never {@link SocketHost.accept}ed** (a foreign socket
559
612
  * the runtime hands back — a whisper sender in another pool, a relay peer)
@@ -596,4 +649,4 @@ interface SocketHost {
596
649
  */
597
650
  setTag?: (socket: SocketHandle, tag: string) => void;
598
651
  }
599
- export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d, ShardAsyncSqlExec as e, ShardDirectory as f, ShardHost as g, ShardJurisdiction as h, ShardKvListOptions as i, ShardKvStore as j, ShardSqlCursor as k, ShardSqlExec as l, ShardStub as m, SocketHandle as n, SocketHost as o, SqlRow as p, resolveShard as r };
652
+ export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d, ShardDirectory as e, ShardHost as f, ShardJurisdiction as g, ShardKvListOptions as h, ShardKvStore as i, ShardRegionHint as j, ShardSqlCursor as k, ShardSqlExec as l, ShardStub as m, SocketHandle as n, SocketHost as o, SqlRow as p, resolveShard as r };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/platform",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.30",
4
4
  "description": "Provider-neutral host contracts for Lunora: shard/socket/directory/scheduler interfaces, binding projections, and the platform capability matrix",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- const e={id:"cloudflare",name:"Cloudflare",features:{shardedState:{level:"native",note:"Durable Objects with SQLite"},globalTables:{level:"native",note:"D1 with Sessions API"},websocketHibernation:{level:"native",note:"DO WebSocket hibernation"},localSql:{level:"native",note:"state.storage.sql (SQLite)"},shardAlarms:{level:"native",note:"state.storage.setAlarm"},crossShardFanout:{level:"emulated",note:"Lunora query coordinator + relay tier over Durable Objects"},queues:{level:"native",note:"Cloudflare Queues"},workflows:{level:"native",note:"Cloudflare Workflows"},scheduler:{level:"emulated",note:"SchedulerDO (Lunora, on DO alarms) + declarative Cron Triggers; no runtime cron registration"},objectStorage:{level:"native",note:"R2"},keyValueStore:{level:"native",note:"Workers KV"},vectorStore:{level:"native",note:"Vectorize; query/upsert namespace scoping is native (remote filter), but getByIds/deleteByIds id-path tenant isolation is facade-enforced (client-side verification) since Vectorize's id operations take no namespace option"},ai:{level:"native",note:"Workers AI"},browser:{level:"native",note:"Browser Rendering"},containers:{level:"native",note:"Cloudflare Containers"},analytics:{level:"native",note:"Analytics Engine"},pipelines:{level:"native",note:"Cloudflare Pipelines"},mail:{level:"emulated",note:"Resend (third-party) via Cloudflare Queues"},secrets:{level:"native",note:"Secrets Store"},hyperdrive:{level:"native",note:"Cloudflare Hyperdrive"}}},t={id:"node",name:"Node",features:{shardedState:{level:"emulated",note:"One better-sqlite3 database per shard key, one process — no distributed placement or failover"},globalTables:{level:"unsupported",note:"No replicated SQL store (D1-equivalent) implemented"},websocketHibernation:{level:"emulated",note:"In-process socket registry; attachments/tags survive a simulated recycle, not a process restart, and nothing is ever actually evicted from memory"},localSql:{level:"native",note:"better-sqlite3 (synchronous, embedded)"},shardAlarms:{level:"unsupported",note:"In-process bookkeeping only — the armed timer clears state and never wakes anything; no dispatch, and nothing re-arms across a restart"},crossShardFanout:{level:"unsupported",note:"No query coordinator / relay tier implemented"},queues:{level:"unsupported",note:"No Cloudflare Queues equivalent implemented"},workflows:{level:"unsupported",note:"No Cloudflare Workflows equivalent implemented"},scheduler:{level:"unsupported",note:"Jobs are stored and timed but never dispatched — no delivery, no retries; also not durable across a process restart"},objectStorage:{level:"unsupported",note:"No R2/S3-equivalent binding implemented"},keyValueStore:{level:"emulated",note:"better-sqlite3 table behind the ShardKvStore API — not a dedicated KV product"},vectorStore:{level:"unsupported",note:"No Vectorize-equivalent binding implemented"},ai:{level:"unsupported",note:"No Workers AI-equivalent binding implemented"},browser:{level:"unsupported",note:"No headless-browser binding implemented"},containers:{level:"unsupported",note:"No container orchestration implemented"},analytics:{level:"unsupported",note:"No Analytics Engine-equivalent binding implemented"},pipelines:{level:"unsupported",note:"No Pipelines-equivalent binding implemented"},mail:{level:"unsupported",note:"@lunora/mail's queue-backed sends need a queues binding, which this target does not provide"},secrets:{level:"unsupported",note:"No Secrets Store-equivalent binding implemented (a real host would likely map this to env vars)"},hyperdrive:{level:"unsupported",note:"No connection-pooling binding implemented"}}};export{e as CLOUDFLARE_CAPABILITIES,t as NODE_CAPABILITIES};
@@ -1 +0,0 @@
1
- import{DatabaseSync as R}from"node:sqlite";let b=0,S=0;const j=()=>(b+=1,`socket-${b}`),C=()=>(S+=1,`job-${S}`),q=n=>n===void 0?null:n,E=n=>typeof n=="string"?new TextEncoder().encode(n).buffer:n instanceof ArrayBuffer?n:ArrayBuffer.isView(n)?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):new ArrayBuffer(0),O=()=>{const n=new R(":memory:"),s={alarmAt:null,alarmTimeout:null,pending:[],running:!1},l=new Map,f=new Map,u=new Map,x={exec:(e,...t)=>{const a=n.prepare(e),r=t.map(q),o=e.trim().toLowerCase().startsWith("select")?a.all(...r):(a.run(...r),[]);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]}}},M={all:async(e,t)=>n.prepare(e).all(...t),run:async(e,t)=>{const a=n.prepare(e).run(...t);return{rowsAffected:Number(a.changes)}}},h=()=>{if(s.running||s.pending.length===0)return;const e=s.pending.shift();e!==void 0&&(s.running=!0,e.function_().then(e.resolve,e.reject).finally(()=>{s.running=!1,h()}))},F=e=>new Promise((t,a)=>{s.pending.push({function_:e,reject:r=>{a(r)},resolve:r=>{t(r)}}),h()});let g=Promise.resolve();const p=async e=>{n.exec("BEGIN");try{const t=await e();return n.exec("COMMIT"),t}catch(t){throw n.exec("ROLLBACK"),t}},D={alarms:{delete:()=>{s.alarmAt=null,s.alarmTimeout!==null&&(clearTimeout(s.alarmTimeout),s.alarmTimeout=null)},get:()=>s.alarmAt,set:e=>{const t=typeof e=="number"?e:e.getTime();s.alarmAt=t,s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);const a=Math.max(0,t-Date.now());s.alarmTimeout=setTimeout(()=>{s.alarmAt=null,s.alarmTimeout=null},a)}},asyncSql:M,runSerialized:F,sql:x,transaction:e=>{const t=g.then(()=>p(e),()=>p(e));return g=t.then(()=>{},()=>{}),t},waitUntil:()=>{}},c=new WeakMap,w=e=>{const t={bufferedAmount:e.bufferedAmount,close:(a,r)=>{e.closed=!0},deserializeAttachment:()=>e.attachment,send:a=>{e.received.push(typeof a=="string"?a:E(a))},serializeAttachment:a=>{e.attachment=a,f.set(e.id,a)}};return e.handle=t,c.set(t,e.id),t},P={accept:(e,t,a)=>{const r=j(),o={attachment:t,bufferedAmount:0,closed:!1,raw:e,handle:null,id:r,received:[],tags:new Set(a)};return l.set(r,o),u.set(r,new Set(a)),t!==void 0&&f.set(r,t),w(o)},getSockets:e=>{const t=[...l.values()];return(e===void 0?t:t.filter(a=>a.tags.has(e))).map(a=>a.handle)},handleFor:e=>[...l.values()].find(t=>t.raw===e)?.handle,idFor:e=>{const t=c.get(e);if(t===void 0)throw new Error("reference host: idFor called with a handle this host never issued");return t},removeTag:(e,t)=>{const a=l.get(c.get(e)??"");a!==void 0&&(t===void 0?a.tags.clear():a.tags.delete(t),u.set(c.get(e)??"",new Set(a.tags)))},setTag:(e,t)=>{const a=c.get(e)??"",r=l.get(a);r!==void 0&&(r.tags.add(t),u.set(a,new Set(r.tags)))}},y={get:e=>({fetch:async()=>new Response(String(e))}),getByName:e=>({fetch:async()=>new Response(e)}),idForName:e=>`shard:${e}`,jurisdiction:e=>y},d=new Map,k={delete:async e=>d.delete(e),get:async e=>d.get(e),list:async e=>{const t=e?.prefix??"",a=new Map;for(const[r,o]of d)r.startsWith(t)&&a.set(r,o);return a},put:async(e,t)=>{d.set(e,structuredClone(t))}},i=new Map,m=new Map,v=new Set,T=(e,t)=>({attempts:t.attempts,functionPath:t.functionPath,id:e,scheduledFor:t.scheduledFor});return{awaitAlarmFired:async e=>{await new Promise(t=>{setTimeout(t,Math.max(0,e-Date.now())+30)})},awaitJobDispatched:async e=>{const t=i.get(e);return t!==void 0&&await new Promise(a=>{setTimeout(a,Math.max(0,t.scheduledFor-Date.now())+30)}),v.has(e)},cleanup:()=>{n.close(),s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);for(const e of i.values())clearTimeout(e.timer)},directory:y,kv:k,readFrames:e=>(l.get(c.get(e)??"")?.received??[]).filter(t=>typeof t=="string"),restoreSocket:(e,t)=>{const a={attachment:t,bufferedAmount:0,closed:!1,handle:null,id:e,raw:void 0,received:[],tags:new Set(u.get(e))};return l.set(e,a),w(a)},scheduler:{cancel:async e=>{const t=i.get(e);return t===void 0?!1:(clearTimeout(t.timer),i.delete(e),!0)},cron:async()=>{},deadLetter:{list:async()=>[...m].map(([e,t])=>T(e,t)),requeue:async e=>{const t=m.get(e);return t===void 0?!1:(m.delete(e),i.set(e,{...t,attempts:0,timer:void 0}),!0)}},list:async()=>[...i].map(([e,t])=>T(e,t)),schedule:async(e,t,a)=>{const r=C();let o;a?.at===void 0?o=Date.now()+(a?.delayMs??0):o=typeof a.at=="number"?a.at:a.at.getTime();const B=Math.max(0,o-Date.now()),L=setTimeout(()=>{const A=i.get(r);A!==void 0&&(A.attempts+=1),v.add(r),i.delete(r)},B);return i.set(r,{args:t,attempts:0,functionPath:e,options:a??{},scheduledFor:o,timer:L}),{id:r,scheduledFor:o}}},simulateDeadLetter:async e=>{const t=i.get(e);return t===void 0?!1:(clearTimeout(t.timer),i.delete(e),m.set(e,{...t,attempts:(t.options.retry?.maxAttempts??5)+1,timer:void 0}),!0)},shard:D,simulateRecycle:()=>{l.clear()},socket:P}};export{O as createReferenceHost};
@@ -1 +0,0 @@
1
- const t=(e,o)=>e.getByName!==void 0?e.getByName(o):e.get(e.idForName(o));export{t as resolveShard};