@lunora/platform 1.0.0-alpha.2 → 1.0.0-alpha.20
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 +26 -18
- package/dist/conformance/suite.d.ts +26 -18
- package/dist/conformance/suite.mjs +1 -1
- package/dist/index.d.mts +279 -31
- package/dist/index.d.ts +279 -31
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-DcCyL_87.mjs +1 -0
- package/dist/packem_shared/createReferenceHost-Cvx-Socp.mjs +1 -0
- package/dist/packem_shared/resolveShard-BzKOUEO4.mjs +1 -0
- package/dist/packem_shared/{socket-host.d-Cq5uVbiH.d.ts → socket-host.d-dVPE86WP.d.mts} +80 -35
- package/dist/packem_shared/{socket-host.d-Cq5uVbiH.d.mts → socket-host.d-dVPE86WP.d.ts} +80 -35
- package/package.json +1 -1
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-CfXSyHOn.mjs +0 -1
- package/dist/packem_shared/createReferenceHost-GO8Hp4ft.mjs +0 -1
- package/dist/packem_shared/resolveShard-uGhAKuTB.mjs +0 -1
|
@@ -180,20 +180,32 @@ interface SchedulerHost {
|
|
|
180
180
|
schedule: (functionPath: string, args: Record<string, unknown>, options?: ScheduleOptions) => Promise<ScheduledJob>;
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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
|
-
*
|
|
195
|
-
*
|
|
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
|
|
405
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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
|
-
*
|
|
195
|
-
*
|
|
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
|
|
405
|
-
*
|
|
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,
|
|
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.
|
|
3
|
+
"version": "1.0.0-alpha.20",
|
|
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};
|