@lunora/platform 1.0.0-alpha.5 → 1.0.0-alpha.50
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.d.mts +1 -1
- package/dist/conformance/index.d.ts +1 -1
- package/dist/conformance/index.mjs +1 -1
- package/dist/conformance/suite.d.mts +57 -18
- package/dist/conformance/suite.d.ts +57 -18
- package/dist/conformance/suite.mjs +1 -1
- package/dist/index.d.mts +737 -53
- package/dist/index.d.ts +737 -53
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/CELLD_CAPABILITIES--dZeylT-.mjs +1 -0
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-B0h9qH2z.mjs +1 -0
- package/dist/packem_shared/NODE_CAPABILITIES-ChAHZ-QD.mjs +1 -0
- package/dist/packem_shared/createReferenceHost-B9XE4Gpl.mjs +1 -0
- package/dist/packem_shared/resolveShard-BzKOUEO4.mjs +1 -0
- package/dist/packem_shared/{socket-host.d-DCVFvmXn.d.ts → socket-host.d-BsnGTydH.d.mts} +121 -41
- package/dist/packem_shared/{socket-host.d-DCVFvmXn.d.mts → socket-host.d-BsnGTydH.d.ts} +121 -41
- package/package.json +1 -1
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-DMLgo_TI.mjs +0 -1
- package/dist/packem_shared/createReferenceHost-fs0Q8_VA.mjs +0 -1
- package/dist/packem_shared/resolveShard-uGhAKuTB.mjs +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{NOOP_EXECUTION_CONTEXT as
|
|
1
|
+
import{NOOP_EXECUTION_CONTEXT as e}from"./packem_shared/NOOP_EXECUTION_CONTEXT-YmXqH-jH.mjs";import{resolveShard as E}from"./packem_shared/resolveShard-BzKOUEO4.mjs";import{default as t}from"./packem_shared/CELLD_CAPABILITIES--dZeylT-.mjs";import{default as A}from"./packem_shared/CLOUDFLARE_CAPABILITIES-B0h9qH2z.mjs";import{default as L}from"./packem_shared/NODE_CAPABILITIES-ChAHZ-QD.mjs";export{t as CELLD_CAPABILITIES,A as CLOUDFLARE_CAPABILITIES,L as NODE_CAPABILITIES,e as NOOP_EXECUTION_CONTEXT,E as resolveShard};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e={id:"celld",name:"celld",features:{agents:{level:"unsupported",note:"The workflow half exists, the inference half does not: Workers AI is not a celld binding, so a generated agent's loop has no model to call. An experimental HTTP adapter behind CELLD_AI_URL is not an `env.AI` binding the emitted context can resolve"},ai:{level:"emulated",note:"Workers AI is not among celld's binding types (Durable Objects, services, vars, assets, D1, KV, Queues, Workflows, R2, worker loaders, containers), so `@cf/…` ids, `ctx.ai.run`, `ctx.ai.websearch` and the `rejectIfBusy` option are unavailable. `<provider>/<model>` slugs route to the OpenAI-compatible proxy named by the LUNORA_AI_PROXY_URL var (LiteLLM, OpenRouter, a self-hosted one; bearer token in LUNORA_AI_PROXY_TOKEN) over plain fetch instead of AI Gateway. celld's experimental CELLD_AI_URL Workers AI adapter is a daemon-level escape hatch, not a binding on env"},aiSearch:{level:"unsupported",note:"AI Search is not among celld's binding types (Durable Objects, services, vars, assets, D1, KV, Queues, Workflows, R2, worker loaders, containers), so codegen omits `ctx.aiSearch`. A REST-backed emulation (account id + API token over fetch) was considered and not built: the REST and binding APIs differ, and nobody on this target has asked for it"},analytics:{level:"unsupported",note:"Analytics Engine is not a celld binding type"},artifacts:{level:"unsupported",note:"Artifacts is not a celld binding type"},authJurisdictionMove:{level:"unsupported",note:"celld implements no jurisdictions — a fleet has only the machines you run, and `newUniqueId({ jurisdiction })` / `namespace.jurisdiction()` throw — so there is no jurisdiction-pinned auth object to copy into, and a schema that pins auth with `.jurisdiction(…)` fails closed"},browser:{level:"unsupported",note:"Browser Rendering is not a celld binding type"},commitOrderedTables:{level:"native",note:"The two host properties the guarantee rests on are both celld's: `storage.transaction` makes the `__commit_seq` bump atomic with the rows it stamps, and a cell executes one event at a time behind the same output gate Cloudflare's Durable Objects use. Derived from celld's documented Durable Object surface, not from a TCK run against a fleet"},containerEgressPolicy:{level:"unsupported",note:"celld does not implement outbound interception: a container with an allowedHosts / deniedHosts / interceptHttps policy refuses to start (`interceptAllOutboundHttp()` is not implemented in celld). Egress is instead fenced per node — a container reaches the Internet and nothing of the node's own"},containerRuntimeScheduling:{level:"unsupported",note:"celld refuses container snapshots, and the durable_object scheduling policy (a start that picks its image from ctx.container.images) has not been verified against celld — rated unsupported until it is"},containerSandboxTools:{level:"unsupported",note:"The backup and mount helpers route storage traffic through interceptOutboundHttp, which celld does not implement (see `containerEgressPolicy`), and the file helpers need the native ctx.container.exec(), which has not been verified against celld — rated unsupported until a TCK run proves both"},containers:{level:"native",note:"`containers` entries give a SQLite-backed Durable Object class a `ctx.container` handle, and `LunoraContainer` on `@cloudflare/containers` runs as published — a request routes worker → container Durable Object → the container's port. celld rates the service Experimental. The container always runs on the node that owns its cell, so every node serving a container class needs a Docker or Podman daemon; a cell moving nodes destroys its container (disk is ephemeral). An egress policy is refused (see `containerEgressPolicy`), as are `inspect()` and snapshots; instance-type disk size is not enforced, and `max_instances` converges fleet-wide rather than holding centrally. Whether celld implements the native `ctx.container.exec()` is not verified; where it does not, `exec` falls back to the image serving `/__lunora/exec`, and spawn() / terminal(), which have no such fallback, refuse with NOT_IMPLEMENTED"},cronTriggers:{level:"native",note:"`triggers.crons` is a supported Wrangler key and celld schedules durably fleet-wide: one handler per occurrence across the whole fleet, one at a time per script, a failure retried with doubling backoff (giving up after six, or on `noRetry()`) without ever delaying the next occurrence, and one catch-up run of the most recent missed occurrence after downtime. Two parser gaps: celld rejects a descending range (`SAT-SUN`, `NOV-FEB`) and `*` inside a list (`1,*`)"},crossShardFanout:{level:"emulated",note:"Same coordinator + relay tier as on Cloudflare, over cells rather than Durable Objects. It rides namespace stubs, so the celld gap that would bite — an RPC stub cannot cross an isolate boundary — does not apply to the fetch-shaped hops the tier makes; a remote cell call cannot be retried once its body starts streaming, because celld keeps no replay copy. Shard keys come from ShardRegistryDO exactly as on Cloudflare: each shard registers on its first write to a .shardBy() table, so one written before the registry was bound is not listed until it is written again"},durableStreams:{level:"emulated",note:"Same shape as Cloudflare: each chunk lands in the cell's SQLite under a monotonic seq and the producer outlives the socket via `waitUntil`. celld has no streaming primitive of its own, and a cell released under memory pressure mid-flight ends the run as STREAM_INTERRUPTED"},edgeRequestMetadata:{level:"unsupported",note:`celld hands the isolate a request.cf object with none of Cloudflare's edge fields — it states it cannot prove geolocation, colo or TLS metadata, and it does not terminate TLS, so no verified client certificate ever reaches it. trustInboundTraceContext: "mtls" therefore collapses to never-trust (the runtime warns once), and spans ship without placement attributes`},globalTables:{level:"native",note:"D1 bindings, backed by the fleet's own SQLite rather than Cloudflare's. Two differences that matter: a result is capped at 100,000 rows or 32 MiB, and there are no read replicas — so the Sessions API's bookmark pinning is satisfied trivially rather than by catching a replica up. Bytes still belong in a BLOB: a TEXT value that is not valid UTF-8 decodes with U+FFFD, as on workerd (Lunora already puts bytes in BLOBs)"},hostTraceFusion:{level:"unsupported",note:"celld exports its own OpenTelemetry (CELLD_OTEL) at the daemon level, but exposes no cloudflare:workers tracing.enterSpan to the isolate, so there is no host trace tree to enter. Already capability-probed at runtime, so fuseCloudflareTraces is a no-op; onSpan remains the source of truth"},httpCache:{level:"unsupported",note:"The Cache API exists only as an always-miss stub — `put()` stores nothing and `match()` returns `undefined` — because celld has no shared cache in front of a node, and `passThroughOnException()` is a no-op for the same reason. Responses degrade to headers-only caching at whatever ingress proxy fronts the fleet"},hyperdrive:{level:"unsupported",note:"Hyperdrive is not a celld binding type. celld does ship outbound TCP through `cloudflare:sockets`, but a socket cannot outlive its event, so there is no pool for a Hyperdrive-shaped binding to hand out"},identityProxy:{level:"unsupported",note:"No Cloudflare Access equivalent. celld does not terminate TLS or manage a domain at all — authentication belongs to the ingress proxy, and nothing puts a verified identity on the execution context"},images:{level:"unsupported",note:"The Images binding is not a celld binding type"},keyValueStore:{level:"native",note:"Workers KV bindings against the fleet bucket. No edge cache, so `cacheTtl` has no effect and `cacheStatus` reads `null`; a value above 1 MiB requires a fleet bucket; and a namespace has a single writer, so write capacity scales by adding namespaces rather than by concurrency"},localSql:{level:"native",note:"`state.storage.sql` over the cell's own SQLite database, replicated to the fleet bucket. One edge: `Cursor.toArray()` raises a celld-specific error when the isolate is near its 128 MB V8 heap limit rather than materialising the set"},logArchive:{level:"unsupported",note:"The read side needs R2 Data Catalog (Iceberg) plus R2 SQL; celld's R2 is a key space in the fleet bucket with neither, and it has no Pipelines binding to write the records in the first place. The admin route fails closed with LOG_ARCHIVE_NOT_CONFIGURED, which the studio renders as an empty state"},mail:{level:"emulated",note:"Resend (third-party) via celld Queues — the same queue-backed send as on Cloudflare, now that a celld queue consumer may live on the worker that exports `fetch()`. Inbound Email Workers are absent"},memoryTables:{level:"emulated",note:"Identical to Cloudflare: the lifetime is real — releasing a cell drops its heap and the framework clears every `.memory()` table on reconstruction — but celld exposes one SQL handle and no memory-backed database, so a memory row is still written to the cell's SQLite and then deleted"},objectStorage:{level:"native",note:"R2 bindings served from the fleet bucket under `r2/<bucket_name>/`. Gaps: no `ssecKey`, no `jurisdiction`, a conditional write cannot use a streamed body above 8 MiB, `createMultipartUpload()` takes no checksum, and a multipart upload cannot resume on another node or across a restart. `list()` orders keys, and compares `startAfter`, by their percent-encoded form, so a key with a non-ASCII or reserved character (`%`, `~`, `#`, `*`, …) can list in a different position than on R2 — harmless for the CDC archive, whose keys vary only in zero-padded digits under a fixed prefix"},objectStorageBackups:{level:"emulated",note:"`lunora backup create|list|restore --bucket` and the unattended `backupCron`/`backupStore` pair work unchanged — R2 supplies the bucket and Cron Triggers the schedule; the snapshot format, manifest, checksum gate and retention report are all Lunora's, which is what keeps this `emulated`. Bounded by what one request body or one isolate's 128 MB heap can hold"},objectStorageCdcArchive:{level:"emulated",note:"The segment format, the archive-before-trim ordering behind `waitUntil` and the de-overlapping read-back are Lunora's; celld supplies the bucket and the `startAfter` listing the segment keys are indexed on. celld has no notion of a changelog to tier, so nothing here is a product being consumed"},pipelines:{level:"unsupported",note:"Pipelines is not a celld binding type"},pointInTimeRecovery:{level:"unsupported",note:"celld's Durable Object storage has no bookmark API (`getBookmarkForTime` / `onNextSessionRestoreBookmark`), so getPitrBookmark / pitrRestore answer PITR_UNAVAILABLE. The fleet bucket's epoch-fenced replication is for durability and takeover, not an addressable history; `lunora backup` (objectStorageBackups) is the recovery tier here"},queues:{level:"native",note:'Queues bindings with batching, per-message ack/retry, delays and dead-letter queues, consumed by the `queue()` handler on the same worker that exports `fetch()` (the v0.4.0 rule forbidding that is gone as of v0.4.1). Differences: a queue is one cell with one writer, so write capacity scales by adding queues; a queue owner refuses more than 256 concurrent producer calls (retryable); retention is a fixed four days; no pull consumers or Queues HTTP API, so a `defineQueue({ mode: "pull" })` queue is refused at deploy'},relationGraph:{level:"emulated",note:"Identical to Cloudflare: the engine-level traversal over the same ctx.db reads, each hop a batched WHERE ... IN (...) against the cell's own SQLite inside its single-threaded event. Reads are local to the owning node, so a multi-hop expansion finishes within one request; there is no graph engine being consumed"},requestOrigin:{level:"emulated",note:"celld does not terminate TLS or front a CDN: request.url's hostname comes from the Host header unless a trusted proxy is declared (`--trust-forwarded-headers`, which then reads X-Forwarded-Host / -Proto). Pin the host at the ingress proxy, or configure a base (publicBaseUrl) rather than trusting the request"},scheduler:{level:"emulated",note:"SchedulerDO's `runAfter`/`runAt` half runs on cell alarms, and declarative crons reach `scheduled()` through celld's fleet-wide Cron Triggers. As on Cloudflare there is no runtime cron registration, and celld's cron parser rejects descending ranges and `*` inside a list"},secrets:{level:"unsupported",note:"No Secrets Store equivalent — `vars` is the only value-carrying binding celld accepts, and `celld deploy` stores them as plain strings in the deployment in the fleet bucket, readable by anyone with bucket read access (node-level injection via CELLD_VAR_* was removed in v0.5). @lunora/platform-celld's README covers guarding the bucket and fetching real secrets from a secret manager at runtime"},services:{level:"native",note:"Verified against celld v0.6.0 for a fetch service and a WorkerEntrypoint RPC service. celld resolves a binding from the target Worker's deployment record, so the service must be deployed into the same fleet (or `celld dev` state) first: `lunora deploy` deploys each service before the app, and every dev server (`lunora dev`, `vite dev`, Rsbuild) boots each once into the app's local state before the app starts. On each of them a service edit re-registers it and restarts the app"},topics:{level:"emulated",note:"The same fan-out as on Cloudflare: each subscription is its own celld queue and a publish sends to every one. Inherits the `queues` row's limits per subscription (256 concurrent producer calls per queue owner, four-day retention)"},serverReactors:{level:"emulated",note:"Reactors ride the existing post-write refresh drain, exactly as on Cloudflare. celld supplies the two properties that make that correct — one event at a time per cell, and `waitUntil` to keep the drain alive past the response — and has no notion of a server-side subscription of its own"},shardAlarms:{level:"native",note:"`storage.setAlarm`/`getAlarm`/`deleteAlarm` and the `alarm()` handler, durable across a cell moving between nodes"},shardedState:{level:"native",note:"Cells are Durable Objects: single-writer, one SQLite database each, replicated to the fleet bucket through a write-behind log"},shardPlacement:{level:"unsupported",note:"celld makes no placement promise: a cell lands on whichever node has capacity, and rebalancing moves hibernated cells to even out per-node cell counts, not to bring a cell nearer its readers — so a `locationHint` has nothing to act on"},shardReadReplicas:{level:"unsupported",note:"The CDC follow loop would run, but with no placement control there is no region to put a replica cell in — the replica would be as far from the reader as the primary. celld replicates to the fleet bucket for durability, not for reads"},vectorStore:{level:"unsupported",note:"Vectorize is not a celld binding type. celld does honour the `sqlite_vec` compatibility flag, which is per-cell vector search inside `storage.sql` — `sqliteVectorStore({ ann })` in `@lunora/ai/rag` indexes with it from a Durable Object — but not the fleet-wide index `ctx.vectors` is built on"},websocketHibernation:{level:"native",note:"`acceptWebSocket`/`getWebSockets`/`getTags`/attachments, and a hibernatable socket survives its cell hibernating on the same node. It closes when the cell moves to another owner (a node stop, drain, or rebalance), so the client reconnects — which Lunora's client already does. `acceptWebSocket()` throws once the isolate passes 90% of its V8 heap limit (roughly 50,000 hibernatable clients at the 128 MB default)"},workerLoaders:{level:"native",note:"Dynamic Workers behind the `worker_loaders` key: a loaded script compiles into its own V8 isolate in the loader's process (an isolate boundary, not a process or VM one), gets only the `env` it is handed, loses every ambient connection under `globalOutbound: null`, and has `limits.cpuMs` / `subRequests` enforced. A process holds at most 256 live Dynamic Workers (255 per script generation), and a load past that throws"},workflowRollback:{level:"unsupported",note:"celld does not implement step rollback: a step.do with a rollback option fails at first use (`step rollbackOptions are not implemented in celld`)"},workflowSchedules:{level:"unsupported",note:"Not verified against celld: whether it reads a workflows[] entry's `schedules` list and starts instances from it is unknown, so a scheduled workflow is refused rather than deployed onto a host that may never start it. Rate it once that is checked"},workflows:{level:"native",note:"Workflows bindings with steps, sleeps, events and retries. celld has no workflow `exports`, so the deploy projection turns each `exports.<Class>` workflow into a `workflows[]` binding named after the class, which the runtime reads off `env` when `ctx.exports` has no match. Differences to keep in mind: `run()` replays from the start so non-step code runs again, a crash after a step's side effect can re-run its callback, step results / event payloads / parameters are capped at 1 MiB each, non-step work cannot stay pending past 60 s, finished instances are retained at most 30 days, `locationHint` is accepted and ignored, and rollback plus sensitive or `ReadableStream` step results are unavailable (see `workflowRollback`). Instance `delete` and `subscribe`, binding `deleteBatch`, function-valued `retries.delay`, and the `limits` / `default_retention` binding settings have not been verified against celld. The studio's Workflows view reads Cloudflare's REST API and shows nothing for a celld fleet"}}};export{e as default};
|
|
@@ -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. From compatibility_date 2026-10-01 (or the durable_object_io_tasks_prevent_eviction flag) that pending waitUntil holds off idle eviction for up to 15 minutes, so a client disconnect no longer interrupts a shorter run, unless durable_object_io_tasks_do_not_prevent_eviction opts out. Deployed Workers apply it by date; a local workerd built before that default (wrangler 4.143's 1.20260926 is one) honours only the explicit flag"},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. The shard keys a fan-out reaches come from ShardRegistryDO, which each shard registers with on its first write to a .shardBy() table; a shard written before the registry was bound is not listed until it is written again"},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"},workerLoaders:{level:"native",note:"The `worker_loaders` binding (Dynamic Workers, in open beta): `load()` compiles a script into its own isolate, `globalOutbound: null` removes its network, and `limits.cpuMs` bounds it"},workflowRollback:{level:"native",note:"Workflows step rollback (the step.do rollback option)"},workflowSchedules:{level:"native",note:"wrangler workflows[].schedules — Cloudflare creates an instance on each cron tick, no scheduled() handler involved"},workflows:{level:"native",note:"Cloudflare Workflows declared in wrangler `exports` (no workflows[] binding) and reached through `ctx.exports.<Class>` from the shard DO, the Worker and every WorkflowEntrypoint; needs Wrangler >= 4.142 / @cloudflare/vite-plugin >= 1.61 locally"},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"},authJurisdictionMove:{level:"native",note:"Durable Object jurisdictions. The un-pinned and the pinned auth object are two objects on one namespace (namespace.jurisdiction()), and the __lunora_admin__:copyAuthToJurisdiction / purgeUnpinnedAuth admin ops copy between them over the objects' secret-gated internal route, driven from the worker. Present only for DO-backed auth pinned with .jurisdiction(…, { pinAuth: true })"},agents:{level:"emulated",note:"The durable agent loop is Lunora's: each defineAgent compiles onto a Cloudflare Workflow declared in wrangler `exports` and reached through `ctx.exports` (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. Write sync and the `backfillVectors` admin op (paged embedding of pre-existing rows; re-walks a table only when its fingerprinted config changes: index names, source field, dimensions, metric, metadata keys, declared `model`, or the table's soft-delete field. A Shape B `select`/`metadata` edit, or an `embed` swap with no declared `model`, still needs `restart: true`) are Lunora's, carried by ShardHost: pages are read under `runSerialized` and synced on the shard's after-commit chain. Shard-local tables only: `.global()` plus a vector index is rejected, since D1/Hyperdrive writes never reach that chain"},ai:{level:"native",note:"Workers AI; `<provider>/<model>` and `dynamic/<route>` ids route through AI Gateway over the same binding (Unified Billing for unified-catalog providers, a key stored on the gateway for gateway-path-only ones; `LUNORA_AI_GATEWAY_ID` else the account's `default` gateway). `rejectIfBusy` on `ctx.ai.model` / `ctx.ai.run` is the binding's own option, forwarded as is. `ctx.ai.websearch` is the binding's `websearch()` (Web Search API, beta: Ceramic / Exa / Linkup) on the same gateway, billed to a provider key stored there (the `default` alias unless `byokAlias` names another) or else AI Gateway credits"},aiSearch:{level:"native",note:"AI Search (formerly AutoRAG) through the `ai_search_namespaces` binding (conventionally `AI_SEARCH` on namespace `default`), passed through as-is: `ctx.aiSearch.get(name)` reaches any instance in the namespace, alongside `create` / `delete` / `list` and multi-instance `search` / `chatCompletions`. ActionCtx only. Remote-only even in dev: there is no local simulator, and miniflare proxies the binding to the service. Text-only over the binding — image and file content parts need the REST API. Usage-billed from 2026-11-01"},browser:{level:"native",note:"Browser Run: Playwright sessions (allowedHosts forwarded as session guardrails), Quick Actions through the binding's quickAction, and /crawl over the account REST API (no binding method; needs an API token)"},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 Durable Object answers through the runtime's native ctx.container.exec() (the image serves the route itself only on a runtime without native exec). spawn() and terminal() stream a process (stdin, a PTY, kill and resize) through the same native exec, and have no fallback on a runtime without it"},containerEgressPolicy:{level:"native",note:"@cloudflare/containers outbound interception (allowedHosts / deniedHosts / interceptHttps)"},containerRuntimeScheduling:{level:"native",note:"Cloudflare Containers' durable_object scheduling policy and container snapshots (both public beta): LunoraContainer resolves the named image through ctx.container.images and forwards image / instance / containerSnapshot to ctx.container.start() through the patched @cloudflare/containers base"},containerSandboxTools:{level:"native",note:"@cloudflare/sandbox (Sandbox SDK 1.0): Files, DirectoryBackup and S3Mount drive the sandbox-shim helper in the image through the native ctx.container.exec(), and the backup and mount gateways are WorkerEntrypoints the container reaches through interceptOutboundHttp. The image must ship /usr/local/bin/sandbox-shim, copied from Cloudflare's shim-only cloudflare/sandbox image"},analytics:{level:"native",note:"Analytics Engine"},artifacts:{level:"native",note:"Cloudflare Artifacts binding (open beta, Workers Paid): repo create/import/fork/list/delete, repo-scoped Git tokens, and reads of commits, trees, blobs and files. It has no write method; writes are a `git push` from a Git client with a minted token. No local simulator, so `lunora dev` reaches the remote service. Namespace jurisdiction is `eu` or `us`, fixed when the namespace is created; a `fedramp` schema using it is a codegen error"},edgeRequestMetadata:{level:"native",note:'request.cf — the edge stamps placement (colo, country) and, where an mTLS-enabled hostname is configured, the verified client certificate under tlsClientAuth. Unforgeable because it is not a header: trustInboundTraceContext: "mtls" and the OTLP placement resource detector both read it directly'},hostTraceFusion:{level:"native",note:"cloudflare:workers' tracing.enterSpan, behind the sink's fuseCloudflareTraces opt-in. Feature-detected on top: span.recordException for failed spans (redacted message only), span.setAttributes for the attribute mirror, and tracing.getActiveSpan to put the ctx.span wide event on the invocation's root span. Leave it off unless you want the CF-native nesting: with it on, a deployment that also ships onSpan to a collector emits the same logical span down two pipelines"},logArchive:{level:"native",note:"R2 Data Catalog (Iceberg) written by pipelineLogSink and queried back over R2 SQL, which the admin route runs server-side because R2 SQL needs a Cloudflare API token that must never reach the browser"},pipelines:{level:"native",note:"Cloudflare Pipelines"},pointInTimeRecovery:{level:"native",note:"SQLite-backed Durable Object bookmarks (getBookmarkForTime / onNextSessionRestoreBookmark) restore SQL and KV to any moment in the last 30 days. Absent under local wrangler dev, where the ops answer PITR_UNAVAILABLE"},mail:{level:"emulated",note:"Resend (third-party) via Cloudflare Queues"},secrets:{level:"native",note:"Secrets Store"},services:{level:"native",note:"Cloudflare service bindings: the bound Worker runs on the same thread of the same server, with no extra request fee. Fetch services and WorkerEntrypoint RPC both work, in deploy and in local dev (auxiliary Workers / multiple wrangler configs)"},topics:{level:"emulated",note:"Lunora's fan-out over Cloudflare Queues — a queue allows one consumer, so each subscription is its own queue (own retries and dead-letter queue) and a publish sends to every one in parallel. Not atomic: a failed send rejects the publish and a retry re-delivers to the subscriptions that already got it. No ordering key"},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()"},requestOrigin:{level:"native",note:"request.url carries the hostname the edge routed the request on (a route, custom domain or workers.dev name bound to this Worker), so a caller cannot point it at a host the Worker does not serve"},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"}}};export{e as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e={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, and createNodeSocketHost can rehydrate a row into a handle — but restoreSocket is not a SocketHost member and neither createNodePlatform nor createNodeShardRegistry surfaces it, so nothing composed here reassociates a reconnecting client; the conformance host is its only caller. Nothing is ever actually evicted from memory either, so this is durability without hibernation's memory saving and without its rehydration"},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). That registry is the host's own, so the write-time ShardRegistryDO registration a Cloudflare shard does has nothing to report to here and does not run"},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. Cloudflare's byte ceilings are enforced on send — 128 KiB per message and 256 KiB per sendBatch, measured over the encoded body — because @lunora/queue leaves both to the platform and this host is the only point where those bytes already exist. mode: "pull" queues are written but not consumed — nothing here serves the HTTP pull endpoint. Gap: message.run and ctx.run dispatch to LUNORA_ORIGIN_URL's /_lunora/scheduler/dispatch, which no Node HTTP server serves, so a handler that calls a Lunora function fails at runtime; its batch retries like any other failure until maxRetries sends it to the deadLetterQueue or parks it`},workerLoaders:{level:"unsupported",note:"No isolate loader: Node has no V8-isolate sandbox for untrusted code, and a `node:vm` context is not a security boundary"},workflowRollback:{level:"emulated",note:"createNodeWorkflowHost unwinds each settled step's rollback in reverse declaration order when a later step fails its last attempt. The compensations are plain calls, not durable steps — a crash during the unwind leaves it half-done — and rollbackConfig is ignored"},workflows:{level:"emulated",note:"createNodeWorkflowHost (@lunora/platform-node) binds each workflow on env under its class name — the key `ctx.exports` carries on Cloudflare — and 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). step.do's per-step retries and rollbacks are emulated by the adapter, not the engine: the attempt loop runs inside the one memoized step, so a crash mid-backoff restarts that step at attempt 1 rather than resuming the countdown, and compensations are plain calls unwound in reverse declaration order, so the adapter ignores rollbackConfig's retries and a crash mid-unwind leaves it half-done (@lunora/workflow itself still reads rollbackConfig.timeout to end a rollback's decline wait). A `retries.delay` function is awaited in that same loop and its result replaces the backoff, as on Cloudflare. defineWorkflow's limits and defaultRetention are wrangler settings this host never reads: no per-instance step cap is enforced, and a finished run's state stays until it is deleted. delete and deleteBatch (capped at 100 ids) drop a run's stored state behind the same in-process barrier as terminate, but leave no tombstone, so a deleted run's status reads `unknown`. 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: step.do's config.timeout is not emulated — the callback receives no AbortSignal, so a timeout here could only reject while the work it was meant to cancel kept running. @lunora/workflow still ends a ctx.runStep decline wait 2s before the step's timeout (Cloudflare's 10-minute default when unset), so here that wait charges an attempt the host would not have ended, and the next attempt resumes it; no pause/restart; subscribe rejects NOT_IMPLEMENTED, since the engine keeps no per-instance event log to replay; ctx.run dispatches to an endpoint no Node HTTP server serves; ctx.parallel's synchronous join cannot interleave within one trigger activation"},workflowSchedules:{level:"unsupported",note:"createNodeWorkflowHost only runs instances something creates: nothing reads a workflow's schedules list, so a scheduled workflow would never start. Gate-bearing: codegen refuses an app that declares one here. Start it from a mutation with ctx.workflows.get(name).create() instead, re-armed through ctx.scheduler.runAt"},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"},authJurisdictionMove:{level:"unsupported",note:"Not applicable rather than missing: this host has no jurisdictions. A single process cannot restrict where state lives, so the shard registry offers no jurisdiction() and a worker built for a schema that declares one fails closed at construction. There is never a pinned auth object to copy into, so the admin ops are never wired"},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 by its class name off `ctx.exports` or `env`, and this host binds neither. That missing mount is the whole gap: inference is not, since a model factory or AI SDK model object needs no binding and `ai` is emulated here through LUNORA_AI_PROXY_URL"},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. The limits R2 would apply are applied here rather than left to the deploy to discover: put verifies a declared sha256 against the bytes received and stores nothing on a mismatch, and refuses customMetadata over R2's summed 2048-byte ceiling. Because R2 has no directories, neither does this bucket's behaviour — a deleted key's empty directory is pruned, so a later object may take that prefix, and delete of a key that is only a prefix is the no-op it is on R2 rather than a raw fs error. 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, so codegen emits neither ctx.vectors nor the `backfillVectors` override; the admin op answers NOT_IMPLEMENTED, and an index's declared `model` is accepted but read by nothing"},ai:{level:"emulated",note:"No Workers AI-equivalent binding implemented, so `@cf/…` ids, `ctx.ai.run`, `ctx.ai.websearch` and the `rejectIfBusy` option are unavailable. `<provider>/<model>` slugs route to the OpenAI-compatible proxy named by the LUNORA_AI_PROXY_URL env var (LiteLLM, OpenRouter, a self-hosted one; bearer token in LUNORA_AI_PROXY_TOKEN) over plain fetch instead of AI Gateway, and a bring-your-own AI SDK model passes straight through"},aiSearch:{level:"unsupported",note:"No AI Search-equivalent binding implemented, so codegen omits `ctx.aiSearch`. A REST-backed emulation (account id + API token over fetch) was considered and not built: the REST and binding APIs differ, and nobody on this target has asked for it"},browser:{level:"unsupported",note:"No headless-browser binding implemented"},images:{level:"unsupported",note:"No Images-equivalent binding implemented"},containerEgressPolicy:{level:"unsupported",note:"No container orchestration implemented, so there is no container egress to police"},containerRuntimeScheduling:{level:"unsupported",note:"No container orchestration implemented, so there is no image to pick at start and no filesystem to snapshot"},containerSandboxTools:{level:"unsupported",note:"No container orchestration implemented, so there is no container filesystem to read, back up or mount a bucket into"},containers:{level:"unsupported",note:"No container orchestration implemented, so there is nothing for ctx.containers.<name>.exec, spawn or terminal to run a command in either"},analytics:{level:"unsupported",note:"No Analytics Engine-equivalent binding implemented"},artifacts:{level:"unsupported",note:"No Artifacts-equivalent binding implemented: a host would need a Git server with repo-scoped tokens, which no host contract carries"},edgeRequestMetadata:{level:"unsupported",note:'Nothing injects per-request platform metadata here — a Node request carries only what the caller wrote. Two telemetry surfaces degrade silently as a result: trustInboundTraceContext: "mtls" can never be satisfied, so it collapses to never-trust and every inbound traceparent is dropped (the runtime warns once when that actually happens, since no codegen gate can see a createWorker option), and the OTLP resource detector finds no placement attributes, so spans ship without them'},hostTraceFusion:{level:"unsupported",note:"No host-native trace tree to merge into, and cloudflare:workers is not importable here. Already capability-probed at runtime, so fuseCloudflareTraces is a no-op rather than a throw; onSpan remains the source of truth for the waterfall either way"},logArchive:{level:"unsupported",note:"createNodeR2Bucket is a directory on the local filesystem with no Iceberg catalog over it and no SQL engine to query it back, so records could be written but never read. The admin route is still registered here: it answers LOG_ARCHIVE_NOT_CONFIGURED (which the studio renders as a not-configured empty state) only while the logArchive table or the R2 SQL credentials are absent. Configure both and it stops failing closed — it builds an R2 SQL client and queries Cloudflare's API over the network, which is not this host serving the archive"},pipelines:{level:"unsupported",note:"No Pipelines-equivalent binding implemented"},pointInTimeRecovery:{level:"unsupported",note:"better-sqlite3 keeps no change log to rewind, and createNodeShardState deliberately exposes no bookmark methods rather than a stub that would report a restore that never happened, so getPitrBookmark / pitrRestore answer PITR_UNAVAILABLE. Recover from `lunora backup` snapshots instead (objectStorageBackups)"},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"},services:{level:"unsupported",note:"No sibling-Worker host: a Node process runs one app, with nothing a service binding could point at. Gate-bearing, so codegen omits ctx.services instead of emitting bindings that resolve to nothing"},topics:{level:"emulated",note:"Each subscription is a createNodeQueueHost queue, so a publish is one durable insert per subscription into _lunora_queue_messages. Inherits the queues row's gap: a subscription handler's message.run dispatches to an endpoint no Node HTTP server serves"},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"},requestOrigin:{level:"emulated",note:"The same runtime code reads it off request.url, but nothing here serves HTTP: the embedding server builds that Request, usually from the Host header the caller sent. Pin the host in the proxy in front, or configure a base (publicBaseUrl / STORAGE_PUBLIC_BASE_URL), which always wins over the origin. Because the caller can choose it here, never build links delivered to OTHER people from ctx.origin (password-reset or invite emails, webhooks, notifications): that is host-header poisoning. Use a configured canonical base URL for those; a signed URL returned to the same caller is fine"},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 default};
|
|
@@ -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
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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
|
-
*
|
|
195
|
-
*
|
|
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
|
|
@@ -301,22 +345,47 @@ type SqlRow = Record<string, unknown>;
|
|
|
301
345
|
/**
|
|
302
346
|
* The result of one statement: a cursor over its rows.
|
|
303
347
|
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
348
|
+
* The three members the ENGINE uses are required, because a host that omits one
|
|
349
|
+
* fails at runtime rather than at compile time. That is not hypothetical — an
|
|
350
|
+
* earlier revision of this contract made `toArray` optional and offered a
|
|
351
|
+
* `rowsAffected` nothing reads, which meant a host could satisfy the type and
|
|
352
|
+
* still be unusable.
|
|
309
353
|
*
|
|
310
354
|
* Iteration is part of the contract because read paths stream cursors directly
|
|
311
355
|
* rather than buffering; `toArray` is the buffered form, and `one` is the
|
|
312
356
|
* exactly-one-row form used by lookups and aggregates.
|
|
357
|
+
*
|
|
358
|
+
* `columnNames` / `raw` are the exception and are deliberately optional: no
|
|
359
|
+
* engine path needs them, one admin read-back reports a better result when they
|
|
360
|
+
* are there, and that surface degrades explicitly when they are not. Requiring
|
|
361
|
+
* them would make a host unusable over a detail it can live without.
|
|
313
362
|
*/
|
|
314
363
|
interface ShardSqlCursor<Row = SqlRow> extends Iterable<Row> {
|
|
364
|
+
/**
|
|
365
|
+
* The result columns in SELECT order, as the statement declares them, before
|
|
366
|
+
* a row object collapses them into keys. Mirrors Cloudflare
|
|
367
|
+
* `SqlStorageCursor`'s field of the same name.
|
|
368
|
+
*
|
|
369
|
+
* Optional, and absent is a supported answer rather than a bug: a host
|
|
370
|
+
* supplies it when its driver has one (Cloudflare's cursor, better-sqlite3's
|
|
371
|
+
* `Statement#columns`, `node:sqlite`'s `columns()` from Node 22.14 on) and
|
|
372
|
+
* omits it otherwise. A reader with it can report a result faithfully;
|
|
373
|
+
* without it, a zero-row result has no column list at all and two same-named
|
|
374
|
+
* result columns (`SELECT u.id, o.id`) collapse to one.
|
|
375
|
+
*/
|
|
376
|
+
readonly columnNames?: string[];
|
|
315
377
|
/**
|
|
316
378
|
* The single row this statement produced.
|
|
317
379
|
* @throws when the result does not hold exactly one row.
|
|
318
380
|
*/
|
|
319
381
|
one: () => Row;
|
|
382
|
+
/**
|
|
383
|
+
* The rows as positional value arrays aligned with `columnNames` rather
|
|
384
|
+
* than as objects — the only shape that survives two result columns sharing
|
|
385
|
+
* a name. Optional alongside `columnNames` (a host offers both or neither),
|
|
386
|
+
* and consuming it consumes the cursor: call it *or* `toArray`, never both.
|
|
387
|
+
*/
|
|
388
|
+
raw?: () => IterableIterator<unknown[]>;
|
|
320
389
|
/** Buffer every row. */
|
|
321
390
|
toArray: () => Row[];
|
|
322
391
|
}
|
|
@@ -340,17 +409,6 @@ interface ShardSqlExec {
|
|
|
340
409
|
/** Execute a SQL statement with optional bound parameters. */
|
|
341
410
|
exec: <Row = SqlRow>(query: string, ...bindings: ReadonlyArray<unknown>) => ShardSqlCursor<Row>;
|
|
342
411
|
}
|
|
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
412
|
/**
|
|
355
413
|
* Alarm scheduling for a shard. Alarms are durable: they survive host
|
|
356
414
|
* recycling and fire at the requested timestamp.
|
|
@@ -368,16 +426,21 @@ interface ShardAlarms {
|
|
|
368
426
|
}
|
|
369
427
|
/**
|
|
370
428
|
* The core shard host contract. One instance per shard key.
|
|
429
|
+
*
|
|
430
|
+
* **At most one LIVE instance per shard key, at any moment — a requirement,
|
|
431
|
+
* not a convention.** `ShardDO` keeps correctness-bearing state in memory on
|
|
432
|
+
* that basis: its in-flight dispatch claim (`@lunora/do`'s `InFlightClaims`)
|
|
433
|
+
* declines a second concurrent delivery of the same idempotency id only because
|
|
434
|
+
* every delivery for the shard reaches the same instance, and treats a missing
|
|
435
|
+
* claim as "the holder is gone" only because a replaced instance takes its
|
|
436
|
+
* running handlers with it. A host that could run two instances of one shard
|
|
437
|
+
* side by side (a second Node process, a celld failover that overlaps the old
|
|
438
|
+
* cell) would let an action run twice concurrently. Cloudflare Durable Objects
|
|
439
|
+
* provide this natively; every other host must enforce it.
|
|
371
440
|
*/
|
|
372
441
|
interface ShardHost {
|
|
373
442
|
/** Durable alarm scheduling for the shard. */
|
|
374
443
|
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
444
|
/**
|
|
382
445
|
* Run `fn` with exclusive ownership of the shard. Concurrent calls are
|
|
383
446
|
* queued; no two closures run at once for the same shard key. On
|
|
@@ -560,9 +623,26 @@ interface SocketHost {
|
|
|
560
623
|
* durable tag minted at accept (Cloudflare), a registry key, a `WeakMap`.
|
|
561
624
|
*
|
|
562
625
|
* Must answer consistently for the same socket within a wake AND across a
|
|
563
|
-
* recycle
|
|
564
|
-
*
|
|
565
|
-
*
|
|
626
|
+
* recycle. Callers outside the O(subscribers) loops are the intended
|
|
627
|
+
* consumers; do not reach for this per socket per frame.
|
|
628
|
+
*
|
|
629
|
+
* **Who actually calls it, since the previous wording named the wrong
|
|
630
|
+
* caller.** It said "the engine uses it to reassociate a rehydrated socket
|
|
631
|
+
* with its subscription state"; the engine does no such thing. Per-socket
|
|
632
|
+
* state is keyed on the handle object itself (`ShardRunner.socketFor`, over
|
|
633
|
+
* {@link SocketHost.handleFor}), and durable connection identity is the
|
|
634
|
+
* engine's own `connectionId` — a UUID minted at upgrade and stamped onto the
|
|
635
|
+
* socket attachment, which is what survives hibernation. `@lunora/shard-engine`
|
|
636
|
+
* and `@lunora/do` dispatch to `handleFor`, `getSockets` and `accept`, and to
|
|
637
|
+
* nothing else on this interface.
|
|
638
|
+
*
|
|
639
|
+
* `idFor` is the portable **identity oracle** instead: the conformance suite
|
|
640
|
+
* compares sockets through it (a host is allowed to hand back a different
|
|
641
|
+
* wrapper object for the same socket, so object identity is not a legal
|
|
642
|
+
* assertion), and it is how a host's own recycle plumbing addresses a socket
|
|
643
|
+
* by id. That is a real job and it is why this stays required — every host
|
|
644
|
+
* has to answer it for the TCK regardless — but implement it as a test and
|
|
645
|
+
* tooling contract, not as something on the frame path.
|
|
566
646
|
*
|
|
567
647
|
* **A socket this host never {@link SocketHost.accept}ed** (a foreign socket
|
|
568
648
|
* the runtime hands back — a whisper sender in another pool, a relay peer)
|
|
@@ -605,4 +685,4 @@ interface SocketHost {
|
|
|
605
685
|
*/
|
|
606
686
|
setTag?: (socket: SocketHandle, tag: string) => void;
|
|
607
687
|
}
|
|
608
|
-
export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d,
|
|
688
|
+
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 };
|