@lunora/do 1.0.0-alpha.10 → 1.0.0-alpha.12
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
|
@@ -5740,6 +5740,39 @@ declare abstract class ShardDO {
|
|
|
5740
5740
|
*/
|
|
5741
5741
|
private resolveReactiveOutcome;
|
|
5742
5742
|
/**
|
|
5743
|
+
* SECURITY BOUNDARY for cross-socket reactive dedup. A read is
|
|
5744
|
+
* identity-INDEPENDENT only when its result cannot vary by the caller's
|
|
5745
|
+
* verified identity — i.e. the admin/reserved introspection reads, which
|
|
5746
|
+
* route to {@link executeAdminSubscription} and ignore the
|
|
5747
|
+
* {@link SubscriptionIdentity} entirely.
|
|
5748
|
+
*
|
|
5749
|
+
* Everything else is identity-DEPENDENT and must NEVER be shared across
|
|
5750
|
+
* sockets: a user query may be `rls()` / `ctx.auth`-scoped (different rows
|
|
5751
|
+
* per identity), and a flag read ({@link FLAGS_FUNCTION_PREFIX}) evaluates
|
|
5752
|
+
* the provider with the subscriber's identity (per-user targeting). Sharing
|
|
5753
|
+
* one socket's result with another would leak one identity's rows/flags to a
|
|
5754
|
+
* different identity, so this predicate gates {@link resolveReactiveOutcomeDeduped}
|
|
5755
|
+
* shut for them.
|
|
5756
|
+
*/
|
|
5757
|
+
protected isIdentityIndependent(functionPath: string): boolean;
|
|
5758
|
+
/**
|
|
5759
|
+
* Memoizing wrapper over {@link resolveReactiveOutcome}: flush-local sharing across sockets.
|
|
5760
|
+
* Within a single {@link refreshSubscriptions} pass, N sockets subscribed to
|
|
5761
|
+
* the SAME identity-independent `(functionPath, args)` re-run the query N
|
|
5762
|
+
* times today (see the Case-6 fan-out characterization). When the read is
|
|
5763
|
+
* identity-independent (admin/reserved — see {@link isIdentityIndependent})
|
|
5764
|
+
* its result is the same for every socket, so the first run is cached (by its
|
|
5765
|
+
* in-flight Promise, since the bounded worker pool runs sockets in parallel)
|
|
5766
|
+
* and shared with the rest — collapsing N runs to ONE.
|
|
5767
|
+
*
|
|
5768
|
+
* Identity-DEPENDENT reads are passed straight through, UNCACHED: each socket
|
|
5769
|
+
* must evaluate under its own by-value identity (RLS / `ctx.auth` / per-user
|
|
5770
|
+
* flags), so they never share a result. The `cache` is created fresh per
|
|
5771
|
+
* flush by the caller, so a result is never reused across passes (it would go
|
|
5772
|
+
* stale after the next write).
|
|
5773
|
+
*/
|
|
5774
|
+
private resolveReactiveOutcomeDeduped;
|
|
5775
|
+
/**
|
|
5743
5776
|
* Constant-time bearer check against `env.LUNORA_ADMIN_TOKEN`. Returns
|
|
5744
5777
|
* `false` (closed) when the token is unset so admin introspection is
|
|
5745
5778
|
* opt-in rather than exposed by default.
|
|
@@ -5919,8 +5952,30 @@ declare abstract class ShardDO {
|
|
|
5919
5952
|
*/
|
|
5920
5953
|
private collectShapePokeParts;
|
|
5921
5954
|
/**
|
|
5955
|
+
* Drain the op-log range `(sinceSeq, upTo]` for `table` into the latest op per
|
|
5956
|
+
* row id (collapsing multiple ops on the same row to the newest). Within one
|
|
5957
|
+
* flush, every shape over the SAME `(table, sinceSeq, upTo)` reads the
|
|
5958
|
+
* identical changelog slice, so the drained map is memoized in the
|
|
5959
|
+
* caller-supplied `cache` (created fresh per flush) — N shapes on a table
|
|
5960
|
+
* share ONE changelog drain instead of re-scanning it per shape. The
|
|
5961
|
+
* per-shape membership probe still runs per shape (its predicate is
|
|
5962
|
+
* identity/args-specific), so only the shared op read is collapsed.
|
|
5963
|
+
*/
|
|
5964
|
+
private readShapeOpRange;
|
|
5965
|
+
/**
|
|
5966
|
+
* Read one page of the `__cdc_log` for a shape diff (table-scoped). A thin
|
|
5967
|
+
* protected seam over {@link readCdcChanges}: it isolates the single
|
|
5968
|
+
* changelog read that {@link readShapeOpRange} memoizes per flush, and gives
|
|
5969
|
+
* tests a point to count the reads the op-range cache collapses.
|
|
5970
|
+
*/
|
|
5971
|
+
protected readShapeCdcPage(sql: SqlExec, sinceSeq: number, tables: ReadonlySet<string>): {
|
|
5972
|
+
changes: CdcChange[];
|
|
5973
|
+
cursor: number;
|
|
5974
|
+
};
|
|
5975
|
+
/**
|
|
5922
5976
|
* Build the row-ops for a shape over the op range `(sinceSeq, upTo]`. Reads
|
|
5923
|
-
* the changelog (drained across pages
|
|
5977
|
+
* the changelog (drained across pages via {@link readShapeOpRange}, shared
|
|
5978
|
+
* across same-range shapes in a flush), collapses to the latest op per row,
|
|
5924
5979
|
* then runs ONE membership probe ({@link selectShapeMemberIds}) over the
|
|
5925
5980
|
* changed ids: a row still in the set → upsert with its post-image doc
|
|
5926
5981
|
* (projected to the shape's columns); a row that left the set, or any delete,
|
package/dist/index.d.ts
CHANGED
|
@@ -5740,6 +5740,39 @@ declare abstract class ShardDO {
|
|
|
5740
5740
|
*/
|
|
5741
5741
|
private resolveReactiveOutcome;
|
|
5742
5742
|
/**
|
|
5743
|
+
* SECURITY BOUNDARY for cross-socket reactive dedup. A read is
|
|
5744
|
+
* identity-INDEPENDENT only when its result cannot vary by the caller's
|
|
5745
|
+
* verified identity — i.e. the admin/reserved introspection reads, which
|
|
5746
|
+
* route to {@link executeAdminSubscription} and ignore the
|
|
5747
|
+
* {@link SubscriptionIdentity} entirely.
|
|
5748
|
+
*
|
|
5749
|
+
* Everything else is identity-DEPENDENT and must NEVER be shared across
|
|
5750
|
+
* sockets: a user query may be `rls()` / `ctx.auth`-scoped (different rows
|
|
5751
|
+
* per identity), and a flag read ({@link FLAGS_FUNCTION_PREFIX}) evaluates
|
|
5752
|
+
* the provider with the subscriber's identity (per-user targeting). Sharing
|
|
5753
|
+
* one socket's result with another would leak one identity's rows/flags to a
|
|
5754
|
+
* different identity, so this predicate gates {@link resolveReactiveOutcomeDeduped}
|
|
5755
|
+
* shut for them.
|
|
5756
|
+
*/
|
|
5757
|
+
protected isIdentityIndependent(functionPath: string): boolean;
|
|
5758
|
+
/**
|
|
5759
|
+
* Memoizing wrapper over {@link resolveReactiveOutcome}: flush-local sharing across sockets.
|
|
5760
|
+
* Within a single {@link refreshSubscriptions} pass, N sockets subscribed to
|
|
5761
|
+
* the SAME identity-independent `(functionPath, args)` re-run the query N
|
|
5762
|
+
* times today (see the Case-6 fan-out characterization). When the read is
|
|
5763
|
+
* identity-independent (admin/reserved — see {@link isIdentityIndependent})
|
|
5764
|
+
* its result is the same for every socket, so the first run is cached (by its
|
|
5765
|
+
* in-flight Promise, since the bounded worker pool runs sockets in parallel)
|
|
5766
|
+
* and shared with the rest — collapsing N runs to ONE.
|
|
5767
|
+
*
|
|
5768
|
+
* Identity-DEPENDENT reads are passed straight through, UNCACHED: each socket
|
|
5769
|
+
* must evaluate under its own by-value identity (RLS / `ctx.auth` / per-user
|
|
5770
|
+
* flags), so they never share a result. The `cache` is created fresh per
|
|
5771
|
+
* flush by the caller, so a result is never reused across passes (it would go
|
|
5772
|
+
* stale after the next write).
|
|
5773
|
+
*/
|
|
5774
|
+
private resolveReactiveOutcomeDeduped;
|
|
5775
|
+
/**
|
|
5743
5776
|
* Constant-time bearer check against `env.LUNORA_ADMIN_TOKEN`. Returns
|
|
5744
5777
|
* `false` (closed) when the token is unset so admin introspection is
|
|
5745
5778
|
* opt-in rather than exposed by default.
|
|
@@ -5919,8 +5952,30 @@ declare abstract class ShardDO {
|
|
|
5919
5952
|
*/
|
|
5920
5953
|
private collectShapePokeParts;
|
|
5921
5954
|
/**
|
|
5955
|
+
* Drain the op-log range `(sinceSeq, upTo]` for `table` into the latest op per
|
|
5956
|
+
* row id (collapsing multiple ops on the same row to the newest). Within one
|
|
5957
|
+
* flush, every shape over the SAME `(table, sinceSeq, upTo)` reads the
|
|
5958
|
+
* identical changelog slice, so the drained map is memoized in the
|
|
5959
|
+
* caller-supplied `cache` (created fresh per flush) — N shapes on a table
|
|
5960
|
+
* share ONE changelog drain instead of re-scanning it per shape. The
|
|
5961
|
+
* per-shape membership probe still runs per shape (its predicate is
|
|
5962
|
+
* identity/args-specific), so only the shared op read is collapsed.
|
|
5963
|
+
*/
|
|
5964
|
+
private readShapeOpRange;
|
|
5965
|
+
/**
|
|
5966
|
+
* Read one page of the `__cdc_log` for a shape diff (table-scoped). A thin
|
|
5967
|
+
* protected seam over {@link readCdcChanges}: it isolates the single
|
|
5968
|
+
* changelog read that {@link readShapeOpRange} memoizes per flush, and gives
|
|
5969
|
+
* tests a point to count the reads the op-range cache collapses.
|
|
5970
|
+
*/
|
|
5971
|
+
protected readShapeCdcPage(sql: SqlExec, sinceSeq: number, tables: ReadonlySet<string>): {
|
|
5972
|
+
changes: CdcChange[];
|
|
5973
|
+
cursor: number;
|
|
5974
|
+
};
|
|
5975
|
+
/**
|
|
5922
5976
|
* Build the row-ops for a shape over the op range `(sinceSeq, upTo]`. Reads
|
|
5923
|
-
* the changelog (drained across pages
|
|
5977
|
+
* the changelog (drained across pages via {@link readShapeOpRange}, shared
|
|
5978
|
+
* across same-range shapes in a flush), collapses to the latest op per row,
|
|
5924
5979
|
* then runs ONE membership probe ({@link selectShapeMemberIds}) over the
|
|
5925
5980
|
* changed ids: a row still in the set → upsert with its post-image doc
|
|
5926
5981
|
* (projected to the shape's columns); a row that left the set, or any delete,
|
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 } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-
|
|
26
|
+
export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-CM5_hVnA.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';
|
|
@@ -403,6 +403,23 @@ const buildPokeFrames = (parts, meta) => {
|
|
|
403
403
|
return frames;
|
|
404
404
|
};
|
|
405
405
|
|
|
406
|
+
const runSocketPool = async (items, processOne, concurrency = 8) => {
|
|
407
|
+
let cursor = 0;
|
|
408
|
+
const worker = async () => {
|
|
409
|
+
let item = items[cursor];
|
|
410
|
+
cursor += 1;
|
|
411
|
+
while (item !== void 0) {
|
|
412
|
+
try {
|
|
413
|
+
await processOne(item);
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
item = items[cursor];
|
|
417
|
+
cursor += 1;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
421
|
+
};
|
|
422
|
+
|
|
406
423
|
const DANGLING_SCAN_CAP = 5e3;
|
|
407
424
|
const DANGLING_RESULT_CAP = 500;
|
|
408
425
|
const DOC_COLUMN = "__doc__";
|
|
@@ -1374,6 +1391,9 @@ class ShardDO {
|
|
|
1374
1391
|
}
|
|
1375
1392
|
const result = await this.handleRpc(payload.functionPath, payload.args ?? {});
|
|
1376
1393
|
this.recordPostDispatchBookkeeping(result, mutatorClass);
|
|
1394
|
+
if (mutatorClass?.kind === "next") {
|
|
1395
|
+
this.advanceClientMutationWatermark();
|
|
1396
|
+
}
|
|
1377
1397
|
const durationMs = Date.now() - dispatchStartedAt;
|
|
1378
1398
|
this.recordFunctionCall(payload.functionPath, durationMs, void 0, this.currentScannedTables, this.currentIndexHits);
|
|
1379
1399
|
this.flushStmtSamples();
|
|
@@ -3091,16 +3111,16 @@ class ShardDO {
|
|
|
3091
3111
|
return jsonResponse({ error: { code: error.code, message: error.message } }, error.status);
|
|
3092
3112
|
}
|
|
3093
3113
|
if (error && typeof error === "object" && error.name === "ValidationError") {
|
|
3094
|
-
const
|
|
3095
|
-
return jsonResponse({ error: { code: "VALIDATION_ERROR", message
|
|
3114
|
+
const message = error instanceof Error ? error.message : "validation failed";
|
|
3115
|
+
return jsonResponse({ error: { code: "VALIDATION_ERROR", message } }, 400);
|
|
3096
3116
|
}
|
|
3097
3117
|
if (error && typeof error === "object" && error.name === "LunoraError") {
|
|
3098
3118
|
const lunoraError = error;
|
|
3099
3119
|
const status = typeof lunoraError.status === "number" ? lunoraError.status : 500;
|
|
3100
3120
|
return jsonResponse({ error: { code: lunoraError.code ?? "INTERNAL", message: lunoraError.message ?? "internal error" } }, status);
|
|
3101
3121
|
}
|
|
3102
|
-
|
|
3103
|
-
return jsonResponse({ error: { code: "RPC_FAILED", message } }, 500);
|
|
3122
|
+
console.error("[@lunora/do] unhandled RPC error:", error);
|
|
3123
|
+
return jsonResponse({ error: { code: "RPC_FAILED", message: "internal error" } }, 500);
|
|
3104
3124
|
}
|
|
3105
3125
|
/**
|
|
3106
3126
|
* Serve a reserved admin-introspection RPC (`__lunora_admin__:*`) for the
|
|
@@ -3920,6 +3940,54 @@ class ShardDO {
|
|
|
3920
3940
|
}
|
|
3921
3941
|
return this.executeSubscription(functionPath, args, identity);
|
|
3922
3942
|
}
|
|
3943
|
+
/**
|
|
3944
|
+
* SECURITY BOUNDARY for cross-socket reactive dedup. A read is
|
|
3945
|
+
* identity-INDEPENDENT only when its result cannot vary by the caller's
|
|
3946
|
+
* verified identity — i.e. the admin/reserved introspection reads, which
|
|
3947
|
+
* route to {@link executeAdminSubscription} and ignore the
|
|
3948
|
+
* {@link SubscriptionIdentity} entirely.
|
|
3949
|
+
*
|
|
3950
|
+
* Everything else is identity-DEPENDENT and must NEVER be shared across
|
|
3951
|
+
* sockets: a user query may be `rls()` / `ctx.auth`-scoped (different rows
|
|
3952
|
+
* per identity), and a flag read ({@link FLAGS_FUNCTION_PREFIX}) evaluates
|
|
3953
|
+
* the provider with the subscriber's identity (per-user targeting). Sharing
|
|
3954
|
+
* one socket's result with another would leak one identity's rows/flags to a
|
|
3955
|
+
* different identity, so this predicate gates {@link resolveReactiveOutcomeDeduped}
|
|
3956
|
+
* shut for them.
|
|
3957
|
+
*/
|
|
3958
|
+
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/member-ordering -- pure predicate over the function path; a protected method so the security boundary lives in one named place (and tests can probe it), co-located with the reactive dedup it gates rather than hoisted away from its only caller
|
|
3959
|
+
isIdentityIndependent(functionPath) {
|
|
3960
|
+
return functionPath.startsWith(ADMIN_FUNCTION_PREFIX);
|
|
3961
|
+
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Memoizing wrapper over {@link resolveReactiveOutcome}: flush-local sharing across sockets.
|
|
3964
|
+
* Within a single {@link refreshSubscriptions} pass, N sockets subscribed to
|
|
3965
|
+
* the SAME identity-independent `(functionPath, args)` re-run the query N
|
|
3966
|
+
* times today (see the Case-6 fan-out characterization). When the read is
|
|
3967
|
+
* identity-independent (admin/reserved — see {@link isIdentityIndependent})
|
|
3968
|
+
* its result is the same for every socket, so the first run is cached (by its
|
|
3969
|
+
* in-flight Promise, since the bounded worker pool runs sockets in parallel)
|
|
3970
|
+
* and shared with the rest — collapsing N runs to ONE.
|
|
3971
|
+
*
|
|
3972
|
+
* Identity-DEPENDENT reads are passed straight through, UNCACHED: each socket
|
|
3973
|
+
* must evaluate under its own by-value identity (RLS / `ctx.auth` / per-user
|
|
3974
|
+
* flags), so they never share a result. The `cache` is created fresh per
|
|
3975
|
+
* flush by the caller, so a result is never reused across passes (it would go
|
|
3976
|
+
* stale after the next write).
|
|
3977
|
+
*/
|
|
3978
|
+
resolveReactiveOutcomeDeduped(functionPath, args, isAdmin, identity, cache) {
|
|
3979
|
+
if (!this.isIdentityIndependent(functionPath)) {
|
|
3980
|
+
return this.resolveReactiveOutcome(functionPath, args, isAdmin, identity);
|
|
3981
|
+
}
|
|
3982
|
+
const key = reactiveCacheKey(functionPath, args, null);
|
|
3983
|
+
const cached = cache.get(key);
|
|
3984
|
+
if (cached !== void 0) {
|
|
3985
|
+
return cached;
|
|
3986
|
+
}
|
|
3987
|
+
const pending = this.resolveReactiveOutcome(functionPath, args, isAdmin, identity);
|
|
3988
|
+
cache.set(key, pending);
|
|
3989
|
+
return pending;
|
|
3990
|
+
}
|
|
3923
3991
|
/**
|
|
3924
3992
|
* Constant-time bearer check against `env.LUNORA_ADMIN_TOKEN`. Returns
|
|
3925
3993
|
* `false` (closed) when the token is unset so admin introspection is
|
|
@@ -3944,6 +4012,7 @@ class ShardDO {
|
|
|
3944
4012
|
* 4. On normal completion send `{type:"complete"}`; on throw send
|
|
3945
4013
|
* `{type:"error"}`. Either way drop the controller.
|
|
3946
4014
|
*/
|
|
4015
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- the stream lifecycle (ack → chunk pump → complete/error) plus the structured-vs-redacted error branch is the wire protocol and reads clearer inline than split across helpers sharing the controller + socket
|
|
3947
4016
|
async handleStream(ws, id, functionPath, args) {
|
|
3948
4017
|
const iterable = this.executeStream(functionPath, args);
|
|
3949
4018
|
if (!iterable) {
|
|
@@ -3984,10 +4053,15 @@ class ShardDO {
|
|
|
3984
4053
|
}
|
|
3985
4054
|
} catch (error) {
|
|
3986
4055
|
const { code } = error;
|
|
3987
|
-
const
|
|
4056
|
+
const isStructured = typeof code === "string";
|
|
4057
|
+
if (!isStructured) {
|
|
4058
|
+
console.error("[@lunora/do] unhandled stream error:", error);
|
|
4059
|
+
}
|
|
4060
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
4061
|
+
const message = isStructured ? rawMessage : "internal error";
|
|
3988
4062
|
ws.send(
|
|
3989
4063
|
JSON.stringify({
|
|
3990
|
-
error: { code:
|
|
4064
|
+
error: { code: isStructured ? code : "INTERNAL_SERVER_ERROR", message },
|
|
3991
4065
|
id,
|
|
3992
4066
|
type: "error"
|
|
3993
4067
|
})
|
|
@@ -4121,6 +4195,7 @@ class ShardDO {
|
|
|
4121
4195
|
const sockets = [...this.state.getWebSockets()];
|
|
4122
4196
|
const frameCursor = this.currentCdcCursor();
|
|
4123
4197
|
const frameEpoch = this.currentCdcEpoch();
|
|
4198
|
+
const reactiveRunCache = /* @__PURE__ */ new Map();
|
|
4124
4199
|
const refreshOne = async (ws) => {
|
|
4125
4200
|
if (this.isSocketExpired(ws)) {
|
|
4126
4201
|
this.dropExpiredSocket(ws);
|
|
@@ -4138,10 +4213,13 @@ class ShardDO {
|
|
|
4138
4213
|
continue;
|
|
4139
4214
|
}
|
|
4140
4215
|
try {
|
|
4141
|
-
const outcome = await this.
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4216
|
+
const outcome = await this.resolveReactiveOutcomeDeduped(
|
|
4217
|
+
functionPath,
|
|
4218
|
+
query.args ?? {},
|
|
4219
|
+
isAdmin,
|
|
4220
|
+
{ identity: attachment.identity, userId: attachment.userId },
|
|
4221
|
+
reactiveRunCache
|
|
4222
|
+
);
|
|
4145
4223
|
if (!outcome) {
|
|
4146
4224
|
continue;
|
|
4147
4225
|
}
|
|
@@ -4152,18 +4230,7 @@ class ShardDO {
|
|
|
4152
4230
|
}
|
|
4153
4231
|
}
|
|
4154
4232
|
};
|
|
4155
|
-
|
|
4156
|
-
let cursor = 0;
|
|
4157
|
-
const worker = async () => {
|
|
4158
|
-
let socket = sockets[cursor];
|
|
4159
|
-
cursor += 1;
|
|
4160
|
-
while (socket !== void 0) {
|
|
4161
|
-
await refreshOne(socket);
|
|
4162
|
-
socket = sockets[cursor];
|
|
4163
|
-
cursor += 1;
|
|
4164
|
-
}
|
|
4165
|
-
};
|
|
4166
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, sockets.length) }, () => worker()));
|
|
4233
|
+
await runSocketPool(sockets, refreshOne);
|
|
4167
4234
|
}
|
|
4168
4235
|
/**
|
|
4169
4236
|
* Seed a freshly-registered subscription with its first value. Runs the
|
|
@@ -4321,6 +4388,7 @@ class ShardDO {
|
|
|
4321
4388
|
const sockets = [...this.state.getWebSockets()];
|
|
4322
4389
|
const checkpoint = frameCursor ?? this.currentCdcCursor() ?? 0;
|
|
4323
4390
|
const sql = this.sql;
|
|
4391
|
+
const opRangeCache = /* @__PURE__ */ new Map();
|
|
4324
4392
|
const pokeOne = async (ws) => {
|
|
4325
4393
|
if (this.isSocketExpired(ws)) {
|
|
4326
4394
|
this.dropExpiredSocket(ws);
|
|
@@ -4331,32 +4399,24 @@ class ShardDO {
|
|
|
4331
4399
|
if (!shapes) {
|
|
4332
4400
|
return;
|
|
4333
4401
|
}
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4402
|
+
try {
|
|
4403
|
+
const identity = { identity: attachment.identity, userId: attachment.userId };
|
|
4404
|
+
const { emptyAdvanced, partAdvanced, parts } = this.collectShapePokeParts(ws, shapes, identity, changed, checkpoint, sql, opRangeCache);
|
|
4405
|
+
for (const subId of emptyAdvanced) {
|
|
4406
|
+
this.recordShapeMemo(ws, subId, checkpoint);
|
|
4407
|
+
}
|
|
4408
|
+
if (parts.length > 0) {
|
|
4409
|
+
await awaitWsDrain(ws);
|
|
4410
|
+
if (this.sendPoke(ws, parts, checkpoint, frameEpoch, void 0)) {
|
|
4411
|
+
for (const subId of partAdvanced) {
|
|
4412
|
+
this.recordShapeMemo(ws, subId, checkpoint);
|
|
4413
|
+
}
|
|
4344
4414
|
}
|
|
4345
4415
|
}
|
|
4416
|
+
} catch {
|
|
4346
4417
|
}
|
|
4347
4418
|
};
|
|
4348
|
-
|
|
4349
|
-
let index = 0;
|
|
4350
|
-
const worker = async () => {
|
|
4351
|
-
let socket = sockets[index];
|
|
4352
|
-
index += 1;
|
|
4353
|
-
while (socket !== void 0) {
|
|
4354
|
-
await pokeOne(socket);
|
|
4355
|
-
socket = sockets[index];
|
|
4356
|
-
index += 1;
|
|
4357
|
-
}
|
|
4358
|
-
};
|
|
4359
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, sockets.length) }, () => worker()));
|
|
4419
|
+
await runSocketPool(sockets, pokeOne);
|
|
4360
4420
|
}
|
|
4361
4421
|
/**
|
|
4362
4422
|
* Diff every op-log-backed shape a socket holds against this flush, splitting
|
|
@@ -4367,7 +4427,7 @@ class ShardDO {
|
|
|
4367
4427
|
* diffs advance unconditionally; part-bearing shapes advance only once the
|
|
4368
4428
|
* caller confirms the poke was delivered.
|
|
4369
4429
|
*/
|
|
4370
|
-
collectShapePokeParts(ws, shapes, identity, changed, checkpoint, sql) {
|
|
4430
|
+
collectShapePokeParts(ws, shapes, identity, changed, checkpoint, sql, opRangeCache) {
|
|
4371
4431
|
const parts = [];
|
|
4372
4432
|
const emptyAdvanced = [];
|
|
4373
4433
|
const partAdvanced = [];
|
|
@@ -4378,7 +4438,7 @@ class ShardDO {
|
|
|
4378
4438
|
continue;
|
|
4379
4439
|
}
|
|
4380
4440
|
const memoCursor = this.shapeMemos.get(ws)?.get(subId)?.cursor ?? 0;
|
|
4381
|
-
const rowsPatch = this.buildShapeDiff(sql, resolved, memoCursor, checkpoint);
|
|
4441
|
+
const rowsPatch = this.buildShapeDiff(sql, resolved, memoCursor, checkpoint, opRangeCache);
|
|
4382
4442
|
if (rowsPatch.length > 0) {
|
|
4383
4443
|
parts.push({ rowsPatch, shapeId: subId });
|
|
4384
4444
|
partAdvanced.push(subId);
|
|
@@ -4392,21 +4452,26 @@ class ShardDO {
|
|
|
4392
4452
|
return { emptyAdvanced, partAdvanced, parts };
|
|
4393
4453
|
}
|
|
4394
4454
|
/**
|
|
4395
|
-
*
|
|
4396
|
-
*
|
|
4397
|
-
*
|
|
4398
|
-
*
|
|
4399
|
-
* (
|
|
4400
|
-
*
|
|
4401
|
-
*
|
|
4455
|
+
* Drain the op-log range `(sinceSeq, upTo]` for `table` into the latest op per
|
|
4456
|
+
* row id (collapsing multiple ops on the same row to the newest). Within one
|
|
4457
|
+
* flush, every shape over the SAME `(table, sinceSeq, upTo)` reads the
|
|
4458
|
+
* identical changelog slice, so the drained map is memoized in the
|
|
4459
|
+
* caller-supplied `cache` (created fresh per flush) — N shapes on a table
|
|
4460
|
+
* share ONE changelog drain instead of re-scanning it per shape. The
|
|
4461
|
+
* per-shape membership probe still runs per shape (its predicate is
|
|
4462
|
+
* identity/args-specific), so only the shared op read is collapsed.
|
|
4402
4463
|
*/
|
|
4403
|
-
|
|
4404
|
-
|
|
4464
|
+
readShapeOpRange(sql, table, sinceSeq, upTo, cache) {
|
|
4465
|
+
const key = `${table}\0${String(sinceSeq)}\0${String(upTo)}`;
|
|
4466
|
+
const cached = cache?.get(key);
|
|
4467
|
+
if (cached !== void 0) {
|
|
4468
|
+
return cached;
|
|
4469
|
+
}
|
|
4405
4470
|
const latest = /* @__PURE__ */ new Map();
|
|
4406
|
-
const tables = /* @__PURE__ */ new Set([
|
|
4471
|
+
const tables = /* @__PURE__ */ new Set([table]);
|
|
4407
4472
|
let from = sinceSeq;
|
|
4408
4473
|
for (; ; ) {
|
|
4409
|
-
const { changes, cursor } =
|
|
4474
|
+
const { changes, cursor } = this.readShapeCdcPage(sql, from, tables);
|
|
4410
4475
|
for (const change of changes) {
|
|
4411
4476
|
latest.set(change.id, change);
|
|
4412
4477
|
}
|
|
@@ -4415,6 +4480,31 @@ class ShardDO {
|
|
|
4415
4480
|
}
|
|
4416
4481
|
from = cursor;
|
|
4417
4482
|
}
|
|
4483
|
+
cache?.set(key, latest);
|
|
4484
|
+
return latest;
|
|
4485
|
+
}
|
|
4486
|
+
/**
|
|
4487
|
+
* Read one page of the `__cdc_log` for a shape diff (table-scoped). A thin
|
|
4488
|
+
* protected seam over {@link readCdcChanges}: it isolates the single
|
|
4489
|
+
* changelog read that {@link readShapeOpRange} memoizes per flush, and gives
|
|
4490
|
+
* tests a point to count the reads the op-range cache collapses.
|
|
4491
|
+
*/
|
|
4492
|
+
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/member-ordering -- thin pass-through seam over the module-level reader; a protected method so the op-range cache + tests share one read point, co-located with the poke path it serves rather than hoisted away from its only caller
|
|
4493
|
+
readShapeCdcPage(sql, sinceSeq, tables) {
|
|
4494
|
+
return readCdcChanges(sql, { sinceSeq, tables });
|
|
4495
|
+
}
|
|
4496
|
+
/**
|
|
4497
|
+
* Build the row-ops for a shape over the op range `(sinceSeq, upTo]`. Reads
|
|
4498
|
+
* the changelog (drained across pages via {@link readShapeOpRange}, shared
|
|
4499
|
+
* across same-range shapes in a flush), collapses to the latest op per row,
|
|
4500
|
+
* then runs ONE membership probe ({@link selectShapeMemberIds}) over the
|
|
4501
|
+
* changed ids: a row still in the set → upsert with its post-image doc
|
|
4502
|
+
* (projected to the shape's columns); a row that left the set, or any delete,
|
|
4503
|
+
* → `delete(key)` (a delete carries no post-image, so membership is
|
|
4504
|
+
* unknowable from the op alone — the client no-ops an unknown key).
|
|
4505
|
+
*/
|
|
4506
|
+
buildShapeDiff(sql, resolved, sinceSeq, upTo, opRangeCache) {
|
|
4507
|
+
const latest = this.readShapeOpRange(sql, resolved.table, sinceSeq, upTo, opRangeCache);
|
|
4418
4508
|
if (latest.size === 0) {
|
|
4419
4509
|
return [];
|
|
4420
4510
|
}
|
|
@@ -4753,6 +4843,10 @@ class ShardDO {
|
|
|
4753
4843
|
const existing = memos.get(subId);
|
|
4754
4844
|
if (existing?.lastJson === json) {
|
|
4755
4845
|
existing.tables = outcome.tables;
|
|
4846
|
+
const settledWatermark = this.socketClientWatermark(ws);
|
|
4847
|
+
if (settledWatermark !== void 0) {
|
|
4848
|
+
trySendFrame(ws, `{"type":"settled","id":${JSON.stringify(subId)},"lastMutationId":${String(settledWatermark)}${cursorSuffix}}`);
|
|
4849
|
+
}
|
|
4756
4850
|
return;
|
|
4757
4851
|
}
|
|
4758
4852
|
const deltaFrames = [];
|