@lunora/do 1.0.0-alpha.120 → 1.0.0-alpha.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -321,10 +321,25 @@ interface SpanHandle {
321
321
  * `@opentelemetry/api` bridge parenting a third-party library's spans — to
322
322
  * reach around the API for it.
323
323
  */
324
- spanContext: () => {
325
- spanId: string;
326
- traceId: string;
327
- };
324
+ spanContext: () => SpanContextIds;
325
+ }
326
+ /**
327
+ * The W3C identity of one span, plus the trace's settled sampling verdict.
328
+ *
329
+ * `sampled` is the propagated head decision (absent means "no verdict reached
330
+ * this tier", which every consumer reads as keep). It rides alongside the ids
331
+ * because everything that needs the ids to announce this span downstream — a
332
+ * hand-built `traceparent`, the `@opentelemetry/api` bridge's `SpanContext` —
333
+ * needs the flag in the same breath, and announcing `sampled` on a trace that
334
+ * was sampled OUT is what leaves a collector holding the middle of a trace.
335
+ */
336
+ interface SpanContextIds {
337
+ /** The trace's settled W3C `sampled` verdict; absent when none was propagated. */
338
+ sampled?: boolean;
339
+ /** This span's id (16-hex). */
340
+ spanId: string;
341
+ /** The trace this span belongs to (32-hex). */
342
+ traceId: string;
328
343
  }
329
344
  interface SpanEvent {
330
345
  /**
@@ -661,8 +676,17 @@ interface ShardDOState {
661
676
  */
662
677
  blockConcurrencyWhile?: <T>(callback: () => Promise<T>) => Promise<T>;
663
678
  getWebSockets: (tag?: string) => WebSocket[];
664
- /** Optional pointer to the DO instance id so we can detect `__root__`. */
679
+ /**
680
+ * Optional pointer to the DO instance id so we can detect `__root__`.
681
+ *
682
+ * `jurisdiction` is the Cloudflare data-residency the id was minted under
683
+ * (`env.SHARD.jurisdiction("eu").idFromName(...)`), preserved on the id
684
+ * itself. It is the ONLY place a DO can learn its own residency, and the
685
+ * DO→DO tiers need it: a sibling resolved off the raw namespace binding is
686
+ * a different object entirely.
687
+ */
665
688
  id?: {
689
+ jurisdiction?: string;
666
690
  name?: string;
667
691
  };
668
692
  /**
@@ -802,6 +826,29 @@ interface QueryReadScope {
802
826
  /** Dependency tracker for this dispatch — the `onRead` channel. */
803
827
  tracker: DependencyTracker;
804
828
  }
829
+ /**
830
+ * The outbound D1 Sessions bookmark ONE dispatch's `.global()` writes produced —
831
+ * the value echoed back as `x-d1-bookmark` so the caller's next global read pins
832
+ * a replica that has seen them.
833
+ *
834
+ * Threaded BY VALUE exactly like {@link QueryReadScope}: minted per dispatch by
835
+ * `beginDispatch`, handed to `handleRpc`'s fifth parameter, and bound into the
836
+ * generated `buildCtx`'s `onBookmark` callback. A shared instance field cannot
837
+ * carry it. A MUTATION is input-gated, so nothing interleaves, but an ACTION is
838
+ * not — it writes a global row, `await`s a third party, and every other dispatch
839
+ * on the DO runs inside that window. A sibling's `beginDispatch`/`endDispatch`
840
+ * cleared the field, so the action answered with no `x-d1-bookmark` at all and
841
+ * the client's next global read went unpinned: read-your-writes silently lost on
842
+ * a replica, which is the one thing the bookmark exists to prevent.
843
+ *
844
+ * `undefined` where there is no HTTP response to carry it (an alarm tick, a
845
+ * lifecycle dispatch, `runAs`); the bookmark is then simply dropped, which is
846
+ * what those paths did before.
847
+ */
848
+ interface DispatchBookmark {
849
+ /** The bookmark this dispatch's last global write reported. */
850
+ value: string | undefined;
851
+ }
805
852
  /**
806
853
  * Shard-level configuration passed through `super(state, env, …)` by the
807
854
  * generated subclass, which sources every key from the app's
@@ -1119,9 +1166,14 @@ declare abstract class ShardDO {
1119
1166
  */
1120
1167
  private currentRequestBookmark;
1121
1168
  /**
1122
- * Per-request D1 bookmark to echo on the outbound response. Handlers
1123
- * call `setOutboundBookmark` after a global-table write so the
1124
- * client can pin subsequent reads on the same replica.
1169
+ * The D1 bookmark to echo on the outbound response, so the client can pin
1170
+ * subsequent reads on a replica that has seen its own write.
1171
+ *
1172
+ * Assigned ONLY by the dispatch tail, from that dispatch's own
1173
+ * {@link DispatchBookmark} sink, at a point where nothing else can be
1174
+ * mid-handler on this instance. Handlers write `sink.value`, never this
1175
+ * field: an action holds its bookmark across `await`s a sibling dispatch runs
1176
+ * inside, and a shared field loses it there.
1125
1177
  */
1126
1178
  private currentResponseBookmark;
1127
1179
  /**
@@ -1134,9 +1186,12 @@ declare abstract class ShardDO {
1134
1186
  private currentRequestUserId;
1135
1187
  /**
1136
1188
  * Per-request caller IP forwarded from the runtime via the
1137
- * `x-lunora-client-ip` header (sourced server-side from Cloudflare's trusted
1138
- * `CF-Connecting-IP`). Surfaced to handlers as `ctx.ip` via `getCurrentIp`;
1139
- * cleared in the `finally` block of `fetch` like the other per-request fields.
1189
+ * `x-lunora-client-ip` header. The runtime sources it from Cloudflare's
1190
+ * `CF-Connecting-IP` and only while running ON Cloudflare, where the edge
1191
+ * stamps that header itself; off the edge it forwards nothing rather than a
1192
+ * value the caller typed, so this stays `undefined`. Surfaced to handlers as
1193
+ * `ctx.ip` via `getCurrentIp`; cleared in the `finally` block of `fetch` like
1194
+ * the other per-request fields.
1140
1195
  */
