@lunora/do 1.0.0-alpha.114 → 1.0.0-alpha.116

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
@@ -879,13 +879,52 @@ declare abstract class ShardDO {
879
879
  */
880
880
  protected static readonly MAX_STREAMS_PER_SOCKET = 8;
881
881
  /**
882
- * Per-socket subscription cap. Each subscription is stored in the
883
- * hibernation attachment (which is serialized JSON), and runaway
884
- * subscribe loops would let a single client wedge the attachment past
885
- * the runtime's size budget — keep the per-socket ceiling well below
886
- * that. 32 is enough for any reasonable client (one per visible
887
- * panel/query) and small enough that an attachment serialization
888
- * failure stays unlikely.
882
+ * The runtime's hard ceiling on one hibernation attachment.
883
+ *
884
+ * MEASURED against workerd rather than taken from a doc page: a
885
+ * `serializeAttachment` of 16385 bytes throws
886
+ * `A WebSocket 'attachment' cannot be larger than 16384 bytes.`, and 8192
887
+ * succeeds. The measurement lives in
888
+ * `__tests__/shard-do.subscription-cap.test.ts`; the workerd half is
889
+ * `__tests__/workerd/shard-do.workerd.test.ts`.
890
+ *
891
+ * This is the bound that actually binds, and the runtime is the only thing
892
+ * that enforces it — deliberately. A pre-flight size check here would need a
893
+ * second size model, and `JSON.stringify` is not it: an attachment
894
+ * legitimately holds the decoded wire types (`bigint`, `Date`, bytes), and
895
+ * stringifying a `bigint` throws. So `subscribe`/`shapeSubscribe` let the
896
+ * runtime refuse, roll the registry back, and spend this constant on saying
897
+ * WHY — the hibernation API makes them swallow the throw itself, and
898
+ * "failed to persist subscription attachment" is not something an app can
899
+ * act on.
900
+ */
901
+ protected static readonly MAX_ATTACHMENT_BYTES = 16384;
902
+ /**
903
+ * Per-socket cap on `subs` + `shapes` together — a coarse backstop on
904
+ * per-poke fan-out work, NOT the storage bound.
905
+ *
906
+ * The storage bound is {@link ShardDO.MAX_ATTACHMENT_BYTES}, and it is the
907
+ * one that can actually stop a legitimate app: every registration is
908
+ * persisted in the hibernation attachment as `{functionPath, table, args,
909
+ * sinceSeq, sinceEpoch}` beside `connectionId`/`userId`/`identity`/
910
+ * `clientId`/`context`/`whispers`, and `args` is the client's to choose, so
911
+ * no fixed count can bound it.
912
+ *
913
+ * 32 against the measured numbers: a realistic registration (a function
914
+ * path, one id argument, a limit, a cursor and an epoch uuid) costs ~218
915
+ * bytes, and a fully decorated socket's fixed fields — identity claims, app
916
+ * `context`, whisper topics — about 550. 32 of them is ~7.5 KB, under half
917
+ * the 16384-byte ceiling, so the count cap never fires before the byte
918
+ * budget for a record of that shape. It fires only for registrations small
919
+ * enough that 32 of them are cheap, which is where a fan-out backstop
920
+ * belongs.
921
+ *
922
+ * This was briefly 8, derived from a 2048-byte attachment budget that the
923
+ * runtime does not impose — 8× too small, and low enough to break an app
924
+ * holding a dozen live queries on one socket at runtime, which is the
925
+ * failure this number exists to avoid.
926
+ *
927
+ * Both numbers are asserted in `__tests__/shard-do.subscription-cap.test.ts`.
889
928
  */
890
929
  protected static readonly MAX_SUBSCRIPTIONS_PER_SOCKET = 32;
891
930
  /**
@@ -1513,14 +1552,6 @@ declare abstract class ShardDO {
1513
1552
  * the scan attribution. Stamped by `getCtxDbIndexUseHook`.
1514
1553
  */
1515
1554
  private currentIndexHits;
1516
- /**
1517
- * Resource meter for the in-flight dispatch. Created per request alongside
1518
- * the scanned-table capture and handed to `createShardCtxDb` by the codegen
1519
- * subclass, so one runaway mutation fails with a
1520
- * `TRANSACTION_LIMIT_EXCEEDED` instead of taking the whole shard's isolate
1521
- * down with it.
1522
- */
1523
- private currentTransactionHeadroom;
1524
1555
  /**
1525
1556
  * Per-DISTINCT-statement SQL samples collected during the current `/rpc`
1526
1557
  * dispatch by the instrumented `sql` getter, keyed by the raw query text.
@@ -1642,14 +1673,13 @@ declare abstract class ShardDO {
1642
1673
  * `headroom` is an optional BY-VALUE override, mirroring
1643
1674
  * {@link ShardDO.runShardWrite}'s pattern: the main `/rpc` dispatch
1644
1675
  * (`handleFetchCloudflare`) captures its freshly-minted tracker in a LOCAL and
1645
- * passes it here explicitly, so the ctx this dispatch builds never depends on
1646
- * `this.currentTransactionHeadroom` still holding the right value by the time
1647
- * the (possibly `await`-interleaved) handler runs a concurrent dispatch's
1648
- * `finally` clearing that shared field could otherwise leave this one
1649
- * unmetered mid-flight. Callers that dispatch through here without minting
1650
- * their own tracker (`dispatchLifecycle`, `handleRunAs`) omit it and the
1651
- * codegen subclass falls back to `this.transactionHeadroom()`, unchanged from
1652
- * before this parameter existed.
1676
+ * passes it here explicitly, so the ctx this dispatch builds is metered
1677
+ * against ITS OWN tracker however the (possibly `await`-interleaved) handler
1678
+ * interleaves with a concurrent one. Callers that dispatch
1679
+ * through here without minting their own tracker (`dispatchLifecycle`,
1680
+ * `handleRunAs`) omit it and the codegen subclass falls back to
1681
+ * `this.transactionHeadroom()`, which mints a FRESH per-ctx budget never
1682
+ * the in-flight dispatch's.
1653
1683
  *
1654
1684
  * `scope` is the same shape of BY-VALUE thread for the reactive cache: the
1655
1685
  * `/rpc` query path routes through {@link ShardDO.runCachedQuery}, which
@@ -2116,9 +2146,10 @@ declare abstract class ShardDO {
2116
2146
  *
2117
2147
  * The single seam every writer-routed single-row write goes through — a studio
2118
2148
  * row edit, a bulk row op, a TTL expiry. `headroom` is an optional BY-VALUE
2119
- * meter: a normal `/rpc` dispatch omits it and the override falls back to
2120
- * `this.transactionHeadroom()`, while {@link ShardDO.pollTtlSweeps} (an alarm
2121
- * work item, no dispatch in flight) passes its own tracker explicitly.
2149
+ * meter: an admin caller omits it and the override falls back to
2150
+ * `this.transactionHeadroom()`, which mints a fresh per-call budget, while
2151
+ * {@link ShardDO.pollTtlSweeps} (an alarm work item draining many rows under
2152
+ * ONE ceiling) passes its own tracker explicitly.
2122
2153
  */
2123
2154
  protected runShardWrite(args: RunShardWriteArgs, _headroom?: TransactionHeadroomTracker): Promise<RunShardWriteResult>;
2124
2155
  /**
@@ -2491,16 +2522,28 @@ declare abstract class ShardDO {
2491
2522
  * `applyCdcChanges(writer, args.changes)`.
2492
2523
  */
2493
2524
  protected runShardApplyCdc(_args: RunShardApplyCdcArgs): Promise<RunShardApplyCdcResult>;
2525
+ /**
2526
+ * Whether `functionPath` is a paid (`.x402({ price })`) procedure. The paywall
2527
+ * lives at the origin worker (`/_lunora/rpc`, REST, `serverQuery`), which a
2528
+ * WebSocket subscription never crosses — so the shard must refuse to seed or
2529
+ * poke a paid query itself, or it is served free. The base class has no
2530
+ * function registry, so the default is `false`; the codegen-generated
2531
+ * subclass overrides it with the real `LUNORA_FUNCTIONS` lookup.
2532
+ */
2533
+ protected isPaidFunction(_functionPath: string): boolean;
2494
2534
  /**
2495
2535
  * Register a subscription on the given socket. Stored via
2496
2536
  * `ws.serializeAttachment` so it survives hibernation.
2497
2537
  *
2498
2538
  * Returns a status so the caller can surface a structured error frame
2499
- * when the cap is hit or the attachment fails to serialize. We never
2500
- * throw out of this path — the WS hibernation API treats a thrown
2501
- * `webSocketMessage` as a fatal-channel error.
2539
+ * when the query is paid, the cap is hit or the attachment fails to
2540
+ * serialize. We never throw out of this path — the WS hibernation API
2541
+ * treats a thrown `webSocketMessage` as a fatal-channel error.
2542
+ *
2543
+ * The `paid` refusal sits here rather than at the envelope so every
2544
+ * registration path — not only the `subscribe` frame — goes through it.
2502
2545
  */
2503
- protected subscribe(ws: ShardSocketLike, subId: string, query: SubscriptionQuery): "ok" | "serialize_failed" | "too_many";
2546
+ protected subscribe(ws: ShardSocketLike, subId: string, query: SubscriptionQuery): "ok" | "paid" | "serialize_failed" | "too_many";
2504
2547
  protected unsubscribe(ws: ShardSocketLike, subId: string): void;
2505
2548
  /**
2506
2549
  * Register a live shape subscription on a socket — the partial-replication
@@ -2747,8 +2790,16 @@ declare abstract class ShardDO {
2747
2790
  * The deferred-iterator shape (`(signal) => AsyncIterable<unknown>`) keeps
2748
2791
  * the cancel signal pluggable per-call without coupling this signature to
2749
2792
  * the wire-frame loop in `handleStream`.
2793
+ *
2794
+ * `identity` is the socket's verified identity, threaded BY VALUE exactly as
2795
+ * {@link ShardDO.executeSubscription} threads it and for the same reason: a
2796
+ * `stream` frame is dispatched fire-and-forget and its iterator is pulled
2797
+ * long after, interleaved with unrelated `/rpc` dispatches, so reading the
2798
+ * shared per-request identity fields instead would run an `rls()` /
2799
+ * `ctx.auth`-scoped stream as nobody while the shard is idle — and as
2800
+ * whoever else is mid-flight while it is not.
2750
2801
  */
2751
- protected executeStream(_functionPath: string, _args: Record<string, unknown>): null | {
2802
+ protected executeStream(_functionPath: string, _args: Record<string, unknown>, _identity?: SubscriptionIdentity): null | {
2752
2803
  durable?: {
2753
2804
  ttlMs?: number;
2754
2805
  };
@@ -2901,11 +2952,24 @@ declare abstract class ShardDO {
2901
2952
  */
2902
2953
  protected transactionLimits(): Partial<TransactionLimits>;
2903
2954
  /**
2904
- * The in-flight dispatch's resource meter, passed to `createShardCtxDb` by
2905
- * the generated subclass. Returns `undefined` outside a dispatch, which
2906
- * leaves `ctx.db` unmetered — the legacy behaviour.
2955
+ * A fresh budget for a dispatch that brought none of its own.
2956
+ *
2957
+ * This used to hand back an INSTANCE FIELD stamped by `beginDispatch` — "the
2958
+ * meter of whichever `/rpc` is in flight". Nothing that reaches it is that
2959
+ * dispatch: the `/rpc` path value-threads its own tracker into `handleRpc`
2960
+ * and never consults this. What reached it were the out-of-band callers —
2961
+ * `dispatchLifecycle`'s `onConnect`/`onDisconnect` hooks, `handleRunAs`, the
2962
+ * admin `runShardWrite` behind the studio's row editor — which either
2963
+ * charged their writes to an unrelated in-flight mutation's budget (failing
2964
+ * one of the two with a ceiling neither caused) or, with no dispatch in
2965
+ * flight, ran completely unmetered.
2966
+ *
2967
+ * So: mint one, the same by-value answer {@link ShardDO.subscriptionHeadroom}
2968
+ * and {@link ShardDO.alarmHeadroom} give their own out-of-band callers, and
2969
+ * for the same reason — an ambient field says "a dispatch is in flight", not
2970
+ * "this caller is that dispatch".
2907
2971
  */
2908
- protected transactionHeadroom(): TransactionHeadroomTracker | undefined;
2972
+ protected transactionHeadroom(): TransactionHeadroomTracker;
2909
2973
  /**
2910
2974
  * A fresh budget for one deferred subscription re-run.
2911
2975
  *
@@ -4101,6 +4165,27 @@ declare abstract class ShardDO {
4101
4165
  */
4102
4166
  private recordSubscriptionRefreshError;
4103
4167
  private refreshSubscriptions;
4168
+ /**
4169
+ * Run {@link ShardDO.seedSubscription} and fail the ONE subscription — never
4170
+ * the socket — when its handler throws.
4171
+ *
4172
+ * The seed dispatches the user's query: it re-validates the args and runs
4173
+ * the procedure's auth/RLS middleware, so an anonymous socket subscribing to
4174
+ * an `authQuery`, a bad argument, or a handler `NOT_FOUND` rejects here.
4175
+ * Under the WS hibernation API a throw out of `webSocketMessage` is a
4176
+ * FATAL-CHANNEL error (see the analysis on `webSocketError`): the runtime
4177
+ * tears the socket down, taking every OTHER live subscription on it with
4178
+ * it — and the client already saw this subscribe's `ack`, which resets its
4179
+ * reconnect backoff, so it reconnects, resubscribes, throws again, and
4180
+ * spins at the initial delay for the life of the page.
4181
+ *
4182
+ * So: drop the just-registered subscription from the attachment and answer
4183
+ * with a structured `error` frame carrying the thrown error's code. Mirrors
4184
+ * `refreshSubscriptions`' per-`(socket, sub)` catch on the write-flush half
4185
+ * of the same path, and `handleShapeSubscribe`'s rollback-then-error on a
4186
+ * failed shape seed.
4187
+ */
4188
+ private seedSubscriptionGuarded;
4104
4189
  /**
4105
4190
  * Seed a freshly-registered subscription with its first value. Runs the
4106
4191
  * query once, then takes one of two paths.
@@ -4115,6 +4200,10 @@ declare abstract class ShardDO {
4115
4200
  *
4116
4201
  * Either way the fresh result memoises this socket's diff baseline so later
4117
4202
  * write-flushes ({@link refreshSubscriptions}) can emit incremental deltas.
4203
+ *
4204
+ * MAY THROW: it runs the real handler (arg re-validation plus the whole
4205
+ * auth/RLS middleware chain). Every caller goes through
4206
+ * {@link ShardDO.seedSubscriptionGuarded}, which owns that failure.
4118
4207
  */
4119
4208
  private seedSubscription;
4120
4209
  /**
@@ -4139,8 +4228,8 @@ declare abstract class ShardDO {
4139
4228
  * thrown `webSocketMessage` is fatal to the hibernating socket).
4140
4229
  */
4141
4230
  private handleShapeSubscribe;
4142
- /** Send a structured `error` frame for a failed `shape_subscribe`, swallowing a send on an already-closed socket. */
4143
- private sendShapeSubscribeError;
4231
+ /** Send a structured `error` frame for a failed `subscribe`/`shape_subscribe`, swallowing a send on an already-closed socket. */
4232
+ private sendSubscriptionError;
4144
4233
  /**
4145
4234
  * Seed a freshly-registered shape subscription. Resolves the shape under the
4146
4235
  * socket's verified identity, then ships either:
@@ -4363,8 +4452,8 @@ declare abstract class ShardDO {
4363
4452
  private scheduleGlobalPoll;
4364
4453
  /**
4365
4454
  * Delete one expired row through {@link ShardDO.runShardWrite}, passing this
4366
- * sweep's own by-value meter (an alarm has no dispatch in flight, so the
4367
- * override's `this.transactionHeadroom()` fallback would be `undefined`),
4455
+ * sweep's own by-value meter (the override's `this.transactionHeadroom()`
4456
+ * fallback would mint a fresh budget per row, bounding nothing),
4368
4457
  * absorbing a `TRANSACTION_LIMIT_EXCEEDED` as "batch full" rather than
4369
4458
  * letting it propagate — split out of {@link ShardDO.pollTtlSweeps} to keep
4370
4459
  * that method's own complexity down. Returns `true` when the limit was hit
@@ -4435,12 +4524,7 @@ declare abstract class ShardDO {
4435
4524
  tables: string[];
4436
4525
  } | undefined>;
4437
4526
  private beginDispatch;
4438
- /**
4439
- * Clear every per-request field {@link ShardDO.beginDispatch} stamped.
4440
- *
4441
- * `dispatchHeadroom` is passed rather than read off `this` so the headroom
4442
- * clear stays identity-guarded — see the comment on it.
4443
- */
4527
+ /** Clear every per-request field {@link ShardDO.beginDispatch} stamped. */
4444
4528
  private endDispatch;
4445
4529
  /**
4446
4530
  * Open one `.global()` poll tick: ask the global changelog what moved since
package/dist/index.d.ts CHANGED
@@ -879,13 +879,52 @@ declare abstract class ShardDO {
879
879
  */
880
880
  protected static readonly MAX_STREAMS_PER_SOCKET = 8;
881
881
  /**
882
- * Per-socket subscription cap. Each subscription is stored in the
883
- * hibernation attachment (which is serialized JSON), and runaway
884
- * subscribe loops would let a single client wedge the attachment past
885
- * the runtime's size budget — keep the per-socket ceiling well below
886
- * that. 32 is enough for any reasonable client (one per visible
887
- * panel/query) and small enough that an attachment serialization
888
- * failure stays unlikely.
882
+ * The runtime's hard ceiling on one hibernation attachment.
883
+ *
884
+ * MEASURED against workerd rather than taken from a doc page: a
885
+ * `serializeAttachment` of 16385 bytes throws
886
+ * `A WebSocket 'attachment' cannot be larger than 16384 bytes.`, and 8192
887
+ * succeeds. The measurement lives in
888
+ * `__tests__/shard-do.subscription-cap.test.ts`; the workerd half is
889
+ * `__tests__/workerd/shard-do.workerd.test.ts`.
890
+ *
891
+ * This is the bound that actually binds, and the runtime is the only thing
892
+ * that enforces it — deliberately. A pre-flight size check here would need a
893
+ * second size model, and `JSON.stringify` is not it: an attachment
894
+ * legitimately holds the decoded wire types (`bigint`, `Date`, bytes), and
895
+ * stringifying a `bigint` throws. So `subscribe`/`shapeSubscribe` let the
896
+ * runtime refuse, roll the registry back, and spend this constant on saying
897
+ * WHY — the hibernation API makes them swallow the throw itself, and
898
+ * "failed to persist subscription attachment" is not something an app can
899
+ * act on.
900
+ */
901
+ protected static readonly MAX_ATTACHMENT_BYTES = 16384;
902
+ /**
903
+ * Per-socket cap on `subs` + `shapes` together — a coarse backstop on
904
+ * per-poke fan-out work, NOT the storage bound.
905
+ *
906
+ * The storage bound is {@link ShardDO.MAX_ATTACHMENT_BYTES}, and it is the
907
+ * one that can actually stop a legitimate app: every registration is
908
+ * persisted in the hibernation attachment as `{functionPath, table, args,
909
+ * sinceSeq, sinceEpoch}` beside `connectionId`/`userId`/`identity`/
910
+ * `clientId`/`context`/`whispers`, and `args` is the client's to choose, so
911
+ * no fixed count can bound it.
912
+ *
913
+ * 32 against the measured numbers: a realistic registration (a function
914
+ * path, one id argument, a limit, a cursor and an epoch uuid) costs ~218
915
+ * bytes, and a fully decorated socket's fixed fields — identity claims, app
916
+ * `context`, whisper topics — about 550. 32 of them is ~7.5 KB, under half
917
+ * the 16384-byte ceiling, so the count cap never fires before the byte
918
+ * budget for a record of that shape. It fires only for registrations small
919
+ * enough that 32 of them are cheap, which is where a fan-out backstop
920
+ * belongs.
921
+ *
922
+ * This was briefly 8, derived from a 2048-byte attachment budget that the
923
+ * runtime does not impose — 8× too small, and low enough to break an app
924
+ * holding a dozen live queries on one socket at runtime, which is the
925
+ * failure this number exists to avoid.
926
+ *
927
+ * Both numbers are asserted in `__tests__/shard-do.subscription-cap.test.ts`.
889
928
  */
890
929
  protected static readonly MAX_SUBSCRIPTIONS_PER_SOCKET = 32;
891
930
  /**
@@ -1513,14 +1552,6 @@ declare abstract class ShardDO {
1513
1552
  * the scan attribution. Stamped by `getCtxDbIndexUseHook`.
1514
1553
  */
1515
1554
  private currentIndexHits;
1516
- /**
1517
- * Resource meter for the in-flight dispatch. Created per request alongside
1518
- * the scanned-table capture and handed to `createShardCtxDb` by the codegen
1519
- * subclass, so one runaway mutation fails with a
1520
- * `TRANSACTION_LIMIT_EXCEEDED` instead of taking the whole shard's isolate
1521
- * down with it.
1522
- */
1523
- private currentTransactionHeadroom;
1524
1555
  /**
1525
1556
  * Per-DISTINCT-statement SQL samples collected during the current `/rpc`
1526
1557
  * dispatch by the instrumented `sql` getter, keyed by the raw query text.
@@ -1642,14 +1673,13 @@ declare abstract class ShardDO {
1642
1673
  * `headroom` is an optional BY-VALUE override, mirroring
1643
1674
  * {@link ShardDO.runShardWrite}'s pattern: the main `/rpc` dispatch
1644
1675
  * (`handleFetchCloudflare`) captures its freshly-minted tracker in a LOCAL and
1645
- * passes it here explicitly, so the ctx this dispatch builds never depends on
1646
- * `this.currentTransactionHeadroom` still holding the right value by the time
1647
- * the (possibly `await`-interleaved) handler runs a concurrent dispatch's
1648
- * `finally` clearing that shared field could otherwise leave this one
1649
- * unmetered mid-flight. Callers that dispatch through here without minting
1650
- * their own tracker (`dispatchLifecycle`, `handleRunAs`) omit it and the
1651
- * codegen subclass falls back to `this.transactionHeadroom()`, unchanged from
1652
- * before this parameter existed.
1676
+ * passes it here explicitly, so the ctx this dispatch builds is metered
1677
+ * against ITS OWN tracker however the (possibly `await`-interleaved) handler
1678
+ * interleaves with a concurrent one. Callers that dispatch
1679
+ * through here without minting their own tracker (`dispatchLifecycle`,
1680
+ * `handleRunAs`) omit it and the codegen subclass falls back to
1681
+ * `this.transactionHeadroom()`, which mints a FRESH per-ctx budget never
1682
+ * the in-flight dispatch's.
1653
1683
  *
1654
1684
  * `scope` is the same shape of BY-VALUE thread for the reactive cache: the
1655
1685
  * `/rpc` query path routes through {@link ShardDO.runCachedQuery}, which
@@ -2116,9 +2146,10 @@ declare abstract class ShardDO {
2116
2146
  *
2117
2147
  * The single seam every writer-routed single-row write goes through — a studio
2118
2148
  * row edit, a bulk row op, a TTL expiry. `headroom` is an optional BY-VALUE
2119
- * meter: a normal `/rpc` dispatch omits it and the override falls back to
2120
- * `this.transactionHeadroom()`, while {@link ShardDO.pollTtlSweeps} (an alarm
2121
- * work item, no dispatch in flight) passes its own tracker explicitly.
2149
+ * meter: an admin caller omits it and the override falls back to
2150
+ * `this.transactionHeadroom()`, which mints a fresh per-call budget, while
2151
+ * {@link ShardDO.pollTtlSweeps} (an alarm work item draining many rows under
2152
+ * ONE ceiling) passes its own tracker explicitly.
2122
2153
  */
2123
2154
  protected runShardWrite(args: RunShardWriteArgs, _headroom?: TransactionHeadroomTracker): Promise<RunShardWriteResult>;
2124
2155
  /**
@@ -2491,16 +2522,28 @@ declare abstract class ShardDO {
2491
2522
  * `applyCdcChanges(writer, args.changes)`.
2492
2523
  */
2493
2524
  protected runShardApplyCdc(_args: RunShardApplyCdcArgs): Promise<RunShardApplyCdcResult>;
2525
+ /**
2526
+ * Whether `functionPath` is a paid (`.x402({ price })`) procedure. The paywall
2527
+ * lives at the origin worker (`/_lunora/rpc`, REST, `serverQuery`), which a
2528
+ * WebSocket subscription never crosses — so the shard must refuse to seed or
2529
+ * poke a paid query itself, or it is served free. The base class has no
2530
+ * function registry, so the default is `false`; the codegen-generated
2531
+ * subclass overrides it with the real `LUNORA_FUNCTIONS` lookup.
2532
+ */
2533
+ protected isPaidFunction(_functionPath: string): boolean;
2494
2534
  /**
2495
2535
  * Register a subscription on the given socket. Stored via
2496
2536
  * `ws.serializeAttachment` so it survives hibernation.
2497
2537
  *
2498
2538
  * Returns a status so the caller can surface a structured error frame
2499
- * when the cap is hit or the attachment fails to serialize. We never
2500
- * throw out of this path — the WS hibernation API treats a thrown
2501
- * `webSocketMessage` as a fatal-channel error.
2539
+ * when the query is paid, the cap is hit or the attachment fails to
2540
+ * serialize. We never throw out of this path — the WS hibernation API
2541
+ * treats a thrown `webSocketMessage` as a fatal-channel error.
2542
+ *
2543
+ * The `paid` refusal sits here rather than at the envelope so every
2544
+ * registration path — not only the `subscribe` frame — goes through it.
2502
2545
  */
2503
- protected subscribe(ws: ShardSocketLike, subId: string, query: SubscriptionQuery): "ok" | "serialize_failed" | "too_many";
2546
+ protected subscribe(ws: ShardSocketLike, subId: string, query: SubscriptionQuery): "ok" | "paid" | "serialize_failed" | "too_many";
2504
2547
  protected unsubscribe(ws: ShardSocketLike, subId: string): void;
2505
2548
  /**
2506
2549
  * Register a live shape subscription on a socket — the partial-replication
@@ -2747,8 +2790,16 @@ declare abstract class ShardDO {
2747
2790
  * The deferred-iterator shape (`(signal) => AsyncIterable<unknown>`) keeps
2748
2791
  * the cancel signal pluggable per-call without coupling this signature to
2749
2792
  * the wire-frame loop in `handleStream`.
2793
+ *
2794
+ * `identity` is the socket's verified identity, threaded BY VALUE exactly as
2795
+ * {@link ShardDO.executeSubscription} threads it and for the same reason: a
2796
+ * `stream` frame is dispatched fire-and-forget and its iterator is pulled
2797
+ * long after, interleaved with unrelated `/rpc` dispatches, so reading the
2798
+ * shared per-request identity fields instead would run an `rls()` /
2799
+ * `ctx.auth`-scoped stream as nobody while the shard is idle — and as
2800
+ * whoever else is mid-flight while it is not.
2750
2801
  */
2751
- protected executeStream(_functionPath: string, _args: Record<string, unknown>): null | {
2802
+ protected executeStream(_functionPath: string, _args: Record<string, unknown>, _identity?: SubscriptionIdentity): null | {
2752
2803
  durable?: {
2753
2804
  ttlMs?: number;
2754
2805
  };
@@ -2901,11 +2952,24 @@ declare abstract class ShardDO {
2901
2952
  */
2902
2953
  protected transactionLimits(): Partial<TransactionLimits>;
2903
2954
  /**
2904
- * The in-flight dispatch's resource meter, passed to `createShardCtxDb` by
2905
- * the generated subclass. Returns `undefined` outside a dispatch, which
2906
- * leaves `ctx.db` unmetered — the legacy behaviour.
2955
+ * A fresh budget for a dispatch that brought none of its own.
2956
+ *
2957
+ * This used to hand back an INSTANCE FIELD stamped by `beginDispatch` — "the
2958
+ * meter of whichever `/rpc` is in flight". Nothing that reaches it is that
2959
+ * dispatch: the `/rpc` path value-threads its own tracker into `handleRpc`
2960
+ * and never consults this. What reached it were the out-of-band callers —
2961
+ * `dispatchLifecycle`'s `onConnect`/`onDisconnect` hooks, `handleRunAs`, the
2962
+ * admin `runShardWrite` behind the studio's row editor — which either
2963
+ * charged their writes to an unrelated in-flight mutation's budget (failing
2964
+ * one of the two with a ceiling neither caused) or, with no dispatch in
2965
+ * flight, ran completely unmetered.
2966
+ *
2967
+ * So: mint one, the same by-value answer {@link ShardDO.subscriptionHeadroom}
2968
+ * and {@link ShardDO.alarmHeadroom} give their own out-of-band callers, and
2969
+ * for the same reason — an ambient field says "a dispatch is in flight", not
2970
+ * "this caller is that dispatch".
2907
2971
  */
2908
- protected transactionHeadroom(): TransactionHeadroomTracker | undefined;
2972
+ protected transactionHeadroom(): TransactionHeadroomTracker;
2909
2973
  /**
2910
2974
  * A fresh budget for one deferred subscription re-run.
2911
2975
  *
@@ -4101,6 +4165,27 @@ declare abstract class ShardDO {
4101
4165
  */
4102
4166
  private recordSubscriptionRefreshError;
4103
4167
  private refreshSubscriptions;
4168
+ /**
4169
+ * Run {@link ShardDO.seedSubscription} and fail the ONE subscription — never
4170
+ * the socket — when its handler throws.
4171
+ *
4172
+ * The seed dispatches the user's query: it re-validates the args and runs
4173
+ * the procedure's auth/RLS middleware, so an anonymous socket subscribing to
4174
+ * an `authQuery`, a bad argument, or a handler `NOT_FOUND` rejects here.
4175
+ * Under the WS hibernation API a throw out of `webSocketMessage` is a
4176
+ * FATAL-CHANNEL error (see the analysis on `webSocketError`): the runtime
4177
+ * tears the socket down, taking every OTHER live subscription on it with
4178
+ * it — and the client already saw this subscribe's `ack`, which resets its
4179
+ * reconnect backoff, so it reconnects, resubscribes, throws again, and
4180
+ * spins at the initial delay for the life of the page.
4181
+ *
4182
+ * So: drop the just-registered subscription from the attachment and answer
4183
+ * with a structured `error` frame carrying the thrown error's code. Mirrors
4184
+ * `refreshSubscriptions`' per-`(socket, sub)` catch on the write-flush half
4185
+ * of the same path, and `handleShapeSubscribe`'s rollback-then-error on a
4186
+ * failed shape seed.
4187
+ */
4188
+ private seedSubscriptionGuarded;
4104
4189
  /**
4105
4190
  * Seed a freshly-registered subscription with its first value. Runs the
4106
4191
  * query once, then takes one of two paths.
@@ -4115,6 +4200,10 @@ declare abstract class ShardDO {
4115
4200
  *
4116
4201
  * Either way the fresh result memoises this socket's diff baseline so later
4117
4202
  * write-flushes ({@link refreshSubscriptions}) can emit incremental deltas.
4203
+ *
4204
+ * MAY THROW: it runs the real handler (arg re-validation plus the whole
4205
+ * auth/RLS middleware chain). Every caller goes through
4206
+ * {@link ShardDO.seedSubscriptionGuarded}, which owns that failure.
4118
4207
  */
4119
4208
  private seedSubscription;
4120
4209
  /**
@@ -4139,8 +4228,8 @@ declare abstract class ShardDO {
4139
4228
  * thrown `webSocketMessage` is fatal to the hibernating socket).
4140
4229
  */
4141
4230
  private handleShapeSubscribe;
4142
- /** Send a structured `error` frame for a failed `shape_subscribe`, swallowing a send on an already-closed socket. */
4143
- private sendShapeSubscribeError;
4231
+ /** Send a structured `error` frame for a failed `subscribe`/`shape_subscribe`, swallowing a send on an already-closed socket. */
4232
+ private sendSubscriptionError;
4144
4233
  /**
4145
4234
  * Seed a freshly-registered shape subscription. Resolves the shape under the
4146
4235
  * socket's verified identity, then ships either:
@@ -4363,8 +4452,8 @@ declare abstract class ShardDO {
4363
4452
  private scheduleGlobalPoll;
4364
4453
  /**
4365
4454
  * Delete one expired row through {@link ShardDO.runShardWrite}, passing this
4366
- * sweep's own by-value meter (an alarm has no dispatch in flight, so the
4367
- * override's `this.transactionHeadroom()` fallback would be `undefined`),
4455
+ * sweep's own by-value meter (the override's `this.transactionHeadroom()`
4456
+ * fallback would mint a fresh budget per row, bounding nothing),
4368
4457
  * absorbing a `TRANSACTION_LIMIT_EXCEEDED` as "batch full" rather than
4369
4458
  * letting it propagate — split out of {@link ShardDO.pollTtlSweeps} to keep
4370
4459
  * that method's own complexity down. Returns `true` when the limit was hit
@@ -4435,12 +4524,7 @@ declare abstract class ShardDO {
4435
4524
  tables: string[];
4436
4525
  } | undefined>;
4437
4526
  private beginDispatch;
4438
- /**
4439
- * Clear every per-request field {@link ShardDO.beginDispatch} stamped.
4440
- *
4441
- * `dispatchHeadroom` is passed rather than read off `this` so the headroom
4442
- * clear stays identity-guarded — see the comment on it.
4443
- */
4527
+ /** Clear every per-request field {@link ShardDO.beginDispatch} stamped. */
4444
4528
  private endDispatch;
4445
4529
  /**
4446
4530
  * Open one `.global()` poll tick: ask the global changelog what moved since
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-C4oEfgFy.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-B4sEEdp9.mjs";import{SHARD_REGISTRY_DO_NAME as R,ShardRegistryDO as d}from"./packem_shared/SHARD_REGISTRY_DO_NAME-CF4ion0i.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-PRr5ltyV.mjs";import{SHARD_REGISTRY_DO_NAME as R,ShardRegistryDO as d}from"./packem_shared/SHARD_REGISTRY_DO_NAME-DObo9_01.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};