@lunora/platform 1.0.0-alpha.2 → 1.0.0-alpha.21

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.
@@ -180,20 +180,32 @@ interface SchedulerHost {
180
180
  schedule: (functionPath: string, args: Record<string, unknown>, options?: ScheduleOptions) => Promise<ScheduledJob>;
181
181
  }
182
182
  /**
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.
183
+ * Edge geography placement region, shared by `@lunora/runtime` (which reads
184
+ * `request.cf` to pick where a shard, replica, or region-local socket should
185
+ * live) and `@lunora/do` (which parses a region out of its own DO name). Kept
186
+ * here inlined into each consumer's bundle so the two sides can never drift
187
+ * on the region vocabulary without creating a runtime dependency edge between
188
+ * the packages.
187
189
  *
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.
190
+ * The values are Cloudflare's Durable Object location hints, which is also the
191
+ * only vocabulary a Lunora deployment needs today: a region is *only* ever used
192
+ * as a placement hint and as a name segment, never as data. Wrong-but-close is
193
+ * fine by construction — a misrouted read is one longer hop, never a wrong
194
+ * answer so this maps coarsely and returns `undefined` rather than guessing
195
+ * when the request carries no usable geography.
193
196
  *
194
- * Placement hints (jurisdiction, region) are provider-mapped and may be
195
- * unsupported per the capability matrix.
197
+ * Zero-dependency by design (see the repo's `shared/` rules): only relative /
198
+ * builtin imports, named exports, no `.js` extensions.
196
199
  */
200
+ /**
201
+ * The placement regions a name may carry and a hint may request — Cloudflare's
202
+ * `DurableObjectLocationHint` values, listed so the set can be validated at a
203
+ * trust boundary (a region parsed out of a DO name is attacker-influenced input
204
+ * on any route that mints names from a client-supplied shard key).
205
+ */
206
+ declare const REGION_HINTS: readonly ["wnam", "enam", "sam", "weur", "eeur", "apac", "apac-ne", "apac-se", "oc", "afr", "me"];
207
+ /** One placement region. Structurally identical to Cloudflare's `DurableObjectLocationHint`. */
208
+ type RegionHint = (typeof REGION_HINTS)[number];
197
209
  /**
198
210
  * Cloudflare Durable Object jurisdictions restrict where a DO runs and
199
211
  * persists data, for data-residency / compliance regimes (GDPR, FedRAMP, US
@@ -206,6 +218,23 @@ interface SchedulerHost {
206
218
  * leave them unsupported.
207
219
  */
208
220
  type ShardJurisdiction = "eu" | "fedramp" | "us" | (Record<never, never> & string);
221
+ /**
222
+ * A geographic placement region — where a shard should be created, when the
223
+ * caller has an opinion.
224
+ *
225
+ * One vocabulary, defined once: `shared/region-hint.ts` owns the region list
226
+ * (it is also what derives a region from edge geography), and this contract
227
+ * re-exports it rather than restating the strings. A second list is how the two
228
+ * ends of a placement request drift apart.
229
+ *
230
+ * Unlike a jurisdiction — a hard constraint the caller must fail closed on — a
231
+ * region is **best effort and advisory**: a provider may ignore it, and on
232
+ * Cloudflare it is honoured only by the call that first creates the object.
233
+ * Everything downstream must work identically whether the hint was honoured,
234
+ * ignored, or never supplied, and no caller may treat a resolved stub's
235
+ * location as known.
236
+ */
237
+ type ShardRegionHint = RegionHint;
209
238
  /**
210
239
  * A resolved shard stub. The engine calls `fetch` (or an equivalent RPC
211
240
  * method) to dispatch work to the shard.
@@ -222,9 +251,9 @@ interface ShardStub {
222
251
  */