1141
1196
  private currentRequestIp;
1142
1197
  /** W3C `traceparent` of the inbound RPC; forwarded onto outbound container fetches. */
@@ -1380,11 +1435,21 @@ declare abstract class ShardDO {
1380
1435
  */
1381
1436
  private durableSnapshotStoreAvailable;
1382
1437
  /**
1383
- * Whether a global-shape poll alarm is currently armed. Guards
1384
- * {@link ShardDO.scheduleGlobalPoll} from re-arming on every seed; reset in
1385
- * {@link ShardDO.alarm} before the poll so a still-subscribed shape re-arms.
1386
- */
1387
- private globalPollScheduled;
1438
+ * When the currently armed poll alarm is due, or `undefined` when none is.
1439
+ * Guards {@link ShardDO.scheduleGlobalPoll} from re-arming on every seed;
1440
+ * cleared in {@link ShardDO.alarm} before the poll so a still-subscribed shape
1441
+ * re-arms.
1442
+ *
1443
+ * The TIME, not a bare boolean. The alarm is shared by three tiers, and the
1444
+ * one an alarm tick re-arms is the EARLIEST due across them — with no global
1445
+ * subscribers that is a `.source()` refresh which can be hours out. A bare
1446
+ * flag made every later caller a no-op, so a fresh `.global()` shape seed
1447
+ * (which wants the 2 s floor) waited out that pending alarm on every warm
1448
+ * instance until eviction. Comparing targets re-arms whenever an EARLIER wake
1449
+ * is asked for, and stays a no-op otherwise — a DO has one alarm, and
1450
+ * `setAlarm` replaces it.
1451
+ */
1452
+ private globalPollArmedAt;
1388
1453
  /** Monotonic per-DO poke id source; correlates a poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
1389
1454
  private pokeSequence;
1390
1455
  /** Per-socket whisper-rate token bucket (see {@link ShardDO.WHISPER_RATE_BURST}). In-memory; resets on hibernation. */
@@ -1701,8 +1766,14 @@ declare abstract class ShardDO {
1701
1766
  * `getCtxDbReadRangeHook(scope)` on the `createShardCtxDb(...)` call that
1702
1767
  * builds the ctx; both factories return unbound (tracker-less) hooks when it
1703
1768
  * is omitted, which is what every non-cached dispatch passes.
1769
+ *
1770
+ * `bookmarks` is the third such thread — see {@link DispatchBookmark}.
1771
+ * Implementations must record the bookmark on it from the global database's
1772
+ * `onBookmark` callback so the bookmark a `.global()` write produced reaches
1773
+ * THIS dispatch's response rather than a shared field a concurrent action can
1774
+ * clear.
1704
1775
  */
1705
- abstract handleRpc(functionPath: string, args: Record<string, unknown>, headroom?: TransactionHeadroomTracker, scope?: QueryReadScope): Promise<unknown>;
1776
+ abstract handleRpc(functionPath: string, args: Record<string, unknown>, headroom?: TransactionHeadroomTracker, scope?: QueryReadScope, bookmarks?: DispatchBookmark): Promise<unknown>;
1706
1777
  /**
1707
1778
  * The registered function paths to dispatch on a lifecycle moment —
1708
1779
  * `connect`/`disconnect` per socket, `init` once per Durable Object instance,
@@ -1904,13 +1975,6 @@ declare abstract class ShardDO {
1904
1975
  * consistency across replicas.
1905
1976
  */
1906
1977
  protected getInboundBookmark(): string | undefined;
1907
- /**
1908
- * Record the post-write D1 bookmark that should be echoed back to the
1909
- * client on the outbound `x-d1-bookmark` header. Safe to call multiple
1910
- * times — the last value wins; only the most recent write's bookmark
1911
- * is meaningful for downstream read pinning.
1912
- */
1913
- protected setOutboundBookmark(bookmark: string | undefined): void;
1914
1978
  /**
1915
1979
  * The userId forwarded by the runtime's `resolveIdentity` hook for the
1916
1980
  * current request, or `undefined` when the request is anonymous. Use
@@ -1919,7 +1983,9 @@ declare abstract class ShardDO {
1919
1983
  protected getCurrentUserId(): string | undefined;
1920
1984
  /**
1921
1985
  * The caller's IP for the current request (Cloudflare's `CF-Connecting-IP`,
1922
- * forwarded server-side), or `undefined` when unknown. Use this to populate
1986
+ * forwarded server-side by the runtime, and only while running on Cloudflare
1987
+ * — off the edge that header is client-written, so the runtime forwards
1988
+ * nothing), or `undefined` when nothing trustworthy says. Use this to populate
1923
1989
  * `ctx.ip` inside `buildCtx`.
1924
1990
  */
1925
1991
  protected getCurrentIp(): string | undefined;
@@ -2385,8 +2451,17 @@ declare abstract class ShardDO {
2385
2451
  * `this.sql` handle. `INSERT OR IGNORE` keeps a concurrent double-dispatch (or
2386
2452
  * the now-skipped post-dispatch call) of the same id idempotent. Also runs the
2387
2453
  * throttled dedup-table GC.
2454
+ *
2455
+ * `encodedResult` has ALREADY been through {@link encodeDispatchResult}, so
2456
+ * the cache holds JSON-safe wire bytes (a raw `bigint` result would otherwise
2457
+ * throw `JSON.stringify`) and a replay answers byte-identical wire form
2458
+ * without a second `encodeWire`. Encoding is the caller's job precisely so it
2459
+ * happens OUTSIDE the swallow below: that `catch` is for a missing dedup table
2460
+ * (pre-migration shard / test stub), not for a return value the codec refuses,
2461
+ * and swallowing the latter is what let an unencodable mutation commit its
2462
+ * writes with no replay guard.
2388
2463
  */
2389
- protected persistIdempotentResult(result: unknown): void;
2464
+ protected persistIdempotentResult(encodedResult: unknown): void;
2390
2465
  /**
2391
2466
  * Whether `functionPath` names a registered custom mutator (a `defineMutator`
2392
2467
  * declaration) rather than an ordinary `mutation`. The base class knows of no
@@ -2488,6 +2563,13 @@ declare abstract class ShardDO {
2488
2563
  * without the replay guard (which a re-dispatch would otherwise re-run) nor
2489
2564
  * without the watermark. Records {@link ShardDO.mutationBookkeeping} under this
2490
2565
  * dispatch's mutation id so `fetch` skips the redundant post-dispatch persist.
2566
+ *
2567
+ * The wire encode of `result` happens HERE, first, so a return value the codec
2568
+ * refuses (a class instance, nesting past its depth cap) throws while the
2569
+ * transaction is still open and rolls the whole mutation back. It runs
2570
+ * unconditionally — a request carrying no `x-lunora-mutation-id` writes no
2571
+ * dedup row but must still not commit writes behind a response that cannot be
2572
+ * serialized.
2491
2573
  */
2492
2574
  protected commitMutationBookkeeping(result: unknown): void;
2493
2575
  /**
@@ -2541,6 +2623,10 @@ declare abstract class ShardDO {
2541
2623
  * poke a paid query itself, or it is served free. The base class has no
2542
2624
  * function registry, so the default is `false`; the codegen-generated
2543
2625
  * subclass overrides it with the real `LUNORA_FUNCTIONS` lookup.
2626
+ *
2627
+ * Also backs the `/rpc` backstop: an origin built without a `functions`
2628
+ * registry cannot read the tag at all, so it charges nothing and marks
2629
+ * nothing — and this is the only place left that still knows the call is paid.
2544
2630
  */
2545
2631
  protected isPaidFunction(_functionPath: string): boolean;
2546
2632
  /**
@@ -3131,25 +3217,6 @@ declare abstract class ShardDO {
3131
3217
  protected recordMetric(event: MetricEvent, sink?: TelemetrySink): void;
3132
3218
  /** The decode + route body of {@link webSocketMessage}, split out so the trace wrapper stays a one-liner. */
3133
3219
  protected handleWebSocketMessage(ws: ShardSocketLike, message: string | ArrayBuffer): Promise<void>;
3134
- /**
3135
- * Stamp everything one RPC dispatch needs off its request, and reset every
3136
- * per-request capture the handler will fill.
3137
- *
3138
- * Split from {@link ShardDO.handleFetchCloudflare} together with
3139
- * {@link ShardDO.endDispatch}, and the pairing is the point: these two own
3140
- * the same set of fields, and the whole correctness story for them is that
3141
- * every field one sets, the other clears. Spread across a 479-line method
3142
- * the two ends were 350 lines apart, so a newly-added per-request field
3143
- * stamped here and forgotten there leaks into the NEXT request on the same
3144
- * DO instance — a cross-request identity bleed with no local symptom.
3145
- * `__tests__/dispatch-lifecycle.test.ts` asserts the symmetry directly.
3146
- *
3147
- * Returns the two values the caller must hold in locals rather than read
3148
- * back off `this`: an `await`-interleaved concurrent dispatch can re-set the
3149
- * shared fields, and the `finally` would then file this dispatch's telemetry
3150
- * under another request's trace (see the comments inside).
3151
- * @returns this dispatch's trace anchor and its transaction-headroom tracker
3152
- */
3153
3220
  /**
3154
3221
  * Cloudflare-specific fetch implementation — WebSocket upgrades and the RPC
3155
3222
  * routes. Injected into {@link ShardRunner} as the host-specific handler while
@@ -4510,6 +4577,11 @@ declare abstract class ShardDO {
4510
4577
  * (a fresh global-shape seed, {@link ShardDO.scheduleSourcePoll}'s initial
4511
4578
  * kick) omits it and gets the original `GLOBAL_SHAPE_POLL_INTERVAL_MS`
4512
4579
  * default, since neither knows a more precise due time yet.
4580
+ *
4581
+ * "Already pending" is decided against {@link ShardDO.globalPollArmedAt}'s
4582
+ * TIME: an alarm due later than the requested target is replaced, one due at
4583
+ * or before it is left alone. Only ever moves the wake EARLIER, so repeated
4584
+ * seeds cannot walk the alarm out.
4513
4585
  */
4514
4586
  private scheduleGlobalPoll;
4515
4587
  /**
@@ -4585,6 +4657,45 @@ declare abstract class ShardDO {
4585
4657
  floor?: number;
4586
4658
  tables: string[];
4587
4659
  } | undefined>;
4660
+ /**
4661
+ * The two answers a `/rpc` can earn between the replica gate and
4662
+ * `beginDispatch`: a reserved admin RPC (served under its own scope and
4663
+ * bearer gate) and the paid-procedure backstop.
4664
+ *
4665
+ * Split out of {@link ShardDO.handleFetchCloudflare} because both are the same
4666
+ * shape — request in, response out, no dispatch bookkeeping in scope — while
4667
+ * everything after them shares eight dispatch locals. `undefined` means
4668
+ * "nothing answered it here; go dispatch".
4669
+ *
4670
+ * Deliberately NOT `async`: the admin branch is the only one that awaits, and
4671
+ * an `async` wrapper would spend a microtask on the common path where this
4672
+ * returns `undefined` — which is enough to move `beginDispatch` after a
4673
+ * concurrent admin request that overlaps it (see
4674
+ * `__tests__/shard-do.system-dispatch.test.ts`).
4675
+ * @param request The inbound `/rpc` request.
4676
+ * @param payload Its parsed body.
4677
+ * @returns The response to return, or `undefined` to continue into dispatch.
4678
+ */
4679
+ private preDispatchAnswer;
4680
+ /**
4681
+ * Stamp everything one RPC dispatch needs off its request, and reset every
4682
+ * per-request capture the handler will fill.
4683
+ *
4684
+ * Split from {@link ShardDO.handleFetchCloudflare} together with
4685
+ * {@link ShardDO.endDispatch}, and the pairing is the point: these two own
4686
+ * the same set of fields, and the whole correctness story for them is that
4687
+ * every field one sets, the other clears. Spread across a 479-line method
4688
+ * the two ends were 350 lines apart, so a newly-added per-request field
4689
+ * stamped here and forgotten there leaks into the NEXT request on the same
4690
+ * DO instance — a cross-request identity bleed with no local symptom.
4691
+ * `__tests__/dispatch-lifecycle.test.ts` asserts the symmetry directly.
4692
+ *
4693
+ * Returns the two values the caller must hold in locals rather than read
4694
+ * back off `this`: an `await`-interleaved concurrent dispatch can re-set the
4695
+ * shared fields, and the `finally` would then file this dispatch's telemetry
4696
+ * under another request's trace (see the comments inside).
4697
+ * @returns this dispatch's trace anchor and its transaction-headroom tracker
4698
+ */
4588
4699
  private beginDispatch;
4589
4700
  /** Clear every per-request field {@link ShardDO.beginDispatch} stamped. */
4590
4701
  private endDispatch;
@@ -4985,4 +5096,4 @@ declare class ShardRegistryDO {
4985
5096
  /** The in-memory map as a JSON-safe `table → [keys]` object, for `/snapshot`. */
4986
5097
  private serializeTables;
4987
5098
  }
4988
- export { type HibernatableWebSocket, type QueryReadScope, ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, type RunShardApplyCdcArgs, type RunShardApplyCdcResult, type RunShardBulkRowArgs, type RunShardBulkRowResult, type RunShardExportArgs, type RunShardImportArgs, type RunShardMigrationArgs, type RunShardRankBeforeArgs, type RunShardRankPageArgs, type RunShardWriteArgs, type RunShardWriteResult, SESSION_DO_TTL_DEFAULT, SHARD_REGISTRY_DO_NAME, SessionDO, type SessionRecord, ShardDO, type ShardDOOptions, type ShardDOState, ShardRegistryDO, type SubscriptionOutcome, type TelemetrySink, type TraceRefLike, serveRelationFanout };
5099
+ export { type DispatchBookmark, type HibernatableWebSocket, type QueryReadScope, ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, type RunShardApplyCdcArgs, type RunShardApplyCdcResult, type RunShardBulkRowArgs, type RunShardBulkRowResult, type RunShardExportArgs, type RunShardImportArgs, type RunShardMigrationArgs, type RunShardRankBeforeArgs, type RunShardRankPageArgs, type RunShardWriteArgs, type RunShardWriteResult, SESSION_DO_TTL_DEFAULT, SHARD_REGISTRY_DO_NAME, SessionDO, type SessionRecord, ShardDO, type ShardDOOptions, type ShardDOState, ShardRegistryDO, type SubscriptionOutcome, type TelemetrySink, type TraceRefLike, serveRelationFanout };
package/dist/index.d.ts CHANGED
@@ -321,10 +321,25 @@ interface SpanHandle {
321
321
  * `@opentelemetry/api` bridge parenting a third-party library's spans — to
322
322
  * reach around the API for it.
323
323
  */
324
- spanContext: () => {
325
- spanId: string;
326
- traceId: string;
327
- };
324
+ spanContext: () => SpanContextIds;
325
+ }
326
+ /**
327
+ * The W3C identity of one span, plus the trace's settled sampling verdict.
328
+ *
329
+ * `sampled` is the propagated head decision (absent means "no verdict reached
330
+ * this tier", which every consumer reads as keep). It rides alongside the ids
331
+ * because everything that needs the ids to announce this span downstream — a
332
+ * hand-built `traceparent`, the `@opentelemetry/api` bridge's `SpanContext` —
333
+ * needs the flag in the same breath, and announcing `sampled` on a trace that
334
+ * was sampled OUT is what leaves a collector holding the middle of a trace.
335
+ */
336
+ interface SpanContextIds {
337
+ /** The trace's settled W3C `sampled` verdict; absent when none was propagated. */
338
+ sampled?: boolean;
339
+ /** This span's id (16-hex). */
340
+ spanId: string;
341
+ /** The trace this span belongs to (32-hex). */
342
+ traceId: string;
328
343
  }
329
344
  interface SpanEvent {
330
345
  /**
@@ -661,8 +676,17 @@ interface ShardDOState {
661
676
  */
662
677
  blockConcurrencyWhile?: <T>(callback: () => Promise<T>) => Promise<T>;
663
678
  getWebSockets: (tag?: string) => WebSocket[];
664
- /** Optional pointer to the DO instance id so we can detect `__root__`. */
679
+ /**
680
+ * Optional pointer to the DO instance id so we can detect `__root__`.
681
+ *
682
+ * `jurisdiction` is the Cloudflare data-residency the id was minted under
683
+ * (`env.SHARD.jurisdiction("eu").idFromName(...)`), preserved on the id
684
+ * itself. It is the ONLY place a DO can learn its own residency, and the
685
+ * DO→DO tiers need it: a sibling resolved off the raw namespace binding is
686
+ * a different object entirely.
687
+ */
665
688
  id?: {
689
+ jurisdiction?: string;
666
690
  name?: string;
667
691
  };
668
692
  /**
@@ -802,6 +826,29 @@ interface QueryReadScope {
802
826
  /** Dependency tracker for this dispatch — the `onRead` channel. */
803
827
  tracker: DependencyTracker;
804
828
  }
829
+ /**
830
+ * The outbound D1 Sessions bookmark ONE dispatch's `.global()` writes produced —
831
+ * the value echoed back as `x-d1-bookmark` so the caller's next global read pins
832
+ * a replica that has seen them.
833
+ *
834
+ * Threaded BY VALUE exactly like {@link QueryReadScope}: minted per dispatch by
835
+ * `beginDispatch`, handed to `handleRpc`'s fifth parameter, and bound into the
836
+ * generated `buildCtx`'s `onBookmark` callback. A shared instance field cannot
837
+ * carry it. A MUTATION is input-gated, so nothing interleaves, but an ACTION is
838
+ * not — it writes a global row, `await`s a third party, and every other dispatch
839
+ * on the DO runs inside that window. A sibling's `beginDispatch`/`endDispatch`
840
+ * cleared the field, so the action answered with no `x-d1-bookmark` at all and
841
+ * the client's next global read went unpinned: read-your-writes silently lost on
842
+ * a replica, which is the one thing the bookmark exists to prevent.
843
+ *
844
+ * `undefined` where there is no HTTP response to carry it (an alarm tick, a
845
+ * lifecycle dispatch, `runAs`); the bookmark is then simply dropped, which is
846
+ * what those paths did before.
847
+ */
848
+ interface DispatchBookmark {
849
+ /** The bookmark this dispatch's last global write reported. */
850
+ value: string | undefined;
851
+ }
805
852
  /**
806
853
  * Shard-level configuration passed through `super(state, env, …)` by the
807
854
  * generated subclass, which sources every key from the app's
@@ -1119,9 +1166,14 @@ declare abstract class ShardDO {
1119
1166
  */
1120
1167
  private currentRequestBookmark;
1121
1168
  /**
1122
- * Per-request D1 bookmark to echo on the outbound response. Handlers
1123
- * call `setOutboundBookmark` after a global-table write so the
1124
- * client can pin subsequent reads on the same replica.
1169
+ * The D1 bookmark to echo on the outbound response, so the client can pin
1170
+ * subsequent reads on a replica that has seen its own write.
1171
+ *
1172
+ * Assigned ONLY by the dispatch tail, from that dispatch's own
1173
+ * {@link DispatchBookmark} sink, at a point where nothing else can be
1174
+ * mid-handler on this instance. Handlers write `sink.value`, never this
1175
+ * field: an action holds its bookmark across `await`s a sibling dispatch runs
1176
+ * inside, and a shared field loses it there.
1125
1177
  */
1126
1178
  private currentResponseBookmark;
1127
1179
  /**
@@ -1134,9 +1186,12 @@ declare abstract class ShardDO {
1134
1186
  private currentRequestUserId;
1135
1187
  /**
1136
1188
  * Per-request caller IP forwarded from the runtime via the
1137
- * `x-lunora-client-ip` header (sourced server-side from Cloudflare's trusted
1138
- * `CF-Connecting-IP`). Surfaced to handlers as `ctx.ip` via `getCurrentIp`;
1139
- * cleared in the `finally` block of `fetch` like the other per-request fields.
1189
+ * `x-lunora-client-ip` header. The runtime sources it from Cloudflare's
1190
+ * `CF-Connecting-IP` and only while running ON Cloudflare, where the edge
1191
+ * stamps that header itself; off the edge it forwards nothing rather than a
1192
+ * value the caller typed, so this stays `undefined`. Surfaced to handlers as
1193
+ * `ctx.ip` via `getCurrentIp`; cleared in the `finally` block of `fetch` like
1194
+ * the other per-request fields.
1140
1195
  */
1141
1196
  private currentRequestIp;
1142
1197
  /** W3C `traceparent` of the inbound RPC; forwarded onto outbound container fetches. */
@@ -1380,11 +1435,21 @@ declare abstract class ShardDO {
1380
1435
  */
1381
1436
  private durableSnapshotStoreAvailable;
1382
1437
  /**
1383
- * Whether a global-shape poll alarm is currently armed. Guards
1384
- * {@link ShardDO.scheduleGlobalPoll} from re-arming on every seed; reset in
1385
- * {@link ShardDO.alarm} before the poll so a still-subscribed shape re-arms.
1386
- */
1387
- private globalPollScheduled;
1438
+ * When the currently armed poll alarm is due, or `undefined` when none is.
1439
+ * Guards {@link ShardDO.scheduleGlobalPoll} from re-arming on every seed;
1440
+ * cleared in {@link ShardDO.alarm} before the poll so a still-subscribed shape
1441
+ * re-arms.
1442
+ *
1443
+ * The TIME, not a bare boolean. The alarm is shared by three tiers, and the
1444
+ * one an alarm tick re-arms is the EARLIEST due across them — with no global
1445
+ * subscribers that is a `.source()` refresh which can be hours out. A bare
1446
+ * flag made every later caller a no-op, so a fresh `.global()` shape seed
1447
+ * (which wants the 2 s floor) waited out that pending alarm on every warm
1448
+ * instance until eviction. Comparing targets re-arms whenever an EARLIER wake
1449
+ * is asked for, and stays a no-op otherwise — a DO has one alarm, and
1450
+ * `setAlarm` replaces it.
1451
+ */
1452
+ private globalPollArmedAt;
1388
1453
  /** Monotonic per-DO poke id source; correlates a poke's `pokeStart`/`pokePart`/`pokeEnd` frames. */
1389
1454
  private pokeSequence;
1390
1455
  /** Per-socket whisper-rate token bucket (see {@link ShardDO.WHISPER_RATE_BURST}). In-memory; resets on hibernation. */
@@ -1701,8 +1766,14 @@ declare abstract class ShardDO {
1701
1766
  * `getCtxDbReadRangeHook(scope)` on the `createShardCtxDb(...)` call that
1702
1767
  * builds the ctx; both factories return unbound (tracker-less) hooks when it
1703
1768
  * is omitted, which is what every non-cached dispatch passes.
1769
+ *
1770
+ * `bookmarks` is the third such thread — see {@link DispatchBookmark}.
1771
+ * Implementations must record the bookmark on it from the global database's
1772
+ * `onBookmark` callback so the bookmark a `.global()` write produced reaches
1773
+ * THIS dispatch's response rather than a shared field a concurrent action can
1774
+ * clear.
1704
1775
  */
1705
- abstract handleRpc(functionPath: string, args: Record<string, unknown>, headroom?: TransactionHeadroomTracker, scope?: QueryReadScope): Promise<unknown>;
1776
+ abstract handleRpc(functionPath: string, args: Record<string, unknown>, headroom?: TransactionHeadroomTracker, scope?: QueryReadScope, bookmarks?: DispatchBookmark): Promise<unknown>;
1706
1777
  /**
1707
1778
  * The registered function paths to dispatch on a lifecycle moment —
1708
1779
  * `connect`/`disconnect` per socket, `init` once per Durable Object instance,
@@ -1904,13 +1975,6 @@ declare abstract class ShardDO {
1904
1975
  * consistency across replicas.
1905
1976
  */
1906
1977
  protected getInboundBookmark(): string | undefined;
1907
- /**
1908
- * Record the post-write D1 bookmark that should be echoed back to the
1909
- * client on the outbound `x-d1-bookmark` header. Safe to call multiple
1910
- * times — the last value wins; only the most recent write's bookmark
1911
- * is meaningful for downstream read pinning.
1912
- */
1913
- protected setOutboundBookmark(bookmark: string | undefined): void;
1914
1978
  /**
1915
1979
  * The userId forwarded by the runtime's `resolveIdentity` hook for the
1916
1980
  * current request, or `undefined` when the request is anonymous. Use
@@ -1919,7 +1983,9 @@ declare abstract class ShardDO {
1919
1983
  protected getCurrentUserId(): string | undefined;
1920
1984
  /**
1921
1985
  * The caller's IP for the current request (Cloudflare's `CF-Connecting-IP`,
1922
- * forwarded server-side), or `undefined` when unknown. Use this to populate
1986
+ * forwarded server-side by the runtime, and only while running on Cloudflare
1987
+ * — off the edge that header is client-written, so the runtime forwards
1988
+ * nothing), or `undefined` when nothing trustworthy says. Use this to populate
1923
1989
  * `ctx.ip` inside `buildCtx`.
1924
1990
  */
1925
1991
  protected getCurrentIp(): string | undefined;
@@ -2385,8 +2451,17 @@ declare abstract class ShardDO {
2385
2451
  * `this.sql` handle. `INSERT OR IGNORE` keeps a concurrent double-dispatch (or
2386
2452
  * the now-skipped post-dispatch call) of the same id idempotent. Also runs the
2387
2453
  * throttled dedup-table GC.
2454
+ *
2455
+ * `encodedResult` has ALREADY been through {@link encodeDispatchResult}, so
2456
+ * the cache holds JSON-safe wire bytes (a raw `bigint` result would otherwise
2457
+ * throw `JSON.stringify`) and a replay answers byte-identical wire form
2458
+ * without a second `encodeWire`. Encoding is the caller's job precisely so it
2459
+ * happens OUTSIDE the swallow below: that `catch` is for a missing dedup table
2460
+ * (pre-migration shard / test stub), not for a return value the codec refuses,
2461
+ * and swallowing the latter is what let an unencodable mutation commit its
2462
+ * writes with no replay guard.
2388
2463
  */
2389
- protected persistIdempotentResult(result: unknown): void;
2464
+ protected persistIdempotentResult(encodedResult: unknown): void;
2390
2465
  /**
2391
2466
  * Whether `functionPath` names a registered custom mutator (a `defineMutator`
2392
2467
  * declaration) rather than an ordinary `mutation`. The base class knows of no
@@ -2488,6 +2563,13 @@ declare abstract class ShardDO {
2488
2563
  * without the replay guard (which a re-dispatch would otherwise re-run) nor
2489
2564
  * without the watermark. Records {@link ShardDO.mutationBookkeeping} under this
2490
2565
  * dispatch's mutation id so `fetch` skips the redundant post-dispatch persist.
2566
+ *
2567
+ * The wire encode of `result` happens HERE, first, so a return value the codec
2568
+ * refuses (a class instance, nesting past its depth cap) throws while the
2569
+ * transaction is still open and rolls the whole mutation back. It runs
2570
+ * unconditionally — a request carrying no `x-lunora-mutation-id` writes no
2571
+ * dedup row but must still not commit writes behind a response that cannot be
2572
+ * serialized.
2491
2573
  */
2492
2574
  protected commitMutationBookkeeping(result: unknown): void;
2493
2575
  /**
@@ -2541,6 +2623,10 @@ declare abstract class ShardDO {
2541
2623
  * poke a paid query itself, or it is served free. The base class has no
2542
2624
  * function registry, so the default is `false`; the codegen-generated
2543
2625
  * subclass overrides it with the real `LUNORA_FUNCTIONS` lookup.
2626
+ *
2627
+ * Also backs the `/rpc` backstop: an origin built without a `functions`
2628
+ * registry cannot read the tag at all, so it charges nothing and marks
2629
+ * nothing — and this is the only place left that still knows the call is paid.
2544
2630
  */
2545
2631
  protected isPaidFunction(_functionPath: string): boolean;
2546
2632
  /**
@@ -3131,25 +3217,6 @@ declare abstract class ShardDO {
3131
3217
  protected recordMetric(event: MetricEvent, sink?: TelemetrySink): void;
3132
3218
  /** The decode + route body of {@link webSocketMessage}, split out so the trace wrapper stays a one-liner. */
3133
3219
  protected handleWebSocketMessage(ws: ShardSocketLike, message: string | ArrayBuffer): Promise<void>;
3134
- /**
3135
- * Stamp everything one RPC dispatch needs off its request, and reset every
3136
- * per-request capture the handler will fill.
3137
- *
3138
- * Split from {@link ShardDO.handleFetchCloudflare} together with
3139
- * {@link ShardDO.endDispatch}, and the pairing is the point: these two own
3140
- * the same set of fields, and the whole correctness story for them is that
3141
- * every field one sets, the other clears. Spread across a 479-line method
3142
- * the two ends were 350 lines apart, so a newly-added per-request field
3143
- * stamped here and forgotten there leaks into the NEXT request on the same
3144
- * DO instance — a cross-request identity bleed with no local symptom.
3145
- * `__tests__/dispatch-lifecycle.test.ts` asserts the symmetry directly.
3146
- *
3147
- * Returns the two values the caller must hold in locals rather than read
3148
- * back off `this`: an `await`-interleaved concurrent dispatch can re-set the
3149
- * shared fields, and the `finally` would then file this dispatch's telemetry
3150
- * under another request's trace (see the comments inside).
3151
- * @returns this dispatch's trace anchor and its transaction-headroom tracker
3152
- */
3153
3220
  /**
3154
3221
  * Cloudflare-specific fetch implementation — WebSocket upgrades and the RPC
3155
3222
  * routes. Injected into {@link ShardRunner} as the host-specific handler while
@@ -4510,6 +4577,11 @@ declare abstract class ShardDO {
4510
4577
  * (a fresh global-shape seed, {@link ShardDO.scheduleSourcePoll}'s initial
4511
4578
  * kick) omits it and gets the original `GLOBAL_SHAPE_POLL_INTERVAL_MS`
4512
4579
  * default, since neither knows a more precise due time yet.
4580
+ *
4581
+ * "Already pending" is decided against {@link ShardDO.globalPollArmedAt}'s
4582
+ * TIME: an alarm due later than the requested target is replaced, one due at
4583
+ * or before it is left alone. Only ever moves the wake EARLIER, so repeated
4584
+ * seeds cannot walk the alarm out.
4513
4585
  */
4514
4586
  private scheduleGlobalPoll;
4515
4587
  /**
@@ -4585,6 +4657,45 @@ declare abstract class ShardDO {
4585
4657
  floor?: number;
4586
4658
  tables: string[];
4587
4659
  } | undefined>;
4660
+ /**
4661
+ * The two answers a `/rpc` can earn between the replica gate and
4662
+ * `beginDispatch`: a reserved admin RPC (served under its own scope and
4663
+ * bearer gate) and the paid-procedure backstop.
4664
+ *
4665
+ * Split out of {@link ShardDO.handleFetchCloudflare} because both are the same
4666
+ * shape — request in, response out, no dispatch bookkeeping in scope — while
4667
+ * everything after them shares eight dispatch locals. `undefined` means
4668
+ * "nothing answered it here; go dispatch".
4669
+ *
4670
+ * Deliberately NOT `async`: the admin branch is the only one that awaits, and
4671
+ * an `async` wrapper would spend a microtask on the common path where this
4672
+ * returns `undefined` — which is enough to move `beginDispatch` after a
4673
+ * concurrent admin request that overlaps it (see
4674
+ * `__tests__/shard-do.system-dispatch.test.ts`).
4675
+ * @param request The inbound `/rpc` request.
4676
+ * @param payload Its parsed body.
4677
+ * @returns The response to return, or `undefined` to continue into dispatch.
4678
+ */
4679
+ private preDispatchAnswer;
4680
+ /**
4681
+ * Stamp everything one RPC dispatch needs off its request, and reset every
4682
+ * per-request capture the handler will fill.
4683
+ *
4684
+ * Split from {@link ShardDO.handleFetchCloudflare} together with
4685
+ * {@link ShardDO.endDispatch}, and the pairing is the point: these two own
4686
+ * the same set of fields, and the whole correctness story for them is that
4687
+ * every field one sets, the other clears. Spread across a 479-line method
4688
+ * the two ends were 350 lines apart, so a newly-added per-request field
4689
+ * stamped here and forgotten there leaks into the NEXT request on the same
4690
+ * DO instance — a cross-request identity bleed with no local symptom.
4691
+ * `__tests__/dispatch-lifecycle.test.ts` asserts the symmetry directly.
4692
+ *
4693
+ * Returns the two values the caller must hold in locals rather than read
4694
+ * back off `this`: an `await`-interleaved concurrent dispatch can re-set the
4695
+ * shared fields, and the `finally` would then file this dispatch's telemetry
4696
+ * under another request's trace (see the comments inside).
4697
+ * @returns this dispatch's trace anchor and its transaction-headroom tracker
4698
+ */
4588
4699
  private beginDispatch;
4589
4700
  /** Clear every per-request field {@link ShardDO.beginDispatch} stamped. */
4590
4701
  private endDispatch;
@@ -4985,4 +5096,4 @@ declare class ShardRegistryDO {
4985
5096
  /** The in-memory map as a JSON-safe `table → [keys]` object, for `/snapshot`. */
4986
5097
  private serializeTables;
4987
5098
  }
4988
- export { type HibernatableWebSocket, type QueryReadScope, ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, type RunShardApplyCdcArgs, type RunShardApplyCdcResult, type RunShardBulkRowArgs, type RunShardBulkRowResult, type RunShardExportArgs, type RunShardImportArgs, type RunShardMigrationArgs, type RunShardRankBeforeArgs, type RunShardRankPageArgs, type RunShardWriteArgs, type RunShardWriteResult, SESSION_DO_TTL_DEFAULT, SHARD_REGISTRY_DO_NAME, SessionDO, type SessionRecord, ShardDO, type ShardDOOptions, type ShardDOState, ShardRegistryDO, type SubscriptionOutcome, type TelemetrySink, type TraceRefLike, serveRelationFanout };
5099
+ export { type DispatchBookmark, type HibernatableWebSocket, type QueryReadScope, ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, type RunShardApplyCdcArgs, type RunShardApplyCdcResult, type RunShardBulkRowArgs, type RunShardBulkRowResult, type RunShardExportArgs, type RunShardImportArgs, type RunShardMigrationArgs, type RunShardRankBeforeArgs, type RunShardRankPageArgs, type RunShardWriteArgs, type RunShardWriteResult, SESSION_DO_TTL_DEFAULT, SHARD_REGISTRY_DO_NAME, SessionDO, type SessionRecord, ShardDO, type ShardDOOptions, type ShardDOState, ShardRegistryDO, type SubscriptionOutcome, type TelemetrySink, type TraceRefLike, serveRelationFanout };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{serveRelationFanout as a}from"./packem_shared/serveRelationFanout-DcSjzAZv.mjs";import{SESSION_DO_TTL_DEFAULT as t,SessionDO as c}from"./packem_shared/SESSION_DO_TTL_DEFAULT-BbU25BWj.mjs";import{ROOT_DO_SIZE_WARN_BYTES as i,ROOT_SHARD_NAME as s,ShardDO as n}from"./packem_shared/ROOT_DO_SIZE_WARN_BYTES-ORORBPuf.mjs";import{SHARD_REGISTRY_DO_NAME as R,ShardRegistryDO as d}from"./packem_shared/SHARD_REGISTRY_DO_NAME-DvxEa7qM.mjs";import{createShardAlarms as h,createShardDirectory as D,createShardHost as O,createShardKvStore as _,createShardPlatform as E,createSocketHost as m,createWorkerPlatform as u}from"@lunora/platform-cloudflare";import{REPROJECTION_MIGRATION_PREFIX as x,UNVOUCHABLE_DEP as I,applyCdcChanges as f,assertShapeShardable as A,backfillSearchIndexes as b,buildReprojectionMigration as M,clearMemoryTables as g,countLegacyRows as N,createReadFootprint as k,createShardCtxDb as y,exportShardRows as C,importShardRows as H,isSourceDue as L,markUnvouchableReads as P,pullExternalSourceIncrementalTick as F,pullExternalSourceTick as U,reprojectionMigrationId as j,reprojectionTables as v,runDataMigration as w,runShardMigrations as B,subscriptionListDeltas as G}from"@lunora/shard-engine";export{x as REPROJECTION_MIGRATION_PREFIX,i as ROOT_DO_SIZE_WARN_BYTES,s as ROOT_SHARD_NAME,t as SESSION_DO_TTL_DEFAULT,R as SHARD_REGISTRY_DO_NAME,c as SessionDO,n as ShardDO,d as ShardRegistryDO,I as UNVOUCHABLE_DEP,f as applyCdcChanges,A as assertShapeShardable,b as backfillSearchIndexes,M as buildReprojectionMigration,g as clearMemoryTables,N as countLegacyRows,k as createReadFootprint,h as createShardAlarms,y as createShardCtxDb,D as createShardDirectory,O as createShardHost,_ as createShardKvStore,E as createShardPlatform,m as createSocketHost,u as createWorkerPlatform,C as exportShardRows,H as importShardRows,L as isSourceDue,P as markUnvouchableReads,F as pullExternalSourceIncrementalTick,U as pullExternalSourceTick,j as reprojectionMigrationId,v as reprojectionTables,w as runDataMigration,B as runShardMigrations,a as serveRelationFanout,G as subscriptionListDeltas};
1
+ import{serveRelationFanout as a}from"./packem_shared/serveRelationFanout-DcSjzAZv.mjs";import{SESSION_DO_TTL_DEFAULT as t,SessionDO as c}from"./packem_shared/SESSION_DO_TTL_DEFAULT-BbU25BWj.mjs";import{ROOT_DO_SIZE_WARN_BYTES as i,ROOT_SHARD_NAME as s,ShardDO as n}from"./packem_shared/ROOT_DO_SIZE_WARN_BYTES-Drst3Tq2.mjs";import{SHARD_REGISTRY_DO_NAME as R,ShardRegistryDO as d}from"./packem_shared/SHARD_REGISTRY_DO_NAME-DvxEa7qM.mjs";import{createShardAlarms as h,createShardDirectory as D,createShardHost as O,createShardKvStore as _,createShardPlatform as E,createSocketHost as m,createWorkerPlatform as u}from"@lunora/platform-cloudflare";import{REPROJECTION_MIGRATION_PREFIX as x,UNVOUCHABLE_DEP as I,applyCdcChanges as f,assertShapeShardable as A,backfillSearchIndexes as b,buildReprojectionMigration as M,clearMemoryTables as g,countLegacyRows as N,createReadFootprint as k,createShardCtxDb as y,exportShardRows as C,importShardRows as H,isSourceDue as L,markUnvouchableReads as P,pullExternalSourceIncrementalTick as F,pullExternalSourceTick as U,reprojectionMigrationId as j,reprojectionTables as v,runDataMigration as w,runShardMigrations as B,subscriptionListDeltas as G}from"@lunora/shard-engine";export{x as REPROJECTION_MIGRATION_PREFIX,i as ROOT_DO_SIZE_WARN_BYTES,s as ROOT_SHARD_NAME,t as SESSION_DO_TTL_DEFAULT,R as SHARD_REGISTRY_DO_NAME,c as SessionDO,n as ShardDO,d as ShardRegistryDO,I as UNVOUCHABLE_DEP,f as applyCdcChanges,A as assertShapeShardable,b as backfillSearchIndexes,M as buildReprojectionMigration,g as clearMemoryTables,N as countLegacyRows,k as createReadFootprint,h as createShardAlarms,y as createShardCtxDb,D as createShardDirectory,O as createShardHost,_ as createShardKvStore,E as createShardPlatform,m as createSocketHost,u as createWorkerPlatform,C as exportShardRows,H as importShardRows,L as isSourceDue,P as markUnvouchableReads,F as pullExternalSourceIncrementalTick,U as pullExternalSourceTick,j as reprojectionMigrationId,v as reprojectionTables,w as runDataMigration,B as runShardMigrations,a as serveRelationFanout,G as subscriptionListDeltas};