@rocicorp/zero 1.4.0 → 1.5.0-canary.1
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/analyze-query/src/analyze-cli.js +2 -2
- package/out/analyze-query/src/analyze-cli.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/auth/auth.d.ts +1 -1
- package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
- package/out/zero-cache/src/auth/auth.js +1 -1
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.d.ts +1 -1
- package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
- package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
- package/out/zero-cache/src/config/normalize.js +8 -0
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +8 -4
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +28 -6
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom/fetch.d.ts +1 -1
- package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
- package/out/zero-cache/src/custom/fetch.js +2 -2
- package/out/zero-cache/src/custom/fetch.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.d.ts +21 -7
- package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +26 -9
- package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +2 -1
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.d.ts.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +5 -2
- package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
- package/out/zero-cache/src/server/syncer.js +3 -3
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +24 -20
- 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 +258 -45
- 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 +119 -83
- 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.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +1 -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.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +2 -1
- 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.d.ts +1 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +3 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/http-service.d.ts +1 -0
- package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/http-service.js +5 -4
- package/out/zero-cache/src/services/http-service.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +1 -2
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts.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.d.ts +4 -3
- package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +57 -38
- package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
- package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js +2 -1
- package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +1 -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.d.ts +41 -27
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +147 -104
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts +6 -0
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +8 -0
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +3 -3
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +119 -86
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js +2 -2
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js +7 -7
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +11 -10
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/connection.d.ts +15 -7
- package/out/zero-client/src/client/connection.d.ts.map +1 -1
- package/out/zero-client/src/client/connection.js.map +1 -1
- package/out/zero-client/src/client/crud-impl.d.ts +1 -1
- package/out/zero-client/src/client/crud-impl.d.ts.map +1 -1
- package/out/zero-client/src/client/crud-impl.js +1 -1
- package/out/zero-client/src/client/crud-impl.js.map +1 -1
- package/out/zero-client/src/client/crud.d.ts +1 -1
- package/out/zero-client/src/client/crud.d.ts.map +1 -1
- package/out/zero-client/src/client/crud.js +1 -1
- package/out/zero-client/src/client/crud.js.map +1 -1
- package/out/zero-client/src/client/keys.d.ts +1 -1
- package/out/zero-client/src/client/keys.d.ts.map +1 -1
- package/out/zero-client/src/client/keys.js.map +1 -1
- package/out/zero-client/src/client/make-replicache-mutators.js +1 -1
- package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
- package/out/zero-client/src/client/mutation-tracker.d.ts +2 -1
- package/out/zero-client/src/client/mutation-tracker.d.ts.map +1 -1
- package/out/zero-client/src/client/mutation-tracker.js +3 -3
- package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
- 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 +2 -2
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-client/src/types/client-state.d.ts +1 -1
- package/out/zero-client/src/types/client-state.d.ts.map +1 -1
- package/out/zero-protocol/src/custom-queries.js +1 -1
- package/out/zero-protocol/src/down.js +1 -1
- package/out/zero-protocol/src/error-kind-enum.d.ts +1 -2
- package/out/zero-protocol/src/error-kind-enum.d.ts.map +1 -1
- package/out/zero-protocol/src/error-kind-enum.js.map +1 -1
- package/out/zero-protocol/src/mutate-server.d.ts +165 -0
- package/out/zero-protocol/src/mutate-server.d.ts.map +1 -0
- package/out/zero-protocol/src/mutate-server.js +24 -0
- package/out/zero-protocol/src/mutate-server.js.map +1 -0
- package/out/zero-protocol/src/mutation.d.ts +229 -0
- package/out/zero-protocol/src/mutation.d.ts.map +1 -0
- package/out/zero-protocol/src/mutation.js +112 -0
- package/out/zero-protocol/src/mutation.js.map +1 -0
- package/out/zero-protocol/src/mutations-patch.js +1 -1
- package/out/zero-protocol/src/mutations-patch.js.map +1 -1
- package/out/zero-protocol/src/push.d.ts +3 -234
- package/out/zero-protocol/src/push.d.ts.map +1 -1
- package/out/zero-protocol/src/push.js +3 -114
- package/out/zero-protocol/src/push.js.map +1 -1
- package/out/zero-protocol/src/query-server.d.ts +150 -0
- package/out/zero-protocol/src/query-server.d.ts.map +1 -0
- package/out/zero-protocol/src/query-server.js +16 -0
- package/out/zero-protocol/src/query-server.js.map +1 -0
- package/out/zero-protocol/src/up.js +1 -1
- package/out/zero-server/src/mod.d.ts +4 -2
- package/out/zero-server/src/mod.d.ts.map +1 -1
- package/out/zero-server/src/process-mutations.d.ts +50 -4
- package/out/zero-server/src/process-mutations.d.ts.map +1 -1
- package/out/zero-server/src/process-mutations.js +73 -36
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zero-server/src/push-processor.d.ts +3 -3
- package/out/zero-server/src/push-processor.d.ts.map +1 -1
- package/out/zero-server/src/push-processor.js.map +1 -1
- package/out/zero-server/src/queries/process-queries.d.ts +45 -53
- package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
- package/out/zero-server/src/queries/process-queries.js +72 -53
- package/out/zero-server/src/queries/process-queries.js.map +1 -1
- package/out/zero-server/src/zql-database.js.map +1 -1
- package/out/zero-types/src/default-types.d.ts +1 -0
- package/out/zero-types/src/default-types.d.ts.map +1 -1
- package/out/zql/src/builder/builder.d.ts.map +1 -1
- package/out/zql/src/builder/builder.js +17 -7
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/ivm/cap.d.ts +32 -0
- package/out/zql/src/ivm/cap.d.ts.map +1 -0
- package/out/zql/src/ivm/cap.js +205 -0
- package/out/zql/src/ivm/cap.js.map +1 -0
- package/out/zql/src/ivm/constraint.js +1 -1
- package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
- package/out/zql/src/ivm/flipped-join.js +61 -15
- package/out/zql/src/ivm/flipped-join.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +3 -4
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/schema.d.ts +8 -0
- package/out/zql/src/ivm/schema.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +2 -2
- package/out/zql/src/mutate/mutator-registry.js.map +1 -1
- package/out/zql/src/mutate/mutator.d.ts +11 -2
- package/out/zql/src/mutate/mutator.d.ts.map +1 -1
- package/out/zql/src/mutate/mutator.js.map +1 -1
- package/out/zql/src/query/query-registry.d.ts +9 -2
- package/out/zql/src/query/query-registry.d.ts.map +1 -1
- package/out/zql/src/query/query-registry.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +4 -1
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assert
|
|
1
|
+
import { assert } from "../../../../shared/src/asserts.js";
|
|
2
2
|
import { getErrorMessage } from "../../../../shared/src/error.js";
|
|
3
3
|
import { groupBy } from "../../../../shared/src/arrays.js";
|
|
4
4
|
import { must } from "../../../../shared/src/must.js";
|
|
@@ -7,8 +7,10 @@ import { Server, ZeroCache } from "../../../../zero-protocol/src/error-origin-en
|
|
|
7
7
|
import { HTTP, Internal, OutOfOrderMutation, UnsupportedPushVersion } from "../../../../zero-protocol/src/error-reason-enum.js";
|
|
8
8
|
import { isProtocolError } from "../../../../zero-protocol/src/error.js";
|
|
9
9
|
import { Custom } from "../../../../zero-protocol/src/mutation-type-enum.js";
|
|
10
|
-
import { CLEANUP_RESULTS_MUTATION_NAME
|
|
10
|
+
import { CLEANUP_RESULTS_MUTATION_NAME } from "../../../../zero-protocol/src/mutation.js";
|
|
11
|
+
import "../../../../zero-protocol/src/push.js";
|
|
11
12
|
import "../../config/zero-config.js";
|
|
13
|
+
import { mutateResponseSchema } from "../../../../zero-protocol/src/mutate-server.js";
|
|
12
14
|
import { getOrCreateCounter } from "../../observability/metrics.js";
|
|
13
15
|
import { Queue } from "../../../../shared/src/queue.js";
|
|
14
16
|
import { Subscription } from "../../types/subscription.js";
|
|
@@ -33,7 +35,7 @@ import { ROOT_CONTEXT, context, propagation } from "@opentelemetry/api";
|
|
|
33
35
|
*/
|
|
34
36
|
var PusherService = class {
|
|
35
37
|
id;
|
|
36
|
-
#
|
|
38
|
+
#connContextManager;
|
|
37
39
|
#pusher;
|
|
38
40
|
#queue;
|
|
39
41
|
#config;
|
|
@@ -41,24 +43,24 @@ var PusherService = class {
|
|
|
41
43
|
#stopped;
|
|
42
44
|
#refCount = 0;
|
|
43
45
|
#isStopped = false;
|
|
44
|
-
constructor(appConfig, lc, clientGroupID,
|
|
45
|
-
this.#
|
|
46
|
+
constructor(appConfig, lc, clientGroupID, connContextManager) {
|
|
47
|
+
this.#connContextManager = connContextManager;
|
|
46
48
|
this.#config = appConfig;
|
|
47
49
|
this.#lc = lc.withContext("component", "pusherService");
|
|
48
50
|
this.#queue = new Queue();
|
|
49
|
-
this.#pusher = new PushWorker(appConfig, lc, this.#
|
|
51
|
+
this.#pusher = new PushWorker(appConfig, lc, this.#connContextManager, this.#queue);
|
|
50
52
|
this.id = clientGroupID;
|
|
51
53
|
}
|
|
52
54
|
initConnection(selector) {
|
|
53
55
|
return this.#pusher.initConnection(selector);
|
|
54
56
|
}
|
|
55
57
|
enqueuePush(selector, push) {
|
|
56
|
-
this.#pusher.enqueuePush(this.#
|
|
58
|
+
this.#pusher.enqueuePush(this.#connContextManager.mustGetConnectionContext(selector), push);
|
|
57
59
|
return { type: "ok" };
|
|
58
60
|
}
|
|
59
61
|
async ackMutationResponses(requester, upToID) {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
+
const connCtx = this.#connContextManager.getConnectionContext(requester);
|
|
63
|
+
if (!connCtx?.mutateContext?.url) return;
|
|
62
64
|
const cleanupBody = {
|
|
63
65
|
clientGroupID: this.id,
|
|
64
66
|
mutations: [{
|
|
@@ -79,7 +81,7 @@ var PusherService = class {
|
|
|
79
81
|
requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`
|
|
80
82
|
};
|
|
81
83
|
try {
|
|
82
|
-
await fetchFromAPIServer(
|
|
84
|
+
await fetchFromAPIServer(mutateResponseSchema, "push", this.#lc, connCtx, {
|
|
83
85
|
appID: this.#config.app.id,
|
|
84
86
|
shardNum: this.#config.shard.num
|
|
85
87
|
}, cleanupBody);
|
|
@@ -95,8 +97,8 @@ var PusherService = class {
|
|
|
95
97
|
*/
|
|
96
98
|
async deleteClientMutations(requester, clientIDs) {
|
|
97
99
|
if (clientIDs.length === 0) return;
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
100
|
+
const connCtx = this.#connContextManager.getConnectionContext(requester);
|
|
101
|
+
if (!connCtx?.mutateContext?.url) return;
|
|
100
102
|
const cleanupBody = {
|
|
101
103
|
clientGroupID: this.id,
|
|
102
104
|
mutations: [{
|
|
@@ -116,7 +118,7 @@ var PusherService = class {
|
|
|
116
118
|
requestID: `cleanup-bulk-${this.id}-${Date.now()}`
|
|
117
119
|
};
|
|
118
120
|
try {
|
|
119
|
-
await fetchFromAPIServer(
|
|
121
|
+
await fetchFromAPIServer(mutateResponseSchema, "push", this.#lc, connCtx, {
|
|
120
122
|
appID: this.#config.app.id,
|
|
121
123
|
shardNum: this.#config.shard.num
|
|
122
124
|
}, cleanupBody);
|
|
@@ -152,16 +154,16 @@ var PusherService = class {
|
|
|
152
154
|
* to the user's API server.
|
|
153
155
|
*/
|
|
154
156
|
var PushWorker = class {
|
|
155
|
-
#
|
|
157
|
+
#connContextManager;
|
|
156
158
|
#queue;
|
|
157
159
|
#lc;
|
|
158
160
|
#config;
|
|
159
161
|
#clients;
|
|
160
162
|
#customMutations = getOrCreateCounter("mutation", "custom", "Number of custom mutations processed");
|
|
161
163
|
#pushes = getOrCreateCounter("mutation", "pushes", "Number of pushes processed by the pusher");
|
|
162
|
-
constructor(config, lc,
|
|
164
|
+
constructor(config, lc, connContextManager, queue) {
|
|
163
165
|
this.#lc = lc.withContext("component", "pusher");
|
|
164
|
-
this.#
|
|
166
|
+
this.#connContextManager = connContextManager;
|
|
165
167
|
this.#queue = queue;
|
|
166
168
|
this.#config = config;
|
|
167
169
|
this.#clients = /* @__PURE__ */ new Map();
|
|
@@ -183,10 +185,10 @@ var PushWorker = class {
|
|
|
183
185
|
});
|
|
184
186
|
return downstream;
|
|
185
187
|
}
|
|
186
|
-
enqueuePush(
|
|
188
|
+
enqueuePush(connCtx, push) {
|
|
187
189
|
this.#queue.enqueue({
|
|
188
190
|
push,
|
|
189
|
-
|
|
191
|
+
connCtx
|
|
190
192
|
});
|
|
191
193
|
}
|
|
192
194
|
async run() {
|
|
@@ -207,7 +209,7 @@ var PushWorker = class {
|
|
|
207
209
|
*/
|
|
208
210
|
#fanOutResponses(response) {
|
|
209
211
|
const connectionTerminations = [];
|
|
210
|
-
if ("kind" in response || "error" in response) {
|
|
212
|
+
if ("kind" in response && response.kind === "PushFailed" || "error" in response) {
|
|
211
213
|
this.#lc.warn?.("The server behind ZERO_MUTATE_URL returned a push error.", response);
|
|
212
214
|
const groupedMutationIDs = groupBy(response.mutationIDs ?? [], (m) => m.clientID);
|
|
213
215
|
for (const [clientID, mutationIDs] of groupedMutationIDs) {
|
|
@@ -236,8 +238,7 @@ var PushWorker = class {
|
|
|
236
238
|
message: response.error === "zeroPusher" ? response.details : response.error === "unsupportedSchemaVersion" ? "Unsupported schema version" : "An unknown error occurred while pushing to the API server"
|
|
237
239
|
};
|
|
238
240
|
this.#failDownstream(client.downstream, pushFailedBody);
|
|
239
|
-
} else
|
|
240
|
-
else unreachable(response);
|
|
241
|
+
} else this.#failDownstream(client.downstream, response);
|
|
241
242
|
}
|
|
242
243
|
} else {
|
|
243
244
|
const groupedMutations = groupBy(response.mutations, (m) => m.id.clientID);
|
|
@@ -274,7 +275,7 @@ var PushWorker = class {
|
|
|
274
275
|
this.#customMutations.add(entry.push.mutations.length, { clientGroupID: entry.push.clientGroupID });
|
|
275
276
|
this.#pushes.add(1, { clientGroupID: entry.push.clientGroupID });
|
|
276
277
|
recordMutation("custom", entry.push.mutations.length);
|
|
277
|
-
const url = must(entry.
|
|
278
|
+
const url = must(entry.connCtx.mutateContext.url, "ZERO_MUTATE_URL is not set");
|
|
278
279
|
this.#lc.debug?.("pushing to", url, "with", entry.push.mutations.length, "mutations");
|
|
279
280
|
let mutationIDs = [];
|
|
280
281
|
try {
|
|
@@ -282,21 +283,24 @@ var PushWorker = class {
|
|
|
282
283
|
id: m.id,
|
|
283
284
|
clientID: m.clientID
|
|
284
285
|
}));
|
|
285
|
-
const response = await fetchFromAPIServer(
|
|
286
|
+
const response = await fetchFromAPIServer(mutateResponseSchema, "push", this.#lc, entry.connCtx, {
|
|
286
287
|
appID: this.#config.app.id,
|
|
287
288
|
shardNum: this.#config.shard.num
|
|
288
289
|
}, entry.push);
|
|
289
|
-
if ("kind" in response || "error" in response) {
|
|
290
|
+
if ("kind" in response && response.kind === "PushFailed" || "error" in response) {
|
|
290
291
|
if (isAuthErrorBody(response)) {
|
|
291
292
|
this.#lc.warn?.("Push auth failed; invalidating connection", {
|
|
292
|
-
clientID: entry.
|
|
293
|
+
clientID: entry.connCtx.clientID,
|
|
293
294
|
response: "kind" in response ? response.message : void 0
|
|
294
295
|
});
|
|
295
|
-
this.#
|
|
296
|
+
this.#connContextManager.failConnection(entry.connCtx, entry.connCtx.revision);
|
|
296
297
|
}
|
|
297
298
|
return response;
|
|
298
299
|
}
|
|
299
|
-
this.#
|
|
300
|
+
this.#connContextManager.validateConnection(entry.connCtx, entry.connCtx.revision, "kind" in response && response.kind === "MutateResponse" && response?.userID !== void 0 ? {
|
|
301
|
+
kind: "server-validated",
|
|
302
|
+
validatedUserID: response.userID
|
|
303
|
+
} : { kind: "client-fallback" });
|
|
300
304
|
return response;
|
|
301
305
|
} catch (e) {
|
|
302
306
|
if (isProtocolError(e) && e.errorBody.kind === "PushFailed") {
|
|
@@ -306,13 +310,28 @@ var PushWorker = class {
|
|
|
306
310
|
};
|
|
307
311
|
if (isAuthErrorBody(response)) {
|
|
308
312
|
this.#lc.warn?.("Push auth failed; invalidating connection", {
|
|
309
|
-
clientID: entry.
|
|
313
|
+
clientID: entry.connCtx.clientID,
|
|
310
314
|
response: "kind" in response ? response.message : void 0
|
|
311
315
|
});
|
|
312
|
-
this.#
|
|
316
|
+
this.#connContextManager.failConnection(entry.connCtx, entry.connCtx.revision);
|
|
313
317
|
}
|
|
314
318
|
return response;
|
|
315
319
|
}
|
|
320
|
+
if (isProtocolError(e) && isAuthErrorBody(e.errorBody)) {
|
|
321
|
+
this.#lc.warn?.("Push validation failed; invalidating connection", {
|
|
322
|
+
clientID: entry.connCtx.clientID,
|
|
323
|
+
response: e.message
|
|
324
|
+
});
|
|
325
|
+
this.#connContextManager.failConnection(entry.connCtx, entry.connCtx.revision);
|
|
326
|
+
return {
|
|
327
|
+
kind: PushFailed,
|
|
328
|
+
origin: ZeroCache,
|
|
329
|
+
reason: HTTP,
|
|
330
|
+
message: e.message,
|
|
331
|
+
status: 401,
|
|
332
|
+
mutationIDs
|
|
333
|
+
};
|
|
334
|
+
}
|
|
316
335
|
return {
|
|
317
336
|
kind: PushFailed,
|
|
318
337
|
origin: ZeroCache,
|
|
@@ -354,7 +373,7 @@ function combinePushes(entries) {
|
|
|
354
373
|
}
|
|
355
374
|
for (const entry of entries) {
|
|
356
375
|
if (entry === "stop" || entry === void 0) return [collect(), true];
|
|
357
|
-
const key = `${entry.
|
|
376
|
+
const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;
|
|
358
377
|
const existing = pushesByConnection.get(key);
|
|
359
378
|
if (existing) existing.push(entry);
|
|
360
379
|
else pushesByConnection.set(key, [entry]);
|
|
@@ -362,16 +381,16 @@ function combinePushes(entries) {
|
|
|
362
381
|
return [collect(), false];
|
|
363
382
|
}
|
|
364
383
|
function assertAreCompatiblePushes(left, right) {
|
|
365
|
-
assert(left.
|
|
366
|
-
assert(left.
|
|
367
|
-
assert(left.
|
|
368
|
-
assert(authEquals(left.
|
|
384
|
+
assert(left.connCtx.clientID === right.connCtx.clientID, "clientID must be the same for all pushes");
|
|
385
|
+
assert(left.connCtx.wsID === right.connCtx.wsID, "wsID must be the same for all pushes");
|
|
386
|
+
assert(left.connCtx.revision === right.connCtx.revision, "revision must be the same for all pushes");
|
|
387
|
+
assert(authEquals(left.connCtx.auth, right.connCtx.auth), "auth must be the same for all pushes with the same clientID");
|
|
369
388
|
assert(left.push.schemaVersion === right.push.schemaVersion, "schemaVersion must be the same for all pushes with the same clientID");
|
|
370
389
|
assert(left.push.pushVersion === right.push.pushVersion, "pushVersion must be the same for all pushes with the same clientID");
|
|
371
|
-
assert(left.
|
|
372
|
-
assert(left.
|
|
373
|
-
assert(left.
|
|
374
|
-
assert(left.
|
|
390
|
+
assert(left.connCtx.mutateContext.headerOptions.cookie === right.connCtx.mutateContext.headerOptions.cookie, "httpCookie must be the same for all pushes with the same clientID");
|
|
391
|
+
assert(left.connCtx.mutateContext.headerOptions.origin === right.connCtx.mutateContext.headerOptions.origin, "origin must be the same for all pushes with the same clientID");
|
|
392
|
+
assert(left.connCtx.user.id === right.connCtx.user.id, "userID must be the same for all pushes with the same clientID");
|
|
393
|
+
assert(left.connCtx.mutateContext.url === right.connCtx.mutateContext.url, "userPushURL must be the same for all pushes with the same clientID");
|
|
375
394
|
}
|
|
376
395
|
//#endregion
|
|
377
396
|
export { PusherService };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pusher.js","names":["#contextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {ROOT_CONTEXT, context, propagation} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {\n CLEANUP_RESULTS_MUTATION_NAME,\n pushResponseSchema,\n type MutationID,\n type PushBody,\n type PushResponse,\n} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #contextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n contextManager: ConnectionContextManager,\n ) {\n this.#contextManager = contextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#contextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n this.#pusher.enqueuePush(\n this.#contextManager.mustGetConnectionContext(selector),\n push,\n );\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const ctx = this.#contextManager.getConnectionContext(requester);\n if (!ctx?.pushContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n pushResponseSchema,\n 'push',\n this.#lc,\n ctx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const ctx = this.#contextManager.getConnectionContext(requester);\n if (!ctx?.pushContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n pushResponseSchema,\n 'push',\n this.#lc,\n ctx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n context: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #contextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n contextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#contextManager = contextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(context: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n context,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: PushResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if ('kind' in response || 'error' in response) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else if ('kind' in response) {\n this.#failDownstream(client.downstream, response);\n } else {\n unreachable(response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<PushResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.context.pushContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n pushResponseSchema,\n 'push',\n this.#lc,\n entry.context,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n );\n if ('kind' in response || 'error' in response) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.context.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#contextManager.failConnection(\n entry.context,\n entry.context.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#contextManager.validateConnection(\n entry.context,\n entry.context.revision,\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.context.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#contextManager.failConnection(\n entry.context,\n entry.context.revision,\n );\n }\n return response;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.context.clientID}:${entry.context.wsID}:${entry.context.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.context.clientID === right.context.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.context.wsID === right.context.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.context.revision === right.context.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.context.auth, right.context.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.context.pushContext.headerOptions.cookie ===\n right.context.pushContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.context.pushContext.headerOptions.origin ===\n right.context.pushContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.context.userID === right.context.userID,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.context.pushContext.url === right.context.pushContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pusher.js","names":["#connContextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {context, propagation, ROOT_CONTEXT} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport {\n mutateResponseSchema,\n type MutateResponse,\n} from '../../../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../../../zero-protocol/src/mutation-id.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {CLEANUP_RESULTS_MUTATION_NAME} from '../../../../zero-protocol/src/mutation.ts';\nimport {type PushBody} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n connContextManager: ConnectionContextManager,\n ) {\n this.#connContextManager = connContextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#connContextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n this.#pusher.enqueuePush(\n this.#connContextManager.mustGetConnectionContext(selector),\n push,\n );\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n connCtx: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #connContextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n connContextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#connContextManager = connContextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(connCtx: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n connCtx,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: MutateResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else {\n this.#failDownstream(client.downstream, response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<MutateResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.connCtx.mutateContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n entry.connCtx,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n );\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#connContextManager.validateConnection(\n entry.connCtx,\n entry.connCtx.revision,\n 'kind' in response &&\n response.kind === 'MutateResponse' &&\n response?.userID !== undefined\n ? {kind: 'server-validated', validatedUserID: response.userID}\n : {kind: 'client-fallback'},\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n\n if (isProtocolError(e) && isAuthErrorBody(e.errorBody)) {\n // The push completed far enough for local validation to reject the\n // connection, so invalidate it and surface the result as PushFailed.\n this.#lc.warn?.('Push validation failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: e.message,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n message: e.message,\n status: 401,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.connCtx.clientID === right.connCtx.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.connCtx.wsID === right.connCtx.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.connCtx.revision === right.connCtx.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.connCtx.auth, right.connCtx.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.cookie ===\n right.connCtx.mutateContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.origin ===\n right.connCtx.mutateContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.user.id === right.connCtx.user.id,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.url === right.connCtx.mutateContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,IAAa,gBAAb,MAAsD;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA,YAAY;CACZ,aAAa;CAEb,YACE,WACA,IACA,eACA,oBACA;AACA,QAAA,qBAA2B;AAC3B,QAAA,SAAe;AACf,QAAA,KAAW,GAAG,YAAY,aAAa,gBAAgB;AACvD,QAAA,QAAc,IAAI,OAAO;AACzB,QAAA,SAAe,IAAI,WACjB,WACA,IACA,MAAA,oBACA,MAAA,MACD;AACD,OAAK,KAAK;;CAGZ,eAAe,UAA8B;AAC3C,SAAO,MAAA,OAAa,eAAe,SAAS;;CAG9C,YACE,UACA,MACsC;AACtC,QAAA,OAAa,YACX,MAAA,mBAAyB,yBAAyB,SAAS,EAC3D,KACD;AAED,SAAO,EACL,MAAM,MACP;;CAGH,MAAM,qBACJ,WACA,QACe;EACf,MAAM,UAAU,MAAA,mBAAyB,qBAAqB,UAAU;AACxE,MAAI,CAAC,SAAS,eAAe,IAE3B;EAGF,MAAM,cAAwB;GAC5B,eAAe,KAAK;GACpB,WAAW,CACT;IACE,MAAM;IACN,IAAI;IACJ,UAAU,OAAO;IACjB,MAAM;IACN,MAAM,CACJ;KACE,MAAM;KACN,eAAe,KAAK;KACpB,UAAU,OAAO;KACjB,gBAAgB,OAAO;KACxB,CACF;IACD,WAAW,KAAK,KAAK;IACtB,CACF;GACD,aAAa;GACb,WAAW,KAAK,KAAK;GACrB,WAAW,WAAW,KAAK,GAAG,GAAG,OAAO,SAAS,GAAG,OAAO;GAC5D;AAED,MAAI;AACF,SAAM,mBACJ,sBACA,QACA,MAAA,IACA,SACA;IAAC,OAAO,MAAA,OAAa,IAAI;IAAI,UAAU,MAAA,OAAa,MAAM;IAAI,EAC9D,YACD;WACM,GAAG;AACV,SAAA,GAAS,OAAO,mCAAmC,EACjD,OAAO,gBAAgB,EAAE,EAC1B,CAAC;;;;;;;;;CAUN,MAAM,sBACJ,WACA,WACe;AACf,MAAI,UAAU,WAAW,EACvB;EAGF,MAAM,UAAU,MAAA,mBAAyB,qBAAqB,UAAU;AACxE,MAAI,CAAC,SAAS,eAAe,IAE3B;EAGF,MAAM,cAAwB;GAC5B,eAAe,KAAK;GACpB,WAAW,CACT;IACE,MAAM;IACN,IAAI;IACJ,UAAU,UAAU;IACpB,MAAM;IACN,MAAM,CACJ;KACE,MAAM;KACN,eAAe,KAAK;KACpB;KACD,CACF;IACD,WAAW,KAAK,KAAK;IACtB,CACF;GACD,aAAa;GACb,WAAW,KAAK,KAAK;GACrB,WAAW,gBAAgB,KAAK,GAAG,GAAG,KAAK,KAAK;GACjD;AAED,MAAI;AACF,SAAM,mBACJ,sBACA,QACA,MAAA,IACA,SACA;IAAC,OAAO,MAAA,OAAa,IAAI;IAAI,UAAU,MAAA,OAAa,MAAM;IAAI,EAC9D,YACD;WACM,GAAG;AACV,SAAA,GAAS,OAAO,wCAAwC,EACtD,OAAO,gBAAgB,EAAE,EAC1B,CAAC;;;CAIN,MAAM;AACJ,SAAO,CAAC,MAAA,WAAiB,mCAAmC;AAC5D,IAAE,MAAA;;CAGJ,QAAQ;AACN,SAAO,CAAC,MAAA,WAAiB,mCAAmC;AAC5D,IAAE,MAAA;AACF,MAAI,MAAA,YAAkB,EACf,MAAK,MAAM;;CAIpB,UAAmB;AACjB,SAAO,MAAA,WAAiB;;CAG1B,MAAqB;AACnB,QAAA,UAAgB,MAAA,OAAa,KAAK;AAClC,SAAO,MAAA;;CAGT,OAAsB;AACpB,MAAI,MAAA,UACF,QAAO,KAAK,MAAA,SAAe,+BAA+B;AAE5D,QAAA,YAAkB;AAClB,QAAA,MAAY,QAAQ,OAAO;AAC3B,SAAO,KAAK,MAAA,SAAe,+BAA+B;;;;;;;AAc9D,IAAM,aAAN,MAAiB;CACf;CACA;CACA;CACA;CACA;CAKA,mBAA4B,mBAC1B,YACA,UACA,uCACD;CACD,UAAmB,mBACjB,YACA,UACA,2CACD;CAED,YACE,QACA,IACA,oBACA,OACA;AACA,QAAA,KAAW,GAAG,YAAY,aAAa,SAAS;AAChD,QAAA,qBAA2B;AAC3B,QAAA,QAAc;AACd,QAAA,SAAe;AACf,QAAA,0BAAgB,IAAI,KAAK;;;;;;CAO3B,eAAe,UAA8B;EAC3C,MAAM,WAAW,MAAA,QAAc,IAAI,SAAS,SAAS;AACrD,MAAI,YAAY,SAAS,SAAS,SAAS,KAEzC,OAAM,IAAI,MAAM,qCAAqC;AAIvD,MAAI,SACF,UAAS,WAAW,QAAQ;EAG9B,MAAM,aAAa,aAAa,OAAmB,EACjD,eAAe;AACb,SAAA,QAAc,OAAO,SAAS,SAAS;KAE1C,CAAC;AACF,QAAA,QAAc,IAAI,SAAS,UAAU;GACnC,MAAM,SAAS;GACf;GACD,CAAC;AACF,SAAO;;CAGT,YAAY,SAA4B,MAAgB;AACtD,QAAA,MAAY,QAAQ;GAClB;GACA;GACD,CAAC;;CAGJ,MAAM,MAAM;AACV,WAAS;GAGP,MAAM,CAAC,QAAQ,aAAa,cAAc,CAF7B,MAAM,MAAA,MAAY,SAAS,EAES,GADpC,MAAA,MAAY,OAAO,CACyB,CAAC;AAC1D,QAAK,MAAM,QAAQ,QAAQ;IACzB,MAAM,gBAAgB,KAAK,KAAK,cAC5B,YAAY,QAAQ,cAAc,EAChC,aAAa,KAAK,KAAK,aACxB,CAAC,GACF,QAAQ,QAAQ;IACpB,MAAM,WAAW,MAAM,QAAQ,KAAK,qBAClC,MAAA,YAAkB,KAAK,CACxB;AACD,UAAM,MAAA,gBAAsB,SAAS;;AAGvC,OAAI,UACF;;;;;;;;CAUN,iBAAiB,UAA0B;EACzC,MAAM,yBAAyC,EAAE;AAGjD,MACG,UAAU,YAAY,SAAS,SAAS,gBACzC,WAAW,UACX;AACA,SAAA,GAAS,OACP,4DACA,SACD;GAID,MAAM,qBAAqB,QACzB,SAAS,eAAe,EAAE,GAC1B,MAAK,EAAE,SACR;AACD,QAAK,MAAM,CAAC,UAAU,gBAAgB,oBAAoB;IACxD,MAAM,SAAS,MAAA,QAAc,IAAI,SAAS;AAC1C,QAAI,CAAC,OACH;AAKF,QAAI,WAAW,UAAU;KAGvB,MAAM,iBACJ,SAAS,UAAU,SACf;MACE,MAAM;MACN,QAAQ;MACR,QAAQ;MACR,QAAQ,SAAS;MACjB,aAAa,SAAS;MACtB;MACA,SAAS,gDAAgD,SAAS;MACnE,GACD,SAAS,UAAU,2BACjB;MACE,MAAM;MACN,QAAQ;MACR,QAAQ;MACR;MACA,SAAS;MACV,GACD;MACE,MAAM;MACN,QAAQ;MACR,QAAQ;MACR;MACA,SACE,SAAS,UAAU,eACf,SAAS,UACT,SAAS,UAAU,6BACjB,+BACA;MACT;AAET,WAAA,eAAqB,OAAO,YAAY,eAAe;UAEvD,OAAA,eAAqB,OAAO,YAAY,SAAS;;SAGhD;GAIL,MAAM,mBAAmB,QAAQ,SAAS,YAAW,MAAK,EAAE,GAAG,SAAS;AACxE,QAAK,MAAM,CAAC,UAAU,cAAc,kBAAkB;IACpD,MAAM,SAAS,MAAA,QAAc,IAAI,SAAS;AAC1C,QAAI,CAAC,OACH;IAGF,IAAI;IACJ,IAAI,IAAI;AACR,WAAO,IAAI,UAAU,QAAQ,KAAK;KAChC,MAAM,IAAI,UAAU;AACpB,SAAI,WAAW,EAAE,OACf,OAAA,GAAS,OACP,gEACA,EAAE,OACH;AAKH,SAAI,WAAW,EAAE,UAAU,EAAE,OAAO,UAAU,eAAe;AAC3D,gBAAU;OACR,MAAM;OACN,QAAQ;OACR,QAAQ;OACR,SAAS;OACT,SAAS,EAAE,OAAO;OAClB,aAAa,UAAU,KAAI,OAAM;QAC/B,UAAU,EAAE,GAAG;QACf,IAAI,EAAE,GAAG;QACV,EAAE;OACJ;AACD;;;AAIJ,QAAI,WAAW,IAAI,UAAU,SAAS,EACpC,OAAA,GAAS,OACP,sFACD;AAGH,QAAI,QACF,wBAAuB,WACrB,MAAA,eAAqB,OAAO,YAAY,QAAQ,CACjD;;;AAKP,yBAAuB,SAAQ,OAAM,IAAI,CAAC;;CAG5C,OAAA,YAAmB,OAA6C;AAC9D,QAAA,gBAAsB,IAAI,MAAM,KAAK,UAAU,QAAQ,EACrD,eAAe,MAAM,KAAK,eAC3B,CAAC;AACF,QAAA,OAAa,IAAI,GAAG,EAClB,eAAe,MAAM,KAAK,eAC3B,CAAC;AAGF,iBAAe,UAAU,MAAM,KAAK,UAAU,OAAO;EAErD,MAAM,MAAM,KACV,MAAM,QAAQ,cAAc,KAC5B,6BACD;AAED,QAAA,GAAS,QACP,cACA,KACA,QACA,MAAM,KAAK,UAAU,QACrB,YACD;EAED,IAAI,cAA4B,EAAE;AAElC,MAAI;AACF,iBAAc,MAAM,KAAK,UAAU,KAAI,OAAM;IAC3C,IAAI,EAAE;IACN,UAAU,EAAE;IACb,EAAE;GAEH,MAAM,WAAW,MAAM,mBACrB,sBACA,QACA,MAAA,IACA,MAAM,SACN;IACE,OAAO,MAAA,OAAa,IAAI;IACxB,UAAU,MAAA,OAAa,MAAM;IAC9B,EACD,MAAM,KACP;AACD,OACG,UAAU,YAAY,SAAS,SAAS,gBACzC,WAAW,UACX;AACA,QAAI,gBAAgB,SAAS,EAAE;AAC7B,WAAA,GAAS,OAAO,6CAA6C;MAC3D,UAAU,MAAM,QAAQ;MACxB,UAAU,UAAU,WAAW,SAAS,UAAU,KAAA;MACnD,CAAC;AACF,WAAA,mBAAyB,eACvB,MAAM,SACN,MAAM,QAAQ,SACf;;AAEH,WAAO;;AAIT,SAAA,mBAAyB,mBACvB,MAAM,SACN,MAAM,QAAQ,UACd,UAAU,YACR,SAAS,SAAS,oBAClB,UAAU,WAAW,KAAA,IACnB;IAAC,MAAM;IAAoB,iBAAiB,SAAS;IAAO,GAC5D,EAAC,MAAM,mBAAkB,CAC9B;AACD,UAAO;WACA,GAAG;AACV,OAAI,gBAAgB,EAAE,IAAI,EAAE,UAAU,SAAS,cAAsB;IACnE,MAAM,WAAW;KACf,GAAG,EAAE;KACL;KACD;AACD,QAAI,gBAAgB,SAAS,EAAE;AAC7B,WAAA,GAAS,OAAO,6CAA6C;MAC3D,UAAU,MAAM,QAAQ;MACxB,UAAU,UAAU,WAAW,SAAS,UAAU,KAAA;MACnD,CAAC;AACF,WAAA,mBAAyB,eACvB,MAAM,SACN,MAAM,QAAQ,SACf;;AAEH,WAAO;;AAGT,OAAI,gBAAgB,EAAE,IAAI,gBAAgB,EAAE,UAAU,EAAE;AAGtD,UAAA,GAAS,OAAO,mDAAmD;KACjE,UAAU,MAAM,QAAQ;KACxB,UAAU,EAAE;KACb,CAAC;AACF,UAAA,mBAAyB,eACvB,MAAM,SACN,MAAM,QAAQ,SACf;AACD,WAAO;KACL,MAAM;KACN,QAAQ;KACR,QAAQ;KACR,SAAS,EAAE;KACX,QAAQ;KACR;KACD;;AAGH,UAAO;IACL,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,SAAS,mBAAmB,gBAAgB,EAAE;IAC9C;IACD;;;CAIL,gBACE,YACA,WACM;AACN,aAAW,KAAK,IAAI,uBAAuB,WAAW,OAAO,CAAC;;;;;;;;;AAUlE,SAAgB,cACd,SAC0B;CAC1B,MAAM,qCAAqB,IAAI,KAA4B;CAE3D,SAAS,UAAU;EACjB,MAAM,MAAqB,EAAE;AAC7B,OAAK,MAAM,WAAW,mBAAmB,QAAQ,EAAE;GACjD,MAAM,YAAyB;IAC7B,GAAG,QAAQ;IACX,MAAM;KACJ,GAAG,QAAQ,GAAG;KACd,WAAW,EAAE;KACd;IACF;AACD,OAAI,KAAK,UAAU;AACnB,QAAK,MAAM,SAAS,SAAS;AAC3B,8BAA0B,WAAW,MAAM;AAC3C,cAAU,KAAK,UAAU,KAAK,GAAG,MAAM,KAAK,UAAU;;;AAG1D,SAAO;;AAGT,MAAK,MAAM,SAAS,SAAS;AAC3B,MAAI,UAAU,UAAU,UAAU,KAAA,EAChC,QAAO,CAAC,SAAS,EAAE,KAAK;EAG1B,MAAM,MAAM,GAAG,MAAM,QAAQ,SAAS,GAAG,MAAM,QAAQ,KAAK,GAAG,MAAM,QAAQ;EAC7E,MAAM,WAAW,mBAAmB,IAAI,IAAI;AAC5C,MAAI,SACF,UAAS,KAAK,MAAM;MAEpB,oBAAmB,IAAI,KAAK,CAAC,MAAM,CAAC;;AAIxC,QAAO,CAAC,SAAS,EAAE,MAAM;;AAK3B,SAAS,0BAA0B,MAAmB,OAAoB;AACxE,QACE,KAAK,QAAQ,aAAa,MAAM,QAAQ,UACxC,2CACD;AACD,QACE,KAAK,QAAQ,SAAS,MAAM,QAAQ,MACpC,uCACD;AACD,QACE,KAAK,QAAQ,aAAa,MAAM,QAAQ,UACxC,2CACD;AACD,QACE,WAAW,KAAK,QAAQ,MAAM,MAAM,QAAQ,KAAK,EACjD,8DACD;AACD,QACE,KAAK,KAAK,kBAAkB,MAAM,KAAK,eACvC,uEACD;AACD,QACE,KAAK,KAAK,gBAAgB,MAAM,KAAK,aACrC,qEACD;AACD,QACE,KAAK,QAAQ,cAAc,cAAc,WACvC,MAAM,QAAQ,cAAc,cAAc,QAC5C,oEACD;AACD,QACE,KAAK,QAAQ,cAAc,cAAc,WACvC,MAAM,QAAQ,cAAc,cAAc,QAC5C,gEACD;AACD,QACE,KAAK,QAAQ,KAAK,OAAO,MAAM,QAAQ,KAAK,IAC5C,gEACD;AACD,QACE,KAAK,QAAQ,cAAc,QAAQ,MAAM,QAAQ,cAAc,KAC/D,qEACD"}
|
|
@@ -19,7 +19,8 @@ var ShadowSyncService = class {
|
|
|
19
19
|
}
|
|
20
20
|
async run() {
|
|
21
21
|
const { intervalMs, sampleRate, maxRowsPerTable, textCopy } = this.#options;
|
|
22
|
-
const
|
|
22
|
+
const minFirstRunDelay = Math.floor(intervalMs * 2 / 3);
|
|
23
|
+
const firstRunDelay = minFirstRunDelay + Math.floor(Math.random() * (intervalMs - minFirstRunDelay));
|
|
23
24
|
this.#lc.info?.(`shadow-syncer started; first run in ${firstRunDelay} ms, then every ${intervalMs} ms`);
|
|
24
25
|
await this.#state.sleep(firstRunDelay);
|
|
25
26
|
while (this.#state.shouldRun()) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shadow-sync-service.js","names":["#lc","#shard","#upstreamURI","#context","#options","#state"],"sources":["../../../../../../zero-cache/src/services/shadow-sync/shadow-sync-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport type {ShardConfig} from '../../types/shards.ts';\nimport type {ServerContext} from '../change-source/pg/initial-sync.ts';\nimport {shadowInitialSync} from '../change-source/pg/initial-sync.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nexport type ShadowSyncOptions = {\n intervalMs: number;\n sampleRate: number;\n maxRowsPerTable: number;\n textCopy?: boolean | undefined;\n};\n\nexport class ShadowSyncService implements Service {\n readonly id = 'shadow-syncer';\n\n readonly #lc: LogContext;\n readonly #shard: ShardConfig;\n readonly #upstreamURI: string;\n readonly #context: ServerContext;\n readonly #options: ShadowSyncOptions;\n readonly #state = new RunningState('shadow-syncer');\n\n constructor(\n lc: LogContext,\n shard: ShardConfig,\n upstreamURI: string,\n context: ServerContext,\n options: ShadowSyncOptions,\n ) {\n this.#lc = lc;\n this.#shard = shard;\n this.#upstreamURI = upstreamURI;\n this.#context = context;\n this.#options = options;\n }\n\n async run() {\n const {intervalMs, sampleRate, maxRowsPerTable, textCopy} = this.#options;\n\n // Why:
|
|
1
|
+
{"version":3,"file":"shadow-sync-service.js","names":["#lc","#shard","#upstreamURI","#context","#options","#state"],"sources":["../../../../../../zero-cache/src/services/shadow-sync/shadow-sync-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport type {ShardConfig} from '../../types/shards.ts';\nimport type {ServerContext} from '../change-source/pg/initial-sync.ts';\nimport {shadowInitialSync} from '../change-source/pg/initial-sync.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {Service} from '../service.ts';\n\nexport type ShadowSyncOptions = {\n intervalMs: number;\n sampleRate: number;\n maxRowsPerTable: number;\n textCopy?: boolean | undefined;\n};\n\nexport class ShadowSyncService implements Service {\n readonly id = 'shadow-syncer';\n\n readonly #lc: LogContext;\n readonly #shard: ShardConfig;\n readonly #upstreamURI: string;\n readonly #context: ServerContext;\n readonly #options: ShadowSyncOptions;\n readonly #state = new RunningState('shadow-syncer');\n\n constructor(\n lc: LogContext,\n shard: ShardConfig,\n upstreamURI: string,\n context: ServerContext,\n options: ShadowSyncOptions,\n ) {\n this.#lc = lc;\n this.#shard = shard;\n this.#upstreamURI = upstreamURI;\n this.#context = context;\n this.#options = options;\n }\n\n async run() {\n const {intervalMs, sampleRate, maxRowsPerTable, textCopy} = this.#options;\n\n // Why: first run fires in [intervalMs * 2/3, intervalMs) — late enough\n // that shadow sync never fires immediately on task startup, but always\n // before one full interval elapses so the canary completes at least once\n // per task lifetime (the replication manager is restarted every ~24h).\n // The last third is randomized so a fleet-wide restart does not cause\n // every task to canary simultaneously.\n const minFirstRunDelay = Math.floor((intervalMs * 2) / 3);\n const firstRunDelay =\n minFirstRunDelay +\n Math.floor(Math.random() * (intervalMs - minFirstRunDelay));\n this.#lc.info?.(\n `shadow-syncer started; first run in ${firstRunDelay} ms, then every ${intervalMs} ms`,\n );\n await this.#state.sleep(firstRunDelay);\n\n while (this.#state.shouldRun()) {\n const start = performance.now();\n try {\n await shadowInitialSync(\n this.#lc,\n this.#shard,\n this.#upstreamURI,\n {sampleRate, maxRowsPerTable},\n this.#context,\n textCopy !== undefined ? {textCopy} : undefined,\n );\n const elapsed = performance.now() - start;\n this.#lc.info?.(\n `shadow initial-sync completed (${elapsed.toFixed(0)} ms)`,\n );\n } catch (e) {\n const elapsed = performance.now() - start;\n this.#lc.error?.(\n `shadow initial-sync failed after ${elapsed.toFixed(0)} ms`,\n e,\n );\n }\n await this.#state.sleep(intervalMs);\n }\n }\n\n stop(): Promise<void> {\n this.#state.stop(this.#lc);\n return promiseVoid;\n }\n}\n"],"mappings":";;;;AAeA,IAAa,oBAAb,MAAkD;CAChD,KAAc;CAEd;CACA;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,gBAAgB;CAEnD,YACE,IACA,OACA,aACA,SACA,SACA;AACA,QAAA,KAAW;AACX,QAAA,QAAc;AACd,QAAA,cAAoB;AACpB,QAAA,UAAgB;AAChB,QAAA,UAAgB;;CAGlB,MAAM,MAAM;EACV,MAAM,EAAC,YAAY,YAAY,iBAAiB,aAAY,MAAA;EAQ5D,MAAM,mBAAmB,KAAK,MAAO,aAAa,IAAK,EAAE;EACzD,MAAM,gBACJ,mBACA,KAAK,MAAM,KAAK,QAAQ,IAAI,aAAa,kBAAkB;AAC7D,QAAA,GAAS,OACP,uCAAuC,cAAc,kBAAkB,WAAW,KACnF;AACD,QAAM,MAAA,MAAY,MAAM,cAAc;AAEtC,SAAO,MAAA,MAAY,WAAW,EAAE;GAC9B,MAAM,QAAQ,YAAY,KAAK;AAC/B,OAAI;AACF,UAAM,kBACJ,MAAA,IACA,MAAA,OACA,MAAA,aACA;KAAC;KAAY;KAAgB,EAC7B,MAAA,SACA,aAAa,KAAA,IAAY,EAAC,UAAS,GAAG,KAAA,EACvC;IACD,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,UAAA,GAAS,OACP,kCAAkC,QAAQ,QAAQ,EAAE,CAAC,MACtD;YACM,GAAG;IACV,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,UAAA,GAAS,QACP,oCAAoC,QAAQ,QAAQ,EAAE,CAAC,MACvD,EACD;;AAEH,SAAM,MAAA,MAAY,MAAM,WAAW;;;CAIvC,OAAsB;AACpB,QAAA,MAAY,KAAK,MAAA,GAAS;AAC1B,SAAO"}
|
|
@@ -5,7 +5,7 @@ import { parse, valita_exports } from "../../../../shared/src/valita.js";
|
|
|
5
5
|
import { rowSchema } from "../../../../zero-protocol/src/data.js";
|
|
6
6
|
import { ProtocolError } from "../../../../zero-protocol/src/error.js";
|
|
7
7
|
import { primaryKeyValueRecordSchema } from "../../../../zero-protocol/src/primary-key.js";
|
|
8
|
-
import { mutationResultSchema } from "../../../../zero-protocol/src/
|
|
8
|
+
import { mutationResultSchema } from "../../../../zero-protocol/src/mutation.js";
|
|
9
9
|
import { upstreamSchema } from "../../types/shards.js";
|
|
10
10
|
import { getOrCreateCounter, getOrCreateLatencyHistogram } from "../../observability/metrics.js";
|
|
11
11
|
import { cmpVersions, cookieToVersion, versionToCookie, versionToNullableCookie } from "./schema/types.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-handler.js","names":["#clientGroupID","#zeroClientsTable","#zeroMutationsTable","#lc","#downstream","#pokeTime","#pokeTransactions","#pokedRows","#baseVersion","#push","#updateLMIDs"],"sources":["../../../../../../zero-cache/src/services/view-syncer/client-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport type {JSONObject} from '../../../../shared/src/bigint-json.ts';\nimport {\n assertJSONValue,\n type JSONObject as SafeJSONObject,\n} from '../../../../shared/src/json.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Writable} from '../../../../shared/src/writable.ts';\nimport type {ErroredQuery} from '../../../../zero-protocol/src/custom-queries.ts';\nimport {rowSchema} from '../../../../zero-protocol/src/data.ts';\nimport type {DeleteClientsBody} from '../../../../zero-protocol/src/delete-clients.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {\n ProtocolError,\n type TransformFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport type {InspectDownBody} from '../../../../zero-protocol/src/inspect-down.ts';\nimport type {\n PokePartBody,\n PokeStartBody,\n} from '../../../../zero-protocol/src/poke.ts';\nimport {primaryKeyValueRecordSchema} from '../../../../zero-protocol/src/primary-key.ts';\nimport {mutationResultSchema} from '../../../../zero-protocol/src/push.ts';\nimport type {RowPatchOp} from '../../../../zero-protocol/src/row-patch.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {\n getLogLevel,\n wrapWithProtocolError,\n} from '../../types/error-with-level.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport {\n cmpVersions,\n cookieToVersion,\n versionToCookie,\n versionToNullableCookie,\n type CVRVersion,\n type DelQueryPatch,\n type NullableCVRVersion,\n type PutQueryPatch,\n type RowID,\n} from './schema/types.ts';\n\nexport type PutRowPatch = {\n type: 'row';\n op: 'put';\n id: RowID;\n contents: JSONObject;\n};\n\nexport type DeleteRowPatch = {\n type: 'row';\n op: 'del';\n id: RowID;\n};\n\nexport type RowPatch = PutRowPatch | DeleteRowPatch;\nexport type ConfigPatch = DelQueryPatch | PutQueryPatch;\n\nexport type Patch = ConfigPatch | RowPatch;\n\nexport type PatchToVersion = {\n patch: Patch;\n toVersion: CVRVersion;\n};\n\nexport interface PokeHandler {\n addPatch(patch: PatchToVersion): Promise<void>;\n cancel(): Promise<void>;\n end(finalVersion: CVRVersion): Promise<void>;\n}\n\nconst NOOP: PokeHandler = {\n addPatch: () => promiseVoid,\n cancel: () => promiseVoid,\n end: () => promiseVoid,\n};\n\n/** Wraps PokeHandlers for multiple clients in a single PokeHandler. */\nexport function startPoke(\n clients: ClientHandler[],\n tentativeVersion: CVRVersion,\n): PokeHandler {\n const pokers = clients.map(c => c.startPoke(tentativeVersion));\n\n // Promise.allSettled() ensures that a failed (e.g. disconnected) client\n // does not prevent other clients from receiving the pokes. However, the\n // rate (per client group) will be limited by the slowest connection.\n return {\n addPatch: async patch => {\n await Promise.allSettled(pokers.map(poker => poker.addPatch(patch)));\n },\n cancel: async () => {\n await Promise.allSettled(pokers.map(poker => poker.cancel()));\n },\n end: async finalVersion => {\n await Promise.allSettled(pokers.map(poker => poker.end(finalVersion)));\n },\n };\n}\n\n// Semi-arbitrary threshold at which poke body parts are flushed.\n// When row size is being computed, that should be used as a threshold instead.\nconst PART_COUNT_FLUSH_THRESHOLD = 100;\n\n/**\n * Handles a single `ViewSyncer` connection.\n */\nexport class ClientHandler {\n readonly #clientGroupID: string;\n readonly clientID: string;\n readonly wsID: string;\n readonly #zeroClientsTable: string;\n readonly #zeroMutationsTable: string;\n readonly #lc: LogContext;\n readonly #downstream: Subscription<Downstream>;\n #baseVersion: NullableCVRVersion;\n\n readonly #pokeTime = getOrCreateLatencyHistogram(\n 'sync',\n 'poke.time',\n 'Time elapsed for each poke transaction. Canceled / noop pokes are excluded.',\n );\n\n readonly #pokeTransactions = getOrCreateCounter(\n 'sync',\n 'poke.transactions',\n 'Count of poke transactions.',\n );\n\n readonly #pokedRows = getOrCreateCounter(\n 'sync',\n 'poke.rows',\n 'Count of poked rows.',\n );\n\n constructor(\n lc: LogContext,\n clientGroupID: string,\n clientID: string,\n wsID: string,\n shard: ShardID,\n baseCookie: string | null,\n downstream: Subscription<Downstream>,\n ) {\n lc.debug?.('new client handler');\n this.#clientGroupID = clientGroupID;\n this.clientID = clientID;\n this.wsID = wsID;\n this.#zeroClientsTable = `${upstreamSchema(shard)}.clients`;\n this.#zeroMutationsTable = `${upstreamSchema(shard)}.mutations`;\n this.#lc = lc;\n this.#downstream = downstream;\n this.#baseVersion = cookieToVersion(baseCookie);\n }\n\n version(): NullableCVRVersion {\n return this.#baseVersion;\n }\n\n async #push(msg: Downstream): Promise<void> {\n const {result} = this.#downstream.push(msg);\n await result;\n }\n\n fail(e: unknown) {\n this.#lc[getLogLevel(e)]?.(\n `view-syncer closing connection with error: ${String(e)}`,\n e,\n );\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n\n close(reason: string) {\n this.#lc.debug?.(`view-syncer closing connection: ${reason}`);\n this.#downstream.cancel();\n }\n\n startPoke(tentativeVersion: CVRVersion): PokeHandler {\n const pokeID = versionToCookie(tentativeVersion);\n const lc = this.#lc.withContext('pokeID', pokeID);\n\n if (cmpVersions(this.#baseVersion, tentativeVersion) >= 0) {\n lc.info?.(`already caught up, not sending poke.`);\n return NOOP;\n }\n\n const baseCookie = versionToNullableCookie(this.#baseVersion);\n const cookie = versionToCookie(tentativeVersion);\n lc.info?.(`starting poke from ${baseCookie} to ${cookie}`);\n\n const start = performance.now();\n\n const pokeStart: PokeStartBody = {pokeID, baseCookie};\n\n let pokeStarted = false;\n let body: PokePartBody | undefined;\n let partCount = 0;\n const ensureBody = async () => {\n if (!pokeStarted) {\n await this.#push(['pokeStart', pokeStart]);\n pokeStarted = true;\n }\n return (body ??= {pokeID});\n };\n const flushBody = async () => {\n if (body) {\n await this.#push(['pokePart', body]);\n body = undefined;\n partCount = 0;\n }\n };\n\n const addPatch = async (patchToVersion: PatchToVersion) => {\n const {patch, toVersion} = patchToVersion;\n if (cmpVersions(toVersion, this.#baseVersion) <= 0) {\n return;\n }\n const body = await ensureBody();\n\n const {type, op} = patch;\n switch (type) {\n case 'query': {\n const patches = patch.clientID\n ? ((body.desiredQueriesPatches ??= {})[patch.clientID] ??= [])\n : (body.gotQueriesPatch ??= []);\n if (op === 'put') {\n patches.push({op, hash: patch.id});\n } else {\n patches.push({op, hash: patch.id});\n }\n break;\n }\n case 'row':\n if (patch.id.table === this.#zeroClientsTable) {\n this.#updateLMIDs((body.lastMutationIDChanges ??= {}), patch);\n } else if (patch.id.table === this.#zeroMutationsTable) {\n const patches = (body.mutationsPatch ??= []);\n if (op === 'put') {\n const row = v.parse(\n ensureSafeJSON(patch.contents),\n mutationRowSchema,\n 'passthrough',\n );\n patches.push({\n op: 'put',\n mutation: {\n id: {\n clientID: row.clientID,\n id: row.mutationID,\n },\n result: row.result,\n },\n });\n } else {\n const {clientID, mutationID} = patch.id.rowKey;\n assert(\n typeof clientID === 'string',\n 'client id must be a string',\n );\n const id = Number(mutationID);\n assert(\n !Number.isNaN(id) && Number.isFinite(id) && id >= 0,\n 'mutation id must be a finite number',\n );\n patches.push({\n op: 'del',\n id: {\n clientID,\n id,\n },\n });\n }\n } else {\n (body.rowsPatch ??= []).push(makeRowPatch(patch));\n }\n break;\n default:\n unreachable(patch);\n }\n\n if (++partCount >= PART_COUNT_FLUSH_THRESHOLD) {\n await flushBody();\n }\n };\n\n return {\n addPatch: async (patchToVersion: PatchToVersion) => {\n try {\n await addPatch(patchToVersion);\n if (patchToVersion.patch.type === 'row') {\n this.#pokedRows.add(1);\n }\n } catch (e) {\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n },\n\n cancel: async () => {\n if (pokeStarted) {\n await this.#push(['pokeEnd', {pokeID, cookie: '', cancel: true}]);\n }\n },\n\n end: async (finalVersion: CVRVersion) => {\n const cookie = versionToCookie(finalVersion);\n if (!pokeStarted) {\n if (cmpVersions(this.#baseVersion, finalVersion) === 0) {\n return; // Nothing changed and nothing was sent.\n }\n await this.#push(['pokeStart', pokeStart]);\n } else if (cmpVersions(this.#baseVersion, finalVersion) >= 0) {\n // Sanity check: If the poke was started, the finalVersion\n // must be > #baseVersion.\n throw new Error(\n `Patches were sent but finalVersion ${finalVersion} is ` +\n `not greater than baseVersion ${this.#baseVersion}`,\n );\n }\n await flushBody();\n await this.#push(['pokeEnd', {pokeID, cookie}]);\n this.#baseVersion = finalVersion;\n\n const elapsed = performance.now() - start;\n this.#pokeTransactions.add(1);\n this.#pokeTime.recordMs(elapsed);\n },\n };\n }\n\n async sendDeleteClients(\n lc: LogContext,\n deletedClientIDs: string[],\n deletedClientGroupIDs: string[],\n ) {\n const deleteClientsBody: Writable<DeleteClientsBody> = {};\n if (deletedClientIDs.length > 0) {\n deleteClientsBody.clientIDs = deletedClientIDs;\n }\n if (deletedClientGroupIDs.length > 0) {\n deleteClientsBody.clientGroupIDs = deletedClientGroupIDs;\n }\n lc.debug?.('sending deleteClients', deleteClientsBody);\n await this.#push(['deleteClients', deleteClientsBody]);\n }\n\n sendQueryTransformApplicationErrors(errors: ErroredQuery[]) {\n void this.#push(['transformError', errors]);\n }\n\n sendQueryTransformFailedError(error: TransformFailedBody) {\n this.fail(new ProtocolError(error));\n }\n\n sendInspectResponse(lc: LogContext, response: InspectDownBody): void {\n lc.debug?.('sending inspect response', response);\n this.#downstream.push(['inspect', response]);\n }\n\n #updateLMIDs(lmids: Record<string, number>, patch: RowPatch) {\n if (patch.op === 'put') {\n const row = ensureSafeJSON(patch.contents);\n const {clientGroupID, clientID, lastMutationID} = v.parse(\n row,\n lmidRowSchema,\n 'passthrough',\n );\n if (clientGroupID !== this.#clientGroupID) {\n this.#lc.error?.(\n `Received clients row for wrong clientGroupID. Ignoring.`,\n clientGroupID,\n );\n } else {\n lmids[clientID] = lastMutationID;\n }\n } else {\n // The 'constrain' and 'del' ops for clients can be ignored.\n patch.op satisfies 'constrain' | 'del';\n }\n }\n}\n\n// Note: The {APP_ID}_{SHARD_ID}.clients table is set up in replicator/initial-sync.ts.\nconst lmidRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n lastMutationID: v.number(), // Actually returned as a bigint, but converted by ensureSafeJSON().\n});\n\nconst mutationRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n mutationID: v.number(),\n result: mutationResultSchema,\n});\n\nfunction makeRowPatch(patch: RowPatch): RowPatchOp {\n const {\n op,\n id: {table: tableName, rowKey: id},\n } = patch;\n\n switch (op) {\n case 'put':\n return {\n op: 'put',\n tableName,\n value: v.parse(ensureSafeJSON(patch.contents), rowSchema),\n };\n\n case 'del':\n return {\n op,\n tableName,\n id: v.parse(id, primaryKeyValueRecordSchema),\n };\n\n default:\n unreachable(op);\n }\n}\n\n/**\n * Column values of type INT8 are returned as the `bigint` from the\n * Postgres library. These are converted to `number` if they are within\n * the safe Number range, allowing the protocol to support numbers larger\n * than 32-bits. Values outside of the safe number range (e.g. > 2^53) will\n * result in an Error.\n */\nexport function ensureSafeJSON(row: JSONObject): SafeJSONObject {\n const modified = Object.entries(row)\n .filter(([k, v]) => {\n if (typeof v === 'bigint') {\n if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) {\n return true; // send this entry onto the next map() step.\n }\n throw new Error(`Value of \"${k}\" exceeds safe Number range (${v})`);\n } else if (typeof v === 'object') {\n assertJSONValue(v);\n }\n return false;\n })\n .map(([k, v]) => [k, Number(v)]);\n\n return modified.length\n ? {...row, ...Object.fromEntries(modified)}\n : (row as SafeJSONObject);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client-handler.js","names":["#clientGroupID","#zeroClientsTable","#zeroMutationsTable","#lc","#downstream","#pokeTime","#pokeTransactions","#pokedRows","#baseVersion","#push","#updateLMIDs"],"sources":["../../../../../../zero-cache/src/services/view-syncer/client-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport type {JSONObject} from '../../../../shared/src/bigint-json.ts';\nimport {\n assertJSONValue,\n type JSONObject as SafeJSONObject,\n} from '../../../../shared/src/json.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Writable} from '../../../../shared/src/writable.ts';\nimport type {ErroredQuery} from '../../../../zero-protocol/src/custom-queries.ts';\nimport {rowSchema} from '../../../../zero-protocol/src/data.ts';\nimport type {DeleteClientsBody} from '../../../../zero-protocol/src/delete-clients.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {\n ProtocolError,\n type TransformFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport type {InspectDownBody} from '../../../../zero-protocol/src/inspect-down.ts';\nimport {mutationResultSchema} from '../../../../zero-protocol/src/mutation.ts';\nimport type {\n PokePartBody,\n PokeStartBody,\n} from '../../../../zero-protocol/src/poke.ts';\nimport {primaryKeyValueRecordSchema} from '../../../../zero-protocol/src/primary-key.ts';\nimport type {RowPatchOp} from '../../../../zero-protocol/src/row-patch.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {\n getLogLevel,\n wrapWithProtocolError,\n} from '../../types/error-with-level.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport {\n cmpVersions,\n cookieToVersion,\n versionToCookie,\n versionToNullableCookie,\n type CVRVersion,\n type DelQueryPatch,\n type NullableCVRVersion,\n type PutQueryPatch,\n type RowID,\n} from './schema/types.ts';\n\nexport type PutRowPatch = {\n type: 'row';\n op: 'put';\n id: RowID;\n contents: JSONObject;\n};\n\nexport type DeleteRowPatch = {\n type: 'row';\n op: 'del';\n id: RowID;\n};\n\nexport type RowPatch = PutRowPatch | DeleteRowPatch;\nexport type ConfigPatch = DelQueryPatch | PutQueryPatch;\n\nexport type Patch = ConfigPatch | RowPatch;\n\nexport type PatchToVersion = {\n patch: Patch;\n toVersion: CVRVersion;\n};\n\nexport interface PokeHandler {\n addPatch(patch: PatchToVersion): Promise<void>;\n cancel(): Promise<void>;\n end(finalVersion: CVRVersion): Promise<void>;\n}\n\nconst NOOP: PokeHandler = {\n addPatch: () => promiseVoid,\n cancel: () => promiseVoid,\n end: () => promiseVoid,\n};\n\n/** Wraps PokeHandlers for multiple clients in a single PokeHandler. */\nexport function startPoke(\n clients: ClientHandler[],\n tentativeVersion: CVRVersion,\n): PokeHandler {\n const pokers = clients.map(c => c.startPoke(tentativeVersion));\n\n // Promise.allSettled() ensures that a failed (e.g. disconnected) client\n // does not prevent other clients from receiving the pokes. However, the\n // rate (per client group) will be limited by the slowest connection.\n return {\n addPatch: async patch => {\n await Promise.allSettled(pokers.map(poker => poker.addPatch(patch)));\n },\n cancel: async () => {\n await Promise.allSettled(pokers.map(poker => poker.cancel()));\n },\n end: async finalVersion => {\n await Promise.allSettled(pokers.map(poker => poker.end(finalVersion)));\n },\n };\n}\n\n// Semi-arbitrary threshold at which poke body parts are flushed.\n// When row size is being computed, that should be used as a threshold instead.\nconst PART_COUNT_FLUSH_THRESHOLD = 100;\n\n/**\n * Handles a single `ViewSyncer` connection.\n */\nexport class ClientHandler {\n readonly #clientGroupID: string;\n readonly clientID: string;\n readonly wsID: string;\n readonly #zeroClientsTable: string;\n readonly #zeroMutationsTable: string;\n readonly #lc: LogContext;\n readonly #downstream: Subscription<Downstream>;\n #baseVersion: NullableCVRVersion;\n\n readonly #pokeTime = getOrCreateLatencyHistogram(\n 'sync',\n 'poke.time',\n 'Time elapsed for each poke transaction. Canceled / noop pokes are excluded.',\n );\n\n readonly #pokeTransactions = getOrCreateCounter(\n 'sync',\n 'poke.transactions',\n 'Count of poke transactions.',\n );\n\n readonly #pokedRows = getOrCreateCounter(\n 'sync',\n 'poke.rows',\n 'Count of poked rows.',\n );\n\n constructor(\n lc: LogContext,\n clientGroupID: string,\n clientID: string,\n wsID: string,\n shard: ShardID,\n baseCookie: string | null,\n downstream: Subscription<Downstream>,\n ) {\n lc.debug?.('new client handler');\n this.#clientGroupID = clientGroupID;\n this.clientID = clientID;\n this.wsID = wsID;\n this.#zeroClientsTable = `${upstreamSchema(shard)}.clients`;\n this.#zeroMutationsTable = `${upstreamSchema(shard)}.mutations`;\n this.#lc = lc;\n this.#downstream = downstream;\n this.#baseVersion = cookieToVersion(baseCookie);\n }\n\n version(): NullableCVRVersion {\n return this.#baseVersion;\n }\n\n async #push(msg: Downstream): Promise<void> {\n const {result} = this.#downstream.push(msg);\n await result;\n }\n\n fail(e: unknown) {\n this.#lc[getLogLevel(e)]?.(\n `view-syncer closing connection with error: ${String(e)}`,\n e,\n );\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n\n close(reason: string) {\n this.#lc.debug?.(`view-syncer closing connection: ${reason}`);\n this.#downstream.cancel();\n }\n\n startPoke(tentativeVersion: CVRVersion): PokeHandler {\n const pokeID = versionToCookie(tentativeVersion);\n const lc = this.#lc.withContext('pokeID', pokeID);\n\n if (cmpVersions(this.#baseVersion, tentativeVersion) >= 0) {\n lc.info?.(`already caught up, not sending poke.`);\n return NOOP;\n }\n\n const baseCookie = versionToNullableCookie(this.#baseVersion);\n const cookie = versionToCookie(tentativeVersion);\n lc.info?.(`starting poke from ${baseCookie} to ${cookie}`);\n\n const start = performance.now();\n\n const pokeStart: PokeStartBody = {pokeID, baseCookie};\n\n let pokeStarted = false;\n let body: PokePartBody | undefined;\n let partCount = 0;\n const ensureBody = async () => {\n if (!pokeStarted) {\n await this.#push(['pokeStart', pokeStart]);\n pokeStarted = true;\n }\n return (body ??= {pokeID});\n };\n const flushBody = async () => {\n if (body) {\n await this.#push(['pokePart', body]);\n body = undefined;\n partCount = 0;\n }\n };\n\n const addPatch = async (patchToVersion: PatchToVersion) => {\n const {patch, toVersion} = patchToVersion;\n if (cmpVersions(toVersion, this.#baseVersion) <= 0) {\n return;\n }\n const body = await ensureBody();\n\n const {type, op} = patch;\n switch (type) {\n case 'query': {\n const patches = patch.clientID\n ? ((body.desiredQueriesPatches ??= {})[patch.clientID] ??= [])\n : (body.gotQueriesPatch ??= []);\n if (op === 'put') {\n patches.push({op, hash: patch.id});\n } else {\n patches.push({op, hash: patch.id});\n }\n break;\n }\n case 'row':\n if (patch.id.table === this.#zeroClientsTable) {\n this.#updateLMIDs((body.lastMutationIDChanges ??= {}), patch);\n } else if (patch.id.table === this.#zeroMutationsTable) {\n const patches = (body.mutationsPatch ??= []);\n if (op === 'put') {\n const row = v.parse(\n ensureSafeJSON(patch.contents),\n mutationRowSchema,\n 'passthrough',\n );\n patches.push({\n op: 'put',\n mutation: {\n id: {\n clientID: row.clientID,\n id: row.mutationID,\n },\n result: row.result,\n },\n });\n } else {\n const {clientID, mutationID} = patch.id.rowKey;\n assert(\n typeof clientID === 'string',\n 'client id must be a string',\n );\n const id = Number(mutationID);\n assert(\n !Number.isNaN(id) && Number.isFinite(id) && id >= 0,\n 'mutation id must be a finite number',\n );\n patches.push({\n op: 'del',\n id: {\n clientID,\n id,\n },\n });\n }\n } else {\n (body.rowsPatch ??= []).push(makeRowPatch(patch));\n }\n break;\n default:\n unreachable(patch);\n }\n\n if (++partCount >= PART_COUNT_FLUSH_THRESHOLD) {\n await flushBody();\n }\n };\n\n return {\n addPatch: async (patchToVersion: PatchToVersion) => {\n try {\n await addPatch(patchToVersion);\n if (patchToVersion.patch.type === 'row') {\n this.#pokedRows.add(1);\n }\n } catch (e) {\n this.#downstream.fail(wrapWithProtocolError(e));\n }\n },\n\n cancel: async () => {\n if (pokeStarted) {\n await this.#push(['pokeEnd', {pokeID, cookie: '', cancel: true}]);\n }\n },\n\n end: async (finalVersion: CVRVersion) => {\n const cookie = versionToCookie(finalVersion);\n if (!pokeStarted) {\n if (cmpVersions(this.#baseVersion, finalVersion) === 0) {\n return; // Nothing changed and nothing was sent.\n }\n await this.#push(['pokeStart', pokeStart]);\n } else if (cmpVersions(this.#baseVersion, finalVersion) >= 0) {\n // Sanity check: If the poke was started, the finalVersion\n // must be > #baseVersion.\n throw new Error(\n `Patches were sent but finalVersion ${finalVersion} is ` +\n `not greater than baseVersion ${this.#baseVersion}`,\n );\n }\n await flushBody();\n await this.#push(['pokeEnd', {pokeID, cookie}]);\n this.#baseVersion = finalVersion;\n\n const elapsed = performance.now() - start;\n this.#pokeTransactions.add(1);\n this.#pokeTime.recordMs(elapsed);\n },\n };\n }\n\n async sendDeleteClients(\n lc: LogContext,\n deletedClientIDs: string[],\n deletedClientGroupIDs: string[],\n ) {\n const deleteClientsBody: Writable<DeleteClientsBody> = {};\n if (deletedClientIDs.length > 0) {\n deleteClientsBody.clientIDs = deletedClientIDs;\n }\n if (deletedClientGroupIDs.length > 0) {\n deleteClientsBody.clientGroupIDs = deletedClientGroupIDs;\n }\n lc.debug?.('sending deleteClients', deleteClientsBody);\n await this.#push(['deleteClients', deleteClientsBody]);\n }\n\n sendQueryTransformApplicationErrors(errors: ErroredQuery[]) {\n void this.#push(['transformError', errors]);\n }\n\n sendQueryTransformFailedError(error: TransformFailedBody) {\n this.fail(new ProtocolError(error));\n }\n\n sendInspectResponse(lc: LogContext, response: InspectDownBody): void {\n lc.debug?.('sending inspect response', response);\n this.#downstream.push(['inspect', response]);\n }\n\n #updateLMIDs(lmids: Record<string, number>, patch: RowPatch) {\n if (patch.op === 'put') {\n const row = ensureSafeJSON(patch.contents);\n const {clientGroupID, clientID, lastMutationID} = v.parse(\n row,\n lmidRowSchema,\n 'passthrough',\n );\n if (clientGroupID !== this.#clientGroupID) {\n this.#lc.error?.(\n `Received clients row for wrong clientGroupID. Ignoring.`,\n clientGroupID,\n );\n } else {\n lmids[clientID] = lastMutationID;\n }\n } else {\n // The 'constrain' and 'del' ops for clients can be ignored.\n patch.op satisfies 'constrain' | 'del';\n }\n }\n}\n\n// Note: The {APP_ID}_{SHARD_ID}.clients table is set up in replicator/initial-sync.ts.\nconst lmidRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n lastMutationID: v.number(), // Actually returned as a bigint, but converted by ensureSafeJSON().\n});\n\nconst mutationRowSchema = v.object({\n clientGroupID: v.string(),\n clientID: v.string(),\n mutationID: v.number(),\n result: mutationResultSchema,\n});\n\nfunction makeRowPatch(patch: RowPatch): RowPatchOp {\n const {\n op,\n id: {table: tableName, rowKey: id},\n } = patch;\n\n switch (op) {\n case 'put':\n return {\n op: 'put',\n tableName,\n value: v.parse(ensureSafeJSON(patch.contents), rowSchema),\n };\n\n case 'del':\n return {\n op,\n tableName,\n id: v.parse(id, primaryKeyValueRecordSchema),\n };\n\n default:\n unreachable(op);\n }\n}\n\n/**\n * Column values of type INT8 are returned as the `bigint` from the\n * Postgres library. These are converted to `number` if they are within\n * the safe Number range, allowing the protocol to support numbers larger\n * than 32-bits. Values outside of the safe number range (e.g. > 2^53) will\n * result in an Error.\n */\nexport function ensureSafeJSON(row: JSONObject): SafeJSONObject {\n const modified = Object.entries(row)\n .filter(([k, v]) => {\n if (typeof v === 'bigint') {\n if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) {\n return true; // send this entry onto the next map() step.\n }\n throw new Error(`Value of \"${k}\" exceeds safe Number range (${v})`);\n } else if (typeof v === 'object') {\n assertJSONValue(v);\n }\n return false;\n })\n .map(([k, v]) => [k, Number(v)]);\n\n return modified.length\n ? {...row, ...Object.fromEntries(modified)}\n : (row as SafeJSONObject);\n}\n"],"mappings":"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"}
|