223
252
  interface DirectShardDirectory {
224
253
  /** Resolve an opaque id (from `idForName`) to a stub, when the provider has ids. */
225
- get?: (id: unknown) => ShardStub;
254
+ get?: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
226
255
  /** Resolve a shard key to a stub. */
227
- getByName: (name: string) => ShardStub;
256
+ getByName: (name: string, locationHint?: ShardRegionHint) => ShardStub;
228
257
  /** Derive a stable, opaque shard id from a shard key, when the provider has ids. */
229
258
  idForName?: (name: string) => unknown;
230
259
  /** See {@link ShardDirectory}. */
@@ -237,7 +266,7 @@ interface DirectShardDirectory {
237
266
  */
238
267
  interface TwoStepShardDirectory {
239
268
  /** Resolve an opaque id (from `idForName`) to a stub. */
240
- get: (id: unknown) => ShardStub;
269
+ get: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
241
270
  /**
242
271
  * Absent — the discriminant that selects the two-step branch.
243
272
  */
@@ -269,8 +298,13 @@ type ShardDirectory = DirectShardDirectory | TwoStepShardDirectory;
269
298
  * Resolve a shard key to a stub against either directory shape. Uses direct
270
299
  * name lookup when the provider has it, and falls back to the two-step
271
300
  * `idForName` + `get` dance otherwise.
301
+ *
302
+ * `locationHint` is forwarded to whichever branch runs. It is advisory in
303
+ * both: a provider with no placement concept ignores the extra argument, which
304
+ * is exactly what an implementation written against the pre-placement
305
+ * signature does.
272
306
  */
273
- declare const resolveShard: (directory: ShardDirectory, name: string) => ShardStub;
307
+ declare const resolveShard: (directory: ShardDirectory, name: string, locationHint?: ShardRegionHint) => ShardStub;
274
308
  /**
275
309
  * `ShardHost` — the provider-neutral contract for a single-writer, durable
276
310
  * shard execution slot. On Cloudflare this is backed by one Durable Object
@@ -340,17 +374,6 @@ interface ShardSqlExec {
340
374
  /** Execute a SQL statement with optional bound parameters. */
341
375
  exec: <Row = SqlRow>(query: string, ...bindings: ReadonlyArray<unknown>) => ShardSqlCursor<Row>;
342
376
  }
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
377
  /**
355
378
  * Alarm scheduling for a shard. Alarms are durable: they survive host
356
379
  * recycling and fire at the requested timestamp.
@@ -372,16 +395,17 @@ interface ShardAlarms {
372
395
  interface ShardHost {
373
396
  /** Durable alarm scheduling for the shard. */
374
397
  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
398
  /**
382
399
  * Run `fn` with exclusive ownership of the shard. Concurrent calls are
383
400
  * queued; no two closures run at once for the same shard key. On
384
401
  * Cloudflare this maps to `state.blockConcurrencyWhile`.
402
+ *
403
+ * A closure that throws must reject with **the value it threw**, and must
404
+ * leave the host usable for the next call. Engine errors carry a `code` and
405
+ * `status` the RPC edge renders from, so a host that lets its platform
406
+ * substitute a copy silently downgrades every coded error to an internal
407
+ * fault — and a host that tears itself down on a throw makes an ordinary
408
+ * application error cost every other caller on that shard.
385
409
  */
386
410
  runSerialized: <T>(function_: () => Promise<T>) => Promise<T>;
387
411
  /**
@@ -401,8 +425,10 @@ interface ShardHost {
401
425
  sql: ShardSqlExec;
402
426
  /**
403
427
  * 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.
428
+ * back and the call rejects with **the value `fn` threw** — see
429
+ * {@link ShardHost.runSerialized} for why the identity matters. Raw
430
+ * `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure; the host
431
+ * manages the transaction boundary.
406
432
  */
407
433
  transaction: <T>(function_: () => Promise<T>) => Promise<T>;
408
434
  /**
@@ -441,6 +467,17 @@ interface ShardHost {
441
467
  * — optional. Presence declares that the host can retag a live socket. Hosts
442
468
  * that cannot (Cloudflare) omit both methods, and callers that need to
443
469
  * retag must instead close and re-accept the socket with new tags.
470
+ *
471
+ * **Reserved-slot budget.** A host may reserve some of its accept-time tag
472
+ * slots for its own bookkeeping — Cloudflare's adapter prepends one identity
473
+ * tag (so `idFor` survives hibernation) before every `acceptWebSocket` call.
474
+ * Cloudflare's own cap is 10 tags per socket, 256 characters each
475
+ * (developers.cloudflare.com/durable-objects/api/state/), so with one slot
476
+ * reserved a portable caller should assume **at most 9 usable tags, each
477
+ * bounded to at most 256 characters** — a budget-exceeding `accept` call
478
+ * fails loudly on the host that enforces it (see
479
+ * {@link SocketHost.accept}) rather than passing silently on hosts with no
480
+ * cap and only failing, opaquely, on Cloudflare.
444
481
  */
445
482
  /**
446
483
  * The socket the engine sends through.
@@ -499,6 +536,14 @@ interface SocketHost {
499
536
  * survive recycling too and must be honoured by
500
537
  * {@link SocketHost.getSockets}. Returns a handle the engine can
501
538
  * send/close through.
539
+ *
540
+ * Portable callers should assume a budget of **at most 9 usable tags, each
541
+ * at most 256 characters** — some hosts (Cloudflare) reserve one tag slot
542
+ * of their own 10-tag cap for bookkeeping; see this file's module-header
543
+ * "Reserved-slot budget" note. A host that enforces a cap rejects an
544
+ * over-budget call rather than accepting it and silently dropping or
545
+ * truncating tags, which would break {@link SocketHost.getSockets}'s
546
+ * exactness requirement.
502
547
  */
503
548
  accept: (socket: unknown, attachment?: unknown, tags?: ReadonlyArray<string>) => SocketHandle;
504
549
  /**
@@ -577,4 +622,4 @@ interface SocketHost {
577
622
  */
578
623
  setTag?: (socket: SocketHandle, tag: string) => void;
579
624
  }
580
- 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 };
625
+ 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 };
@@ -180,20 +180,32 @@ interface SchedulerHost {
180
180
  schedule: (functionPath: string, args: Record<string, unknown>, options?: ScheduleOptions) => Promise<ScheduledJob>;
181
181
  }
