@lunora/do 1.0.0-alpha.6 → 1.0.0-alpha.7
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 +61 -32
- package/dist/index.d.ts +61 -32
- package/dist/index.mjs +2 -1
- package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-DfwcxW8F.mjs → ROOT_DO_SIZE_WARN_BYTES-2DxWrdla.mjs} +56 -116
- package/dist/packem_shared/subscriptionListDeltas-ce84gpwL.mjs +111 -0
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -3666,6 +3666,44 @@ declare class SessionDO {
|
|
|
3666
3666
|
private handleRevoke;
|
|
3667
3667
|
}
|
|
3668
3668
|
/**
|
|
3669
|
+
* Diff the previously-sent list snapshot (`previousJson`, the memo's
|
|
3670
|
+
* `lastJson`) against the new query result and produce per-row
|
|
3671
|
+
* {@link MutationDelta}s the client can merge in place via `applyDelta` —
|
|
3672
|
+
* Convex-parity live-pagination deltas (server half of gap #20).
|
|
3673
|
+
*
|
|
3674
|
+
* Returns `undefined` (caller falls back to a full `{type:"data"}` snapshot)
|
|
3675
|
+
* unless ALL of these hold:
|
|
3676
|
+
*
|
|
3677
|
+
* 1. `previousJson` parses to an array (there IS a previous list to diff against).
|
|
3678
|
+
* 2. `nextResult` is also an array.
|
|
3679
|
+
* 3. Every row in both arrays is a plain object carrying a string `_id`.
|
|
3680
|
+
* 4. Order preservation — rows present in BOTH arrays appear in the same relative order.
|
|
3681
|
+
* 5. Chattiness cap — the number of deltas does not exceed the new array length (a near-total change is cheaper as a snapshot).
|
|
3682
|
+
*
|
|
3683
|
+
* Diff is keyed by `_id`: rows only in prev → `delete`; rows only in next →
|
|
3684
|
+
* `insert`; rows in both whose JSON differs → `update`. Insert/update carry the
|
|
3685
|
+
* full new `row`; delete omits it (matching the wire contract `@lunora/client`
|
|
3686
|
+
* parses). Deltas are ordered deletes-then-inserts/updates so the client never
|
|
3687
|
+
* sees a transient over-length page.
|
|
3688
|
+
*
|
|
3689
|
+
* Per-row serialization is done exactly **once** per refresh (finding #6). Each
|
|
3690
|
+
* row is stringified a single time into a fingerprint reused for both the
|
|
3691
|
+
* `prev !== next` change-detection compare and — when the caller passes the
|
|
3692
|
+
* optional `frames` sink — the pre-serialized delta frame body. The returned
|
|
3693
|
+
* `MutationDelta[]` shape is unchanged; `frames`, when supplied, receives the
|
|
3694
|
+
* exact `JSON.stringify(delta)` string for each returned delta, in the same
|
|
3695
|
+
* order, so the caller can splice it straight into the `{type:"delta"}` frame
|
|
3696
|
+
* without serializing the delta (and the row inside it) a second time.
|
|
3697
|
+
* @returns the per-row deltas to send, or `undefined` when any precondition fails and a full snapshot should be sent instead
|
|
3698
|
+
*/
|
|
3699
|
+
declare const subscriptionListDeltas: (previousJson: string, nextResult: unknown, table: string, frames?: string[]) => MutationDelta[] | undefined;
|
|
3700
|
+
/**
|
|
3701
|
+
* Send one WebSocket frame, reporting whether it left the socket. A throw from
|
|
3702
|
+
* `ws.send` (socket closed mid-flush, outbound buffer gone) is the only
|
|
3703
|
+
* delivery-failure signal the runtime exposes; callers use the boolean to decide
|
|
3704
|
+
* whether to advance a subscription's delivered-diff baseline.
|
|
3705
|
+
*/
|
|
3706
|
+
/**
|
|
3669
3707
|
* Optional programmatic log sink, resolved from `createShardDO({ observability })`.
|
|
3670
3708
|
* Structurally a subset of `@lunora/runtime`'s `ObservabilitySink`, so a user can
|
|
3671
3709
|
* pass the SAME sink object to `createWorker` (which drives `onRpc`) and
|
|
@@ -3918,38 +3956,6 @@ interface RunShardRankPageArgs {
|
|
|
3918
3956
|
take?: number;
|
|
3919
3957
|
}
|
|
3920
3958
|
/**
|
|
3921
|
-
* Diff the previously-sent list snapshot (`previousJson`, the memo's
|
|
3922
|
-
* `lastJson`) against the new query result and produce per-row
|
|
3923
|
-
* {@link MutationDelta}s the client can merge in place via `applyDelta` —
|
|
3924
|
-
* Convex-parity live-pagination deltas (server half of gap #20).
|
|
3925
|
-
*
|
|
3926
|
-
* Returns `undefined` (caller falls back to a full `{type:"data"}` snapshot)
|
|
3927
|
-
* unless ALL of these hold:
|
|
3928
|
-
*
|
|
3929
|
-
* 1. `previousJson` parses to an array (there IS a previous list to diff against).
|
|
3930
|
-
* 2. `nextResult` is also an array.
|
|
3931
|
-
* 3. Every row in both arrays is a plain object carrying a string `_id`.
|
|
3932
|
-
* 4. Order preservation — rows present in BOTH arrays appear in the same relative order.
|
|
3933
|
-
* 5. Chattiness cap — the number of deltas does not exceed the new array length (a near-total change is cheaper as a snapshot).
|
|
3934
|
-
*
|
|
3935
|
-
* Diff is keyed by `_id`: rows only in prev → `delete`; rows only in next →
|
|
3936
|
-
* `insert`; rows in both whose JSON differs → `update`. Insert/update carry the
|
|
3937
|
-
* full new `row`; delete omits it (matching the wire contract `@lunora/client`
|
|
3938
|
-
* parses). Deltas are ordered deletes-then-inserts/updates so the client never
|
|
3939
|
-
* sees a transient over-length page.
|
|
3940
|
-
*
|
|
3941
|
-
* Per-row serialization is done exactly **once** per refresh (finding #6). Each
|
|
3942
|
-
* row is stringified a single time into a fingerprint reused for both the
|
|
3943
|
-
* `prev !== next` change-detection compare and — when the caller passes the
|
|
3944
|
-
* optional `frames` sink — the pre-serialized delta frame body. The returned
|
|
3945
|
-
* `MutationDelta[]` shape is unchanged; `frames`, when supplied, receives the
|
|
3946
|
-
* exact `JSON.stringify(delta)` string for each returned delta, in the same
|
|
3947
|
-
* order, so the caller can splice it straight into the `{type:"delta"}` frame
|
|
3948
|
-
* without serializing the delta (and the row inside it) a second time.
|
|
3949
|
-
* @returns the per-row deltas to send, or `undefined` when any precondition fails and a full snapshot should be sent instead
|
|
3950
|
-
*/
|
|
3951
|
-
declare const subscriptionListDeltas: (previousJson: string, nextResult: unknown, table: string, frames?: string[]) => MutationDelta[] | undefined;
|
|
3952
|
-
/**
|
|
3953
3959
|
* Threshold at which a `__root__` DO triggers the size warning. 1 GiB —
|
|
3954
3960
|
* exactly 10% of the 10 GiB per-DO SQLite ceiling, leaving plenty of runway
|
|
3955
3961
|
* to plan a `.shardBy()` migration before the wall hits.
|
|
@@ -4131,6 +4137,18 @@ declare abstract class ShardDO {
|
|
|
4131
4137
|
*/
|
|
4132
4138
|
private pendingChangedTables;
|
|
4133
4139
|
/**
|
|
4140
|
+
* Coalesced set of tables awaiting a subscription-refresh pass, merged
|
|
4141
|
+
* across every {@link ShardDO.flushChangedTables} call that lands while a
|
|
4142
|
+
* pass is already draining. The single drain loop
|
|
4143
|
+
* ({@link ShardDO.drainSubscriptionRefreshes}) owns this set; a burst of N
|
|
4144
|
+
* writes to the same table therefore collapses into one (or two) refresh
|
|
4145
|
+
* passes instead of N, so each affected subscription's handler re-runs once
|
|
4146
|
+
* per burst rather than once per write. `undefined` when nothing is pending.
|
|
4147
|
+
*/
|
|
4148
|
+
private pendingRefreshTables;
|
|
4149
|
+
/** True while {@link ShardDO.drainSubscriptionRefreshes} is running; the single-waiter gate that coalesces concurrent flushes. */
|
|
4150
|
+
private refreshInFlight;
|
|
4151
|
+
/**
|
|
4134
4152
|
* Last pushed result per `(socket, subId)`, keyed by socket. Lets
|
|
4135
4153
|
* `refreshSubscriptions` skip re-running queries whose tables were
|
|
4136
4154
|
* untouched and suppress pushes when the re-run result is unchanged. Held
|
|
@@ -5303,6 +5321,17 @@ declare abstract class ShardDO {
|
|
|
5303
5321
|
*/
|
|
5304
5322
|
private flushChangedTables;
|
|
5305
5323
|
/**
|
|
5324
|
+
* Drain {@link ShardDO.pendingRefreshTables} one coalesced batch at a time
|
|
5325
|
+
* until it is empty, then release the {@link ShardDO.refreshInFlight} gate.
|
|
5326
|
+
* Tables merged by a `flushChangedTables` that lands mid-pass are picked up
|
|
5327
|
+
* by the next loop iteration, so every committed write is observed by a
|
|
5328
|
+
* refresh that runs after it — bursts simply share a pass. The post-write
|
|
5329
|
+
* high-watermark and live-socket set are re-read inside each
|
|
5330
|
+
* `refreshSubscriptions` call, so a later batch always reflects the latest
|
|
5331
|
+
* committed state.
|
|
5332
|
+
*/
|
|
5333
|
+
private drainSubscriptionRefreshes;
|
|
5334
|
+
/**
|
|
5306
5335
|
* For every live subscription whose query reads one of `changed`, re-run
|
|
5307
5336
|
* the query and push a fresh `{ type: "data" }` frame when the result
|
|
5308
5337
|
* differs from the last one sent. Subscriptions with no `functionPath`
|
package/dist/index.d.ts
CHANGED
|
@@ -3666,6 +3666,44 @@ declare class SessionDO {
|
|
|
3666
3666
|
private handleRevoke;
|
|
3667
3667
|
}
|
|
3668
3668
|
/**
|
|
3669
|
+
* Diff the previously-sent list snapshot (`previousJson`, the memo's
|
|
3670
|
+
* `lastJson`) against the new query result and produce per-row
|
|
3671
|
+
* {@link MutationDelta}s the client can merge in place via `applyDelta` —
|
|
3672
|
+
* Convex-parity live-pagination deltas (server half of gap #20).
|
|
3673
|
+
*
|
|
3674
|
+
* Returns `undefined` (caller falls back to a full `{type:"data"}` snapshot)
|
|
3675
|
+
* unless ALL of these hold:
|
|
3676
|
+
*
|
|
3677
|
+
* 1. `previousJson` parses to an array (there IS a previous list to diff against).
|
|
3678
|
+
* 2. `nextResult` is also an array.
|
|
3679
|
+
* 3. Every row in both arrays is a plain object carrying a string `_id`.
|
|
3680
|
+
* 4. Order preservation — rows present in BOTH arrays appear in the same relative order.
|
|
3681
|
+
* 5. Chattiness cap — the number of deltas does not exceed the new array length (a near-total change is cheaper as a snapshot).
|
|
3682
|
+
*
|
|
3683
|
+
* Diff is keyed by `_id`: rows only in prev → `delete`; rows only in next →
|
|
3684
|
+
* `insert`; rows in both whose JSON differs → `update`. Insert/update carry the
|
|
3685
|
+
* full new `row`; delete omits it (matching the wire contract `@lunora/client`
|
|
3686
|
+
* parses). Deltas are ordered deletes-then-inserts/updates so the client never
|
|
3687
|
+
* sees a transient over-length page.
|
|
3688
|
+
*
|
|
3689
|
+
* Per-row serialization is done exactly **once** per refresh (finding #6). Each
|
|
3690
|
+
* row is stringified a single time into a fingerprint reused for both the
|
|
3691
|
+
* `prev !== next` change-detection compare and — when the caller passes the
|
|
3692
|
+
* optional `frames` sink — the pre-serialized delta frame body. The returned
|
|
3693
|
+
* `MutationDelta[]` shape is unchanged; `frames`, when supplied, receives the
|
|
3694
|
+
* exact `JSON.stringify(delta)` string for each returned delta, in the same
|
|
3695
|
+
* order, so the caller can splice it straight into the `{type:"delta"}` frame
|
|
3696
|
+
* without serializing the delta (and the row inside it) a second time.
|
|
3697
|
+
* @returns the per-row deltas to send, or `undefined` when any precondition fails and a full snapshot should be sent instead
|
|
3698
|
+
*/
|
|
3699
|
+
declare const subscriptionListDeltas: (previousJson: string, nextResult: unknown, table: string, frames?: string[]) => MutationDelta[] | undefined;
|
|
3700
|
+
/**
|
|
3701
|
+
* Send one WebSocket frame, reporting whether it left the socket. A throw from
|
|
3702
|
+
* `ws.send` (socket closed mid-flush, outbound buffer gone) is the only
|
|
3703
|
+
* delivery-failure signal the runtime exposes; callers use the boolean to decide
|
|
3704
|
+
* whether to advance a subscription's delivered-diff baseline.
|
|
3705
|
+
*/
|
|
3706
|
+
/**
|
|
3669
3707
|
* Optional programmatic log sink, resolved from `createShardDO({ observability })`.
|
|
3670
3708
|
* Structurally a subset of `@lunora/runtime`'s `ObservabilitySink`, so a user can
|
|
3671
3709
|
* pass the SAME sink object to `createWorker` (which drives `onRpc`) and
|
|
@@ -3918,38 +3956,6 @@ interface RunShardRankPageArgs {
|
|
|
3918
3956
|
take?: number;
|
|
3919
3957
|
}
|
|
3920
3958
|
/**
|
|
3921
|
-
* Diff the previously-sent list snapshot (`previousJson`, the memo's
|
|
3922
|
-
* `lastJson`) against the new query result and produce per-row
|
|
3923
|
-
* {@link MutationDelta}s the client can merge in place via `applyDelta` —
|
|
3924
|
-
* Convex-parity live-pagination deltas (server half of gap #20).
|
|
3925
|
-
*
|
|
3926
|
-
* Returns `undefined` (caller falls back to a full `{type:"data"}` snapshot)
|
|
3927
|
-
* unless ALL of these hold:
|
|
3928
|
-
*
|
|
3929
|
-
* 1. `previousJson` parses to an array (there IS a previous list to diff against).
|
|
3930
|
-
* 2. `nextResult` is also an array.
|
|
3931
|
-
* 3. Every row in both arrays is a plain object carrying a string `_id`.
|
|
3932
|
-
* 4. Order preservation — rows present in BOTH arrays appear in the same relative order.
|
|
3933
|
-
* 5. Chattiness cap — the number of deltas does not exceed the new array length (a near-total change is cheaper as a snapshot).
|
|
3934
|
-
*
|
|
3935
|
-
* Diff is keyed by `_id`: rows only in prev → `delete`; rows only in next →
|
|
3936
|
-
* `insert`; rows in both whose JSON differs → `update`. Insert/update carry the
|
|
3937
|
-
* full new `row`; delete omits it (matching the wire contract `@lunora/client`
|
|
3938
|
-
* parses). Deltas are ordered deletes-then-inserts/updates so the client never
|
|
3939
|
-
* sees a transient over-length page.
|
|
3940
|
-
*
|
|
3941
|
-
* Per-row serialization is done exactly **once** per refresh (finding #6). Each
|
|
3942
|
-
* row is stringified a single time into a fingerprint reused for both the
|
|
3943
|
-
* `prev !== next` change-detection compare and — when the caller passes the
|
|
3944
|
-
* optional `frames` sink — the pre-serialized delta frame body. The returned
|
|
3945
|
-
* `MutationDelta[]` shape is unchanged; `frames`, when supplied, receives the
|
|
3946
|
-
* exact `JSON.stringify(delta)` string for each returned delta, in the same
|
|
3947
|
-
* order, so the caller can splice it straight into the `{type:"delta"}` frame
|
|
3948
|
-
* without serializing the delta (and the row inside it) a second time.
|
|
3949
|
-
* @returns the per-row deltas to send, or `undefined` when any precondition fails and a full snapshot should be sent instead
|
|
3950
|
-
*/
|
|
3951
|
-
declare const subscriptionListDeltas: (previousJson: string, nextResult: unknown, table: string, frames?: string[]) => MutationDelta[] | undefined;
|
|
3952
|
-
/**
|
|
3953
3959
|
* Threshold at which a `__root__` DO triggers the size warning. 1 GiB —
|
|
3954
3960
|
* exactly 10% of the 10 GiB per-DO SQLite ceiling, leaving plenty of runway
|
|
3955
3961
|
* to plan a `.shardBy()` migration before the wall hits.
|
|
@@ -4131,6 +4137,18 @@ declare abstract class ShardDO {
|
|
|
4131
4137
|
*/
|
|
4132
4138
|
private pendingChangedTables;
|
|
4133
4139
|
/**
|
|
4140
|
+
* Coalesced set of tables awaiting a subscription-refresh pass, merged
|
|
4141
|
+
* across every {@link ShardDO.flushChangedTables} call that lands while a
|
|
4142
|
+
* pass is already draining. The single drain loop
|
|
4143
|
+
* ({@link ShardDO.drainSubscriptionRefreshes}) owns this set; a burst of N
|
|
4144
|
+
* writes to the same table therefore collapses into one (or two) refresh
|
|
4145
|
+
* passes instead of N, so each affected subscription's handler re-runs once
|
|
4146
|
+
* per burst rather than once per write. `undefined` when nothing is pending.
|
|
4147
|
+
*/
|
|
4148
|
+
private pendingRefreshTables;
|
|
4149
|
+
/** True while {@link ShardDO.drainSubscriptionRefreshes} is running; the single-waiter gate that coalesces concurrent flushes. */
|
|
4150
|
+
private refreshInFlight;
|
|
4151
|
+
/**
|
|
4134
4152
|
* Last pushed result per `(socket, subId)`, keyed by socket. Lets
|
|
4135
4153
|
* `refreshSubscriptions` skip re-running queries whose tables were
|
|
4136
4154
|
* untouched and suppress pushes when the re-run result is unchanged. Held
|
|
@@ -5303,6 +5321,17 @@ declare abstract class ShardDO {
|
|
|
5303
5321
|
*/
|
|
5304
5322
|
private flushChangedTables;
|
|
5305
5323
|
/**
|
|
5324
|
+
* Drain {@link ShardDO.pendingRefreshTables} one coalesced batch at a time
|
|
5325
|
+
* until it is empty, then release the {@link ShardDO.refreshInFlight} gate.
|
|
5326
|
+
* Tables merged by a `flushChangedTables` that lands mid-pass are picked up
|
|
5327
|
+
* by the next loop iteration, so every committed write is observed by a
|
|
5328
|
+
* refresh that runs after it — bursts simply share a pass. The post-write
|
|
5329
|
+
* high-watermark and live-socket set are re-read inside each
|
|
5330
|
+
* `refreshSubscriptions` call, so a later batch always reflects the latest
|
|
5331
|
+
* committed state.
|
|
5332
|
+
*/
|
|
5333
|
+
private drainSubscriptionRefreshes;
|
|
5334
|
+
/**
|
|
5306
5335
|
* For every live subscription whose query reads one of `changed`, re-run
|
|
5307
5336
|
* the query and push a fresh `{ type: "data" }` frame when the result
|
|
5308
5337
|
* differs from the last one sent. Subscriptions with no `functionPath`
|
package/dist/index.mjs
CHANGED
|
@@ -23,7 +23,7 @@ export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_share
|
|
|
23
23
|
export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
|
|
24
24
|
export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
|
|
25
25
|
export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs';
|
|
26
|
-
export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO
|
|
26
|
+
export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-2DxWrdla.mjs';
|
|
27
27
|
export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs';
|
|
28
28
|
export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-dDcFE1YZ.mjs';
|
|
29
29
|
export { createSystemReader } from './packem_shared/createSystemReader-8CzSZP9V.mjs';
|
|
@@ -33,3 +33,4 @@ export { compileWhereSql } from './packem_shared/compileWhereSql-CXrhFA3G.mjs';
|
|
|
33
33
|
export { CDC_LOG_TABLE, applyCdcChanges, readCdcChanges, trimCdcChanges } from './packem_shared/CDC_LOG_TABLE-Ctdmxmrv.mjs';
|
|
34
34
|
export { backfillAggregateIndexes, backfillRankIndexes } from './packem_shared/backfillAggregateIndexes-BbVPvciS.mjs';
|
|
35
35
|
export { runShardMigrations } from './packem_shared/runShardMigrations-PabobOjF.mjs';
|
|
36
|
+
export { subscriptionListDeltas } from './packem_shared/subscriptionListDeltas-ce84gpwL.mjs';
|
|
@@ -12,6 +12,7 @@ import { ReactiveCache, reactiveCacheKey } from './ReactiveCache-ByVzgH3d.mjs';
|
|
|
12
12
|
import { redact, standardRules } from '@visulima/redact';
|
|
13
13
|
import { i as isDevEnvironment, c as buildSettings, b as buildSecurityAudit } from './security-audit-CucgBice.mjs';
|
|
14
14
|
import { runReadonlySql } from './MAX_SQL_ROWS-dDcFE1YZ.mjs';
|
|
15
|
+
import { trySendFrame, subscriptionListDeltas, sendDeltaFrames } from './subscriptionListDeltas-ce84gpwL.mjs';
|
|
15
16
|
import { ConflictError } from './ConflictError-C0STs6bU.mjs';
|
|
16
17
|
import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-Ctdmxmrv.mjs';
|
|
17
18
|
import { r as readIdempotent, w as writeIdempotent, t as trimIdempotent } from './ctx-db-idempotency-DkC9rP91.mjs';
|
|
@@ -413,97 +414,7 @@ const findDanglingReferences = (sql, storageColumns, liveKeys) => {
|
|
|
413
414
|
|
|
414
415
|
const WS_KEEPALIVE_PING = "lunora-ping";
|
|
415
416
|
const WS_KEEPALIVE_PONG = "lunora-pong";
|
|
416
|
-
const
|
|
417
|
-
const DELTA_FALLBACK_TABLE = "__lunora__";
|
|
418
|
-
const readRowId = (row) => {
|
|
419
|
-
if (typeof row !== "object" || row === null || Array.isArray(row)) {
|
|
420
|
-
return void 0;
|
|
421
|
-
}
|
|
422
|
-
const id = row[ROW_ID_FIELD];
|
|
423
|
-
return typeof id === "string" ? id : void 0;
|
|
424
|
-
};
|
|
425
|
-
const indexRowsById = (rows) => {
|
|
426
|
-
const byId = /* @__PURE__ */ new Map();
|
|
427
|
-
const order = [];
|
|
428
|
-
for (const row of rows) {
|
|
429
|
-
const id = readRowId(row);
|
|
430
|
-
if (id === void 0 || byId.has(id)) {
|
|
431
|
-
return void 0;
|
|
432
|
-
}
|
|
433
|
-
byId.set(id, row);
|
|
434
|
-
order.push(id);
|
|
435
|
-
}
|
|
436
|
-
return { byId, order };
|
|
437
|
-
};
|
|
438
|
-
const survivorsKeepOrder = (previous, next) => {
|
|
439
|
-
const survivingPrevious = previous.order.filter((id) => next.byId.has(id));
|
|
440
|
-
const survivingNext = next.order.filter((id) => previous.byId.has(id));
|
|
441
|
-
if (survivingPrevious.length !== survivingNext.length) {
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
return survivingPrevious.every((id, index) => survivingNext[index] === id);
|
|
445
|
-
};
|
|
446
|
-
const collectDeleteDeltas = (previous, next, deltaTable, tableJson) => {
|
|
447
|
-
const out = [];
|
|
448
|
-
for (const id of previous.order) {
|
|
449
|
-
if (!next.byId.has(id)) {
|
|
450
|
-
out.push({
|
|
451
|
-
delta: { key: id, op: "delete", table: deltaTable },
|
|
452
|
-
frame: `{"key":${JSON.stringify(id)},"op":"delete","table":${tableJson}}`
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return out;
|
|
457
|
-
};
|
|
458
|
-
const collectUpsertDeltas = (previous, next, deltaTable, tableJson) => {
|
|
459
|
-
const out = [];
|
|
460
|
-
for (const id of next.order) {
|
|
461
|
-
const nextRow = next.byId.get(id);
|
|
462
|
-
const previousRow = previous.byId.get(id);
|
|
463
|
-
const nextFingerprint = JSON.stringify(nextRow);
|
|
464
|
-
const previousFingerprint = previousRow === void 0 ? void 0 : JSON.stringify(previousRow);
|
|
465
|
-
if (previousFingerprint === nextFingerprint) {
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
const op = previousFingerprint === void 0 ? "insert" : "update";
|
|
469
|
-
out.push({
|
|
470
|
-
delta: { key: id, op, row: nextRow, table: deltaTable },
|
|
471
|
-
frame: `{"key":${JSON.stringify(id)},"op":"${op}","row":${nextFingerprint},"table":${tableJson}}`
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
return out;
|
|
475
|
-
};
|
|
476
|
-
const subscriptionListDeltas = (previousJson, nextResult, table, frames) => {
|
|
477
|
-
let parsed;
|
|
478
|
-
try {
|
|
479
|
-
parsed = JSON.parse(previousJson);
|
|
480
|
-
} catch {
|
|
481
|
-
return void 0;
|
|
482
|
-
}
|
|
483
|
-
if (!Array.isArray(parsed) || !Array.isArray(nextResult)) {
|
|
484
|
-
return void 0;
|
|
485
|
-
}
|
|
486
|
-
const previous = indexRowsById(parsed);
|
|
487
|
-
const next = indexRowsById(nextResult);
|
|
488
|
-
if (previous === void 0 || next === void 0) {
|
|
489
|
-
return void 0;
|
|
490
|
-
}
|
|
491
|
-
if (!survivorsKeepOrder(previous, next)) {
|
|
492
|
-
return void 0;
|
|
493
|
-
}
|
|
494
|
-
const deltaTable = table === "" ? DELTA_FALLBACK_TABLE : table;
|
|
495
|
-
const tableJson = JSON.stringify(deltaTable);
|
|
496
|
-
const framed = [...collectDeleteDeltas(previous, next, deltaTable, tableJson), ...collectUpsertDeltas(previous, next, deltaTable, tableJson)];
|
|
497
|
-
if (framed.length > next.order.length) {
|
|
498
|
-
return void 0;
|
|
499
|
-
}
|
|
500
|
-
if (frames !== void 0) {
|
|
501
|
-
for (const { frame } of framed) {
|
|
502
|
-
frames.push(frame);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return framed.map(({ delta }) => delta);
|
|
506
|
-
};
|
|
417
|
+
const UNDELIVERED_BASELINE = "<undelivered>";
|
|
507
418
|
const ROOT_DO_SIZE_WARN_BYTES = 1073741824;
|
|
508
419
|
const CDC_RESUME_SCAN_LIMIT = 1e4;
|
|
509
420
|
const IDEMPOTENCY_RETENTION_MS = 864e5;
|
|
@@ -1133,6 +1044,18 @@ class ShardDO {
|
|
|
1133
1044
|
* the common read-only path allocates nothing.
|
|
1134
1045
|
*/
|
|
1135
1046
|
pendingChangedTables = void 0;
|
|
1047
|
+
/**
|
|
1048
|
+
* Coalesced set of tables awaiting a subscription-refresh pass, merged
|
|
1049
|
+
* across every {@link ShardDO.flushChangedTables} call that lands while a
|
|
1050
|
+
* pass is already draining. The single drain loop
|
|
1051
|
+
* ({@link ShardDO.drainSubscriptionRefreshes}) owns this set; a burst of N
|
|
1052
|
+
* writes to the same table therefore collapses into one (or two) refresh
|
|
1053
|
+
* passes instead of N, so each affected subscription's handler re-runs once
|
|
1054
|
+
* per burst rather than once per write. `undefined` when nothing is pending.
|
|
1055
|
+
*/
|
|
1056
|
+
pendingRefreshTables = void 0;
|
|
1057
|
+
/** True while {@link ShardDO.drainSubscriptionRefreshes} is running; the single-waiter gate that coalesces concurrent flushes. */
|
|
1058
|
+
refreshInFlight = false;
|
|
1136
1059
|
/**
|
|
1137
1060
|
* Last pushed result per `(socket, subId)`, keyed by socket. Lets
|
|
1138
1061
|
* `refreshSubscriptions` skip re-running queries whose tables were
|
|
@@ -2249,10 +2172,7 @@ class ShardDO {
|
|
|
2249
2172
|
if (!this.matchesSubscription(query, delta)) {
|
|
2250
2173
|
continue;
|
|
2251
2174
|
}
|
|
2252
|
-
|
|
2253
|
-
ws.send(`{"type":"delta","id":${JSON.stringify(subId)},"delta":${deltaJson}}`);
|
|
2254
|
-
} catch {
|
|
2255
|
-
}
|
|
2175
|
+
trySendFrame(ws, `{"type":"delta","id":${JSON.stringify(subId)},"delta":${deltaJson}}`);
|
|
2256
2176
|
}
|
|
2257
2177
|
}
|
|
2258
2178
|
}
|
|
@@ -3567,11 +3487,47 @@ class ShardDO {
|
|
|
3567
3487
|
if (!changed || changed.size === 0) {
|
|
3568
3488
|
return;
|
|
3569
3489
|
}
|
|
3490
|
+
if (this.pendingRefreshTables) {
|
|
3491
|
+
for (const table of changed) {
|
|
3492
|
+
this.pendingRefreshTables.add(table);
|
|
3493
|
+
}
|
|
3494
|
+
} else {
|
|
3495
|
+
this.pendingRefreshTables = changed;
|
|
3496
|
+
}
|
|
3497
|
+
if (this.refreshInFlight) {
|
|
3498
|
+
return;
|
|
3499
|
+
}
|
|
3570
3500
|
if (typeof this.state.waitUntil === "function") {
|
|
3571
|
-
this.state.waitUntil(this.
|
|
3501
|
+
this.state.waitUntil(this.drainSubscriptionRefreshes());
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
await this.drainSubscriptionRefreshes();
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Drain {@link ShardDO.pendingRefreshTables} one coalesced batch at a time
|
|
3508
|
+
* until it is empty, then release the {@link ShardDO.refreshInFlight} gate.
|
|
3509
|
+
* Tables merged by a `flushChangedTables` that lands mid-pass are picked up
|
|
3510
|
+
* by the next loop iteration, so every committed write is observed by a
|
|
3511
|
+
* refresh that runs after it — bursts simply share a pass. The post-write
|
|
3512
|
+
* high-watermark and live-socket set are re-read inside each
|
|
3513
|
+
* `refreshSubscriptions` call, so a later batch always reflects the latest
|
|
3514
|
+
* committed state.
|
|
3515
|
+
*/
|
|
3516
|
+
async drainSubscriptionRefreshes() {
|
|
3517
|
+
if (this.refreshInFlight) {
|
|
3572
3518
|
return;
|
|
3573
3519
|
}
|
|
3574
|
-
|
|
3520
|
+
this.refreshInFlight = true;
|
|
3521
|
+
try {
|
|
3522
|
+
let batch = this.pendingRefreshTables;
|
|
3523
|
+
while (batch && batch.size > 0) {
|
|
3524
|
+
this.pendingRefreshTables = void 0;
|
|
3525
|
+
await this.refreshSubscriptions(batch);
|
|
3526
|
+
batch = this.pendingRefreshTables;
|
|
3527
|
+
}
|
|
3528
|
+
} finally {
|
|
3529
|
+
this.refreshInFlight = false;
|
|
3530
|
+
}
|
|
3575
3531
|
}
|
|
3576
3532
|
/**
|
|
3577
3533
|
* For every live subscription whose query reads one of `changed`, re-run
|
|
@@ -3763,21 +3719,8 @@ class ShardDO {
|
|
|
3763
3719
|
}
|
|
3764
3720
|
const deltaFrames = [];
|
|
3765
3721
|
const deltas = existing === void 0 ? void 0 : subscriptionListDeltas(existing.lastJson, outcome.result, outcome.tables.values().next().value ?? "", deltaFrames);
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
const idJson = JSON.stringify(subId);
|
|
3769
|
-
for (const deltaBody of deltaFrames) {
|
|
3770
|
-
try {
|
|
3771
|
-
ws.send(`{"type":"delta","id":${idJson},"delta":${deltaBody}${cursorSuffix}}`);
|
|
3772
|
-
} catch {
|
|
3773
|
-
}
|
|
3774
|
-
}
|
|
3775
|
-
return;
|
|
3776
|
-
}
|
|
3777
|
-
try {
|
|
3778
|
-
ws.send(`{"type":"data","id":${JSON.stringify(subId)},"data":${json}${cursorSuffix}}`);
|
|
3779
|
-
} catch {
|
|
3780
|
-
}
|
|
3722
|
+
const delivered = deltas === void 0 ? trySendFrame(ws, `{"type":"data","id":${JSON.stringify(subId)},"data":${json}${cursorSuffix}}`) : sendDeltaFrames(ws, subId, deltaFrames, cursorSuffix);
|
|
3723
|
+
memos.set(subId, { lastJson: delivered ? json : existing?.lastJson ?? UNDELIVERED_BASELINE, tables: outcome.tables });
|
|
3781
3724
|
}
|
|
3782
3725
|
/**
|
|
3783
3726
|
* Gate the upgrade request against two complementary controls:
|
|
@@ -3999,10 +3942,7 @@ class ShardDO {
|
|
|
3999
3942
|
if (ws === sender || this.readAttachment(ws).whispers?.includes(topic) !== true) {
|
|
4000
3943
|
continue;
|
|
4001
3944
|
}
|
|
4002
|
-
|
|
4003
|
-
ws.send(frame);
|
|
4004
|
-
} catch {
|
|
4005
|
-
}
|
|
3945
|
+
trySendFrame(ws, frame);
|
|
4006
3946
|
}
|
|
4007
3947
|
}
|
|
4008
3948
|
// eslint-disable-next-line class-methods-use-this -- cohesive DO instance method grouped with the hibernation/attachment helpers; reads only the socket
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const ROW_ID_FIELD = "_id";
|
|
2
|
+
const DELTA_FALLBACK_TABLE = "__lunora__";
|
|
3
|
+
const readRowId = (row) => {
|
|
4
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) {
|
|
5
|
+
return void 0;
|
|
6
|
+
}
|
|
7
|
+
const id = row[ROW_ID_FIELD];
|
|
8
|
+
return typeof id === "string" ? id : void 0;
|
|
9
|
+
};
|
|
10
|
+
const indexRowsById = (rows) => {
|
|
11
|
+
const byId = /* @__PURE__ */ new Map();
|
|
12
|
+
const order = [];
|
|
13
|
+
for (const row of rows) {
|
|
14
|
+
const id = readRowId(row);
|
|
15
|
+
if (id === void 0 || byId.has(id)) {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
byId.set(id, row);
|
|
19
|
+
order.push(id);
|
|
20
|
+
}
|
|
21
|
+
return { byId, order };
|
|
22
|
+
};
|
|
23
|
+
const survivorsKeepOrder = (previous, next) => {
|
|
24
|
+
const survivingPrevious = previous.order.filter((id) => next.byId.has(id));
|
|
25
|
+
const survivingNext = next.order.filter((id) => previous.byId.has(id));
|
|
26
|
+
if (survivingPrevious.length !== survivingNext.length) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return survivingPrevious.every((id, index) => survivingNext[index] === id);
|
|
30
|
+
};
|
|
31
|
+
const collectDeleteDeltas = (previous, next, deltaTable, tableJson) => {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const id of previous.order) {
|
|
34
|
+
if (!next.byId.has(id)) {
|
|
35
|
+
out.push({
|
|
36
|
+
delta: { key: id, op: "delete", table: deltaTable },
|
|
37
|
+
frame: `{"key":${JSON.stringify(id)},"op":"delete","table":${tableJson}}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
43
|
+
const collectUpsertDeltas = (previous, next, deltaTable, tableJson) => {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const id of next.order) {
|
|
46
|
+
const nextRow = next.byId.get(id);
|
|
47
|
+
const previousRow = previous.byId.get(id);
|
|
48
|
+
const nextFingerprint = JSON.stringify(nextRow);
|
|
49
|
+
const previousFingerprint = previousRow === void 0 ? void 0 : JSON.stringify(previousRow);
|
|
50
|
+
if (previousFingerprint === nextFingerprint) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const op = previousFingerprint === void 0 ? "insert" : "update";
|
|
54
|
+
out.push({
|
|
55
|
+
delta: { key: id, op, row: nextRow, table: deltaTable },
|
|
56
|
+
frame: `{"key":${JSON.stringify(id)},"op":"${op}","row":${nextFingerprint},"table":${tableJson}}`
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
};
|
|
61
|
+
const subscriptionListDeltas = (previousJson, nextResult, table, frames) => {
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(previousJson);
|
|
65
|
+
} catch {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(parsed) || !Array.isArray(nextResult)) {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
const previous = indexRowsById(parsed);
|
|
72
|
+
const next = indexRowsById(nextResult);
|
|
73
|
+
if (previous === void 0 || next === void 0) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
if (!survivorsKeepOrder(previous, next)) {
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
const deltaTable = table === "" ? DELTA_FALLBACK_TABLE : table;
|
|
80
|
+
const tableJson = JSON.stringify(deltaTable);
|
|
81
|
+
const framed = [...collectDeleteDeltas(previous, next, deltaTable, tableJson), ...collectUpsertDeltas(previous, next, deltaTable, tableJson)];
|
|
82
|
+
if (framed.length > next.order.length) {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
if (frames !== void 0) {
|
|
86
|
+
for (const { frame } of framed) {
|
|
87
|
+
frames.push(frame);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return framed.map(({ delta }) => delta);
|
|
91
|
+
};
|
|
92
|
+
const trySendFrame = (ws, frame) => {
|
|
93
|
+
try {
|
|
94
|
+
ws.send(frame);
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const sendDeltaFrames = (ws, subId, deltaFrames, cursorSuffix) => {
|
|
101
|
+
const idJson = JSON.stringify(subId);
|
|
102
|
+
let delivered = true;
|
|
103
|
+
for (const deltaBody of deltaFrames) {
|
|
104
|
+
if (!trySendFrame(ws, `{"type":"delta","id":${idJson},"delta":${deltaBody}${cursorSuffix}}`)) {
|
|
105
|
+
delivered = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return delivered;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export { sendDeltaFrames, subscriptionListDeltas, trySendFrame };
|