@rocicorp/zero 1.12.0-canary.12 → 1.12.0-canary.13
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/out/replicache/src/btree/diff.d.ts.map +1 -1
- package/out/replicache/src/btree/diff.js +1 -2
- package/out/replicache/src/btree/diff.js.map +1 -1
- package/out/replicache/src/btree/read.d.ts +31 -1
- package/out/replicache/src/btree/read.d.ts.map +1 -1
- package/out/replicache/src/btree/read.js +74 -19
- package/out/replicache/src/btree/read.js.map +1 -1
- package/out/replicache/src/btree/splice.d.ts.map +1 -1
- package/out/replicache/src/btree/splice.js +2 -1
- package/out/replicache/src/btree/splice.js.map +1 -1
- package/out/replicache/src/btree/write.d.ts.map +1 -1
- package/out/replicache/src/btree/write.js +2 -1
- package/out/replicache/src/btree/write.js.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.d.ts +1 -0
- package/out/replicache/src/persist/collect-idb-databases.d.ts.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.js +13 -5
- package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
- package/out/replicache/src/scan-iterator.js +1 -1
- package/out/replicache/src/sync/request-id.js +2 -2
- package/out/replicache/src/sync/request-id.js.map +1 -1
- package/out/shared/src/bigint-json.d.ts.map +1 -1
- package/out/shared/src/bigint-json.js +1 -0
- package/out/shared/src/bigint-json.js.map +1 -1
- package/out/shared/src/btree-set.d.ts +21 -0
- package/out/shared/src/btree-set.d.ts.map +1 -1
- package/out/shared/src/btree-set.js +54 -0
- package/out/shared/src/btree-set.js.map +1 -1
- package/out/shared/src/headers.js +20 -0
- package/out/shared/src/headers.js.map +1 -0
- package/out/shared/src/natural-runs.d.ts +191 -0
- package/out/shared/src/natural-runs.d.ts.map +1 -0
- package/out/shared/src/natural-runs.js +337 -0
- package/out/shared/src/natural-runs.js.map +1 -0
- package/out/shared/src/random-uint64.d.ts.map +1 -1
- package/out/shared/src/random-uint64.js +2 -2
- package/out/shared/src/random-uint64.js.map +1 -1
- package/out/shared/src/random-values.d.ts +19 -1
- package/out/shared/src/random-values.d.ts.map +1 -1
- package/out/shared/src/random-values.js +38 -4
- package/out/shared/src/random-values.js.map +1 -1
- package/out/shared/src/tuple-map.d.ts +22 -0
- package/out/shared/src/tuple-map.d.ts.map +1 -0
- package/out/shared/src/tuple-map.js +48 -0
- package/out/shared/src/tuple-map.js.map +1 -0
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
- package/out/zero-cache/src/auth/auth.js +5 -0
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +21 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +47 -9
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +2 -2
- package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
- package/out/zero-cache/src/db/pg-to-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/pg-to-lite.js +3 -2
- package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
- package/out/zero-cache/src/db/pg-type-parser.js +55 -2
- package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +5 -2
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/inspector-delegate.js +6 -29
- package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
- package/out/zero-cache/src/server/main.js +5 -0
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/replicator.js +3 -2
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/syncer.js +5 -4
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.d.ts.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.js +2 -1
- package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
- package/out/zero-cache/src/services/admin-auth.d.ts +22 -0
- package/out/zero-cache/src/services/admin-auth.d.ts.map +1 -0
- package/out/zero-cache/src/services/admin-auth.js +42 -0
- package/out/zero-cache/src/services/admin-auth.js.map +1 -0
- package/out/zero-cache/src/services/auth-attempt-counter.d.ts +33 -0
- package/out/zero-cache/src/services/auth-attempt-counter.d.ts.map +1 -0
- package/out/zero-cache/src/services/auth-attempt-counter.js +61 -0
- package/out/zero-cache/src/services/auth-attempt-counter.js.map +1 -0
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +110 -13
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +13 -7
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +38 -5
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +10 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +4 -0
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +10 -3
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +23 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +2 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js +10 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/heapz.d.ts.map +1 -1
- package/out/zero-cache/src/services/heapz.js +4 -5
- package/out/zero-cache/src/services/heapz.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/error.d.ts +5 -0
- package/out/zero-cache/src/services/mutagen/error.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/error.js +7 -1
- package/out/zero-cache/src/services/mutagen/error.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
- package/out/zero-cache/src/services/plannerz.d.ts.map +1 -1
- package/out/zero-cache/src/services/plannerz.js +3 -7
- package/out/zero-cache/src/services/plannerz.js.map +1 -1
- package/out/zero-cache/src/services/profz.d.ts.map +1 -1
- package/out/zero-cache/src/services/profz.js +4 -13
- package/out/zero-cache/src/services/profz.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js +29 -2
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js +3 -3
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/statz.d.ts.map +1 -1
- package/out/zero-cache/src/services/statz.js +2 -7
- package/out/zero-cache/src/services/statz.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +22 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +40 -2
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +54 -17
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +11 -0
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js +107 -0
- package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +11 -6
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +2 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js +16 -14
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js +4 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/init.js +3 -0
- package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +89 -51
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/client-group-log-id.d.ts +9 -0
- package/out/zero-cache/src/types/client-group-log-id.d.ts.map +1 -0
- package/out/zero-cache/src/types/client-group-log-id.js +16 -0
- package/out/zero-cache/src/types/client-group-log-id.js.map +1 -0
- package/out/zero-cache/src/types/lite.d.ts.map +1 -1
- package/out/zero-cache/src/types/lite.js +1 -0
- package/out/zero-cache/src/types/lite.js.map +1 -1
- package/out/zero-cache/src/types/pg.d.ts +13 -2
- package/out/zero-cache/src/types/pg.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg.js +2 -2
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/types/poke-chunk.d.ts +25 -4
- package/out/zero-cache/src/types/poke-chunk.d.ts.map +1 -1
- package/out/zero-cache/src/types/poke-chunk.js +46 -5
- package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connection.js +10 -3
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/rehome.d.ts +10 -0
- package/out/zero-cache/src/workers/rehome.d.ts.map +1 -1
- package/out/zero-cache/src/workers/rehome.js +4 -2
- package/out/zero-cache/src/workers/rehome.js.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js +3 -2
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +15 -15
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.d.ts +4 -0
- package/out/zero-client/src/client/inspector/inspector.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.js +4 -0
- package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.d.ts +10 -2
- package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.js +38 -31
- package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/query.d.ts +2 -2
- package/out/zero-client/src/client/inspector/query.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/query.js +4 -4
- package/out/zero-client/src/client/inspector/query.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.d.ts +3 -0
- package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js +10 -5
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/reconnect-backoff.d.ts +31 -0
- package/out/zero-client/src/client/reconnect-backoff.d.ts.map +1 -0
- package/out/zero-client/src/client/reconnect-backoff.js +44 -0
- package/out/zero-client/src/client/reconnect-backoff.js.map +1 -0
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +7 -0
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-client/src/util/nanoid.js +2 -2
- package/out/zero-client/src/util/nanoid.js.map +1 -1
- package/out/zero-protocol/src/up.d.ts +42 -42
- package/out/zero-server/src/process-mutations.d.ts +10 -0
- package/out/zero-server/src/process-mutations.d.ts.map +1 -1
- package/out/zero-server/src/process-mutations.js +39 -6
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +89 -6
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/planner/planner-connection.d.ts.map +1 -1
- package/out/zql/src/planner/planner-connection.js +37 -3
- package/out/zql/src/planner/planner-connection.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assert } from "../../../../shared/src/asserts.js";
|
|
2
2
|
import { deepEqual } from "../../../../shared/src/json.js";
|
|
3
3
|
import { sleep } from "../../../../shared/src/sleep.js";
|
|
4
|
-
import { parse } from "../../../../shared/src/valita.js";
|
|
4
|
+
import { parse, test, valita_exports } from "../../../../shared/src/valita.js";
|
|
5
5
|
import { some } from "../../../../shared/src/iterables.js";
|
|
6
6
|
import { astSchema } from "../../../../zero-protocol/src/ast.js";
|
|
7
7
|
import { ClientNotFound, Rehome, SchemaVersionNotSupported } from "../../../../zero-protocol/src/error-kind-enum.js";
|
|
@@ -27,6 +27,7 @@ import { CustomKeySet } from "../../../../shared/src/custom-key-set.js";
|
|
|
27
27
|
import { RowRecordCache } from "./row-record-cache.js";
|
|
28
28
|
import { trace } from "@opentelemetry/api";
|
|
29
29
|
//#region ../zero-cache/src/services/view-syncer/cvr-store.ts
|
|
30
|
+
var pinnedUserSchema = valita_exports.object({ id: valita_exports.string().nullable() });
|
|
30
31
|
var flushCounter = 0;
|
|
31
32
|
var RESULT_ATTRIBUTE = "result";
|
|
32
33
|
var ERROR_KIND_ATTRIBUTE = "error.kind";
|
|
@@ -70,7 +71,9 @@ function asQuery(row) {
|
|
|
70
71
|
transformationVersion: maybeVersion(row.transformationVersion)
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
|
-
const
|
|
74
|
+
const parsed = test(row.clientAST, astSchema);
|
|
75
|
+
if (!parsed.ok) throw new InvalidClientASTError(row.queryHash, parsed.error);
|
|
76
|
+
const ast = parsed.value;
|
|
74
77
|
return row.internal ? {
|
|
75
78
|
type: "internal",
|
|
76
79
|
id: row.queryHash,
|
|
@@ -192,7 +195,8 @@ var CVRStore = class {
|
|
|
192
195
|
clients: {},
|
|
193
196
|
queries: {},
|
|
194
197
|
clientSchema: null,
|
|
195
|
-
profileID: null
|
|
198
|
+
profileID: null,
|
|
199
|
+
pinnedUser: null
|
|
196
200
|
};
|
|
197
201
|
const [instance, clientsRows, queryRows, desiresRows] = await runTx(this.#db, (tx) => {
|
|
198
202
|
lc.debug?.(`CVR tx started after ${Date.now() - start} ms`);
|
|
@@ -205,6 +209,7 @@ var CVRStore = class {
|
|
|
205
209
|
"grantedAt",
|
|
206
210
|
"clientSchema",
|
|
207
211
|
"profileID",
|
|
212
|
+
"pinnedUser",
|
|
208
213
|
"deleted",
|
|
209
214
|
rows."version" as "rowsVersion"
|
|
210
215
|
FROM ${this.#cvr("instances")} AS cvr
|
|
@@ -246,11 +251,12 @@ var CVRStore = class {
|
|
|
246
251
|
ttlClock: ttlClockFromNumber(0),
|
|
247
252
|
replicaVersion: null,
|
|
248
253
|
clientSchema: null,
|
|
249
|
-
profileID: null
|
|
254
|
+
profileID: null,
|
|
255
|
+
pinnedUser: null
|
|
250
256
|
});
|
|
251
257
|
else {
|
|
252
258
|
assert(instance.length === 1, () => `Expected exactly one CVR instance, got ${instance.length}`);
|
|
253
|
-
const { version, lastActive, ttlClock, replicaVersion, owner, grantedAt, rowsVersion, clientSchema, profileID, deleted } = instance[0];
|
|
259
|
+
const { version, lastActive, ttlClock, replicaVersion, owner, grantedAt, rowsVersion, clientSchema, profileID, pinnedUser, deleted } = instance[0];
|
|
254
260
|
if (deleted) throw new ClientNotFoundError("Client has been purged due to inactivity");
|
|
255
261
|
if (owner !== this.#taskID) {
|
|
256
262
|
if ((grantedAt ?? 0) > lastConnectTime) throw new OwnershipError(owner, grantedAt, lastConnectTime);
|
|
@@ -269,6 +275,7 @@ var CVRStore = class {
|
|
|
269
275
|
cvr.ttlClock = ttlClock;
|
|
270
276
|
cvr.replicaVersion = replicaVersion;
|
|
271
277
|
cvr.profileID = profileID;
|
|
278
|
+
cvr.pinnedUser = pinnedUser === null ? null : parse(pinnedUser, pinnedUserSchema);
|
|
272
279
|
try {
|
|
273
280
|
cvr.clientSchema = clientSchema === null ? null : parse(clientSchema, clientSchemaSchema, "strip");
|
|
274
281
|
} catch (e) {
|
|
@@ -351,7 +358,7 @@ var CVRStore = class {
|
|
|
351
358
|
assert(result.length === 1, () => `Expected exactly one rowsVersion result, got ${result.length}`);
|
|
352
359
|
return result[0][0];
|
|
353
360
|
}
|
|
354
|
-
putInstance({ version, replicaVersion, lastActive, clientSchema, profileID, ttlClock }) {
|
|
361
|
+
putInstance({ version, replicaVersion, lastActive, clientSchema, profileID, pinnedUser, ttlClock }) {
|
|
355
362
|
this.#pendingInstanceWrite = (tx, lastConnectTime) => {
|
|
356
363
|
const change = {
|
|
357
364
|
clientGroupID: this.#id,
|
|
@@ -362,7 +369,8 @@ var CVRStore = class {
|
|
|
362
369
|
owner: this.#taskID,
|
|
363
370
|
grantedAt: lastConnectTime,
|
|
364
371
|
clientSchema,
|
|
365
|
-
profileID
|
|
372
|
+
profileID,
|
|
373
|
+
pinnedUser
|
|
366
374
|
};
|
|
367
375
|
return tx`
|
|
368
376
|
INSERT INTO ${this.#cvr("instances")} ${tx(change)}
|
|
@@ -707,7 +715,13 @@ var CVRStore = class {
|
|
|
707
715
|
"inactivatedAtMs" = excluded."inactivatedAtMs"
|
|
708
716
|
`;
|
|
709
717
|
}
|
|
710
|
-
|
|
718
|
+
/**
|
|
719
|
+
* Also checks that the CVR has not been pinned to a user other than
|
|
720
|
+
* `pinnedUser` since it was loaded: the first pin does not change the CVR
|
|
721
|
+
* version, so the version check alone would let two view-syncers that both
|
|
722
|
+
* loaded an unpinned CVR pin it to different users in turn.
|
|
723
|
+
*/
|
|
724
|
+
async #checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime, pinnedUser) {
|
|
711
725
|
const start = Date.now();
|
|
712
726
|
lc.debug?.("checking cvr version and ownership");
|
|
713
727
|
const expected = versionString(expectedCurrentVersion);
|
|
@@ -724,17 +738,19 @@ var CVRStore = class {
|
|
|
724
738
|
)
|
|
725
739
|
ON CONFLICT ("clientGroupID") DO NOTHING
|
|
726
740
|
`;
|
|
727
|
-
const result = await tx`SELECT "version", "owner", "grantedAt"
|
|
741
|
+
const result = await tx`SELECT "version", "owner", "grantedAt", "pinnedUser"
|
|
742
|
+
FROM ${this.#cvr("instances")}
|
|
728
743
|
WHERE "clientGroupID" = ${this.#id}
|
|
729
744
|
FOR UPDATE`;
|
|
730
|
-
const { version, owner, grantedAt } = result.length > 0 ? result[0] : {
|
|
745
|
+
const { version, owner, grantedAt, pinnedUser: storedPinnedUser = null } = result.length > 0 ? result[0] : {
|
|
731
746
|
version: EMPTY_CVR_VERSION.stateVersion,
|
|
732
747
|
owner: null,
|
|
733
748
|
grantedAt: null
|
|
734
749
|
};
|
|
735
750
|
lc.debug?.("checked cvr version and ownership in " + (Date.now() - start) + " ms");
|
|
736
751
|
if (owner !== this.#taskID && (grantedAt ?? 0) > lastConnectTime) throw new OwnershipError(owner, grantedAt, lastConnectTime);
|
|
737
|
-
if (
|
|
752
|
+
if (storedPinnedUser !== null && storedPinnedUser.id !== pinnedUser?.id) throw new PinnedUserMismatchError();
|
|
753
|
+
if (version !== expected) throw new ConcurrentModificationException(`Expected ${expected}, got ${version}`);
|
|
738
754
|
}
|
|
739
755
|
async #flush(lc, expectedCurrentVersion, cvr, lastConnectTime, verifyNoop) {
|
|
740
756
|
const stats = {
|
|
@@ -756,7 +772,7 @@ var CVRStore = class {
|
|
|
756
772
|
}
|
|
757
773
|
}
|
|
758
774
|
if (this.#pendingRowRecordUpdates.size === 0 && this.#writes.size === 0 && this.#pendingInstanceWrite === void 0 && this.#pendingQueryUpdates.size === 0 && this.#pendingQueryPartialUpdates.size === 0 && this.#pendingDesireUpdates.size === 0) {
|
|
759
|
-
if (verifyNoop) await runTx(this.#db, (tx) => this.#checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime), { mode: READ_COMMITTED });
|
|
775
|
+
if (verifyNoop) await runTx(this.#db, (tx) => this.#checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime, cvr.pinnedUser), { mode: READ_COMMITTED });
|
|
760
776
|
return null;
|
|
761
777
|
}
|
|
762
778
|
this.putInstance(cvr);
|
|
@@ -764,7 +780,7 @@ var CVRStore = class {
|
|
|
764
780
|
lc.debug?.("flush tx beginning");
|
|
765
781
|
const results = await runTx(this.#db, async (tx) => {
|
|
766
782
|
lc.debug?.(`flush tx begun after ${Date.now() - start} ms`);
|
|
767
|
-
await this.#checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime);
|
|
783
|
+
await this.#checkVersionAndOwnership(lc, tx, expectedCurrentVersion, lastConnectTime, cvr.pinnedUser);
|
|
768
784
|
const writeQueries = [];
|
|
769
785
|
if (this.#pendingInstanceWrite) {
|
|
770
786
|
writeQueries.push(this.#pendingInstanceWrite(tx, lastConnectTime));
|
|
@@ -925,7 +941,7 @@ async function checkVersion(tx, schema, clientGroupID, expectedCurrentVersion) {
|
|
|
925
941
|
SELECT version FROM ${tx(schema)}.instances
|
|
926
942
|
WHERE "clientGroupID" = ${clientGroupID}`;
|
|
927
943
|
const { version } = result.length > 0 ? result[0] : { version: EMPTY_CVR_VERSION.stateVersion };
|
|
928
|
-
if (version !== expected) throw new ConcurrentModificationException(expected, version);
|
|
944
|
+
if (version !== expected) throw new ConcurrentModificationException(`Expected ${expected}, got ${version}`);
|
|
929
945
|
}
|
|
930
946
|
var ClientNotFoundError = class extends ProtocolErrorWithLevel {
|
|
931
947
|
constructor(message) {
|
|
@@ -938,14 +954,24 @@ var ClientNotFoundError = class extends ProtocolErrorWithLevel {
|
|
|
938
954
|
};
|
|
939
955
|
var ConcurrentModificationException = class extends ProtocolErrorWithLevel {
|
|
940
956
|
name = "ConcurrentModificationException";
|
|
941
|
-
constructor(
|
|
957
|
+
constructor(detail) {
|
|
942
958
|
super({
|
|
943
959
|
kind: Rehome,
|
|
944
|
-
message: `CVR has been concurrently modified.
|
|
960
|
+
message: `CVR has been concurrently modified. ${detail}`,
|
|
945
961
|
origin: ZeroCache
|
|
946
962
|
}, "warn");
|
|
947
963
|
}
|
|
948
964
|
};
|
|
965
|
+
/**
|
|
966
|
+
* The CVR was pinned to a different user than the one the flushing
|
|
967
|
+
* view-syncer validated, so that view-syncer's connections can no longer be
|
|
968
|
+
* trusted: it should stop and let its clients reconnect.
|
|
969
|
+
*/
|
|
970
|
+
var PinnedUserMismatchError = class extends ConcurrentModificationException {
|
|
971
|
+
constructor() {
|
|
972
|
+
super("Client group was pinned to a different user");
|
|
973
|
+
}
|
|
974
|
+
};
|
|
949
975
|
var OwnershipError = class extends ProtocolErrorWithLevel {
|
|
950
976
|
name = "OwnershipError";
|
|
951
977
|
constructor(owner, grantedAt, lastConnectTime) {
|
|
@@ -957,6 +983,16 @@ var OwnershipError = class extends ProtocolErrorWithLevel {
|
|
|
957
983
|
}, "info");
|
|
958
984
|
}
|
|
959
985
|
};
|
|
986
|
+
var InvalidClientASTError = class extends ProtocolErrorWithLevel {
|
|
987
|
+
name = "InvalidClientASTError";
|
|
988
|
+
constructor(queryHash, error) {
|
|
989
|
+
super({
|
|
990
|
+
kind: SchemaVersionNotSupported,
|
|
991
|
+
message: `Could not parse AST of query ${queryHash} stored in CVR: ${error}`,
|
|
992
|
+
origin: ZeroCache
|
|
993
|
+
}, "warn");
|
|
994
|
+
}
|
|
995
|
+
};
|
|
960
996
|
var InvalidClientSchemaError = class extends ProtocolErrorWithLevel {
|
|
961
997
|
name = "InvalidClientSchemaError";
|
|
962
998
|
constructor(cause) {
|
|
@@ -972,6 +1008,7 @@ function cvrErrorKind(e) {
|
|
|
972
1008
|
if (e instanceof ConcurrentModificationException) return "concurrent_modification";
|
|
973
1009
|
if (e instanceof OwnershipError) return "ownership";
|
|
974
1010
|
if (e instanceof InvalidClientSchemaError) return "invalid_client_schema";
|
|
1011
|
+
if (e instanceof InvalidClientASTError) return "invalid_client_ast";
|
|
975
1012
|
return "error";
|
|
976
1013
|
}
|
|
977
1014
|
var RowsVersionBehindError = class extends Error {
|
|
@@ -985,6 +1022,6 @@ var RowsVersionBehindError = class extends Error {
|
|
|
985
1022
|
}
|
|
986
1023
|
};
|
|
987
1024
|
//#endregion
|
|
988
|
-
export { CVRStore, ClientNotFoundError, ConcurrentModificationException, OwnershipError, RowsVersionBehindError, checkVersion };
|
|
1025
|
+
export { CVRStore, ClientNotFoundError, ConcurrentModificationException, OwnershipError, PinnedUserMismatchError, RowsVersionBehindError, checkVersion };
|
|
989
1026
|
|
|
990
1027
|
//# sourceMappingURL=cvr-store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cvr-store.js","names":["Mode.READONLY","v.parse","Mode.READ_COMMITTED","ErrorKind.ClientNotFound","ErrorOrigin.ZeroCache","ErrorKind.Rehome","ErrorKind.SchemaVersionNotSupported"],"sources":["../../../../../../zero-cache/src/services/view-syncer/cvr-store.ts"],"sourcesContent":["import {trace} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport type {MaybeRow, PendingQuery, Row} from 'postgres';\nimport {startAsyncSpan} from '../../../../otel/src/span.ts';\nimport {version} from '../../../../otel/src/version.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {CustomKeyMap} from '../../../../shared/src/custom-key-map.ts';\nimport {CustomKeySet} from '../../../../shared/src/custom-key-set.ts';\nimport {some} from '../../../../shared/src/iterables.ts';\nimport {\n deepEqual,\n type ReadonlyJSONValue,\n} from '../../../../shared/src/json.ts';\nimport {sleep} from '../../../../shared/src/sleep.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {astSchema} from '../../../../zero-protocol/src/ast.ts';\nimport {clientSchemaSchema} from '../../../../zero-protocol/src/client-schema.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport type {InspectQueryRow} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {clampTTL, DEFAULT_TTL_MS} from '../../../../zql/src/query/ttl.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {recordRowsSynced} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../types/pg.ts';\nimport {rowIDString} from '../../types/row-key.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport type {Patch, PatchToVersion} from './client-handler.ts';\nimport type {CVR, CVRSnapshot} from './cvr.ts';\nimport {RowRecordCache} from './row-record-cache.ts';\nimport {formatSignature, parseSignature} from './row-set-signature.ts';\nimport {\n type ClientsRow,\n type DesiresRow,\n type InstancesRow,\n type QueriesRow,\n type RowsRow,\n} from './schema/cvr.ts';\nimport {\n type ClientQueryRecord,\n type ClientRecord,\n cmpVersions,\n type CustomQueryRecord,\n type CVRVersion,\n EMPTY_CVR_VERSION,\n type InternalQueryRecord,\n type NullableCVRVersion,\n type QueryPatch,\n type QueryRecord,\n queryRecordToQueryRow,\n type RowID,\n type RowRecord,\n versionFromString,\n versionString,\n} from './schema/types.ts';\nimport {\n type TTLClock,\n ttlClockAsNumber,\n ttlClockFromNumber,\n} from './ttl-clock.ts';\n\nexport type CVRFlushStats = {\n instances: number;\n queries: number;\n desires: number;\n clients: number;\n rows: number;\n rowsDeferred: number;\n statements: number;\n};\n\nlet flushCounter = 0;\n\nconst RESULT_ATTRIBUTE = 'result';\nconst ERROR_KIND_ATTRIBUTE = 'error.kind';\nconst CVR_FLUSH_TYPE_ATTRIBUTE = 'flush.type';\n\n/**\n * Convert TTL/timestamp values for both old (seconds-based) and new (ms-based) columns.\n * Old columns: inactivatedAt (TIMESTAMPTZ), ttl (INTERVAL) - need conversion ms->seconds\n * New columns: inactivatedAtMs (DOUBLE PRECISION), ttlMs (DOUBLE PRECISION) - store ms directly\n */\nfunction convertTTLValues(\n inactivatedAt: TTLClock | undefined,\n ttl: number,\n): {\n ttlInterval: number | null;\n ttlMs: number | null;\n inactivatedAtTimestamp: TTLClock | null;\n inactivatedAtMs: TTLClock | null;\n} {\n return {\n ttlInterval: ttl < 0 ? null : ttl / 1000, // INTERVAL needs seconds\n ttlMs: ttl < 0 ? null : ttl, // New column stores ms directly\n inactivatedAtTimestamp:\n inactivatedAt === undefined\n ? null\n : ttlClockFromNumber(ttlClockAsNumber(inactivatedAt) / 1000),\n inactivatedAtMs: inactivatedAt ?? null,\n };\n}\n\nconst tracer = trace.getTracer('cvr-store', version);\n\n/**\n * QueriesRow with queryArgs as a stringified JSON value.\n * Used for batched config writes where queryArgs are pre-stringified\n * to handle the postgres.js boolean array bug.\n */\ntype StringifiedQueriesRow = Omit<QueriesRow, 'queryArgs'> & {\n queryArgs: string | null;\n};\n\n/**\n * Applies a pending row-set signature change (`null`: the query was deleted)\n * to the persisted signatures, passed as `this`. A module-level function with\n * `thisArg` rather than a closure, so that `forEach` allocates nothing and\n * looks each entry up once.\n */\nfunction applyPendingRowSetSignature(\n this: Map<string, bigint>,\n sig: bigint | null,\n queryHash: string,\n): void {\n if (sig === null) {\n this.delete(queryHash);\n } else {\n this.set(queryHash, sig);\n }\n}\n\nfunction asQuery(row: QueriesRow): QueryRecord {\n const maybeVersion = (s: string | null) =>\n s === null ? undefined : versionFromString(s);\n\n if (row.clientAST === null) {\n // custom query\n assert(\n row.queryName !== null && row.queryArgs !== null,\n 'queryName and queryArgs must be set for custom queries',\n );\n return {\n type: 'custom',\n id: row.queryHash,\n name: row.queryName,\n args: row.queryArgs,\n patchVersion: maybeVersion(row.patchVersion),\n clientState: {},\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies CustomQueryRecord;\n }\n\n const ast = astSchema.parse(row.clientAST);\n return row.internal\n ? ({\n type: 'internal',\n id: row.queryHash,\n ast,\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies InternalQueryRecord)\n : ({\n type: 'client',\n id: row.queryHash,\n ast,\n patchVersion: maybeVersion(row.patchVersion),\n clientState: {},\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies ClientQueryRecord);\n}\n\n// The time to wait between load attempts.\nconst LOAD_ATTEMPT_INTERVAL_MS = 500;\n// The maximum number of load() attempts if the rowsVersion is behind.\n// This currently results in a maximum catchup time of ~5 seconds, after\n// which we give up and consider the CVR invalid.\n//\n// TODO: Make this configurable with something like --max-catchup-wait-ms,\n// as it is technically application specific.\nconst MAX_LOAD_ATTEMPTS = 10;\n\nexport class CVRStore {\n readonly #schema: string;\n readonly #taskID: string;\n readonly #id: string;\n readonly #failService: (e: unknown) => void;\n readonly #db: PostgresDB;\n readonly #writes: Set<{\n stats: Partial<CVRFlushStats>;\n write: (\n tx: PostgresTransaction,\n lastConnectTime: number,\n ) => PendingQuery<MaybeRow[]>;\n }> = new Set();\n // Stored separately so repeated putInstance() calls (e.g. setClientSchema,\n // setProfileID, and the final call in #flush) replace each other rather than\n // accumulating as independent statements in #writes.\n #pendingInstanceWrite:\n | ((\n tx: PostgresTransaction,\n lastConnectTime: number,\n ) => PendingQuery<MaybeRow[]>)\n | undefined = undefined;\n readonly #pendingRowRecordUpdates = new CustomKeyMap<RowID, RowRecord | null>(\n rowIDString,\n );\n readonly #forceUpdates = new CustomKeySet<RowID>(rowIDString);\n readonly #rowCache: RowRecordCache;\n readonly #cvrLoads = getOrCreateCounter(\n 'sync',\n 'cvr.load_attempts',\n 'CVR load attempts, labeled by result.',\n );\n readonly #cvrLoadTime = getOrCreateLatencyHistogram(\n 'sync',\n 'cvr.load_duration',\n 'Time to load a CVR.',\n );\n readonly #cvrFlushes = getOrCreateCounter(\n 'sync',\n 'cvr.flush_attempts',\n 'CVR flush attempts, labeled by result and flush.type.',\n );\n readonly #loadAttemptIntervalMs: number;\n readonly #maxLoadAttempts: number;\n #rowCount: number = 0;\n readonly #pendingQueryUpdates = new Map<string, StringifiedQueriesRow>();\n readonly #pendingDesireUpdates = new Map<string, DesiresRow>();\n readonly #pendingQueryPartialUpdates = new Map<string, Partial<QueriesRow>>();\n\n /**\n * The persisted `rowSetSignature` of each query of the loaded CVR that has\n * one: an XOR of the hashes of the rows attached to the query (see\n * row-set-signature.ts), maintained by the `PipelineDriver` and persisted\n * by `CVRQueryDrivenUpdater.flush`. Used to detect drift when re-hydrating\n * queries containing the `Cap` operator, which may pick a different N-row\n * subset on re-execution. A query without one (e.g. from before signatures\n * were introduced) is skipped by drift detection.\n *\n * This is kept here, rather than in the `QueryRecord`s of the CVR, because\n * it changes on most advancements, and changing a `QueryRecord` requires\n * the CVR updater to copy the CVR's whole `queries` map.\n *\n * Only updated once changes are flushed, so that, like the loaded/flushed\n * CVR, it holds the persisted signatures of the CVR's (non-deleted) queries.\n */\n readonly #rowSetSignatures = new Map<string, bigint>();\n /** Signature changes to apply to {@link #rowSetSignatures} on flush. */\n readonly #pendingRowSetSignatures = new Map<string, bigint | null>();\n\n constructor(\n lc: LogContext,\n cvrDb: PostgresDB,\n shard: ShardID,\n taskID: string,\n cvrID: string,\n failService: (e: unknown) => void,\n loadAttemptIntervalMs = LOAD_ATTEMPT_INTERVAL_MS,\n maxLoadAttempts = MAX_LOAD_ATTEMPTS,\n deferredRowFlushThreshold = 100, // somewhat arbitrary\n setTimeoutFn = setTimeout,\n ) {\n this.#failService = failService;\n this.#db = cvrDb;\n this.#schema = cvrSchema(shard);\n this.#taskID = taskID;\n this.#id = cvrID;\n this.#rowCache = new RowRecordCache(\n lc,\n cvrDb,\n shard,\n cvrID,\n failService,\n deferredRowFlushThreshold,\n setTimeoutFn,\n );\n this.#loadAttemptIntervalMs = loadAttemptIntervalMs;\n this.#maxLoadAttempts = maxLoadAttempts;\n }\n\n #cvr(table: string) {\n return this.#db(`${this.#schema}.${table}`);\n }\n\n #updateQueryFields(queryHash: string, fields: Partial<QueriesRow>): void {\n // Track as partial-only update for batched flush. Merge into any\n // pre-existing partial update for the same hash so independent callers\n // (e.g. updateQuery + updateRowSetSignature) don't clobber each other.\n const existing = this.#pendingQueryPartialUpdates.get(queryHash);\n this.#pendingQueryPartialUpdates.set(\n queryHash,\n existing ? {...existing, ...fields} : fields,\n );\n }\n\n load(lc: LogContext, lastConnectTime: number): Promise<CVR> {\n const start = performance.now();\n // Another view-syncer may have written the CVR since the row records\n // were cached, so reload them along with the CVR.\n this.#rowCache.clear();\n return startAsyncSpan(tracer, 'cvr.load', async () => {\n try {\n let err: RowsVersionBehindError | undefined;\n for (let i = 0; i < this.#maxLoadAttempts; i++) {\n if (i > 0) {\n await sleep(this.#loadAttemptIntervalMs);\n }\n const result = await this.#load(lc, lastConnectTime);\n if (result instanceof RowsVersionBehindError) {\n lc.info?.(`attempt ${i + 1}: ${String(result)}`);\n err = result;\n continue;\n }\n this.#recordLoad(performance.now() - start, {\n [RESULT_ATTRIBUTE]: 'success',\n });\n return result;\n }\n assert(err, 'Expected error to be set after retry loop exhausted');\n throw new ClientNotFoundError(\n `max attempts exceeded waiting for CVR@${err.cvrVersion} to catch up from ${err.rowsVersion}`,\n );\n } catch (e) {\n this.#recordLoad(performance.now() - start, {\n [RESULT_ATTRIBUTE]: 'error',\n [ERROR_KIND_ATTRIBUTE]: cvrErrorKind(e),\n });\n throw e;\n }\n });\n }\n\n #recordLoad(elapsedMs: number, attributes: Record<string, string>) {\n this.#cvrLoads.add(1, attributes);\n this.#cvrLoadTime.recordMs(elapsedMs, attributes);\n }\n\n async #load(\n lc: LogContext,\n lastConnectTime: number,\n ): Promise<CVR | RowsVersionBehindError> {\n const start = Date.now();\n\n const id = this.#id;\n const cvr: CVR = {\n id,\n version: EMPTY_CVR_VERSION,\n lastActive: 0,\n ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0, not Date.now()\n replicaVersion: null,\n clients: {},\n queries: {},\n clientSchema: null,\n profileID: null,\n };\n\n const [instance, clientsRows, queryRows, desiresRows] = await runTx(\n this.#db,\n tx => {\n lc.debug?.(`CVR tx started after ${Date.now() - start} ms`);\n return [\n tx<\n (Omit<InstancesRow, 'clientGroupID'> & {\n profileID: string | null;\n deleted: boolean;\n rowsVersion: string | null;\n })[]\n >`SELECT cvr.\"version\",\n \"lastActive\",\n \"ttlClock\",\n \"replicaVersion\",\n \"owner\",\n \"grantedAt\",\n \"clientSchema\",\n \"profileID\",\n \"deleted\",\n rows.\"version\" as \"rowsVersion\"\n FROM ${this.#cvr('instances')} AS cvr\n LEFT JOIN ${this.#cvr('rowsVersion')} AS rows\n ON cvr.\"clientGroupID\" = rows.\"clientGroupID\"\n WHERE cvr.\"clientGroupID\" = ${id}`,\n tx<Pick<ClientsRow, 'clientID'>[]>`SELECT \"clientID\" FROM ${this.#cvr(\n 'clients',\n )}\n WHERE \"clientGroupID\" = ${id}`,\n tx<QueriesRow[]>`SELECT\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n \"queryArgs\",\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\",\n \"rowSetSignature\"\n FROM ${this.#cvr('queries')}\n WHERE \"clientGroupID\" = ${id} AND deleted IS DISTINCT FROM true`,\n tx<DesiresRow[]>`SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttlMs\" AS \"ttl\",\n \"inactivatedAtMs\" AS \"inactivatedAt\"\n FROM ${this.#cvr('desires')}\n WHERE \"clientGroupID\" = ${id}`,\n ];\n },\n {mode: Mode.READONLY},\n );\n lc.debug?.(\n `CVR tx completed after ${Date.now() - start} ms ` +\n `(${clientsRows.length} clients, ${queryRows.length} queries, ${desiresRows.length} desires)`,\n );\n\n if (instance.length === 0) {\n // This is the first time we see this CVR.\n this.putInstance({\n version: cvr.version,\n lastActive: 0,\n ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0 for new instances\n replicaVersion: null,\n clientSchema: null,\n profileID: null,\n });\n } else {\n assert(\n instance.length === 1,\n () => `Expected exactly one CVR instance, got ${instance.length}`,\n );\n const {\n version,\n lastActive,\n ttlClock,\n replicaVersion,\n owner,\n grantedAt,\n rowsVersion,\n clientSchema,\n profileID,\n deleted,\n } = instance[0];\n\n if (deleted) {\n throw new ClientNotFoundError(\n 'Client has been purged due to inactivity',\n );\n }\n\n if (owner !== this.#taskID) {\n if ((grantedAt ?? 0) > lastConnectTime) {\n throw new OwnershipError(owner, grantedAt, lastConnectTime);\n } else {\n // Fire-and-forget an ownership change to signal the current owner.\n // Note that the query is structured such that it only succeeds in the\n // correct conditions (i.e. gated on `grantedAt`).\n void this.#db`\n UPDATE ${this.#cvr('instances')} \n SET \"owner\" = ${this.#taskID}, \n \"grantedAt\" = ${lastConnectTime}\n WHERE \"clientGroupID\" = ${this.#id} AND\n (\"grantedAt\" IS NULL OR\n \"grantedAt\" <= to_timestamp(${lastConnectTime / 1000}))\n `\n .execute()\n .catch(this.#failService);\n }\n }\n\n if (version !== (rowsVersion ?? EMPTY_CVR_VERSION.stateVersion)) {\n // This will cause the load() method to wait for row catchup and retry.\n // Assuming the ownership signal succeeds, the current owner will stop\n // modifying the CVR and flush its pending row changes.\n return new RowsVersionBehindError(version, rowsVersion);\n }\n\n cvr.version = versionFromString(version);\n cvr.lastActive = lastActive;\n cvr.ttlClock = ttlClock;\n cvr.replicaVersion = replicaVersion;\n cvr.profileID = profileID;\n\n try {\n // Strip: a newer zero-cache that was rolled back may have stored\n // fields this one doesn't know.\n cvr.clientSchema =\n clientSchema === null\n ? null\n : v.parse(clientSchema, clientSchemaSchema, 'strip');\n } catch (e) {\n throw new InvalidClientSchemaError(e);\n }\n }\n\n for (const row of clientsRows) {\n cvr.clients[row.clientID] = {\n id: row.clientID,\n desiredQueryIDs: [],\n };\n }\n\n this.#rowSetSignatures.clear();\n for (const row of queryRows) {\n const query = asQuery(row);\n cvr.queries[row.queryHash] = query;\n if (row.rowSetSignature) {\n this.#rowSetSignatures.set(\n row.queryHash,\n parseSignature(row.rowSetSignature),\n );\n }\n }\n\n for (const row of desiresRows) {\n const client = cvr.clients[row.clientID];\n // Note: row.inactivatedAt is mapped from inactivatedAtMs in the SQL query\n if (!row.deleted && row.inactivatedAt === null) {\n if (client) {\n client.desiredQueryIDs.push(row.queryHash);\n } else {\n // This can happen if the client was deleted but the queries are still alive.\n lc.debug?.(\n `Not adding to desiredQueryIDs for client ${row.clientID} because it has been deleted.`,\n );\n }\n }\n\n const query = cvr.queries[row.queryHash];\n if (\n query &&\n query.type !== 'internal' &&\n (!row.deleted || row.inactivatedAt !== null)\n ) {\n query.clientState[row.clientID] = {\n inactivatedAt: row.inactivatedAt ?? undefined,\n ttl: clampTTL(row.ttl ?? DEFAULT_TTL_MS),\n version: versionFromString(row.patchVersion),\n };\n }\n }\n\n lc.info?.(\n `loaded cvr@${versionString(cvr.version)} (${Date.now() - start} ms)`,\n );\n\n // why do we not sort `desiredQueryIDs` here?\n\n return cvr;\n }\n\n getRowRecords(): Promise<ReadonlyMap<RowID, RowRecord>> {\n return this.#rowCache.getRowRecords();\n }\n\n putRowRecord(row: RowRecord): void {\n this.#pendingRowRecordUpdates.set(row.id, row);\n }\n\n /**\n * Note: Removing a row from the CVR should be represented by a\n * {@link putRowRecord()} with `refCounts: null` in order to properly\n * produce the appropriate delete patch when catching up old clients.\n *\n * This `delRowRecord()` method, on the other hand, is only used for\n * \"canceling\" the put of a row that was not in the CVR in the first place.\n */\n delRowRecord(id: RowID): void {\n this.#pendingRowRecordUpdates.set(id, null);\n }\n\n /**\n * Overrides the default logic that removes no-op writes and forces\n * the updates for the given row `ids`. This has no effect if there\n * are no corresponding puts or dels for the associated row records.\n */\n forceUpdates(...ids: RowID[]) {\n for (const id of ids) {\n this.#forceUpdates.add(id);\n }\n }\n\n /**\n * Updates the `ttlClock` of the CVR instance. The ttlClock starts at 0 when\n * the CVR instance is first created and increments based on elapsed time\n * since the base time established by the ViewSyncerService.\n */\n async updateTTLClock(ttlClock: TTLClock, lastActive: number): Promise<void> {\n await this.#db`UPDATE ${this.#cvr('instances')}\n SET \"lastActive\" = ${lastActive},\n \"ttlClock\" = ${ttlClock}\n WHERE \"clientGroupID\" = ${this.#id}`.execute();\n }\n\n /**\n * @returns This returns the current `ttlClock` of the CVR instance. The ttlClock\n * represents elapsed time since the instance was created (starting from 0).\n * If the CVR has never been initialized for this client group, it returns\n * `undefined`.\n */\n async getTTLClock(): Promise<TTLClock | undefined> {\n const result = await this.#db<Pick<InstancesRow, 'ttlClock'>[]>`\n SELECT \"ttlClock\" FROM ${this.#cvr('instances')}\n WHERE \"clientGroupID\" = ${this.#id}`.values();\n if (result.length === 0) {\n // This can happen if the CVR has not been initialized yet.\n return undefined;\n }\n assert(\n result.length === 1,\n () => `Expected exactly one rowsVersion result, got ${result.length}`,\n );\n return result[0][0];\n }\n\n putInstance({\n version,\n replicaVersion,\n lastActive,\n clientSchema,\n profileID,\n ttlClock,\n }: Pick<\n CVRSnapshot,\n | 'version'\n | 'replicaVersion'\n | 'lastActive'\n | 'clientSchema'\n | 'profileID'\n | 'ttlClock'\n >): void {\n // Overwrite any previously queued instance write — only the last call\n // matters since they all target the same row.\n this.#pendingInstanceWrite = (tx, lastConnectTime) => {\n const change: InstancesRow = {\n clientGroupID: this.#id,\n version: versionString(version),\n lastActive,\n ttlClock,\n replicaVersion,\n owner: this.#taskID,\n grantedAt: lastConnectTime,\n clientSchema,\n profileID,\n };\n return tx`\n INSERT INTO ${this.#cvr('instances')} ${tx(change)} \n ON CONFLICT (\"clientGroupID\") DO UPDATE SET ${tx(change)}`;\n };\n }\n\n markQueryAsDeleted(version: CVRVersion, queryPatch: QueryPatch): void {\n this.#updateQueryFields(queryPatch.id, {\n patchVersion: versionString(version),\n deleted: true,\n transformationHash: null,\n transformationVersion: null,\n });\n // A query that is added back starts without a signature.\n this.#pendingRowSetSignatures.set(queryPatch.id, null);\n }\n\n putQuery(query: QueryRecord): void {\n // Keep the persisted signature. A change queued for this flush (by\n // updateRowSetSignature() or markQueryAsDeleted()) is a partial update,\n // which #flushQueries() merges over this row.\n const sig = this.#rowSetSignatures.get(query.id);\n const change = queryRecordToQueryRow(\n this.#id,\n query,\n sig === undefined ? null : formatSignature(sig),\n );\n\n const c = {\n ...change,\n // Pre-stringify queryArgs to handle postgres.js boolean array bug\n queryArgs:\n change.queryArgs !== null ? JSON.stringify(change.queryArgs) : null,\n transformationHash: change.transformationHash ?? null,\n transformationVersion: change.transformationVersion ?? null,\n deleted: change.deleted ?? false,\n };\n this.#pendingQueryUpdates.set(query.id, c);\n }\n\n updateQuery(query: QueryRecord) {\n const maybeVersionString = (v: CVRVersion | undefined) =>\n v ? versionString(v) : null;\n this.#updateQueryFields(query.id, {\n patchVersion:\n query.type === 'internal'\n ? null\n : maybeVersionString(query.patchVersion),\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n deleted: false,\n });\n }\n\n /**\n * The persisted row-set signature of the query, or `undefined` if it has\n * none (e.g. a query from before signatures were introduced). Reflects the\n * loaded/flushed CVR; changes made with {@link updateRowSetSignature} are\n * only visible after they are flushed.\n */\n rowSetSignature(queryHash: string): bigint | undefined {\n return this.#rowSetSignatures.get(queryHash);\n }\n\n updateRowSetSignature(queryHash: string, signature: bigint): void {\n this.#updateQueryFields(queryHash, {\n rowSetSignature: formatSignature(signature),\n });\n this.#pendingRowSetSignatures.set(queryHash, signature);\n }\n\n insertClient(client: ClientRecord): void {\n const change: ClientsRow = {\n clientGroupID: this.#id,\n clientID: client.id,\n };\n\n this.#writes.add({\n stats: {clients: 1},\n write: tx => tx`INSERT INTO ${this.#cvr('clients')} ${tx(change)}`,\n });\n }\n\n deleteClient(clientID: string) {\n this.#writes.add({\n stats: {clients: 1},\n write: sql =>\n sql`DELETE FROM ${this.#cvr('clients')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"clientID\" = ${clientID}`,\n });\n }\n\n putDesiredQuery(\n newVersion: CVRVersion,\n query: {id: string},\n client: {id: string},\n deleted: boolean,\n inactivatedAt: TTLClock | undefined,\n ttl: number,\n ): void {\n const {ttlMs, inactivatedAtMs} = convertTTLValues(inactivatedAt, ttl);\n\n const change: DesiresRow = {\n clientGroupID: this.#id,\n clientID: client.id,\n deleted,\n inactivatedAt: inactivatedAtMs,\n patchVersion: versionString(newVersion),\n queryHash: query.id,\n ttl: ttlMs,\n };\n\n // Use composite key to deduplicate/replace entries for the same client-query pair\n const key = `${client.id}:${query.id}`;\n this.#pendingDesireUpdates.set(key, change);\n }\n\n catchupRowPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n excludeQueryHashes: string[] = [],\n ): AsyncGenerator<RowsRow[], void, undefined> {\n return this.#rowCache.catchupRowPatches(\n lc,\n afterVersion,\n upToCVR,\n current,\n excludeQueryHashes,\n );\n }\n\n async catchupConfigPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n ): Promise<PatchToVersion[]> {\n if (cmpVersions(afterVersion, upToCVR.version) >= 0) {\n return [];\n }\n\n const startMs = Date.now();\n const start = afterVersion ? versionString(afterVersion) : '';\n const end = versionString(upToCVR.version);\n lc.debug?.(`scanning config patches for clients from ${start}`);\n\n const reader = new TransactionPool(lc, {mode: Mode.READONLY}).run(this.#db);\n try {\n // Verify that we are reading the right version of the CVR.\n await reader.processReadTask(tx =>\n checkVersion(tx, this.#schema, this.#id, current),\n );\n\n const [allDesires, queryRows] = await reader.processReadTask(tx =>\n Promise.all([\n tx<DesiresRow[]>`\n SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"inactivatedAt\"\n FROM ${this.#cvr('desires')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`,\n tx<Pick<QueriesRow, 'deleted' | 'queryHash' | 'patchVersion'>[]>`\n SELECT deleted, \"queryHash\", \"patchVersion\" FROM ${this.#cvr('queries')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`,\n ]),\n );\n\n const patches: PatchToVersion[] = [];\n for (const row of queryRows) {\n const {queryHash: id} = row;\n const patch: Patch = row.deleted\n ? {type: 'query', op: 'del', id}\n : {type: 'query', op: 'put', id};\n const v = row.patchVersion;\n assert(v, 'patchVersion must be set for query patches');\n patches.push({patch, toVersion: versionFromString(v)});\n }\n for (const row of allDesires) {\n const {clientID, queryHash: id} = row;\n const patch: Patch = row.deleted\n ? {type: 'query', op: 'del', id, clientID}\n : {type: 'query', op: 'put', id, clientID};\n patches.push({patch, toVersion: versionFromString(row.patchVersion)});\n }\n\n lc.debug?.(\n `${patches.length} config patches (${Date.now() - startMs} ms)`,\n );\n return patches;\n } finally {\n reader.setDone();\n }\n }\n\n #flushQueries(\n tx: PostgresTransaction,\n lc: LogContext,\n ): PendingQuery<Row[]>[] {\n // Merge partial updates into full updates\n const partialOnly = new Map<string, Partial<QueriesRow>>();\n for (const [queryHash, partial] of this.#pendingQueryPartialUpdates) {\n const existing = this.#pendingQueryUpdates.get(queryHash);\n if (existing) {\n // Merge partial into full update\n Object.assign(existing, partial);\n } else {\n // Track partial-only updates to batch separately\n partialOnly.set(queryHash, partial);\n }\n }\n\n const queries: PendingQuery<Row[]>[] = [];\n\n // Batch full updates\n if (this.#pendingQueryUpdates.size > 0) {\n const rows = [...this.#pendingQueryUpdates.values()];\n lc.debug?.(`Batch flushing ${rows.length} full query updates`);\n\n queries.push(tx`\n INSERT INTO ${this.#cvr('queries')} (\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n \"queryArgs\",\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\",\n \"rowSetSignature\"\n )\n SELECT\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n CASE\n WHEN \"queryArgs\" IS NULL THEN NULL\n ELSE \"queryArgs\"::json\n END,\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\",\n \"rowSetSignature\"\n FROM json_to_recordset(${rows}) AS x(\n \"clientGroupID\" TEXT,\n \"queryHash\" TEXT,\n \"clientAST\" JSONB,\n \"queryName\" TEXT,\n \"queryArgs\" TEXT,\n \"patchVersion\" TEXT,\n \"transformationHash\" TEXT,\n \"transformationVersion\" TEXT,\n \"internal\" BOOLEAN,\n \"deleted\" BOOLEAN,\n \"rowSetSignature\" TEXT\n )\n ON CONFLICT (\"clientGroupID\", \"queryHash\") DO UPDATE SET\n \"clientAST\" = excluded.\"clientAST\",\n \"queryName\" = excluded.\"queryName\",\n \"queryArgs\" = CASE\n WHEN excluded.\"queryArgs\" IS NULL THEN NULL\n ELSE excluded.\"queryArgs\"::json\n END,\n \"patchVersion\" = excluded.\"patchVersion\",\n \"transformationHash\" = excluded.\"transformationHash\",\n \"transformationVersion\" = excluded.\"transformationVersion\",\n \"internal\" = excluded.\"internal\",\n \"deleted\" = excluded.\"deleted\",\n \"rowSetSignature\" = excluded.\"rowSetSignature\"\n `);\n }\n\n // Batch partial-only updates\n if (partialOnly.size > 0) {\n lc.debug?.(`Batch flushing ${partialOnly.size} partial query updates`);\n const rows = Array.from(\n partialOnly.entries(),\n ([queryHash, partial]) => ({\n clientGroupID: this.#id,\n queryHash,\n patchVersionSet: partial.patchVersion !== undefined,\n patchVersion: partial.patchVersion ?? null,\n deletedSet: partial.deleted !== undefined,\n deleted: partial.deleted ?? null,\n transformationHashSet: partial.transformationHash !== undefined,\n transformationHash: partial.transformationHash ?? null,\n transformationVersionSet: partial.transformationVersion !== undefined,\n transformationVersion: partial.transformationVersion ?? null,\n rowSetSignatureSet: partial.rowSetSignature !== undefined,\n rowSetSignature: partial.rowSetSignature ?? null,\n }),\n );\n queries.push(tx`\n UPDATE ${this.#cvr('queries')} AS q\n SET\n \"patchVersion\" = CASE\n WHEN u.\"patchVersionSet\" THEN u.\"patchVersion\"\n ELSE q.\"patchVersion\"\n END,\n \"deleted\" = CASE\n WHEN u.\"deletedSet\" THEN u.\"deleted\"\n ELSE q.\"deleted\"\n END,\n \"transformationHash\" = CASE\n WHEN u.\"transformationHashSet\" THEN u.\"transformationHash\"\n ELSE q.\"transformationHash\"\n END,\n \"transformationVersion\" = CASE\n WHEN u.\"transformationVersionSet\" THEN u.\"transformationVersion\"\n ELSE q.\"transformationVersion\"\n END,\n \"rowSetSignature\" = CASE\n WHEN u.\"rowSetSignatureSet\" THEN u.\"rowSetSignature\"\n ELSE q.\"rowSetSignature\"\n END\n FROM json_to_recordset(${rows}) AS u(\n \"clientGroupID\" TEXT,\n \"queryHash\" TEXT,\n \"patchVersionSet\" BOOLEAN,\n \"patchVersion\" TEXT,\n \"deletedSet\" BOOLEAN,\n \"deleted\" BOOLEAN,\n \"transformationHashSet\" BOOLEAN,\n \"transformationHash\" TEXT,\n \"transformationVersionSet\" BOOLEAN,\n \"transformationVersion\" TEXT,\n \"rowSetSignatureSet\" BOOLEAN,\n \"rowSetSignature\" TEXT\n )\n WHERE q.\"clientGroupID\" = u.\"clientGroupID\"\n AND q.\"queryHash\" = u.\"queryHash\"\n `);\n }\n\n return queries;\n }\n\n #flushDesires(\n tx: PostgresTransaction,\n lc: LogContext,\n ): PendingQuery<Row[]> | null {\n if (this.#pendingDesireUpdates.size === 0) {\n return null;\n }\n\n const rows = Array.from(this.#pendingDesireUpdates.values(), row => {\n const {ttlInterval, ttlMs, inactivatedAtTimestamp, inactivatedAtMs} =\n convertTTLValues(row.inactivatedAt ?? undefined, row.ttl ?? -1);\n return {\n clientGroupID: row.clientGroupID,\n clientID: row.clientID,\n queryHash: row.queryHash,\n patchVersion: row.patchVersion,\n deleted: row.deleted,\n ttl: ttlInterval,\n ttlMs,\n inactivatedAt: inactivatedAtTimestamp,\n inactivatedAtMs,\n };\n });\n\n lc.debug?.(`Batch flushing ${rows.length} desire updates`);\n\n return tx`\n INSERT INTO ${this.#cvr('desires')} (\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"ttlMs\",\n \"inactivatedAt\",\n \"inactivatedAtMs\"\n )\n SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"ttlMs\",\n CASE\n WHEN \"inactivatedAt\" IS NULL THEN NULL\n -- Divide by 1000 because postgres.js serializeTimestamp treats numbers as ms\n -- and to_timestamp expects seconds. This matches non-batched behavior.\n ELSE to_timestamp(\"inactivatedAt\" / 1000.0)\n END,\n \"inactivatedAtMs\"\n FROM json_to_recordset(${rows}) AS x(\n \"clientGroupID\" TEXT,\n \"clientID\" TEXT,\n \"queryHash\" TEXT,\n \"patchVersion\" TEXT,\n \"deleted\" BOOLEAN,\n \"ttl\" INTERVAL,\n \"ttlMs\" DOUBLE PRECISION,\n \"inactivatedAt\" DOUBLE PRECISION,\n \"inactivatedAtMs\" DOUBLE PRECISION\n )\n ON CONFLICT (\"clientGroupID\", \"clientID\", \"queryHash\") DO UPDATE SET\n \"patchVersion\" = excluded.\"patchVersion\",\n \"deleted\" = excluded.\"deleted\",\n \"ttl\" = excluded.\"ttl\",\n \"ttlMs\" = excluded.\"ttlMs\",\n \"inactivatedAt\" = excluded.\"inactivatedAt\",\n \"inactivatedAtMs\" = excluded.\"inactivatedAtMs\"\n `;\n }\n\n async #checkVersionAndOwnership(\n lc: LogContext,\n tx: PostgresTransaction,\n expectedCurrentVersion: CVRVersion,\n lastConnectTime: number,\n ): Promise<void> {\n const start = Date.now();\n lc.debug?.('checking cvr version and ownership');\n const expected = versionString(expectedCurrentVersion);\n\n if (expected === EMPTY_CVR_VERSION.stateVersion) {\n // SELECT FOR UPDATE cannot lock a row that does not exist. Ensure that\n // new CVRs have a row before taking the lock so concurrent first flushes\n // serialize on the primary key. After a conflicting insert commits, the\n // SELECT below sees its version and rejects the stale writer.\n await tx`\n INSERT INTO ${this.#cvr('instances')} (\n \"clientGroupID\",\n \"version\",\n \"lastActive\"\n )\n VALUES (\n ${this.#id},\n ${EMPTY_CVR_VERSION.stateVersion},\n to_timestamp(0)\n )\n ON CONFLICT (\"clientGroupID\") DO NOTHING\n `;\n }\n\n const result = await tx<\n Pick<InstancesRow, 'version' | 'owner' | 'grantedAt'>[]\n >`SELECT \"version\", \"owner\", \"grantedAt\" FROM ${this.#cvr('instances')}\n WHERE \"clientGroupID\" = ${this.#id}\n FOR UPDATE`;\n const {version, owner, grantedAt} =\n result.length > 0\n ? result[0]\n : {\n version: EMPTY_CVR_VERSION.stateVersion,\n owner: null,\n grantedAt: null,\n };\n lc.debug?.(\n 'checked cvr version and ownership in ' + (Date.now() - start) + ' ms',\n );\n if (owner !== this.#taskID && (grantedAt ?? 0) > lastConnectTime) {\n throw new OwnershipError(owner, grantedAt, lastConnectTime);\n }\n if (version !== expected) {\n throw new ConcurrentModificationException(expected, version);\n }\n }\n\n async #flush(\n lc: LogContext,\n expectedCurrentVersion: CVRVersion,\n cvr: CVRSnapshot,\n lastConnectTime: number,\n verifyNoop: boolean,\n ): Promise<CVRFlushStats | null> {\n const stats: CVRFlushStats = {\n instances: 0,\n queries: 0,\n desires: 0,\n clients: 0,\n rows: 0,\n rowsDeferred: 0,\n statements: 0,\n };\n if (this.#pendingRowRecordUpdates.size) {\n const existingRowRecords = await this.getRowRecords();\n this.#rowCount = existingRowRecords.size;\n for (const [id, row] of this.#pendingRowRecordUpdates.entries()) {\n if (this.#forceUpdates.has(id)) {\n continue;\n }\n const existing = existingRowRecords.get(id);\n if (\n // Don't delete or add an unreferenced row if it's not in the CVR.\n (existing === undefined && !row?.refCounts) ||\n // Don't write a row record that exactly matches what's in the CVR.\n deepEqual(\n (row ?? undefined) as ReadonlyJSONValue | undefined,\n existing as ReadonlyJSONValue | undefined,\n )\n ) {\n this.#pendingRowRecordUpdates.delete(id);\n }\n }\n }\n if (\n this.#pendingRowRecordUpdates.size === 0 &&\n this.#writes.size === 0 &&\n this.#pendingInstanceWrite === undefined &&\n this.#pendingQueryUpdates.size === 0 &&\n this.#pendingQueryPartialUpdates.size === 0 &&\n this.#pendingDesireUpdates.size === 0\n ) {\n if (verifyNoop) {\n // Nothing to write, but the caller has already acted on the CVR\n // (e.g. poked patches computed against it), so it still needs to know\n // whether the CVR is current. Otherwise a stale view-syncer whose\n // writes happen to match what another view-syncer already committed\n // never learns that it is behind.\n await runTx(\n this.#db,\n tx =>\n this.#checkVersionAndOwnership(\n lc,\n tx,\n expectedCurrentVersion,\n lastConnectTime,\n ),\n {mode: Mode.READ_COMMITTED},\n );\n }\n return null;\n }\n // Note: The CVR instance itself is only updated if there are material\n // changes (i.e. changes to the CVR contents) to flush.\n this.putInstance(cvr);\n const start = Date.now();\n lc.debug?.('flush tx beginning');\n\n // Use an async callback so we can await the version/ownership check and\n // validate it INSIDE the transaction. If validation fails, the exception\n // causes postgres.js to ROLLBACK, ensuring no writes are committed on error.\n const results = await runTx(\n this.#db,\n async tx => {\n lc.debug?.(`flush tx begun after ${Date.now() - start} ms`);\n\n // Acquire row-level lock and validate version/ownership before queuing writes.\n // Throwing here (inside the begin callback) rolls back the transaction so that\n // no writes are committed when concurrent modification or ownership errors occur.\n await this.#checkVersionAndOwnership(\n lc,\n tx,\n expectedCurrentVersion,\n lastConnectTime,\n );\n\n const writeQueries = [];\n if (this.#pendingInstanceWrite) {\n writeQueries.push(this.#pendingInstanceWrite(tx, lastConnectTime));\n stats.instances++;\n stats.statements++;\n }\n for (const write of this.#writes) {\n stats.clients += write.stats.clients ?? 0;\n stats.rows += write.stats.rows ?? 0;\n\n writeQueries.push(write.write(tx, lastConnectTime));\n stats.statements++;\n }\n\n // Batch flush config writes\n // Flush queries first (desires depend on queries via foreign key)\n const hasQueryUpdates =\n this.#pendingQueryUpdates.size > 0 ||\n this.#pendingQueryPartialUpdates.size > 0;\n\n const desireFlush = this.#flushDesires(tx, lc);\n\n let queryFlushes: PendingQuery<Row[]>[] = [];\n if (hasQueryUpdates) {\n queryFlushes = this.#flushQueries(tx, lc);\n\n // Count both full updates and partial-only updates\n const partialOnlyCount = [\n ...this.#pendingQueryPartialUpdates.keys(),\n ].filter(key => !this.#pendingQueryUpdates.has(key)).length;\n\n stats.queries = this.#pendingQueryUpdates.size + partialOnlyCount;\n stats.statements +=\n (this.#pendingQueryUpdates.size > 0 ? 1 : 0) +\n (partialOnlyCount > 0 ? 1 : 0);\n\n if (desireFlush) {\n stats.desires = this.#pendingDesireUpdates.size;\n stats.statements++;\n }\n } else if (desireFlush) {\n stats.desires = this.#pendingDesireUpdates.size;\n stats.statements++;\n }\n\n const rowUpdates = this.#rowCache.executeRowUpdates(\n tx,\n cvr.version,\n this.#pendingRowRecordUpdates,\n 'allow-defer',\n lc,\n );\n stats.statements += rowUpdates.length;\n\n // Pipeline writes now that the version check has passed.\n const pipelined = [\n ...writeQueries,\n ...queryFlushes,\n ...(desireFlush ? [desireFlush] : []),\n ...rowUpdates,\n ];\n\n lc.debug?.(`returning ${pipelined.length} queries for pipelining`);\n\n // Explicitly await all pipelined queries. When the begin callback is async,\n // postgres.js does not call Promise.all() on the return value the way it does\n // for sync callbacks, so we must do it ourselves.\n return Promise.all(pipelined);\n },\n {mode: Mode.READ_COMMITTED},\n );\n\n lc.debug?.(`flush tx completed after ${Date.now() - start} ms`);\n\n // Calculate how many row update queries were in the pipeline.\n // Note: the version check was awaited separately and is not in the results array.\n const baseQueries =\n (this.#pendingInstanceWrite ? 1 : 0) +\n this.#writes.size +\n (this.#pendingQueryUpdates.size > 0 ? 1 : 0) +\n (some(\n this.#pendingQueryPartialUpdates.keys(),\n key => !this.#pendingQueryUpdates.has(key),\n )\n ? 1\n : 0) +\n (this.#pendingDesireUpdates.size > 0 ? 1 : 0);\n const rowUpdateCount = results.length - baseQueries;\n\n const rowsFlushed = rowUpdateCount > 0;\n if (!rowsFlushed) {\n stats.rowsDeferred = this.#pendingRowRecordUpdates.size;\n } else {\n stats.rows += this.#pendingRowRecordUpdates.size;\n }\n\n this.#rowCount = await this.#rowCache.apply(\n this.#pendingRowRecordUpdates,\n cvr.version,\n rowsFlushed,\n );\n recordRowsSynced(this.#rowCount);\n\n return stats;\n }\n\n get rowCount(): number {\n return this.#rowCount;\n }\n\n /**\n * Flushes pending writes, checking that `expectedCurrentVersion` is still\n * the current version of the CVR and that this task still owns it.\n *\n * If there is nothing to write, the flush is a no-op and no check is done,\n * unless `verifyNoop` is set.\n */\n async flush(\n lc: LogContext,\n expectedCurrentVersion: CVRVersion,\n cvr: CVRSnapshot,\n lastConnectTime: number,\n verifyNoop = false,\n ): Promise<CVRFlushStats | null> {\n const start = performance.now();\n lc = lc.withContext('cvrFlushID', flushCounter++);\n try {\n const stats = await this.#flush(\n lc,\n expectedCurrentVersion,\n cvr,\n lastConnectTime,\n verifyNoop,\n );\n this.#pendingRowSetSignatures.forEach(\n applyPendingRowSetSignature,\n this.#rowSetSignatures,\n );\n if (stats) {\n const elapsed = performance.now() - start;\n lc.info?.(\n `flushed cvr@${versionString(cvr.version)} ` +\n `${JSON.stringify(stats)} in (${elapsed} ms)`,\n );\n this.#rowCache.recordSyncFlushStats(stats, elapsed);\n }\n this.#cvrFlushes.add(1, {\n [RESULT_ATTRIBUTE]: 'success',\n [CVR_FLUSH_TYPE_ATTRIBUTE]: stats ? 'sync' : 'noop',\n });\n return stats;\n } catch (e) {\n this.#cvrFlushes.add(1, {\n [RESULT_ATTRIBUTE]: 'error',\n [CVR_FLUSH_TYPE_ATTRIBUTE]: 'sync',\n [ERROR_KIND_ATTRIBUTE]: cvrErrorKind(e),\n });\n // Clear cached state if an error (e.g. ConcurrentModificationException) is encountered.\n this.#rowCache.clear();\n throw e;\n } finally {\n this.discardPending();\n }\n }\n\n /**\n * Discards the writes queued since the last {@link flush}, i.e. those of\n * an updater that is abandoned without being flushed (such as an\n * advancement that aborts with a pipeline reset). Otherwise they would be\n * written by the next flush, even though the poke that carried them to the\n * clients was cancelled.\n *\n * The row cache is not affected: it only learns of writes when they are\n * flushed.\n */\n discardPending(): void {\n this.#writes.clear();\n this.#pendingInstanceWrite = undefined;\n this.#pendingRowRecordUpdates.clear();\n this.#forceUpdates.clear();\n this.#pendingQueryUpdates.clear();\n this.#pendingDesireUpdates.clear();\n this.#pendingQueryPartialUpdates.clear();\n this.#pendingRowSetSignatures.clear();\n }\n\n hasPendingUpdates(): boolean {\n return this.#rowCache.hasPendingUpdates();\n }\n\n /** Resolves when all pending updates are flushed. */\n flushed(lc: LogContext): Promise<void> {\n return this.#rowCache.flushed(lc);\n }\n\n async inspectQueries(\n lc: LogContext,\n ttlClock: TTLClock,\n clientID?: string,\n ): Promise<InspectQueryRow[]> {\n const db = this.#db;\n const clientGroupID = this.#id;\n\n const reader = new TransactionPool(lc, {mode: Mode.READONLY}).run(db);\n try {\n return await reader.processReadTask(\n tx => tx<InspectQueryRow[]>`\n SELECT DISTINCT ON (d.\"clientID\", d.\"queryHash\")\n d.\"clientID\",\n d.\"queryHash\" AS \"queryID\",\n COALESCE(d.\"ttlMs\", ${DEFAULT_TTL_MS}) AS \"ttl\",\n d.\"inactivatedAtMs\" AS \"inactivatedAt\",\n (SELECT COUNT(*)::INT FROM ${this.#cvr('rows')} r \n WHERE r.\"clientGroupID\" = d.\"clientGroupID\" \n AND r.\"refCounts\" ? d.\"queryHash\") AS \"rowCount\",\n q.\"clientAST\" AS \"ast\",\n (q.\"patchVersion\" IS NOT NULL) AS \"got\",\n COALESCE(d.\"deleted\", FALSE) AS \"deleted\",\n q.\"queryName\" AS \"name\",\n q.\"queryArgs\" AS \"args\"\n FROM ${this.#cvr('desires')} d\n LEFT JOIN ${this.#cvr('queries')} q\n ON q.\"clientGroupID\" = d.\"clientGroupID\"\n AND q.\"queryHash\" = d.\"queryHash\"\n WHERE d.\"clientGroupID\" = ${clientGroupID}\n ${clientID ? tx`AND d.\"clientID\" = ${clientID}` : tx``}\n AND NOT (\n d.\"inactivatedAtMs\" IS NOT NULL \n AND d.\"ttlMs\" IS NOT NULL \n AND (d.\"inactivatedAtMs\" + d.\"ttlMs\") <= ${ttlClockAsNumber(ttlClock)}\n )\n ORDER BY d.\"clientID\", d.\"queryHash\"`,\n );\n } finally {\n reader.setDone();\n }\n }\n}\n\n/**\n * This is similar to {@link CVRStore.#checkVersionAndOwnership} except\n * that it only checks the version and is suitable for snapshot reads\n * (i.e. by doing a plain `SELECT` rather than a `SELECT ... FOR UPDATE`).\n */\nexport async function checkVersion(\n tx: PostgresTransaction,\n schema: string,\n clientGroupID: string,\n expectedCurrentVersion: CVRVersion,\n): Promise<void> {\n const expected = versionString(expectedCurrentVersion);\n const result = await tx<Pick<InstancesRow, 'version'>[]>`\n SELECT version FROM ${tx(schema)}.instances \n WHERE \"clientGroupID\" = ${clientGroupID}`;\n const {version} =\n result.length > 0 ? result[0] : {version: EMPTY_CVR_VERSION.stateVersion};\n if (version !== expected) {\n throw new ConcurrentModificationException(expected, version);\n }\n}\n\nexport class ClientNotFoundError extends ProtocolErrorWithLevel {\n constructor(message: string) {\n super(\n {\n kind: ErrorKind.ClientNotFound,\n message,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n}\n\nexport class ConcurrentModificationException extends ProtocolErrorWithLevel {\n readonly name = 'ConcurrentModificationException';\n\n constructor(expectedVersion: string, actualVersion: string) {\n super(\n {\n kind: ErrorKind.Rehome,\n message: `CVR has been concurrently modified. Expected ${expectedVersion}, got ${actualVersion}`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n}\n\nexport class OwnershipError extends ProtocolErrorWithLevel {\n readonly name = 'OwnershipError';\n\n constructor(\n owner: string | null,\n grantedAt: number | null,\n lastConnectTime: number,\n ) {\n super(\n {\n kind: ErrorKind.Rehome,\n message:\n `CVR ownership was transferred to ${owner} at ` +\n `${new Date(grantedAt ?? 0).toISOString()} ` +\n `(last connect time: ${new Date(lastConnectTime).toISOString()})`,\n maxBackoffMs: 0,\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n }\n}\n\nclass InvalidClientSchemaError extends ProtocolErrorWithLevel {\n readonly name = 'InvalidClientSchemaError';\n\n constructor(cause: unknown) {\n super(\n {\n kind: ErrorKind.SchemaVersionNotSupported,\n message: `Could not parse clientSchema stored in CVR: ${String(cause)}`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n {cause},\n );\n }\n}\n\nfunction cvrErrorKind(e: unknown): string {\n if (e instanceof ClientNotFoundError) {\n return 'client_not_found';\n }\n if (e instanceof ConcurrentModificationException) {\n return 'concurrent_modification';\n }\n if (e instanceof OwnershipError) {\n return 'ownership';\n }\n if (e instanceof InvalidClientSchemaError) {\n return 'invalid_client_schema';\n }\n return 'error';\n}\n\nexport class RowsVersionBehindError extends Error {\n readonly name = 'RowsVersionBehindError';\n readonly cvrVersion: string;\n readonly rowsVersion: string | null;\n\n constructor(cvrVersion: string, rowsVersion: string | null) {\n super(`rowsVersion (${rowsVersion}) is behind CVR ${cvrVersion}`);\n this.cvrVersion = cvrVersion;\n this.rowsVersion = rowsVersion;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cvr-store.js","names":["v","v.test","Mode.READONLY","v.parse","Mode.READ_COMMITTED","ErrorKind.ClientNotFound","ErrorOrigin.ZeroCache","ErrorKind.Rehome","ErrorKind.SchemaVersionNotSupported"],"sources":["../../../../../../zero-cache/src/services/view-syncer/cvr-store.ts"],"sourcesContent":["import {trace} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport type {MaybeRow, PendingQuery, Row} from 'postgres';\nimport {startAsyncSpan} from '../../../../otel/src/span.ts';\nimport {version} from '../../../../otel/src/version.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {CustomKeyMap} from '../../../../shared/src/custom-key-map.ts';\nimport {CustomKeySet} from '../../../../shared/src/custom-key-set.ts';\nimport {some} from '../../../../shared/src/iterables.ts';\nimport {\n deepEqual,\n type ReadonlyJSONValue,\n} from '../../../../shared/src/json.ts';\nimport {sleep} from '../../../../shared/src/sleep.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {astSchema} from '../../../../zero-protocol/src/ast.ts';\nimport {clientSchemaSchema} from '../../../../zero-protocol/src/client-schema.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport type {InspectQueryRow} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {clampTTL, DEFAULT_TTL_MS} from '../../../../zql/src/query/ttl.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {recordRowsSynced} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../types/pg.ts';\nimport {rowIDString} from '../../types/row-key.ts';\nimport {cvrSchema, type ShardID} from '../../types/shards.ts';\nimport type {Patch, PatchToVersion} from './client-handler.ts';\nimport type {CVR, CVRSnapshot} from './cvr.ts';\nimport {RowRecordCache} from './row-record-cache.ts';\nimport {formatSignature, parseSignature} from './row-set-signature.ts';\nimport {\n type ClientsRow,\n type DesiresRow,\n type InstancesRow,\n type PinnedUser,\n type QueriesRow,\n type RowsRow,\n} from './schema/cvr.ts';\nimport {\n type ClientQueryRecord,\n type ClientRecord,\n cmpVersions,\n type CustomQueryRecord,\n type CVRVersion,\n EMPTY_CVR_VERSION,\n type InternalQueryRecord,\n type NullableCVRVersion,\n type QueryPatch,\n type QueryRecord,\n queryRecordToQueryRow,\n type RowID,\n type RowRecord,\n versionFromString,\n versionString,\n} from './schema/types.ts';\nimport {\n type TTLClock,\n ttlClockAsNumber,\n ttlClockFromNumber,\n} from './ttl-clock.ts';\n\nconst pinnedUserSchema = v.object({id: v.string().nullable()});\n\nexport type CVRFlushStats = {\n instances: number;\n queries: number;\n desires: number;\n clients: number;\n rows: number;\n rowsDeferred: number;\n statements: number;\n};\n\nlet flushCounter = 0;\n\nconst RESULT_ATTRIBUTE = 'result';\nconst ERROR_KIND_ATTRIBUTE = 'error.kind';\nconst CVR_FLUSH_TYPE_ATTRIBUTE = 'flush.type';\n\n/**\n * Convert TTL/timestamp values for both old (seconds-based) and new (ms-based) columns.\n * Old columns: inactivatedAt (TIMESTAMPTZ), ttl (INTERVAL) - need conversion ms->seconds\n * New columns: inactivatedAtMs (DOUBLE PRECISION), ttlMs (DOUBLE PRECISION) - store ms directly\n */\nfunction convertTTLValues(\n inactivatedAt: TTLClock | undefined,\n ttl: number,\n): {\n ttlInterval: number | null;\n ttlMs: number | null;\n inactivatedAtTimestamp: TTLClock | null;\n inactivatedAtMs: TTLClock | null;\n} {\n return {\n ttlInterval: ttl < 0 ? null : ttl / 1000, // INTERVAL needs seconds\n ttlMs: ttl < 0 ? null : ttl, // New column stores ms directly\n inactivatedAtTimestamp:\n inactivatedAt === undefined\n ? null\n : ttlClockFromNumber(ttlClockAsNumber(inactivatedAt) / 1000),\n inactivatedAtMs: inactivatedAt ?? null,\n };\n}\n\nconst tracer = trace.getTracer('cvr-store', version);\n\n/**\n * QueriesRow with queryArgs as a stringified JSON value.\n * Used for batched config writes where queryArgs are pre-stringified\n * to handle the postgres.js boolean array bug.\n */\ntype StringifiedQueriesRow = Omit<QueriesRow, 'queryArgs'> & {\n queryArgs: string | null;\n};\n\n/**\n * Applies a pending row-set signature change (`null`: the query was deleted)\n * to the persisted signatures, passed as `this`. A module-level function with\n * `thisArg` rather than a closure, so that `forEach` allocates nothing and\n * looks each entry up once.\n */\nfunction applyPendingRowSetSignature(\n this: Map<string, bigint>,\n sig: bigint | null,\n queryHash: string,\n): void {\n if (sig === null) {\n this.delete(queryHash);\n } else {\n this.set(queryHash, sig);\n }\n}\n\nfunction asQuery(row: QueriesRow): QueryRecord {\n const maybeVersion = (s: string | null) =>\n s === null ? undefined : versionFromString(s);\n\n if (row.clientAST === null) {\n // custom query\n assert(\n row.queryName !== null && row.queryArgs !== null,\n 'queryName and queryArgs must be set for custom queries',\n );\n return {\n type: 'custom',\n id: row.queryHash,\n name: row.queryName,\n args: row.queryArgs,\n patchVersion: maybeVersion(row.patchVersion),\n clientState: {},\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies CustomQueryRecord;\n }\n\n const parsed = v.test(row.clientAST, astSchema);\n if (!parsed.ok) {\n // A newer zero-cache that was rolled back may have stored an AST this one\n // can't parse (clients of different protocol versions share a client\n // group). The row's rows records reference its hash, so it can't simply\n // be skipped. Fail the group instead: the client disables it and starts\n // over with a fresh CVR.\n throw new InvalidClientASTError(row.queryHash, parsed.error);\n }\n const ast = parsed.value;\n return row.internal\n ? ({\n type: 'internal',\n id: row.queryHash,\n ast,\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies InternalQueryRecord)\n : ({\n type: 'client',\n id: row.queryHash,\n ast,\n patchVersion: maybeVersion(row.patchVersion),\n clientState: {},\n transformationHash: row.transformationHash ?? undefined,\n transformationVersion: maybeVersion(row.transformationVersion),\n } satisfies ClientQueryRecord);\n}\n\n// The time to wait between load attempts.\nconst LOAD_ATTEMPT_INTERVAL_MS = 500;\n// The maximum number of load() attempts if the rowsVersion is behind.\n// This currently results in a maximum catchup time of ~5 seconds, after\n// which we give up and consider the CVR invalid.\n//\n// TODO: Make this configurable with something like --max-catchup-wait-ms,\n// as it is technically application specific.\nconst MAX_LOAD_ATTEMPTS = 10;\n\nexport class CVRStore {\n readonly #schema: string;\n readonly #taskID: string;\n readonly #id: string;\n readonly #failService: (e: unknown) => void;\n readonly #db: PostgresDB;\n readonly #writes: Set<{\n stats: Partial<CVRFlushStats>;\n write: (\n tx: PostgresTransaction,\n lastConnectTime: number,\n ) => PendingQuery<MaybeRow[]>;\n }> = new Set();\n // Stored separately so repeated putInstance() calls (e.g. setClientSchema,\n // setProfileID, and the final call in #flush) replace each other rather than\n // accumulating as independent statements in #writes.\n #pendingInstanceWrite:\n | ((\n tx: PostgresTransaction,\n lastConnectTime: number,\n ) => PendingQuery<MaybeRow[]>)\n | undefined = undefined;\n readonly #pendingRowRecordUpdates = new CustomKeyMap<RowID, RowRecord | null>(\n rowIDString,\n );\n readonly #forceUpdates = new CustomKeySet<RowID>(rowIDString);\n readonly #rowCache: RowRecordCache;\n readonly #cvrLoads = getOrCreateCounter(\n 'sync',\n 'cvr.load_attempts',\n 'CVR load attempts, labeled by result.',\n );\n readonly #cvrLoadTime = getOrCreateLatencyHistogram(\n 'sync',\n 'cvr.load_duration',\n 'Time to load a CVR.',\n );\n readonly #cvrFlushes = getOrCreateCounter(\n 'sync',\n 'cvr.flush_attempts',\n 'CVR flush attempts, labeled by result and flush.type.',\n );\n readonly #loadAttemptIntervalMs: number;\n readonly #maxLoadAttempts: number;\n #rowCount: number = 0;\n readonly #pendingQueryUpdates = new Map<string, StringifiedQueriesRow>();\n readonly #pendingDesireUpdates = new Map<string, DesiresRow>();\n readonly #pendingQueryPartialUpdates = new Map<string, Partial<QueriesRow>>();\n\n /**\n * The persisted `rowSetSignature` of each query of the loaded CVR that has\n * one: an XOR of the hashes of the rows attached to the query (see\n * row-set-signature.ts), maintained by the `PipelineDriver` and persisted\n * by `CVRQueryDrivenUpdater.flush`. Used to detect drift when re-hydrating\n * queries containing the `Cap` operator, which may pick a different N-row\n * subset on re-execution. A query without one (e.g. from before signatures\n * were introduced) is skipped by drift detection.\n *\n * This is kept here, rather than in the `QueryRecord`s of the CVR, because\n * it changes on most advancements, and changing a `QueryRecord` requires\n * the CVR updater to copy the CVR's whole `queries` map.\n *\n * Only updated once changes are flushed, so that, like the loaded/flushed\n * CVR, it holds the persisted signatures of the CVR's (non-deleted) queries.\n */\n readonly #rowSetSignatures = new Map<string, bigint>();\n /** Signature changes to apply to {@link #rowSetSignatures} on flush. */\n readonly #pendingRowSetSignatures = new Map<string, bigint | null>();\n\n constructor(\n lc: LogContext,\n cvrDb: PostgresDB,\n shard: ShardID,\n taskID: string,\n cvrID: string,\n failService: (e: unknown) => void,\n loadAttemptIntervalMs = LOAD_ATTEMPT_INTERVAL_MS,\n maxLoadAttempts = MAX_LOAD_ATTEMPTS,\n deferredRowFlushThreshold = 100, // somewhat arbitrary\n setTimeoutFn = setTimeout,\n ) {\n this.#failService = failService;\n this.#db = cvrDb;\n this.#schema = cvrSchema(shard);\n this.#taskID = taskID;\n this.#id = cvrID;\n this.#rowCache = new RowRecordCache(\n lc,\n cvrDb,\n shard,\n cvrID,\n failService,\n deferredRowFlushThreshold,\n setTimeoutFn,\n );\n this.#loadAttemptIntervalMs = loadAttemptIntervalMs;\n this.#maxLoadAttempts = maxLoadAttempts;\n }\n\n #cvr(table: string) {\n return this.#db(`${this.#schema}.${table}`);\n }\n\n #updateQueryFields(queryHash: string, fields: Partial<QueriesRow>): void {\n // Track as partial-only update for batched flush. Merge into any\n // pre-existing partial update for the same hash so independent callers\n // (e.g. updateQuery + updateRowSetSignature) don't clobber each other.\n const existing = this.#pendingQueryPartialUpdates.get(queryHash);\n this.#pendingQueryPartialUpdates.set(\n queryHash,\n existing ? {...existing, ...fields} : fields,\n );\n }\n\n load(lc: LogContext, lastConnectTime: number): Promise<CVR> {\n const start = performance.now();\n // Another view-syncer may have written the CVR since the row records\n // were cached, so reload them along with the CVR.\n this.#rowCache.clear();\n return startAsyncSpan(tracer, 'cvr.load', async () => {\n try {\n let err: RowsVersionBehindError | undefined;\n for (let i = 0; i < this.#maxLoadAttempts; i++) {\n if (i > 0) {\n await sleep(this.#loadAttemptIntervalMs);\n }\n const result = await this.#load(lc, lastConnectTime);\n if (result instanceof RowsVersionBehindError) {\n lc.info?.(`attempt ${i + 1}: ${String(result)}`);\n err = result;\n continue;\n }\n this.#recordLoad(performance.now() - start, {\n [RESULT_ATTRIBUTE]: 'success',\n });\n return result;\n }\n assert(err, 'Expected error to be set after retry loop exhausted');\n throw new ClientNotFoundError(\n `max attempts exceeded waiting for CVR@${err.cvrVersion} to catch up from ${err.rowsVersion}`,\n );\n } catch (e) {\n this.#recordLoad(performance.now() - start, {\n [RESULT_ATTRIBUTE]: 'error',\n [ERROR_KIND_ATTRIBUTE]: cvrErrorKind(e),\n });\n throw e;\n }\n });\n }\n\n #recordLoad(elapsedMs: number, attributes: Record<string, string>) {\n this.#cvrLoads.add(1, attributes);\n this.#cvrLoadTime.recordMs(elapsedMs, attributes);\n }\n\n async #load(\n lc: LogContext,\n lastConnectTime: number,\n ): Promise<CVR | RowsVersionBehindError> {\n const start = Date.now();\n\n const id = this.#id;\n const cvr: CVR = {\n id,\n version: EMPTY_CVR_VERSION,\n lastActive: 0,\n ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0, not Date.now()\n replicaVersion: null,\n clients: {},\n queries: {},\n clientSchema: null,\n profileID: null,\n pinnedUser: null,\n };\n\n const [instance, clientsRows, queryRows, desiresRows] = await runTx(\n this.#db,\n tx => {\n lc.debug?.(`CVR tx started after ${Date.now() - start} ms`);\n return [\n tx<\n (Omit<InstancesRow, 'clientGroupID'> & {\n profileID: string | null;\n pinnedUser: PinnedUser | null;\n deleted: boolean;\n rowsVersion: string | null;\n })[]\n >`SELECT cvr.\"version\",\n \"lastActive\",\n \"ttlClock\",\n \"replicaVersion\",\n \"owner\",\n \"grantedAt\",\n \"clientSchema\",\n \"profileID\",\n \"pinnedUser\",\n \"deleted\",\n rows.\"version\" as \"rowsVersion\"\n FROM ${this.#cvr('instances')} AS cvr\n LEFT JOIN ${this.#cvr('rowsVersion')} AS rows\n ON cvr.\"clientGroupID\" = rows.\"clientGroupID\"\n WHERE cvr.\"clientGroupID\" = ${id}`,\n tx<Pick<ClientsRow, 'clientID'>[]>`SELECT \"clientID\" FROM ${this.#cvr(\n 'clients',\n )}\n WHERE \"clientGroupID\" = ${id}`,\n tx<QueriesRow[]>`SELECT\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n \"queryArgs\",\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\",\n \"rowSetSignature\"\n FROM ${this.#cvr('queries')}\n WHERE \"clientGroupID\" = ${id} AND deleted IS DISTINCT FROM true`,\n tx<DesiresRow[]>`SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttlMs\" AS \"ttl\",\n \"inactivatedAtMs\" AS \"inactivatedAt\"\n FROM ${this.#cvr('desires')}\n WHERE \"clientGroupID\" = ${id}`,\n ];\n },\n {mode: Mode.READONLY},\n );\n lc.debug?.(\n `CVR tx completed after ${Date.now() - start} ms ` +\n `(${clientsRows.length} clients, ${queryRows.length} queries, ${desiresRows.length} desires)`,\n );\n\n if (instance.length === 0) {\n // This is the first time we see this CVR.\n this.putInstance({\n version: cvr.version,\n lastActive: 0,\n ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0 for new instances\n replicaVersion: null,\n clientSchema: null,\n profileID: null,\n pinnedUser: null,\n });\n } else {\n assert(\n instance.length === 1,\n () => `Expected exactly one CVR instance, got ${instance.length}`,\n );\n const {\n version,\n lastActive,\n ttlClock,\n replicaVersion,\n owner,\n grantedAt,\n rowsVersion,\n clientSchema,\n profileID,\n pinnedUser,\n deleted,\n } = instance[0];\n\n if (deleted) {\n throw new ClientNotFoundError(\n 'Client has been purged due to inactivity',\n );\n }\n\n if (owner !== this.#taskID) {\n if ((grantedAt ?? 0) > lastConnectTime) {\n throw new OwnershipError(owner, grantedAt, lastConnectTime);\n } else {\n // Fire-and-forget an ownership change to signal the current owner.\n // Note that the query is structured such that it only succeeds in the\n // correct conditions (i.e. gated on `grantedAt`).\n void this.#db`\n UPDATE ${this.#cvr('instances')} \n SET \"owner\" = ${this.#taskID}, \n \"grantedAt\" = ${lastConnectTime}\n WHERE \"clientGroupID\" = ${this.#id} AND\n (\"grantedAt\" IS NULL OR\n \"grantedAt\" <= to_timestamp(${lastConnectTime / 1000}))\n `\n .execute()\n .catch(this.#failService);\n }\n }\n\n if (version !== (rowsVersion ?? EMPTY_CVR_VERSION.stateVersion)) {\n // This will cause the load() method to wait for row catchup and retry.\n // Assuming the ownership signal succeeds, the current owner will stop\n // modifying the CVR and flush its pending row changes.\n return new RowsVersionBehindError(version, rowsVersion);\n }\n\n cvr.version = versionFromString(version);\n cvr.lastActive = lastActive;\n cvr.ttlClock = ttlClock;\n cvr.replicaVersion = replicaVersion;\n cvr.profileID = profileID;\n cvr.pinnedUser =\n pinnedUser === null ? null : v.parse(pinnedUser, pinnedUserSchema);\n\n try {\n // Strip: a newer zero-cache that was rolled back may have stored\n // fields this one doesn't know.\n cvr.clientSchema =\n clientSchema === null\n ? null\n : v.parse(clientSchema, clientSchemaSchema, 'strip');\n } catch (e) {\n throw new InvalidClientSchemaError(e);\n }\n }\n\n for (const row of clientsRows) {\n cvr.clients[row.clientID] = {\n id: row.clientID,\n desiredQueryIDs: [],\n };\n }\n\n this.#rowSetSignatures.clear();\n for (const row of queryRows) {\n const query = asQuery(row);\n cvr.queries[row.queryHash] = query;\n if (row.rowSetSignature) {\n this.#rowSetSignatures.set(\n row.queryHash,\n parseSignature(row.rowSetSignature),\n );\n }\n }\n\n for (const row of desiresRows) {\n const client = cvr.clients[row.clientID];\n // Note: row.inactivatedAt is mapped from inactivatedAtMs in the SQL query\n if (!row.deleted && row.inactivatedAt === null) {\n if (client) {\n client.desiredQueryIDs.push(row.queryHash);\n } else {\n // This can happen if the client was deleted but the queries are still alive.\n lc.debug?.(\n `Not adding to desiredQueryIDs for client ${row.clientID} because it has been deleted.`,\n );\n }\n }\n\n const query = cvr.queries[row.queryHash];\n if (\n query &&\n query.type !== 'internal' &&\n (!row.deleted || row.inactivatedAt !== null)\n ) {\n query.clientState[row.clientID] = {\n inactivatedAt: row.inactivatedAt ?? undefined,\n ttl: clampTTL(row.ttl ?? DEFAULT_TTL_MS),\n version: versionFromString(row.patchVersion),\n };\n }\n }\n\n lc.info?.(\n `loaded cvr@${versionString(cvr.version)} (${Date.now() - start} ms)`,\n );\n\n // why do we not sort `desiredQueryIDs` here?\n\n return cvr;\n }\n\n getRowRecords(): Promise<ReadonlyMap<RowID, RowRecord>> {\n return this.#rowCache.getRowRecords();\n }\n\n putRowRecord(row: RowRecord): void {\n this.#pendingRowRecordUpdates.set(row.id, row);\n }\n\n /**\n * Note: Removing a row from the CVR should be represented by a\n * {@link putRowRecord()} with `refCounts: null` in order to properly\n * produce the appropriate delete patch when catching up old clients.\n *\n * This `delRowRecord()` method, on the other hand, is only used for\n * \"canceling\" the put of a row that was not in the CVR in the first place.\n */\n delRowRecord(id: RowID): void {\n this.#pendingRowRecordUpdates.set(id, null);\n }\n\n /**\n * Overrides the default logic that removes no-op writes and forces\n * the updates for the given row `ids`. This has no effect if there\n * are no corresponding puts or dels for the associated row records.\n */\n forceUpdates(...ids: RowID[]) {\n for (const id of ids) {\n this.#forceUpdates.add(id);\n }\n }\n\n /**\n * Updates the `ttlClock` of the CVR instance. The ttlClock starts at 0 when\n * the CVR instance is first created and increments based on elapsed time\n * since the base time established by the ViewSyncerService.\n */\n async updateTTLClock(ttlClock: TTLClock, lastActive: number): Promise<void> {\n await this.#db`UPDATE ${this.#cvr('instances')}\n SET \"lastActive\" = ${lastActive},\n \"ttlClock\" = ${ttlClock}\n WHERE \"clientGroupID\" = ${this.#id}`.execute();\n }\n\n /**\n * @returns This returns the current `ttlClock` of the CVR instance. The ttlClock\n * represents elapsed time since the instance was created (starting from 0).\n * If the CVR has never been initialized for this client group, it returns\n * `undefined`.\n */\n async getTTLClock(): Promise<TTLClock | undefined> {\n const result = await this.#db<Pick<InstancesRow, 'ttlClock'>[]>`\n SELECT \"ttlClock\" FROM ${this.#cvr('instances')}\n WHERE \"clientGroupID\" = ${this.#id}`.values();\n if (result.length === 0) {\n // This can happen if the CVR has not been initialized yet.\n return undefined;\n }\n assert(\n result.length === 1,\n () => `Expected exactly one rowsVersion result, got ${result.length}`,\n );\n return result[0][0];\n }\n\n putInstance({\n version,\n replicaVersion,\n lastActive,\n clientSchema,\n profileID,\n pinnedUser,\n ttlClock,\n }: Pick<\n CVRSnapshot,\n | 'version'\n | 'replicaVersion'\n | 'lastActive'\n | 'clientSchema'\n | 'profileID'\n | 'pinnedUser'\n | 'ttlClock'\n >): void {\n // Overwrite any previously queued instance write — only the last call\n // matters since they all target the same row.\n this.#pendingInstanceWrite = (tx, lastConnectTime) => {\n const change: InstancesRow = {\n clientGroupID: this.#id,\n version: versionString(version),\n lastActive,\n ttlClock,\n replicaVersion,\n owner: this.#taskID,\n grantedAt: lastConnectTime,\n clientSchema,\n profileID,\n pinnedUser,\n };\n return tx`\n INSERT INTO ${this.#cvr('instances')} ${tx(change)} \n ON CONFLICT (\"clientGroupID\") DO UPDATE SET ${tx(change)}`;\n };\n }\n\n markQueryAsDeleted(version: CVRVersion, queryPatch: QueryPatch): void {\n this.#updateQueryFields(queryPatch.id, {\n patchVersion: versionString(version),\n deleted: true,\n transformationHash: null,\n transformationVersion: null,\n });\n // A query that is added back starts without a signature.\n this.#pendingRowSetSignatures.set(queryPatch.id, null);\n }\n\n putQuery(query: QueryRecord): void {\n // Keep the persisted signature. A change queued for this flush (by\n // updateRowSetSignature() or markQueryAsDeleted()) is a partial update,\n // which #flushQueries() merges over this row.\n const sig = this.#rowSetSignatures.get(query.id);\n const change = queryRecordToQueryRow(\n this.#id,\n query,\n sig === undefined ? null : formatSignature(sig),\n );\n\n const c = {\n ...change,\n // Pre-stringify queryArgs to handle postgres.js boolean array bug\n queryArgs:\n change.queryArgs !== null ? JSON.stringify(change.queryArgs) : null,\n transformationHash: change.transformationHash ?? null,\n transformationVersion: change.transformationVersion ?? null,\n deleted: change.deleted ?? false,\n };\n this.#pendingQueryUpdates.set(query.id, c);\n }\n\n updateQuery(query: QueryRecord) {\n const maybeVersionString = (v: CVRVersion | undefined) =>\n v ? versionString(v) : null;\n this.#updateQueryFields(query.id, {\n patchVersion:\n query.type === 'internal'\n ? null\n : maybeVersionString(query.patchVersion),\n transformationHash: query.transformationHash ?? null,\n transformationVersion: maybeVersionString(query.transformationVersion),\n deleted: false,\n });\n }\n\n /**\n * The persisted row-set signature of the query, or `undefined` if it has\n * none (e.g. a query from before signatures were introduced). Reflects the\n * loaded/flushed CVR; changes made with {@link updateRowSetSignature} are\n * only visible after they are flushed.\n */\n rowSetSignature(queryHash: string): bigint | undefined {\n return this.#rowSetSignatures.get(queryHash);\n }\n\n updateRowSetSignature(queryHash: string, signature: bigint): void {\n this.#updateQueryFields(queryHash, {\n rowSetSignature: formatSignature(signature),\n });\n this.#pendingRowSetSignatures.set(queryHash, signature);\n }\n\n insertClient(client: ClientRecord): void {\n const change: ClientsRow = {\n clientGroupID: this.#id,\n clientID: client.id,\n };\n\n this.#writes.add({\n stats: {clients: 1},\n write: tx => tx`INSERT INTO ${this.#cvr('clients')} ${tx(change)}`,\n });\n }\n\n deleteClient(clientID: string) {\n this.#writes.add({\n stats: {clients: 1},\n write: sql =>\n sql`DELETE FROM ${this.#cvr('clients')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"clientID\" = ${clientID}`,\n });\n }\n\n putDesiredQuery(\n newVersion: CVRVersion,\n query: {id: string},\n client: {id: string},\n deleted: boolean,\n inactivatedAt: TTLClock | undefined,\n ttl: number,\n ): void {\n const {ttlMs, inactivatedAtMs} = convertTTLValues(inactivatedAt, ttl);\n\n const change: DesiresRow = {\n clientGroupID: this.#id,\n clientID: client.id,\n deleted,\n inactivatedAt: inactivatedAtMs,\n patchVersion: versionString(newVersion),\n queryHash: query.id,\n ttl: ttlMs,\n };\n\n // Use composite key to deduplicate/replace entries for the same client-query pair\n const key = `${client.id}:${query.id}`;\n this.#pendingDesireUpdates.set(key, change);\n }\n\n catchupRowPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n excludeQueryHashes: string[] = [],\n ): AsyncGenerator<RowsRow[], void, undefined> {\n return this.#rowCache.catchupRowPatches(\n lc,\n afterVersion,\n upToCVR,\n current,\n excludeQueryHashes,\n );\n }\n\n async catchupConfigPatches(\n lc: LogContext,\n afterVersion: NullableCVRVersion,\n upToCVR: CVRSnapshot,\n current: CVRVersion,\n ): Promise<PatchToVersion[]> {\n if (cmpVersions(afterVersion, upToCVR.version) >= 0) {\n return [];\n }\n\n const startMs = Date.now();\n const start = afterVersion ? versionString(afterVersion) : '';\n const end = versionString(upToCVR.version);\n lc.debug?.(`scanning config patches for clients from ${start}`);\n\n const reader = new TransactionPool(lc, {mode: Mode.READONLY}).run(this.#db);\n try {\n // Verify that we are reading the right version of the CVR.\n await reader.processReadTask(tx =>\n checkVersion(tx, this.#schema, this.#id, current),\n );\n\n const [allDesires, queryRows] = await reader.processReadTask(tx =>\n Promise.all([\n tx<DesiresRow[]>`\n SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"inactivatedAt\"\n FROM ${this.#cvr('desires')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`,\n tx<Pick<QueriesRow, 'deleted' | 'queryHash' | 'patchVersion'>[]>`\n SELECT deleted, \"queryHash\", \"patchVersion\" FROM ${this.#cvr('queries')}\n WHERE \"clientGroupID\" = ${this.#id}\n AND \"patchVersion\" > ${start}\n AND \"patchVersion\" <= ${end}`,\n ]),\n );\n\n const patches: PatchToVersion[] = [];\n for (const row of queryRows) {\n const {queryHash: id} = row;\n const patch: Patch = row.deleted\n ? {type: 'query', op: 'del', id}\n : {type: 'query', op: 'put', id};\n const v = row.patchVersion;\n assert(v, 'patchVersion must be set for query patches');\n patches.push({patch, toVersion: versionFromString(v)});\n }\n for (const row of allDesires) {\n const {clientID, queryHash: id} = row;\n const patch: Patch = row.deleted\n ? {type: 'query', op: 'del', id, clientID}\n : {type: 'query', op: 'put', id, clientID};\n patches.push({patch, toVersion: versionFromString(row.patchVersion)});\n }\n\n lc.debug?.(\n `${patches.length} config patches (${Date.now() - startMs} ms)`,\n );\n return patches;\n } finally {\n reader.setDone();\n }\n }\n\n #flushQueries(\n tx: PostgresTransaction,\n lc: LogContext,\n ): PendingQuery<Row[]>[] {\n // Merge partial updates into full updates\n const partialOnly = new Map<string, Partial<QueriesRow>>();\n for (const [queryHash, partial] of this.#pendingQueryPartialUpdates) {\n const existing = this.#pendingQueryUpdates.get(queryHash);\n if (existing) {\n // Merge partial into full update\n Object.assign(existing, partial);\n } else {\n // Track partial-only updates to batch separately\n partialOnly.set(queryHash, partial);\n }\n }\n\n const queries: PendingQuery<Row[]>[] = [];\n\n // Batch full updates\n if (this.#pendingQueryUpdates.size > 0) {\n const rows = [...this.#pendingQueryUpdates.values()];\n lc.debug?.(`Batch flushing ${rows.length} full query updates`);\n\n queries.push(tx`\n INSERT INTO ${this.#cvr('queries')} (\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n \"queryArgs\",\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\",\n \"rowSetSignature\"\n )\n SELECT\n \"clientGroupID\",\n \"queryHash\",\n \"clientAST\",\n \"queryName\",\n CASE\n WHEN \"queryArgs\" IS NULL THEN NULL\n ELSE \"queryArgs\"::json\n END,\n \"patchVersion\",\n \"transformationHash\",\n \"transformationVersion\",\n \"internal\",\n \"deleted\",\n \"rowSetSignature\"\n FROM json_to_recordset(${rows}) AS x(\n \"clientGroupID\" TEXT,\n \"queryHash\" TEXT,\n \"clientAST\" JSONB,\n \"queryName\" TEXT,\n \"queryArgs\" TEXT,\n \"patchVersion\" TEXT,\n \"transformationHash\" TEXT,\n \"transformationVersion\" TEXT,\n \"internal\" BOOLEAN,\n \"deleted\" BOOLEAN,\n \"rowSetSignature\" TEXT\n )\n ON CONFLICT (\"clientGroupID\", \"queryHash\") DO UPDATE SET\n \"clientAST\" = excluded.\"clientAST\",\n \"queryName\" = excluded.\"queryName\",\n \"queryArgs\" = CASE\n WHEN excluded.\"queryArgs\" IS NULL THEN NULL\n ELSE excluded.\"queryArgs\"::json\n END,\n \"patchVersion\" = excluded.\"patchVersion\",\n \"transformationHash\" = excluded.\"transformationHash\",\n \"transformationVersion\" = excluded.\"transformationVersion\",\n \"internal\" = excluded.\"internal\",\n \"deleted\" = excluded.\"deleted\",\n \"rowSetSignature\" = excluded.\"rowSetSignature\"\n `);\n }\n\n // Batch partial-only updates\n if (partialOnly.size > 0) {\n lc.debug?.(`Batch flushing ${partialOnly.size} partial query updates`);\n const rows = Array.from(\n partialOnly.entries(),\n ([queryHash, partial]) => ({\n clientGroupID: this.#id,\n queryHash,\n patchVersionSet: partial.patchVersion !== undefined,\n patchVersion: partial.patchVersion ?? null,\n deletedSet: partial.deleted !== undefined,\n deleted: partial.deleted ?? null,\n transformationHashSet: partial.transformationHash !== undefined,\n transformationHash: partial.transformationHash ?? null,\n transformationVersionSet: partial.transformationVersion !== undefined,\n transformationVersion: partial.transformationVersion ?? null,\n rowSetSignatureSet: partial.rowSetSignature !== undefined,\n rowSetSignature: partial.rowSetSignature ?? null,\n }),\n );\n queries.push(tx`\n UPDATE ${this.#cvr('queries')} AS q\n SET\n \"patchVersion\" = CASE\n WHEN u.\"patchVersionSet\" THEN u.\"patchVersion\"\n ELSE q.\"patchVersion\"\n END,\n \"deleted\" = CASE\n WHEN u.\"deletedSet\" THEN u.\"deleted\"\n ELSE q.\"deleted\"\n END,\n \"transformationHash\" = CASE\n WHEN u.\"transformationHashSet\" THEN u.\"transformationHash\"\n ELSE q.\"transformationHash\"\n END,\n \"transformationVersion\" = CASE\n WHEN u.\"transformationVersionSet\" THEN u.\"transformationVersion\"\n ELSE q.\"transformationVersion\"\n END,\n \"rowSetSignature\" = CASE\n WHEN u.\"rowSetSignatureSet\" THEN u.\"rowSetSignature\"\n ELSE q.\"rowSetSignature\"\n END\n FROM json_to_recordset(${rows}) AS u(\n \"clientGroupID\" TEXT,\n \"queryHash\" TEXT,\n \"patchVersionSet\" BOOLEAN,\n \"patchVersion\" TEXT,\n \"deletedSet\" BOOLEAN,\n \"deleted\" BOOLEAN,\n \"transformationHashSet\" BOOLEAN,\n \"transformationHash\" TEXT,\n \"transformationVersionSet\" BOOLEAN,\n \"transformationVersion\" TEXT,\n \"rowSetSignatureSet\" BOOLEAN,\n \"rowSetSignature\" TEXT\n )\n WHERE q.\"clientGroupID\" = u.\"clientGroupID\"\n AND q.\"queryHash\" = u.\"queryHash\"\n `);\n }\n\n return queries;\n }\n\n #flushDesires(\n tx: PostgresTransaction,\n lc: LogContext,\n ): PendingQuery<Row[]> | null {\n if (this.#pendingDesireUpdates.size === 0) {\n return null;\n }\n\n const rows = Array.from(this.#pendingDesireUpdates.values(), row => {\n const {ttlInterval, ttlMs, inactivatedAtTimestamp, inactivatedAtMs} =\n convertTTLValues(row.inactivatedAt ?? undefined, row.ttl ?? -1);\n return {\n clientGroupID: row.clientGroupID,\n clientID: row.clientID,\n queryHash: row.queryHash,\n patchVersion: row.patchVersion,\n deleted: row.deleted,\n ttl: ttlInterval,\n ttlMs,\n inactivatedAt: inactivatedAtTimestamp,\n inactivatedAtMs,\n };\n });\n\n lc.debug?.(`Batch flushing ${rows.length} desire updates`);\n\n return tx`\n INSERT INTO ${this.#cvr('desires')} (\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"ttlMs\",\n \"inactivatedAt\",\n \"inactivatedAtMs\"\n )\n SELECT\n \"clientGroupID\",\n \"clientID\",\n \"queryHash\",\n \"patchVersion\",\n \"deleted\",\n \"ttl\",\n \"ttlMs\",\n CASE\n WHEN \"inactivatedAt\" IS NULL THEN NULL\n -- Divide by 1000 because postgres.js serializeTimestamp treats numbers as ms\n -- and to_timestamp expects seconds. This matches non-batched behavior.\n ELSE to_timestamp(\"inactivatedAt\" / 1000.0)\n END,\n \"inactivatedAtMs\"\n FROM json_to_recordset(${rows}) AS x(\n \"clientGroupID\" TEXT,\n \"clientID\" TEXT,\n \"queryHash\" TEXT,\n \"patchVersion\" TEXT,\n \"deleted\" BOOLEAN,\n \"ttl\" INTERVAL,\n \"ttlMs\" DOUBLE PRECISION,\n \"inactivatedAt\" DOUBLE PRECISION,\n \"inactivatedAtMs\" DOUBLE PRECISION\n )\n ON CONFLICT (\"clientGroupID\", \"clientID\", \"queryHash\") DO UPDATE SET\n \"patchVersion\" = excluded.\"patchVersion\",\n \"deleted\" = excluded.\"deleted\",\n \"ttl\" = excluded.\"ttl\",\n \"ttlMs\" = excluded.\"ttlMs\",\n \"inactivatedAt\" = excluded.\"inactivatedAt\",\n \"inactivatedAtMs\" = excluded.\"inactivatedAtMs\"\n `;\n }\n\n /**\n * Also checks that the CVR has not been pinned to a user other than\n * `pinnedUser` since it was loaded: the first pin does not change the CVR\n * version, so the version check alone would let two view-syncers that both\n * loaded an unpinned CVR pin it to different users in turn.\n */\n async #checkVersionAndOwnership(\n lc: LogContext,\n tx: PostgresTransaction,\n expectedCurrentVersion: CVRVersion,\n lastConnectTime: number,\n pinnedUser: PinnedUser | null,\n ): Promise<void> {\n const start = Date.now();\n lc.debug?.('checking cvr version and ownership');\n const expected = versionString(expectedCurrentVersion);\n\n if (expected === EMPTY_CVR_VERSION.stateVersion) {\n // SELECT FOR UPDATE cannot lock a row that does not exist. Ensure that\n // new CVRs have a row before taking the lock so concurrent first flushes\n // serialize on the primary key. After a conflicting insert commits, the\n // SELECT below sees its version and rejects the stale writer.\n await tx`\n INSERT INTO ${this.#cvr('instances')} (\n \"clientGroupID\",\n \"version\",\n \"lastActive\"\n )\n VALUES (\n ${this.#id},\n ${EMPTY_CVR_VERSION.stateVersion},\n to_timestamp(0)\n )\n ON CONFLICT (\"clientGroupID\") DO NOTHING\n `;\n }\n\n const result = await tx<\n Pick<InstancesRow, 'version' | 'owner' | 'grantedAt' | 'pinnedUser'>[]\n >`SELECT \"version\", \"owner\", \"grantedAt\", \"pinnedUser\"\n FROM ${this.#cvr('instances')}\n WHERE \"clientGroupID\" = ${this.#id}\n FOR UPDATE`;\n const {\n version,\n owner,\n grantedAt,\n pinnedUser: storedPinnedUser = null,\n } = result.length > 0\n ? result[0]\n : {\n version: EMPTY_CVR_VERSION.stateVersion,\n owner: null,\n grantedAt: null,\n };\n lc.debug?.(\n 'checked cvr version and ownership in ' + (Date.now() - start) + ' ms',\n );\n if (owner !== this.#taskID && (grantedAt ?? 0) > lastConnectTime) {\n throw new OwnershipError(owner, grantedAt, lastConnectTime);\n }\n // Checked before the version so that a flusher that lost both races is\n // told about the pin, which its caller acts on, not the stale version.\n if (storedPinnedUser !== null && storedPinnedUser.id !== pinnedUser?.id) {\n throw new PinnedUserMismatchError();\n }\n if (version !== expected) {\n throw new ConcurrentModificationException(\n `Expected ${expected}, got ${version}`,\n );\n }\n }\n\n async #flush(\n lc: LogContext,\n expectedCurrentVersion: CVRVersion,\n cvr: CVRSnapshot,\n lastConnectTime: number,\n verifyNoop: boolean,\n ): Promise<CVRFlushStats | null> {\n const stats: CVRFlushStats = {\n instances: 0,\n queries: 0,\n desires: 0,\n clients: 0,\n rows: 0,\n rowsDeferred: 0,\n statements: 0,\n };\n if (this.#pendingRowRecordUpdates.size) {\n const existingRowRecords = await this.getRowRecords();\n this.#rowCount = existingRowRecords.size;\n for (const [id, row] of this.#pendingRowRecordUpdates.entries()) {\n if (this.#forceUpdates.has(id)) {\n continue;\n }\n const existing = existingRowRecords.get(id);\n if (\n // Don't delete or add an unreferenced row if it's not in the CVR.\n (existing === undefined && !row?.refCounts) ||\n // Don't write a row record that exactly matches what's in the CVR.\n deepEqual(\n (row ?? undefined) as ReadonlyJSONValue | undefined,\n existing as ReadonlyJSONValue | undefined,\n )\n ) {\n this.#pendingRowRecordUpdates.delete(id);\n }\n }\n }\n if (\n this.#pendingRowRecordUpdates.size === 0 &&\n this.#writes.size === 0 &&\n this.#pendingInstanceWrite === undefined &&\n this.#pendingQueryUpdates.size === 0 &&\n this.#pendingQueryPartialUpdates.size === 0 &&\n this.#pendingDesireUpdates.size === 0\n ) {\n if (verifyNoop) {\n // Nothing to write, but the caller has already acted on the CVR\n // (e.g. poked patches computed against it), so it still needs to know\n // whether the CVR is current. Otherwise a stale view-syncer whose\n // writes happen to match what another view-syncer already committed\n // never learns that it is behind.\n await runTx(\n this.#db,\n tx =>\n this.#checkVersionAndOwnership(\n lc,\n tx,\n expectedCurrentVersion,\n lastConnectTime,\n cvr.pinnedUser,\n ),\n {mode: Mode.READ_COMMITTED},\n );\n }\n return null;\n }\n // Note: The CVR instance itself is only updated if there are material\n // changes (i.e. changes to the CVR contents) to flush.\n this.putInstance(cvr);\n const start = Date.now();\n lc.debug?.('flush tx beginning');\n\n // Use an async callback so we can await the version/ownership check and\n // validate it INSIDE the transaction. If validation fails, the exception\n // causes postgres.js to ROLLBACK, ensuring no writes are committed on error.\n const results = await runTx(\n this.#db,\n async tx => {\n lc.debug?.(`flush tx begun after ${Date.now() - start} ms`);\n\n // Acquire row-level lock and validate version/ownership before queuing writes.\n // Throwing here (inside the begin callback) rolls back the transaction so that\n // no writes are committed when concurrent modification or ownership errors occur.\n await this.#checkVersionAndOwnership(\n lc,\n tx,\n expectedCurrentVersion,\n lastConnectTime,\n cvr.pinnedUser,\n );\n\n const writeQueries = [];\n if (this.#pendingInstanceWrite) {\n writeQueries.push(this.#pendingInstanceWrite(tx, lastConnectTime));\n stats.instances++;\n stats.statements++;\n }\n for (const write of this.#writes) {\n stats.clients += write.stats.clients ?? 0;\n stats.rows += write.stats.rows ?? 0;\n\n writeQueries.push(write.write(tx, lastConnectTime));\n stats.statements++;\n }\n\n // Batch flush config writes\n // Flush queries first (desires depend on queries via foreign key)\n const hasQueryUpdates =\n this.#pendingQueryUpdates.size > 0 ||\n this.#pendingQueryPartialUpdates.size > 0;\n\n const desireFlush = this.#flushDesires(tx, lc);\n\n let queryFlushes: PendingQuery<Row[]>[] = [];\n if (hasQueryUpdates) {\n queryFlushes = this.#flushQueries(tx, lc);\n\n // Count both full updates and partial-only updates\n const partialOnlyCount = [\n ...this.#pendingQueryPartialUpdates.keys(),\n ].filter(key => !this.#pendingQueryUpdates.has(key)).length;\n\n stats.queries = this.#pendingQueryUpdates.size + partialOnlyCount;\n stats.statements +=\n (this.#pendingQueryUpdates.size > 0 ? 1 : 0) +\n (partialOnlyCount > 0 ? 1 : 0);\n\n if (desireFlush) {\n stats.desires = this.#pendingDesireUpdates.size;\n stats.statements++;\n }\n } else if (desireFlush) {\n stats.desires = this.#pendingDesireUpdates.size;\n stats.statements++;\n }\n\n const rowUpdates = this.#rowCache.executeRowUpdates(\n tx,\n cvr.version,\n this.#pendingRowRecordUpdates,\n 'allow-defer',\n lc,\n );\n stats.statements += rowUpdates.length;\n\n // Pipeline writes now that the version check has passed.\n const pipelined = [\n ...writeQueries,\n ...queryFlushes,\n ...(desireFlush ? [desireFlush] : []),\n ...rowUpdates,\n ];\n\n lc.debug?.(`returning ${pipelined.length} queries for pipelining`);\n\n // Explicitly await all pipelined queries. When the begin callback is async,\n // postgres.js does not call Promise.all() on the return value the way it does\n // for sync callbacks, so we must do it ourselves.\n return Promise.all(pipelined);\n },\n {mode: Mode.READ_COMMITTED},\n );\n\n lc.debug?.(`flush tx completed after ${Date.now() - start} ms`);\n\n // Calculate how many row update queries were in the pipeline.\n // Note: the version check was awaited separately and is not in the results array.\n const baseQueries =\n (this.#pendingInstanceWrite ? 1 : 0) +\n this.#writes.size +\n (this.#pendingQueryUpdates.size > 0 ? 1 : 0) +\n (some(\n this.#pendingQueryPartialUpdates.keys(),\n key => !this.#pendingQueryUpdates.has(key),\n )\n ? 1\n : 0) +\n (this.#pendingDesireUpdates.size > 0 ? 1 : 0);\n const rowUpdateCount = results.length - baseQueries;\n\n const rowsFlushed = rowUpdateCount > 0;\n if (!rowsFlushed) {\n stats.rowsDeferred = this.#pendingRowRecordUpdates.size;\n } else {\n stats.rows += this.#pendingRowRecordUpdates.size;\n }\n\n this.#rowCount = await this.#rowCache.apply(\n this.#pendingRowRecordUpdates,\n cvr.version,\n rowsFlushed,\n );\n recordRowsSynced(this.#rowCount);\n\n return stats;\n }\n\n get rowCount(): number {\n return this.#rowCount;\n }\n\n /**\n * Flushes pending writes, checking that `expectedCurrentVersion` is still\n * the current version of the CVR and that this task still owns it.\n *\n * If there is nothing to write, the flush is a no-op and no check is done,\n * unless `verifyNoop` is set.\n */\n async flush(\n lc: LogContext,\n expectedCurrentVersion: CVRVersion,\n cvr: CVRSnapshot,\n lastConnectTime: number,\n verifyNoop = false,\n ): Promise<CVRFlushStats | null> {\n const start = performance.now();\n lc = lc.withContext('cvrFlushID', flushCounter++);\n try {\n const stats = await this.#flush(\n lc,\n expectedCurrentVersion,\n cvr,\n lastConnectTime,\n verifyNoop,\n );\n this.#pendingRowSetSignatures.forEach(\n applyPendingRowSetSignature,\n this.#rowSetSignatures,\n );\n if (stats) {\n const elapsed = performance.now() - start;\n lc.info?.(\n `flushed cvr@${versionString(cvr.version)} ` +\n `${JSON.stringify(stats)} in (${elapsed} ms)`,\n );\n this.#rowCache.recordSyncFlushStats(stats, elapsed);\n }\n this.#cvrFlushes.add(1, {\n [RESULT_ATTRIBUTE]: 'success',\n [CVR_FLUSH_TYPE_ATTRIBUTE]: stats ? 'sync' : 'noop',\n });\n return stats;\n } catch (e) {\n this.#cvrFlushes.add(1, {\n [RESULT_ATTRIBUTE]: 'error',\n [CVR_FLUSH_TYPE_ATTRIBUTE]: 'sync',\n [ERROR_KIND_ATTRIBUTE]: cvrErrorKind(e),\n });\n // Clear cached state if an error (e.g. ConcurrentModificationException) is encountered.\n this.#rowCache.clear();\n throw e;\n } finally {\n this.discardPending();\n }\n }\n\n /**\n * Discards the writes queued since the last {@link flush}, i.e. those of\n * an updater that is abandoned without being flushed (such as an\n * advancement that aborts with a pipeline reset). Otherwise they would be\n * written by the next flush, even though the poke that carried them to the\n * clients was cancelled.\n *\n * The row cache is not affected: it only learns of writes when they are\n * flushed.\n */\n discardPending(): void {\n this.#writes.clear();\n this.#pendingInstanceWrite = undefined;\n this.#pendingRowRecordUpdates.clear();\n this.#forceUpdates.clear();\n this.#pendingQueryUpdates.clear();\n this.#pendingDesireUpdates.clear();\n this.#pendingQueryPartialUpdates.clear();\n this.#pendingRowSetSignatures.clear();\n }\n\n hasPendingUpdates(): boolean {\n return this.#rowCache.hasPendingUpdates();\n }\n\n /** Resolves when all pending updates are flushed. */\n flushed(lc: LogContext): Promise<void> {\n return this.#rowCache.flushed(lc);\n }\n\n async inspectQueries(\n lc: LogContext,\n ttlClock: TTLClock,\n clientID?: string,\n ): Promise<InspectQueryRow[]> {\n const db = this.#db;\n const clientGroupID = this.#id;\n\n const reader = new TransactionPool(lc, {mode: Mode.READONLY}).run(db);\n try {\n return await reader.processReadTask(\n tx => tx<InspectQueryRow[]>`\n SELECT DISTINCT ON (d.\"clientID\", d.\"queryHash\")\n d.\"clientID\",\n d.\"queryHash\" AS \"queryID\",\n COALESCE(d.\"ttlMs\", ${DEFAULT_TTL_MS}) AS \"ttl\",\n d.\"inactivatedAtMs\" AS \"inactivatedAt\",\n (SELECT COUNT(*)::INT FROM ${this.#cvr('rows')} r \n WHERE r.\"clientGroupID\" = d.\"clientGroupID\" \n AND r.\"refCounts\" ? d.\"queryHash\") AS \"rowCount\",\n q.\"clientAST\" AS \"ast\",\n (q.\"patchVersion\" IS NOT NULL) AS \"got\",\n COALESCE(d.\"deleted\", FALSE) AS \"deleted\",\n q.\"queryName\" AS \"name\",\n q.\"queryArgs\" AS \"args\"\n FROM ${this.#cvr('desires')} d\n LEFT JOIN ${this.#cvr('queries')} q\n ON q.\"clientGroupID\" = d.\"clientGroupID\"\n AND q.\"queryHash\" = d.\"queryHash\"\n WHERE d.\"clientGroupID\" = ${clientGroupID}\n ${clientID ? tx`AND d.\"clientID\" = ${clientID}` : tx``}\n AND NOT (\n d.\"inactivatedAtMs\" IS NOT NULL \n AND d.\"ttlMs\" IS NOT NULL \n AND (d.\"inactivatedAtMs\" + d.\"ttlMs\") <= ${ttlClockAsNumber(ttlClock)}\n )\n ORDER BY d.\"clientID\", d.\"queryHash\"`,\n );\n } finally {\n reader.setDone();\n }\n }\n}\n\n/**\n * This is similar to {@link CVRStore.#checkVersionAndOwnership} except\n * that it only checks the version and is suitable for snapshot reads\n * (i.e. by doing a plain `SELECT` rather than a `SELECT ... FOR UPDATE`).\n */\nexport async function checkVersion(\n tx: PostgresTransaction,\n schema: string,\n clientGroupID: string,\n expectedCurrentVersion: CVRVersion,\n): Promise<void> {\n const expected = versionString(expectedCurrentVersion);\n const result = await tx<Pick<InstancesRow, 'version'>[]>`\n SELECT version FROM ${tx(schema)}.instances \n WHERE \"clientGroupID\" = ${clientGroupID}`;\n const {version} =\n result.length > 0 ? result[0] : {version: EMPTY_CVR_VERSION.stateVersion};\n if (version !== expected) {\n throw new ConcurrentModificationException(\n `Expected ${expected}, got ${version}`,\n );\n }\n}\n\nexport class ClientNotFoundError extends ProtocolErrorWithLevel {\n constructor(message: string) {\n super(\n {\n kind: ErrorKind.ClientNotFound,\n message,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n}\n\nexport class ConcurrentModificationException extends ProtocolErrorWithLevel {\n readonly name = 'ConcurrentModificationException';\n\n constructor(detail: string) {\n super(\n {\n kind: ErrorKind.Rehome,\n message: `CVR has been concurrently modified. ${detail}`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n}\n\n/**\n * The CVR was pinned to a different user than the one the flushing\n * view-syncer validated, so that view-syncer's connections can no longer be\n * trusted: it should stop and let its clients reconnect.\n */\nexport class PinnedUserMismatchError extends ConcurrentModificationException {\n constructor() {\n super('Client group was pinned to a different user');\n }\n}\n\nexport class OwnershipError extends ProtocolErrorWithLevel {\n readonly name = 'OwnershipError';\n\n constructor(\n owner: string | null,\n grantedAt: number | null,\n lastConnectTime: number,\n ) {\n super(\n {\n kind: ErrorKind.Rehome,\n message:\n `CVR ownership was transferred to ${owner} at ` +\n `${new Date(grantedAt ?? 0).toISOString()} ` +\n `(last connect time: ${new Date(lastConnectTime).toISOString()})`,\n maxBackoffMs: 0,\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n }\n}\n\nclass InvalidClientASTError extends ProtocolErrorWithLevel {\n readonly name = 'InvalidClientASTError';\n\n constructor(queryHash: string, error: string) {\n super(\n {\n kind: ErrorKind.SchemaVersionNotSupported,\n message: `Could not parse AST of query ${queryHash} stored in CVR: ${error}`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n );\n }\n}\n\nclass InvalidClientSchemaError extends ProtocolErrorWithLevel {\n readonly name = 'InvalidClientSchemaError';\n\n constructor(cause: unknown) {\n super(\n {\n kind: ErrorKind.SchemaVersionNotSupported,\n message: `Could not parse clientSchema stored in CVR: ${String(cause)}`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'warn',\n {cause},\n );\n }\n}\n\nfunction cvrErrorKind(e: unknown): string {\n if (e instanceof ClientNotFoundError) {\n return 'client_not_found';\n }\n if (e instanceof ConcurrentModificationException) {\n return 'concurrent_modification';\n }\n if (e instanceof OwnershipError) {\n return 'ownership';\n }\n if (e instanceof InvalidClientSchemaError) {\n return 'invalid_client_schema';\n }\n if (e instanceof InvalidClientASTError) {\n return 'invalid_client_ast';\n }\n return 'error';\n}\n\nexport class RowsVersionBehindError extends Error {\n readonly name = 'RowsVersionBehindError';\n readonly cvrVersion: string;\n readonly rowsVersion: string | null;\n\n constructor(cvrVersion: string, rowsVersion: string | null) {\n super(`rowsVersion (${rowsVersion}) is behind CVR ${cvrVersion}`);\n this.cvrVersion = cvrVersion;\n this.rowsVersion = rowsVersion;\n }\n}\n"],"mappings":"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"}
|
|
@@ -127,6 +127,17 @@ var CVRUpdater = class {
|
|
|
127
127
|
return this._cvr.version;
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
|
+
* Records the user the client group is pinned to. The pin is set once, by
|
|
131
|
+
* the group's first validated connection, and never changes afterwards.
|
|
132
|
+
*/
|
|
133
|
+
setPinnedUser(pinnedUser) {
|
|
134
|
+
const current = this._cvr.pinnedUser;
|
|
135
|
+
if (current === null) {
|
|
136
|
+
this._cvr.pinnedUser = pinnedUser;
|
|
137
|
+
this._cvrStore.putInstance(this._cvr);
|
|
138
|
+
} else assert(current.id === pinnedUser.id, () => `Client group is pinned to a different user (${current.id} vs ${pinnedUser.id})`);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
130
141
|
* @param verifyNoop Check that the original CVR is still current even if
|
|
131
142
|
* there is nothing to write. See {@link CVRStore.flush}.
|
|
132
143
|
*/
|