182
182
  /**
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.
183
+ * Edge geography placement region, shared by `@lunora/runtime` (which reads
184
+ * `request.cf` to pick where a shard, replica, or region-local socket should
185
+ * live) and `@lunora/do` (which parses a region out of its own DO name). Kept
186
+ * here inlined into each consumer's bundle so the two sides can never drift
187
+ * on the region vocabulary without creating a runtime dependency edge between
188
+ * the packages.
187
189
  *
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.
190
+ * The values are Cloudflare's Durable Object location hints, which is also the
191
+ * only vocabulary a Lunora deployment needs today: a region is *only* ever used
192
+ * as a placement hint and as a name segment, never as data. Wrong-but-close is
193
+ * fine by construction — a misrouted read is one longer hop, never a wrong
194
+ * answer so this maps coarsely and returns `undefined` rather than guessing
195
+ * when the request carries no usable geography.
193
196
  *
194
- * Placement hints (jurisdiction, region) are provider-mapped and may be
195
- * unsupported per the capability matrix.
197
+ * Zero-dependency by design (see the repo's `shared/` rules): only relative /
198
+ * builtin imports, named exports, no `.js` extensions.
196
199
  */
200
+ /**
201
+ * The placement regions a name may carry and a hint may request — Cloudflare's
202
+ * `DurableObjectLocationHint` values, listed so the set can be validated at a
203
+ * trust boundary (a region parsed out of a DO name is attacker-influenced input
204
+ * on any route that mints names from a client-supplied shard key).
205
+ */
206
+ declare const REGION_HINTS: readonly ["wnam", "enam", "sam", "weur", "eeur", "apac", "apac-ne", "apac-se", "oc", "afr", "me"];
207
+ /** One placement region. Structurally identical to Cloudflare's `DurableObjectLocationHint`. */
208
+ type RegionHint = (typeof REGION_HINTS)[number];
197
209
  /**
198
210
  * Cloudflare Durable Object jurisdictions restrict where a DO runs and
199
211
  * persists data, for data-residency / compliance regimes (GDPR, FedRAMP, US
@@ -206,6 +218,23 @@ interface SchedulerHost {
206
218
  * leave them unsupported.
207
219
  */
208
220
  type ShardJurisdiction = "eu" | "fedramp" | "us" | (Record<never, never> & string);
221
+ /**
222
+ * A geographic placement region — where a shard should be created, when the
223
+ * caller has an opinion.
224
+ *
225
+ * One vocabulary, defined once: `shared/region-hint.ts` owns the region list
226
+ * (it is also what derives a region from edge geography), and this contract
227
+ * re-exports it rather than restating the strings. A second list is how the two
228
+ * ends of a placement request drift apart.
229
+ *
230
+ * Unlike a jurisdiction — a hard constraint the caller must fail closed on — a
231
+ * region is **best effort and advisory**: a provider may ignore it, and on
232
+ * Cloudflare it is honoured only by the call that first creates the object.
233
+ * Everything downstream must work identically whether the hint was honoured,
234
+ * ignored, or never supplied, and no caller may treat a resolved stub's
235
+ * location as known.
236
+ */
237
+ type ShardRegionHint = RegionHint;
209
238
  /**
210
239
  * A resolved shard stub. The engine calls `fetch` (or an equivalent RPC
211
240
  * method) to dispatch work to the shard.
@@ -222,9 +251,9 @@ interface ShardStub {
222
251
  */
223
252
  interface DirectShardDirectory {
224
253
  /** Resolve an opaque id (from `idForName`) to a stub, when the provider has ids. */
225
- get?: (id: unknown) => ShardStub;
254
+ get?: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
226
255
  /** Resolve a shard key to a stub. */
227
- getByName: (name: string) => ShardStub;
256
+ getByName: (name: string, locationHint?: ShardRegionHint) => ShardStub;
228
257
  /** Derive a stable, opaque shard id from a shard key, when the provider has ids. */
229
258
  idForName?: (name: string) => unknown;
230
259
  /** See {@link ShardDirectory}. */
@@ -237,7 +266,7 @@ interface DirectShardDirectory {
237
266
  */
238
267
  interface TwoStepShardDirectory {
239
268
  /** Resolve an opaque id (from `idForName`) to a stub. */
240
- get: (id: unknown) => ShardStub;
269
+ get: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
241
270
  /**
242
271
  * Absent — the discriminant that selects the two-step branch.
243
272
  */
@@ -269,8 +298,13 @@ type ShardDirectory = DirectShardDirectory | TwoStepShardDirectory;
269
298
  * Resolve a shard key to a stub against either directory shape. Uses direct
270
299
  * name lookup when the provider has it, and falls back to the two-step
271
300
  * `idForName` + `get` dance otherwise.
301
+ *
302
+ * `locationHint` is forwarded to whichever branch runs. It is advisory in
303
+ * both: a provider with no placement concept ignores the extra argument, which
304
+ * is exactly what an implementation written against the pre-placement
305
+ * signature does.
272
306
  */
273
- declare const resolveShard: (directory: ShardDirectory, name: string) => ShardStub;
307
+ declare const resolveShard: (directory: ShardDirectory, name: string, locationHint?: ShardRegionHint) => ShardStub;
274
308
  /**
275
309
  * `ShardHost` — the provider-neutral contract for a single-writer, durable
276
310
  * shard execution slot. On Cloudflare this is backed by one Durable Object
@@ -340,17 +374,6 @@ interface ShardSqlExec {
340
374
  /** Execute a SQL statement with optional bound parameters. */
341
375
  exec: <Row = SqlRow>(query: string, ...bindings: ReadonlyArray<unknown>) => ShardSqlCursor<Row>;
342
376
  }
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
377
  /**
355
378
  * Alarm scheduling for a shard. Alarms are durable: they survive host
356
379
  * recycling and fire at the requested timestamp.
@@ -372,16 +395,17 @@ interface ShardAlarms {
372
395
  interface ShardHost {
373
396
  /** Durable alarm scheduling for the shard. */
374
397
  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
398
  /**
382
399
  * Run `fn` with exclusive ownership of the shard. Concurrent calls are
383
400
  * queued; no two closures run at once for the same shard key. On
384
401
  * Cloudflare this maps to `state.blockConcurrencyWhile`.
402
+ *
403
+ * A closure that throws must reject with **the value it threw**, and must
404
+ * leave the host usable for the next call. Engine errors carry a `code` and
405
+ * `status` the RPC edge renders from, so a host that lets its platform
406
+ * substitute a copy silently downgrades every coded error to an internal
407
+ * fault — and a host that tears itself down on a throw makes an ordinary
408
+ * application error cost every other caller on that shard.
385
409
  */
386
410
  runSerialized: <T>(function_: () => Promise<T>) => Promise<T>;
387
411
  /**
@@ -401,8 +425,10 @@ interface ShardHost {
401
425
  sql: ShardSqlExec;
402
426
  /**
403
427
  * 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.
428
+ * back and the call rejects with **the value `fn` threw** — see
429
+ * {@link ShardHost.runSerialized} for why the identity matters. Raw
430
+ * `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure; the host
431
+ * manages the transaction boundary.
406
432
  */
407
433
  transaction: <T>(function_: () => Promise<T>) => Promise<T>;
408
434
  /**
@@ -441,6 +467,17 @@ interface ShardHost {
441
467
  * — optional. Presence declares that the host can retag a live socket. Hosts
442
468
  * that cannot (Cloudflare) omit both methods, and callers that need to
443
469
  * retag must instead close and re-accept the socket with new tags.
470
+ *
471
+ * **Reserved-slot budget.** A host may reserve some of its accept-time tag
472
+ * slots for its own bookkeeping — Cloudflare's adapter prepends one identity
473
+ * tag (so `idFor` survives hibernation) before every `acceptWebSocket` call.
474
+ * Cloudflare's own cap is 10 tags per socket, 256 characters each
475
+ * (developers.cloudflare.com/durable-objects/api/state/), so with one slot
476
+ * reserved a portable caller should assume **at most 9 usable tags, each
477
+ * bounded to at most 256 characters** — a budget-exceeding `accept` call
478
+ * fails loudly on the host that enforces it (see
479
+ * {@link SocketHost.accept}) rather than passing silently on hosts with no
480
+ * cap and only failing, opaquely, on Cloudflare.
444
481
  */
445
482
  /**
446
483
  * The socket the engine sends through.
@@ -499,6 +536,14 @@ interface SocketHost {
499
536
  * survive recycling too and must be honoured by
500
537
  * {@link SocketHost.getSockets}. Returns a handle the engine can
501
538
  * send/close through.
539
+ *
540
+ * Portable callers should assume a budget of **at most 9 usable tags, each
541
+ * at most 256 characters** — some hosts (Cloudflare) reserve one tag slot
542
+ * of their own 10-tag cap for bookkeeping; see this file's module-header
543
+ * "Reserved-slot budget" note. A host that enforces a cap rejects an
544
+ * over-budget call rather than accepting it and silently dropping or
545
+ * truncating tags, which would break {@link SocketHost.getSockets}'s
546
+ * exactness requirement.
502
547
  */
503
548
  accept: (socket: unknown, attachment?: unknown, tags?: ReadonlyArray<string>) => SocketHandle;
504
549
  /**
@@ -577,4 +622,4 @@ interface SocketHost {
577
622
  */
578
623
  setTag?: (socket: SocketHandle, tag: string) => void;
579
624
  }
580
- 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 };
625
+ 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.2",
3
+ "version": "1.0.0-alpha.21",
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"},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:"emulated",note:"In-process setTimeout; the timestamp can be persisted to SQLite but nothing re-arms it on process 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:"emulated",note:"In-process setTimeout only; not durable across a process restart, and no dynamic cron registration is implemented"},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 k}from"node:sqlite";let w=0,v=0;const B=()=>(w+=1,`socket-${w}`),D=()=>(v+=1,`job-${v}`),L=n=>n===void 0?null:n,C=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),R=()=>{const n=new k(":memory:"),s={alarmAt:null,alarmTimeout:null,pending:[],running:!1},i=new Map,f=new Map,u=new Map,T={exec:(e,...t)=>{const r=n.prepare(e),a=t.map(L),o=e.trim().toLowerCase().startsWith("select")?r.all(...a):(r.run(...a),[]);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]}}},A={all:async(e,t)=>n.prepare(e).all(...t),run:async(e,t)=>{const r=n.prepare(e).run(...t);return{rowsAffected:Number(r.changes)}}},g=()=>{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,g()}))},b={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 r=Math.max(0,t-Date.now());s.alarmTimeout=setTimeout(()=>{s.alarmAt=null,s.alarmTimeout=null},r)}},asyncSql:A,runSerialized:e=>new Promise((t,r)=>{s.pending.push({function_:e,reject:a=>{r(a)},resolve:a=>{t(a)}}),g()}),sql:T,transaction:async e=>{n.exec("BEGIN");try{const t=await e();return n.exec("COMMIT"),t}catch(t){throw n.exec("ROLLBACK"),t}},waitUntil:()=>{}},c=new WeakMap,h=e=>{const t={bufferedAmount:e.bufferedAmount,close:(r,a)=>{e.closed=!0},deserializeAttachment:()=>e.attachment,send:r=>{e.received.push(typeof r=="string"?r:C(r))},serializeAttachment:r=>{e.attachment=r,f.set(e.id,r)}};return e.handle=t,c.set(t,e.id),t},M={accept:(e,t,r)=>{const a=B(),o={attachment:t,bufferedAmount:0,closed:!1,raw:e,handle:null,id:a,received:[],tags:new Set(r)};return i.set(a,o),u.set(a,new Set(r)),t!==void 0&&f.set(a,t),h(o)},getSockets:e=>{const t=[...i.values()];return(e===void 0?t:t.filter(r=>r.tags.has(e))).map(r=>r.handle)},handleFor:e=>[...i.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 r=i.get(c.get(e)??"");r!==void 0&&(t===void 0?r.tags.clear():r.tags.delete(t),u.set(c.get(e)??"",new Set(r.tags)))},setTag:(e,t)=>{const r=c.get(e)??"",a=i.get(r);a!==void 0&&(a.tags.add(t),u.set(r,new Set(a.tags)))}},p={get:e=>({fetch:async()=>new Response(String(e))}),getByName:e=>({fetch:async()=>new Response(e)}),idForName:e=>`shard:${e}`,jurisdiction:e=>p},d=new Map,S={delete:async e=>d.delete(e),get:async e=>d.get(e),list:async e=>{const t=e?.prefix??"",r=new Map;for(const[a,o]of d)a.startsWith(t)&&r.set(a,o);return r},put:async(e,t)=>{d.set(e,structuredClone(t))}},l=new Map,m=new Map,y=(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)})},cleanup:()=>{n.close(),s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);for(const e of l.values())clearTimeout(e.timer)},directory:p,kv:S,readFrames:e=>(i.get(c.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(u.get(e))};return i.set(e,r),h(r)},scheduler:{cancel:async e=>{const t=l.get(e);return t===void 0?!1:(clearTimeout(t.timer),l.delete(e),!0)},cron:async()=>{},deadLetter:{list:async()=>[...m].map(([e,t])=>y(e,t)),requeue:async e=>{const t=m.get(e);return t===void 0?!1:(m.delete(e),l.set(e,{...t,attempts:0,timer:void 0}),!0)}},list:async()=>[...l].map(([e,t])=>y(e,t)),schedule:async(e,t,r)=>{const a=D();let o;r?.at===void 0?o=Date.now()+(r?.delayMs??0):o=typeof r.at=="number"?r.at:r.at.getTime();const x=Math.max(0,o-Date.now()),F=setTimeout(()=>{l.delete(a)},x);return l.set(a,{args:t,attempts:0,functionPath:e,options:r??{},scheduledFor:o,timer:F}),{id:a,scheduledFor:o}}},simulateDeadLetter:async e=>{const t=l.get(e);return t===void 0?!1:(clearTimeout(t.timer),l.delete(e),m.set(e,{...t,attempts:(t.options.retry?.maxAttempts??5)+1,timer:void 0}),!0)},shard:b,simulateRecycle:()=>{i.clear()},socket:M}};export{R 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};