@rocicorp/zero 1.11.0-head-22fa3c27-20261014 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/replicache/src/deleted-clients.d.ts +2 -2
- package/out/replicache/src/deleted-clients.js +2 -2
- package/out/replicache/src/deleted-clients.js.map +1 -1
- package/out/replicache/src/persist/client-group-gc.d.ts +1 -1
- package/out/replicache/src/persist/client-group-gc.js +1 -1
- package/out/replicache/src/persist/client-group-gc.js.map +1 -1
- package/out/replicache/src/replicache-options.d.ts +1 -1
- package/out/shared/src/browser-env.d.ts +1 -1
- package/out/shared/src/browser-env.js.map +1 -1
- package/out/shared/src/set-utils.d.ts +1 -1
- package/out/shared/src/set-utils.js +1 -1
- package/out/shared/src/set-utils.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -1
- 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 +4 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +3 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts +4 -4
- package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/drain-tracker.js +4 -8
- package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
- package/out/zero-cache/src/services/http-service.d.ts +6 -0
- package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/http-service.js +11 -0
- package/out/zero-cache/src/services/http-service.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +3 -2
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +17 -4
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/replicator/drain-handler.d.ts +8 -0
- package/out/zero-cache/src/services/replicator/drain-handler.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/drain-handler.js +38 -4
- package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
- package/out/zero-client/src/client/connection-manager.js +1 -1
- package/out/zero-client/src/client/connection-manager.js.map +1 -1
- package/out/zero-client/src/client/custom.d.ts +1 -1
- package/out/zero-client/src/client/custom.js +1 -1
- package/out/zero-client/src/client/custom.js.map +1 -1
- package/out/zero-client/src/client/options.d.ts +6 -6
- package/out/zero-client/src/client/options.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts +1 -1
- package/out/zero-client/src/client/zero.js +1 -1
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-protocol/src/inspect-up.d.ts +3 -3
- package/out/zero-protocol/src/inspect-up.d.ts.map +1 -1
- package/out/zero-protocol/src/inspect-up.js +2 -2
- package/out/zero-protocol/src/inspect-up.js.map +1 -1
- package/out/zero-react/src/use-zero-online.d.ts +1 -1
- package/out/zero-react/src/use-zero-online.js +1 -1
- package/out/zero-react/src/use-zero-online.js.map +1 -1
- package/out/zero-react/src/zero-provider.d.ts +1 -1
- package/out/zero-react/src/zero-provider.js +1 -1
- package/out/zero-react/src/zero-provider.js.map +1 -1
- package/out/zero-server/src/closable-db-transaction.d.ts +1 -1
- package/out/zero-server/src/closable-db-transaction.js +1 -1
- package/out/zero-server/src/closable-db-transaction.js.map +1 -1
- package/out/zero-server/src/custom.d.ts +1 -1
- package/out/zero-server/src/custom.js +1 -1
- package/out/zero-server/src/custom.js.map +1 -1
- package/out/zero-server/src/process-mutations.d.ts +3 -3
- package/out/zero-server/src/process-mutations.js +1 -1
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zero-server/src/push-processor.d.ts +1 -1
- package/out/zero-server/src/push-processor.js +1 -1
- package/out/zero-server/src/push-processor.js.map +1 -1
- package/out/zero-server/src/queries/process-queries.d.ts +3 -3
- package/out/zero-server/src/queries/process-queries.js +2 -2
- package/out/zero-server/src/queries/process-queries.js.map +1 -1
- package/out/zero-server/src/schema.d.ts +1 -1
- package/out/zero-server/src/schema.js +1 -1
- package/out/zero-server/src/schema.js.map +1 -1
- package/out/zero-server/src/zql-database.d.ts +2 -2
- package/out/zero-server/src/zql-database.js +2 -2
- package/out/zero-server/src/zql-database.js.map +1 -1
- package/out/zero-solid/src/use-query.d.ts +2 -2
- package/out/zero-solid/src/use-query.js +1 -1
- package/out/zero-solid/src/use-query.js.map +1 -1
- package/out/zero-solid/src/use-zero-online.d.ts +1 -1
- package/out/zero-solid/src/use-zero-online.js +1 -1
- package/out/zero-solid/src/use-zero-online.js.map +1 -1
- package/out/zero-solid/src/use-zero.d.ts +2 -2
- package/out/zero-solid/src/use-zero.js +2 -2
- package/out/zero-solid/src/use-zero.js.map +1 -1
- package/out/zql/src/ivm/change.d.ts +3 -3
- package/out/zql/src/ivm/change.js.map +1 -1
- package/out/zql/src/ivm/data.d.ts +1 -1
- package/out/zql/src/ivm/data.js.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
- package/out/zql/src/mutate/custom.d.ts +3 -3
- package/out/zql/src/mutate/custom.js.map +1 -1
- package/out/zql/src/query/create-builder.d.ts +1 -1
- package/out/zql/src/query/create-builder.d.ts.map +1 -1
- package/out/zql/src/query/create-builder.js +1 -1
- package/out/zql/src/query/create-builder.js.map +1 -1
- package/out/zql/src/query/named.d.ts +2 -2
- package/out/zql/src/query/named.js +2 -2
- package/out/zql/src/query/named.js.map +1 -1
- package/out/zql/src/query/query-impl.js +3 -3
- package/out/zql/src/query/query-impl.js.map +1 -1
- package/out/zql/src/query/query-internals.d.ts +5 -5
- package/out/zql/src/query/query-internals.js.map +1 -1
- package/out/zql/src/query/query-transitions.d.ts +5 -5
- package/out/zql/src/query/query-transitions.js +2 -2
- package/out/zql/src/query/query-transitions.js.map +1 -1
- package/out/zql/src/query/query.d.ts +8 -8
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import * as v from '../../shared/src/valita.ts';
|
|
|
2
2
|
import type { Read, Write } from './dag/store.ts';
|
|
3
3
|
import { type ClientGroupID, type ClientID } from './sync/ids.ts';
|
|
4
4
|
/**
|
|
5
|
-
* We keep track of deleted clients in the {@
|
|
5
|
+
* We keep track of deleted clients in the {@link DELETED_CLIENTS_HEAD_NAME}
|
|
6
6
|
* head.
|
|
7
7
|
*/
|
|
8
8
|
export declare const DELETED_CLIENTS_HEAD_NAME = "deleted-clients-v2";
|
|
@@ -19,7 +19,7 @@ export declare function removeFromDeletedClients(old: DeletedClients, toRemove:
|
|
|
19
19
|
export declare function setDeletedClients(dagWrite: Write, deletedClients: DeletedClients): Promise<DeletedClients>;
|
|
20
20
|
export declare function getDeletedClients(dagRead: Read): Promise<DeletedClients>;
|
|
21
21
|
/**
|
|
22
|
-
* Adds deleted clients to the {@
|
|
22
|
+
* Adds deleted clients to the {@link DELETED_CLIENTS_HEAD_NAME} head.
|
|
23
23
|
* @returns the new list of deleted clients (sorted and deduped).
|
|
24
24
|
*/
|
|
25
25
|
export declare function addDeletedClients(dagWrite: Write, deletedClientsToAdd: DeletedClients): Promise<DeletedClients>;
|
|
@@ -5,7 +5,7 @@ import { clientGroupIDSchema, clientIDSchema } from "./sync/ids.js";
|
|
|
5
5
|
import { getClients, setClients } from "./persist/clients.js";
|
|
6
6
|
//#region ../replicache/src/deleted-clients.ts
|
|
7
7
|
/**
|
|
8
|
-
* We keep track of deleted clients in the {@
|
|
8
|
+
* We keep track of deleted clients in the {@link DELETED_CLIENTS_HEAD_NAME}
|
|
9
9
|
* head.
|
|
10
10
|
*/
|
|
11
11
|
var DELETED_CLIENTS_HEAD_NAME = "deleted-clients-v2";
|
|
@@ -54,7 +54,7 @@ async function getDeletedClients(dagRead) {
|
|
|
54
54
|
return res.value;
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
* Adds deleted clients to the {@
|
|
57
|
+
* Adds deleted clients to the {@link DELETED_CLIENTS_HEAD_NAME} head.
|
|
58
58
|
* @returns the new list of deleted clients (sorted and deduped).
|
|
59
59
|
*/
|
|
60
60
|
async function addDeletedClients(dagWrite, deletedClientsToAdd) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deleted-clients.js","names":[],"sources":["../../../../replicache/src/deleted-clients.ts"],"sourcesContent":["import {stringCompare} from '../../shared/src/string-compare.ts';\nimport * as v from '../../shared/src/valita.ts';\nimport type {Read, Write} from './dag/store.ts';\nimport {deepFreeze} from './frozen-json.ts';\nimport {getClients, setClients} from './persist/clients.ts';\nimport {\n clientGroupIDSchema,\n clientIDSchema,\n type ClientGroupID,\n type ClientID,\n} from './sync/ids.ts';\n\n/**\n * We keep track of deleted clients in the {@
|
|
1
|
+
{"version":3,"file":"deleted-clients.js","names":[],"sources":["../../../../replicache/src/deleted-clients.ts"],"sourcesContent":["import {stringCompare} from '../../shared/src/string-compare.ts';\nimport * as v from '../../shared/src/valita.ts';\nimport type {Read, Write} from './dag/store.ts';\nimport {deepFreeze} from './frozen-json.ts';\nimport {getClients, setClients} from './persist/clients.ts';\nimport {\n clientGroupIDSchema,\n clientIDSchema,\n type ClientGroupID,\n type ClientID,\n} from './sync/ids.ts';\n\n/**\n * We keep track of deleted clients in the {@link DELETED_CLIENTS_HEAD_NAME}\n * head.\n */\nexport const DELETED_CLIENTS_HEAD_NAME = 'deleted-clients-v2';\n\ntype ClientIDPair = {\n clientGroupID: ClientGroupID;\n clientID: ClientID;\n};\n\nexport type DeletedClients = readonly Readonly<ClientIDPair>[];\n\nexport type WritableDeletedClients = ClientIDPair[];\n\nexport const deletedClientsSchema: v.Type<DeletedClients> = v.readonlyArray(\n v.readonlyObject({\n clientGroupID: clientGroupIDSchema,\n clientID: clientIDSchema,\n }),\n);\n\nfunction compare(a: ClientIDPair, b: ClientIDPair): number {\n const cg = stringCompare(a.clientGroupID, b.clientGroupID);\n if (cg !== 0) {\n return cg;\n }\n return stringCompare(a.clientID, b.clientID);\n}\n\nexport function normalizeDeletedClients(\n deletedClients: DeletedClients,\n): DeletedClients {\n return deletedClients\n .toSorted(compare)\n .filter((item, i, arr) => i === 0 || compare(item, arr[i - 1]) !== 0);\n}\n\nexport function mergeDeletedClients(\n a: DeletedClients,\n b: DeletedClients,\n): DeletedClients {\n const merged: WritableDeletedClients = [];\n a = normalizeDeletedClients(a);\n b = normalizeDeletedClients(b);\n for (let i = 0, j = 0; i < a.length || j < b.length;) {\n if (i < a.length && (j >= b.length || compare(a[i], b[j]) < 0)) {\n merged.push(a[i]);\n i++;\n } else if (j < b.length && (i >= a.length || compare(b[j], a[i]) < 0)) {\n merged.push(b[j]);\n j++;\n } else {\n // equal\n merged.push(a[i]);\n i++;\n j++;\n }\n }\n return merged;\n}\n\nexport function removeFromDeletedClients(\n old: DeletedClients,\n toRemove: DeletedClients,\n): DeletedClients {\n old = normalizeDeletedClients(old);\n toRemove = normalizeDeletedClients(toRemove);\n const result: WritableDeletedClients = [];\n for (let i = 0, j = 0; i < old.length;) {\n if (j >= toRemove.length || compare(old[i], toRemove[j]) < 0) {\n result.push(old[i]);\n i++;\n } else if (j < toRemove.length && compare(old[i], toRemove[j]) === 0) {\n // equal, skip\n i++;\n j++;\n } else {\n // old[i] > toRemove[j]\n j++;\n }\n }\n return result;\n}\n\nexport async function setDeletedClients(\n dagWrite: Write,\n deletedClients: DeletedClients,\n): Promise<DeletedClients> {\n // sort and dedupe\n\n const data = normalizeDeletedClients(deletedClients);\n\n const chunkData = deepFreeze(data);\n const chunk = dagWrite.createChunk(chunkData, []);\n await dagWrite.putChunk(chunk);\n await dagWrite.setHead(DELETED_CLIENTS_HEAD_NAME, chunk.hash);\n return data;\n}\n\nexport async function getDeletedClients(\n dagRead: Read,\n): Promise<DeletedClients> {\n const hash = await dagRead.getHead(DELETED_CLIENTS_HEAD_NAME);\n if (hash === undefined) {\n return [];\n }\n const chunk = await dagRead.mustGetChunk(hash);\n\n const res = v.test(chunk.data, deletedClientsSchema);\n if (!res.ok) {\n // If not ok then we ignore this. It might be in the old format but we do\n // not know the clientGroupID of the old clients.\n return [];\n }\n\n return res.value;\n}\n\n/**\n * Adds deleted clients to the {@link DELETED_CLIENTS_HEAD_NAME} head.\n * @returns the new list of deleted clients (sorted and deduped).\n */\nexport async function addDeletedClients(\n dagWrite: Write,\n deletedClientsToAdd: DeletedClients,\n): Promise<DeletedClients> {\n const oldDeletedClients = await getDeletedClients(dagWrite);\n\n return setDeletedClients(\n dagWrite,\n mergeDeletedClients(oldDeletedClients, deletedClientsToAdd),\n );\n}\n\nexport async function removeDeletedClients(\n dagWrite: Write,\n deletedClientsToRemove: DeletedClients,\n): Promise<DeletedClients> {\n const oldDeletedClients = await getDeletedClients(dagWrite);\n return setDeletedClients(\n dagWrite,\n removeFromDeletedClients(oldDeletedClients, deletedClientsToRemove),\n );\n}\n\nexport async function confirmDeletedClients(\n dagWrite: Write,\n deletedClientIds: readonly ClientID[],\n deletedClientGroupIds: readonly ClientGroupID[],\n): Promise<DeletedClients> {\n const deletedClientIDSet = new Set(deletedClientIds);\n const deletedClientGroupIDSet = new Set(deletedClientGroupIds);\n const oldDeletedClients = await getDeletedClients(dagWrite);\n const clients = new Map(await getClients(dagWrite));\n for (const clientID of deletedClientIds) {\n clients.delete(clientID);\n }\n for (const clientGroupID of deletedClientGroupIds) {\n for (const [clientID, client] of clients) {\n if (client.clientGroupID === clientGroupID) {\n clients.delete(clientID);\n }\n }\n }\n\n await setClients(clients, dagWrite);\n\n return setDeletedClients(\n dagWrite,\n oldDeletedClients.filter(\n ({clientGroupID, clientID}) =>\n !deletedClientGroupIDSet.has(clientGroupID) &&\n !deletedClientIDSet.has(clientID),\n ),\n );\n}\n"],"mappings":";;;;;;;;;;AAgBA,IAAa,4BAA4B;AAWzC,IAAa,uBAA+C,cAC1D,eAAiB;CACf,eAAe;CACf,UAAU;AACZ,CAAC,CACH;AAEA,SAAS,QAAQ,GAAiB,GAAyB;CACzD,MAAM,KAAK,cAAc,EAAE,eAAe,EAAE,aAAa;CACzD,IAAI,OAAO,GACT,OAAO;CAET,OAAO,cAAc,EAAE,UAAU,EAAE,QAAQ;AAC7C;AAEA,SAAgB,wBACd,gBACgB;CAChB,OAAO,eACJ,SAAS,OAAO,EAChB,QAAQ,MAAM,GAAG,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI,IAAI,EAAE,MAAM,CAAC;AACxE;AAEA,SAAgB,oBACd,GACA,GACgB;CAChB,MAAM,SAAiC,CAAC;CACxC,IAAI,wBAAwB,CAAC;CAC7B,IAAI,wBAAwB,CAAC;CAC7B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,UAAU,IAAI,EAAE,SAC3C,IAAI,IAAI,EAAE,WAAW,KAAK,EAAE,UAAU,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI;EAC9D,OAAO,KAAK,EAAE,EAAE;EAChB;CACF,OAAO,IAAI,IAAI,EAAE,WAAW,KAAK,EAAE,UAAU,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI;EACrE,OAAO,KAAK,EAAE,EAAE;EAChB;CACF,OAAO;EAEL,OAAO,KAAK,EAAE,EAAE;EAChB;EACA;CACF;CAEF,OAAO;AACT;AAyBA,eAAsB,kBACpB,UACA,gBACyB;CAGzB,MAAM,OAAO,wBAAwB,cAAc;CAEnD,MAAM,YAAY,WAAW,IAAI;CACjC,MAAM,QAAQ,SAAS,YAAY,WAAW,CAAC,CAAC;CAChD,MAAM,SAAS,SAAS,KAAK;CAC7B,MAAM,SAAS,QAAQ,2BAA2B,MAAM,IAAI;CAC5D,OAAO;AACT;AAEA,eAAsB,kBACpB,SACyB;CACzB,MAAM,OAAO,MAAM,QAAQ,QAAQ,yBAAyB;CAC5D,IAAI,SAAS,KAAA,GACX,OAAO,CAAC;CAIV,MAAM,MAAM,MAAO,MAFC,QAAQ,aAAa,IAAI,GAEpB,MAAM,oBAAoB;CACnD,IAAI,CAAC,IAAI,IAGP,OAAO,CAAC;CAGV,OAAO,IAAI;AACb;;;;;AAMA,eAAsB,kBACpB,UACA,qBACyB;CAGzB,OAAO,kBACL,UACA,oBAAoB,MAJU,kBAAkB,QAAQ,GAIjB,mBAAmB,CAC5D;AACF;AAaA,eAAsB,sBACpB,UACA,kBACA,uBACyB;CACzB,MAAM,qBAAqB,IAAI,IAAI,gBAAgB;CACnD,MAAM,0BAA0B,IAAI,IAAI,qBAAqB;CAC7D,MAAM,oBAAoB,MAAM,kBAAkB,QAAQ;CAC1D,MAAM,UAAU,IAAI,IAAI,MAAM,WAAW,QAAQ,CAAC;CAClD,KAAK,MAAM,YAAY,kBACrB,QAAQ,OAAO,QAAQ;CAEzB,KAAK,MAAM,iBAAiB,uBAC1B,KAAK,MAAM,CAAC,UAAU,WAAW,SAC/B,IAAI,OAAO,kBAAkB,eAC3B,QAAQ,OAAO,QAAQ;CAK7B,MAAM,WAAW,SAAS,QAAQ;CAElC,OAAO,kBACL,UACA,kBAAkB,QACf,EAAC,eAAe,eACf,CAAC,wBAAwB,IAAI,aAAa,KAC1C,CAAC,mBAAmB,IAAI,QAAQ,CACpC,CACF;AACF"}
|
|
@@ -6,7 +6,7 @@ export declare function getLatestGCUpdate(): Promise<ClientGroupMap> | undefined
|
|
|
6
6
|
export declare function initClientGroupGC(dagStore: Store, enableMutationRecovery: boolean, lc: LogContext, signal: AbortSignal, onStorageFailure?: ((failure: StorageFailureError) => void) | undefined): void;
|
|
7
7
|
/**
|
|
8
8
|
* This removes client groups that have no clients and no pending mutations.
|
|
9
|
-
* If {@
|
|
9
|
+
* If {@link enableMutationRecovery} is true, it will keep client groups with
|
|
10
10
|
* pending mutations. If it is false, it will remove client groups even when they
|
|
11
11
|
* have pending mutations.
|
|
12
12
|
*/
|
|
@@ -13,7 +13,7 @@ function initClientGroupGC(dagStore, enableMutationRecovery, lc, signal, onStora
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* This removes client groups that have no clients and no pending mutations.
|
|
16
|
-
* If {@
|
|
16
|
+
* If {@link enableMutationRecovery} is true, it will keep client groups with
|
|
17
17
|
* pending mutations. If it is false, it will remove client groups even when they
|
|
18
18
|
* have pending mutations.
|
|
19
19
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-group-gc.js","names":[],"sources":["../../../../../replicache/src/persist/client-group-gc.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {initBgIntervalProcess} from '../bg-interval.ts';\nimport type {Store} from '../dag/store.ts';\nimport type {StorageFailureError} from '../storage-failure.ts';\nimport type {ClientGroupID} from '../sync/ids.ts';\nimport {withWrite} from '../with-transactions.ts';\nimport {\n clientGroupHasPendingMutations,\n getClientGroups,\n setClientGroups,\n type ClientGroupMap,\n} from './client-groups.ts';\nimport {getClients} from './clients.ts';\n\nconst GC_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\nlet latestGCUpdate: Promise<ClientGroupMap> | undefined;\nexport function getLatestGCUpdate(): Promise<ClientGroupMap> | undefined {\n return latestGCUpdate;\n}\n\nexport function initClientGroupGC(\n dagStore: Store,\n enableMutationRecovery: boolean,\n lc: LogContext,\n signal: AbortSignal,\n onStorageFailure?: ((failure: StorageFailureError) => void) | undefined,\n): void {\n initBgIntervalProcess(\n 'ClientGroupGC',\n () => {\n latestGCUpdate = gcClientGroups(dagStore, enableMutationRecovery);\n return latestGCUpdate;\n },\n () => GC_INTERVAL_MS,\n lc,\n signal,\n onStorageFailure,\n );\n}\n\n/**\n * This removes client groups that have no clients and no pending mutations.\n * If {@
|
|
1
|
+
{"version":3,"file":"client-group-gc.js","names":[],"sources":["../../../../../replicache/src/persist/client-group-gc.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {initBgIntervalProcess} from '../bg-interval.ts';\nimport type {Store} from '../dag/store.ts';\nimport type {StorageFailureError} from '../storage-failure.ts';\nimport type {ClientGroupID} from '../sync/ids.ts';\nimport {withWrite} from '../with-transactions.ts';\nimport {\n clientGroupHasPendingMutations,\n getClientGroups,\n setClientGroups,\n type ClientGroupMap,\n} from './client-groups.ts';\nimport {getClients} from './clients.ts';\n\nconst GC_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\nlet latestGCUpdate: Promise<ClientGroupMap> | undefined;\nexport function getLatestGCUpdate(): Promise<ClientGroupMap> | undefined {\n return latestGCUpdate;\n}\n\nexport function initClientGroupGC(\n dagStore: Store,\n enableMutationRecovery: boolean,\n lc: LogContext,\n signal: AbortSignal,\n onStorageFailure?: ((failure: StorageFailureError) => void) | undefined,\n): void {\n initBgIntervalProcess(\n 'ClientGroupGC',\n () => {\n latestGCUpdate = gcClientGroups(dagStore, enableMutationRecovery);\n return latestGCUpdate;\n },\n () => GC_INTERVAL_MS,\n lc,\n signal,\n onStorageFailure,\n );\n}\n\n/**\n * This removes client groups that have no clients and no pending mutations.\n * If {@link enableMutationRecovery} is true, it will keep client groups with\n * pending mutations. If it is false, it will remove client groups even when they\n * have pending mutations.\n */\nexport function gcClientGroups(\n dagStore: Store,\n enableMutationRecovery: boolean,\n): Promise<ClientGroupMap> {\n return withWrite(dagStore, async tx => {\n const clients = await getClients(tx);\n const clientGroupIDs = new Set();\n for (const client of clients.values()) {\n clientGroupIDs.add(client.clientGroupID);\n }\n const clientGroups = new Map();\n const removeClientGroups: Set<ClientGroupID> = new Set();\n for (const [clientGroupID, clientGroup] of await getClientGroups(tx)) {\n if (\n clientGroupIDs.has(clientGroupID) ||\n (enableMutationRecovery && clientGroupHasPendingMutations(clientGroup))\n ) {\n clientGroups.set(clientGroupID, clientGroup);\n } else {\n removeClientGroups.add(clientGroupID);\n }\n }\n await setClientGroups(clientGroups, tx);\n // Client group GC doesn't delete individual clients, so we don't call onClientsDeleted\n return clientGroups;\n });\n}\n"],"mappings":";;;;;AAcA,IAAM,iBAAiB,MAAS;AAEhC,IAAI;AAKJ,SAAgB,kBACd,UACA,wBACA,IACA,QACA,kBACM;CACN,sBACE,uBACM;EACJ,iBAAiB,eAAe,UAAU,sBAAsB;EAChE,OAAO;CACT,SACM,gBACN,IACA,QACA,gBACF;AACF;;;;;;;AAQA,SAAgB,eACd,UACA,wBACyB;CACzB,OAAO,UAAU,UAAU,OAAM,OAAM;EACrC,MAAM,UAAU,MAAM,WAAW,EAAE;EACnC,MAAM,iCAAiB,IAAI,IAAI;EAC/B,KAAK,MAAM,UAAU,QAAQ,OAAO,GAClC,eAAe,IAAI,OAAO,aAAa;EAEzC,MAAM,+BAAe,IAAI,IAAI;EAC7B,MAAM,qCAAyC,IAAI,IAAI;EACvD,KAAK,MAAM,CAAC,eAAe,gBAAgB,MAAM,gBAAgB,EAAE,GACjE,IACE,eAAe,IAAI,aAAa,KAC/B,0BAA0B,+BAA+B,WAAW,GAErE,aAAa,IAAI,eAAe,WAAW;OAE3C,mBAAmB,IAAI,aAAa;EAGxC,MAAM,gBAAgB,cAAc,EAAE;EAEtC,OAAO;CACT,CAAC;AACH"}
|
|
@@ -204,7 +204,7 @@ export interface ReplicacheOptions<MD extends MutatorDefs> {
|
|
|
204
204
|
* This means that this is the maximum time a tab can be in the background
|
|
205
205
|
* (frozen or in fbcache) and still be able to sync when it comes back to the
|
|
206
206
|
* foreground. If tab comes back after this time the
|
|
207
|
-
* {@
|
|
207
|
+
* {@link onClientStateNotFound} callback is called on the Replicache
|
|
208
208
|
* instance.
|
|
209
209
|
*/
|
|
210
210
|
clientMaxAgeMs?: number | undefined;
|
|
@@ -7,7 +7,7 @@ export declare function getBrowserGlobal<T extends keyof GlobalThis>(name: T): G
|
|
|
7
7
|
* This is important because some methods (e.g. `requestAnimationFrame`) are not
|
|
8
8
|
* bound to the global object by default.
|
|
9
9
|
*
|
|
10
|
-
* If you end up using {@
|
|
10
|
+
* If you end up using {@link getBrowserGlobal} instead in a case like this:
|
|
11
11
|
*
|
|
12
12
|
* ```js
|
|
13
13
|
* this.#raf = getBrowserGlobal('requestAnimationFrame') ?? rafFallback;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-env.js","names":[],"sources":["../../../../shared/src/browser-env.ts"],"sourcesContent":["// Helpers for some objects from the browser environment. These are wrapped in\n// functions because Replicache runs in environments that do not have these\n// objects (such as Web Workers, Deno etc).\n\ntype GlobalThis = typeof globalThis;\n\nconst overrides = new Map<keyof GlobalThis, GlobalThis[keyof GlobalThis]>();\n\nexport function overrideBrowserGlobal<T extends keyof GlobalThis>(\n name: T,\n value: GlobalThis[T],\n) {\n overrides.set(name, value);\n}\n\nexport function clearBrowserOverrides() {\n overrides.clear();\n}\n\nexport function getBrowserGlobal<T extends keyof GlobalThis>(\n name: T,\n): GlobalThis[T] | undefined {\n if (overrides.has(name)) {\n return overrides.get(name);\n }\n return globalThis[name];\n}\n\n/**\n * Returns the global method with the given name, bound to the global object.\n * This is important because some methods (e.g. `requestAnimationFrame`) are not\n * bound to the global object by default.\n *\n * If you end up using {@
|
|
1
|
+
{"version":3,"file":"browser-env.js","names":[],"sources":["../../../../shared/src/browser-env.ts"],"sourcesContent":["// Helpers for some objects from the browser environment. These are wrapped in\n// functions because Replicache runs in environments that do not have these\n// objects (such as Web Workers, Deno etc).\n\ntype GlobalThis = typeof globalThis;\n\nconst overrides = new Map<keyof GlobalThis, GlobalThis[keyof GlobalThis]>();\n\nexport function overrideBrowserGlobal<T extends keyof GlobalThis>(\n name: T,\n value: GlobalThis[T],\n) {\n overrides.set(name, value);\n}\n\nexport function clearBrowserOverrides() {\n overrides.clear();\n}\n\nexport function getBrowserGlobal<T extends keyof GlobalThis>(\n name: T,\n): GlobalThis[T] | undefined {\n if (overrides.has(name)) {\n return overrides.get(name);\n }\n return globalThis[name];\n}\n\n/**\n * Returns the global method with the given name, bound to the global object.\n * This is important because some methods (e.g. `requestAnimationFrame`) are not\n * bound to the global object by default.\n *\n * If you end up using {@link getBrowserGlobal} instead in a case like this:\n *\n * ```js\n * this.#raf = getBrowserGlobal('requestAnimationFrame') ?? rafFallback;\n * ...\n * this.#raf(() => ...);\n * ```\n *\n * You will end up with `Uncaught TypeError: Illegal invocation` because `this`\n * is not bound to the global object\n */\nexport function getBrowserGlobalMethod<T extends keyof GlobalThis>(\n name: T,\n): GlobalThis[T] | undefined {\n return getBrowserGlobal(name)?.bind(globalThis);\n}\n\nexport function mustGetBrowserGlobal<T extends keyof GlobalThis>(\n name: T,\n): GlobalThis[T] {\n const r = getBrowserGlobal(name);\n if (r === undefined) {\n throw new Error(\n `Unsupported JavaScript environment: Could not find ${name}.`,\n );\n }\n return r;\n}\n"],"mappings":";AAMA,IAAM,4BAAY,IAAI,IAAoD;AAa1E,SAAgB,iBACd,MAC2B;CAC3B,IAAI,UAAU,IAAI,IAAI,GACpB,OAAO,UAAU,IAAI,IAAI;CAE3B,OAAO,WAAW;AACpB;AAwBA,SAAgB,qBACd,MACe;CACf,MAAM,IAAI,iBAAiB,IAAI;CAC/B,IAAI,MAAM,KAAA,GACR,MAAM,IAAI,MACR,sDAAsD,KAAK,EAC7D;CAEF,OAAO;AACT"}
|
|
@@ -16,7 +16,7 @@ export declare function equals<T>(a: ReadonlySetLike<T>, b: ReadonlySetLike<T>):
|
|
|
16
16
|
export declare function union<T>(...sets: ReadonlySetLike<T>[]): Set<T>;
|
|
17
17
|
export declare function intersection<T>(a: ReadonlySetLike<T>, b: ReadonlySetLike<T>): Set<T>;
|
|
18
18
|
/**
|
|
19
|
-
* Returns the elements in {@
|
|
19
|
+
* Returns the elements in {@link a} that are not in {@link b}.
|
|
20
20
|
*/
|
|
21
21
|
export declare function difference<T>(a: ReadonlySetLike<T>, b: ReadonlySetLike<T>): Set<T>;
|
|
22
22
|
export declare function symmetricDifferences<T>(a: ReadonlySetLike<T>, b: ReadonlySetLike<T>): [onlyA: Set<T>, onlyB: Set<T>];
|
|
@@ -25,7 +25,7 @@ function intersection(a, b) {
|
|
|
25
25
|
return result;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Returns the elements in {@
|
|
28
|
+
* Returns the elements in {@link a} that are not in {@link b}.
|
|
29
29
|
*/
|
|
30
30
|
function difference(a, b) {
|
|
31
31
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"set-utils.js","names":[],"sources":["../../../../shared/src/set-utils.ts"],"sourcesContent":["// TODO: Change lib to pick up this type definition\n\ninterface ReadonlySetLike<T> {\n /**\n * Despite its name, returns an iterator of the values in the set-like.\n */\n keys(): Iterator<T>;\n /**\n * @returns a boolean indicating whether an element with the specified value exists in the set-like or not.\n */\n has(value: T): boolean;\n /**\n * @returns the number of (unique) elements in the set-like.\n */\n readonly size: number;\n}\n\nexport function equals<T>(\n a: ReadonlySetLike<T>,\n b: ReadonlySetLike<T>,\n): boolean {\n if (a.size !== b.size) {\n return false;\n }\n const iterator = a.keys();\n for (\n let {done, value} = iterator.next();\n !done;\n {done, value} = iterator.next()\n ) {\n if (!b.has(value)) {\n return false;\n }\n }\n return true;\n}\n\nexport function union<T>(...sets: ReadonlySetLike<T>[]): Set<T> {\n const result = new Set<T>();\n for (const set of sets) {\n const iterator = set.keys();\n for (\n let {done, value} = iterator.next();\n !done;\n {done, value} = iterator.next()\n ) {\n result.add(value);\n }\n }\n return result;\n}\n\nexport function intersection<T>(\n a: ReadonlySetLike<T>,\n b: ReadonlySetLike<T>,\n): Set<T> {\n const result = new Set<T>();\n if (a.size > b.size) {\n // Optimization: iterate over the smaller Set.\n const swap = a;\n a = b;\n b = swap;\n }\n const iterator = a.keys();\n for (\n let {done, value} = iterator.next();\n !done;\n {done, value} = iterator.next()\n ) {\n if (b.has(value)) {\n result.add(value);\n }\n }\n return result;\n}\n\n/**\n * Returns the elements in {@
|
|
1
|
+
{"version":3,"file":"set-utils.js","names":[],"sources":["../../../../shared/src/set-utils.ts"],"sourcesContent":["// TODO: Change lib to pick up this type definition\n\ninterface ReadonlySetLike<T> {\n /**\n * Despite its name, returns an iterator of the values in the set-like.\n */\n keys(): Iterator<T>;\n /**\n * @returns a boolean indicating whether an element with the specified value exists in the set-like or not.\n */\n has(value: T): boolean;\n /**\n * @returns the number of (unique) elements in the set-like.\n */\n readonly size: number;\n}\n\nexport function equals<T>(\n a: ReadonlySetLike<T>,\n b: ReadonlySetLike<T>,\n): boolean {\n if (a.size !== b.size) {\n return false;\n }\n const iterator = a.keys();\n for (\n let {done, value} = iterator.next();\n !done;\n {done, value} = iterator.next()\n ) {\n if (!b.has(value)) {\n return false;\n }\n }\n return true;\n}\n\nexport function union<T>(...sets: ReadonlySetLike<T>[]): Set<T> {\n const result = new Set<T>();\n for (const set of sets) {\n const iterator = set.keys();\n for (\n let {done, value} = iterator.next();\n !done;\n {done, value} = iterator.next()\n ) {\n result.add(value);\n }\n }\n return result;\n}\n\nexport function intersection<T>(\n a: ReadonlySetLike<T>,\n b: ReadonlySetLike<T>,\n): Set<T> {\n const result = new Set<T>();\n if (a.size > b.size) {\n // Optimization: iterate over the smaller Set.\n const swap = a;\n a = b;\n b = swap;\n }\n const iterator = a.keys();\n for (\n let {done, value} = iterator.next();\n !done;\n {done, value} = iterator.next()\n ) {\n if (b.has(value)) {\n result.add(value);\n }\n }\n return result;\n}\n\n/**\n * Returns the elements in {@link a} that are not in {@link b}.\n */\nexport function difference<T>(\n a: ReadonlySetLike<T>,\n b: ReadonlySetLike<T>,\n): Set<T> {\n const result = new Set<T>();\n const iterator = a.keys();\n for (\n let {done, value} = iterator.next();\n !done;\n {done, value} = iterator.next()\n ) {\n if (!b.has(value)) {\n result.add(value);\n }\n }\n return result;\n}\n\nexport function symmetricDifferences<T>(\n a: ReadonlySetLike<T>,\n b: ReadonlySetLike<T>,\n): [onlyA: Set<T>, onlyB: Set<T>] {\n const onlyA = new Set<T>();\n const iteratorA = a.keys();\n for (\n let {done, value} = iteratorA.next();\n !done;\n {done, value} = iteratorA.next()\n ) {\n onlyA.add(value);\n }\n\n const onlyB = new Set<T>();\n const iteratorB = b.keys();\n for (\n let {done, value} = iteratorB.next();\n !done;\n {done, value} = iteratorB.next()\n ) {\n if (a.has(value)) {\n onlyA.delete(value);\n onlyB.delete(value);\n } else {\n onlyB.add(value);\n }\n }\n return [onlyA, onlyB];\n}\n"],"mappings":";AAiBA,SAAgB,OACd,GACA,GACS;CACT,IAAI,EAAE,SAAS,EAAE,MACf,OAAO;CAET,MAAM,WAAW,EAAE,KAAK;CACxB,KACE,IAAI,EAAC,MAAM,UAAS,SAAS,KAAK,GAClC,CAAC,MACD,CAAC,MAAM,SAAS,SAAS,KAAK,GAE9B,IAAI,CAAC,EAAE,IAAI,KAAK,GACd,OAAO;CAGX,OAAO;AACT;AAEA,SAAgB,MAAS,GAAG,MAAoC;CAC9D,MAAM,yBAAS,IAAI,IAAO;CAC1B,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,WAAW,IAAI,KAAK;EAC1B,KACE,IAAI,EAAC,MAAM,UAAS,SAAS,KAAK,GAClC,CAAC,MACD,CAAC,MAAM,SAAS,SAAS,KAAK,GAE9B,OAAO,IAAI,KAAK;CAEpB;CACA,OAAO;AACT;AAEA,SAAgB,aACd,GACA,GACQ;CACR,MAAM,yBAAS,IAAI,IAAO;CAC1B,IAAI,EAAE,OAAO,EAAE,MAAM;EAEnB,MAAM,OAAO;EACb,IAAI;EACJ,IAAI;CACN;CACA,MAAM,WAAW,EAAE,KAAK;CACxB,KACE,IAAI,EAAC,MAAM,UAAS,SAAS,KAAK,GAClC,CAAC,MACD,CAAC,MAAM,SAAS,SAAS,KAAK,GAE9B,IAAI,EAAE,IAAI,KAAK,GACb,OAAO,IAAI,KAAK;CAGpB,OAAO;AACT;;;;AAKA,SAAgB,WACd,GACA,GACQ;CACR,MAAM,yBAAS,IAAI,IAAO;CAC1B,MAAM,WAAW,EAAE,KAAK;CACxB,KACE,IAAI,EAAC,MAAM,UAAS,SAAS,KAAK,GAClC,CAAC,MACD,CAAC,MAAM,SAAS,SAAS,KAAK,GAE9B,IAAI,CAAC,EAAE,IAAI,KAAK,GACd,OAAO,IAAI,KAAK;CAGpB,OAAO;AACT;AAEA,SAAgB,qBACd,GACA,GACgC;CAChC,MAAM,wBAAQ,IAAI,IAAO;CACzB,MAAM,YAAY,EAAE,KAAK;CACzB,KACE,IAAI,EAAC,MAAM,UAAS,UAAU,KAAK,GACnC,CAAC,MACD,CAAC,MAAM,SAAS,UAAU,KAAK,GAE/B,MAAM,IAAI,KAAK;CAGjB,MAAM,wBAAQ,IAAI,IAAO;CACzB,MAAM,YAAY,EAAE,KAAK;CACzB,KACE,IAAI,EAAC,MAAM,UAAS,UAAU,KAAK,GACnC,CAAC,MACD,CAAC,MAAM,SAAS,UAAU,KAAK,GAE/B,IAAI,EAAE,IAAI,KAAK,GAAG;EAChB,MAAM,OAAO,KAAK;EAClB,MAAM,OAAO,KAAK;CACpB,OACE,MAAM,IAAI,KAAK;CAGnB,OAAO,CAAC,OAAO,KAAK;AACtB"}
|
package/out/zero/package.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
var package_default = {
|
|
2
2
|
name: "@rocicorp/zero",
|
|
3
|
-
version: "1.11.0
|
|
3
|
+
version: "1.11.0",
|
|
4
4
|
description: "Zero is a web framework for serverless web development.",
|
|
5
5
|
homepage: "https://zero.rocicorp.dev",
|
|
6
6
|
bugs: { "url": "https://bugs.rocicorp.dev" },
|
package/out/zero/package.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package.js","names":[],"sources":["../../package.json"],"sourcesContent":["{\n \"name\": \"@rocicorp/zero\",\n \"version\": \"1.11.0
|
|
1
|
+
{"version":3,"file":"package.js","names":[],"sources":["../../package.json"],"sourcesContent":["{\n \"name\": \"@rocicorp/zero\",\n \"version\": \"1.11.0\",\n \"description\": \"Zero is a web framework for serverless web development.\",\n \"homepage\": \"https://zero.rocicorp.dev\",\n \"bugs\": {\n \"url\": \"https://bugs.rocicorp.dev\"\n },\n \"license\": \"Apache-2.0\",\n \"author\": \"Rocicorp, Inc.\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/rocicorp/mono.git\",\n \"directory\": \"packages/zero\"\n },\n \"bin\": {\n \"analyze-query\": \"./out/zero/src/analyze-query.js\",\n \"ast-to-zql\": \"./out/zero/src/ast-to-zql.js\",\n \"transform-query\": \"./out/zero/src/transform-query.js\",\n \"zero-build-schema\": \"./out/zero/src/build-schema.js\",\n \"zero-cache\": \"./out/zero/src/cli.js\",\n \"zero-cache-dev\": \"./out/zero/src/zero-cache-dev.js\",\n \"zero-deploy-permissions\": \"./out/zero/src/deploy-permissions.js\",\n \"zero-out\": \"./out/zero/src/zero-out.js\"\n },\n \"files\": [\n \"out\",\n \"!*.tsbuildinfo\"\n ],\n \"type\": \"module\",\n \"main\": \"out/zero/src/zero.js\",\n \"module\": \"out/zero/src/zero.js\",\n \"types\": \"out/zero/src/zero.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./out/zero/src/zero.d.ts\",\n \"default\": \"./out/zero/src/zero.js\"\n },\n \"./analyze\": {\n \"types\": \"./out/zero/src/analyze.d.ts\",\n \"default\": \"./out/zero/src/analyze.js\"\n },\n \"./bindings\": {\n \"types\": \"./out/zero/src/bindings.d.ts\",\n \"default\": \"./out/zero/src/bindings.js\"\n },\n \"./change-protocol/v0\": {\n \"types\": \"./out/zero/src/change-protocol/v0.d.ts\",\n \"default\": \"./out/zero/src/change-protocol/v0.js\"\n },\n \"./expo-sqlite\": {\n \"types\": \"./out/zero/src/expo-sqlite.d.ts\",\n \"default\": \"./out/zero/src/expo-sqlite.js\"\n },\n \"./op-sqlite\": {\n \"types\": \"./out/zero/src/op-sqlite.d.ts\",\n \"default\": \"./out/zero/src/op-sqlite.js\"\n },\n \"./pg\": {\n \"types\": \"./out/zero/src/pg.d.ts\",\n \"default\": \"./out/zero/src/pg.js\"\n },\n \"./react\": {\n \"types\": \"./out/zero/src/react.d.ts\",\n \"default\": \"./out/zero/src/react.js\"\n },\n \"./react-native\": {\n \"types\": \"./out/zero/src/react-native.d.ts\",\n \"default\": \"./out/zero/src/react-native.js\"\n },\n \"./server\": {\n \"types\": \"./out/zero/src/server.d.ts\",\n \"default\": \"./out/zero/src/server.js\"\n },\n \"./server/adapters/drizzle\": {\n \"types\": \"./out/zero/src/adapters/drizzle.d.ts\",\n \"default\": \"./out/zero/src/adapters/drizzle.js\"\n },\n \"./server/adapters/kysely\": {\n \"types\": \"./out/zero/src/adapters/kysely.d.ts\",\n \"default\": \"./out/zero/src/adapters/kysely.js\"\n },\n \"./server/adapters/prisma\": {\n \"types\": \"./out/zero/src/adapters/prisma.d.ts\",\n \"default\": \"./out/zero/src/adapters/prisma.js\"\n },\n \"./server/adapters/pg\": {\n \"types\": \"./out/zero/src/adapters/pg.d.ts\",\n \"default\": \"./out/zero/src/adapters/pg.js\"\n },\n \"./server/adapters/postgresjs\": {\n \"types\": \"./out/zero/src/adapters/postgresjs.d.ts\",\n \"default\": \"./out/zero/src/adapters/postgresjs.js\"\n },\n \"./solid\": {\n \"types\": \"./out/zero/src/solid.d.ts\",\n \"default\": \"./out/zero/src/solid.js\"\n },\n \"./sqlite\": {\n \"types\": \"./out/zero/src/sqlite.d.ts\",\n \"default\": \"./out/zero/src/sqlite.js\"\n },\n \"./zqlite\": {\n \"types\": \"./out/zero/src/zqlite.d.ts\",\n \"default\": \"./out/zero/src/zqlite.js\"\n }\n },\n \"scripts\": {\n \"build\": \"node --experimental-strip-types --no-warnings tool/build.ts\",\n \"build:watch\": \"node --experimental-strip-types --no-warnings tool/build.ts --watch\",\n \"check-types\": \"tsc -p tsconfig.client.json && tsc -p tsconfig.server.json\",\n \"check-types:client:watch\": \"tsc -p tsconfig.client.json --watch\",\n \"check-types:server:watch\": \"tsc -p tsconfig.server.json --watch\",\n \"format\": \"oxfmt .\",\n \"check-format\": \"oxfmt --check .\",\n \"lint\": \"oxlint --quiet --config ../../oxlint.config.ts src/\",\n \"docs\": \"node --experimental-strip-types --no-warnings tool/generate-docs.ts\",\n \"docs:server\": \"node --watch --experimental-strip-types --no-warnings tool/generate-docs.ts --server\",\n \"fmt\": \"oxfmt .\",\n \"check-fmt\": \"oxfmt --check .\"\n },\n \"dependencies\": {\n \"@badrap/valita\": \"0.5.5\",\n \"@databases/escape-identifier\": \"^1.0.3\",\n \"@databases/sql\": \"^3.3.0\",\n \"@dotenvx/dotenvx\": \"^1.39.0\",\n \"@drdgvhbh/postgres-error-codes\": \"^0.0.6\",\n \"@fastify/cors\": \"^10.0.0\",\n \"@fastify/websocket\": \"^11.0.0\",\n \"@google-cloud/precise-date\": \"^4.0.0\",\n \"@opentelemetry/api\": \"^1.9.0\",\n \"@opentelemetry/api-logs\": \"^0.218.0\",\n \"@opentelemetry/auto-instrumentations-node\": \"^0.76.0\",\n \"@opentelemetry/exporter-metrics-otlp-http\": \"^0.218.0\",\n \"@opentelemetry/resources\": \"^2.7.1\",\n \"@opentelemetry/sdk-metrics\": \"^2.7.1\",\n \"@opentelemetry/sdk-node\": \"^0.218.0\",\n \"@opentelemetry/sdk-trace-node\": \"^2.7.1\",\n \"@opentelemetry/semantic-conventions\": \"^1.41.1\",\n \"@postgresql-typed/oids\": \"^0.2.0\",\n \"@rocicorp/lock\": \"^1.0.4\",\n \"@rocicorp/logger\": \"^6.1.0\",\n \"@rocicorp/resolver\": \"^1.0.2\",\n \"@rocicorp/zero-sqlite3\": \"^1.1.5\",\n \"@standard-schema/spec\": \"^1.0.0\",\n \"@types/basic-auth\": \"^1.1.8\",\n \"@types/ws\": \"^8.5.12\",\n \"basic-auth\": \"^2.0.1\",\n \"chalk-template\": \"^1.1.0\",\n \"chokidar\": \"^4.0.1\",\n \"cloudevents\": \"^10.0.0\",\n \"command-line-args\": \"^6.0.1\",\n \"command-line-usage\": \"^7.0.3\",\n \"compare-utf8\": \"^0.2.3\",\n \"defu\": \"^6.1.4\",\n \"eventemitter3\": \"^5.0.1\",\n \"fastify\": \"^5.0.0\",\n \"is-in-subnet\": \"^4.0.1\",\n \"jose\": \"^5.9.3\",\n \"json-custom-numbers\": \"^3.1.1\",\n \"kasi\": \"^1.1.0\",\n \"nanoid\": \"^5.1.2\",\n \"oxfmt\": \"^0.65.0\",\n \"parse-prometheus-text-format\": \"^1.1.1\",\n \"pg-format\": \"npm:pg-format-fix@^1.0.5\",\n \"postgres\": \"3.4.7\",\n \"semver\": \"^7.5.4\",\n \"url-pattern\": \"^1.0.3\",\n \"urlpattern-polyfill\": \"^10.1.0\",\n \"ws\": \"^8.18.1\"\n },\n \"devDependencies\": {\n \"@op-engineering/op-sqlite\": \">=15\",\n \"analyze-query\": \"workspace:*\",\n \"ast-to-zql\": \"workspace:*\",\n \"expo-sqlite\": \">=15\",\n \"oxc-parser\": \"^0.129.0\",\n \"replicache\": \"workspace:*\",\n \"shared\": \"workspace:*\",\n \"syncpack\": \"^14.3.0\",\n \"typedoc\": \"^0.28.17\",\n \"typescript\": \"~7.0.2\",\n \"vite\": \"^8.0.13\",\n \"vitest\": \"^5.0.0\",\n \"zero-cache\": \"workspace:*\",\n \"zero-client\": \"workspace:*\",\n \"zero-pg\": \"workspace:*\",\n \"zero-react\": \"workspace:*\",\n \"zero-server\": \"workspace:*\",\n \"zero-solid\": \"workspace:*\",\n \"zqlite\": \"workspace:*\"\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"packageManager\": \"pnpm@11.11.0\"\n}\n"],"mappings":""}
|
|
@@ -21,7 +21,9 @@ export declare class ChangeStreamerHttpServer extends HttpService {
|
|
|
21
21
|
/**
|
|
22
22
|
* Signals (drainable) subscribers to switch over to another
|
|
23
23
|
* replication-manager, resolving when they have disconnected. Subscribers
|
|
24
|
-
* continue to be served in the meantime.
|
|
24
|
+
* continue to be served in the meantime, but new connections (e.g.
|
|
25
|
+
* switchover attempts, reconnects, and snapshot reservations) are refused,
|
|
26
|
+
* so that they are routed to another replication-manager.
|
|
25
27
|
*/
|
|
26
28
|
drain(): Promise<void>;
|
|
27
29
|
protected _onStop(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-streamer-http.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,WAAW,CAAC;AAE/C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAIjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,2BAA2B,CAAC;AAGpE,OAAO,EAAC,KAAK,MAAM,EAAC,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAC,KAAK,OAAO,EAAC,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAKL,KAAK,KAAK,EACV,KAAK,MAAM,EACZ,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAIzD,OAAO,EAAC,WAAW,EAAE,KAAK,OAAO,IAAI,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAG5E,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,KAAK,YAAY,EAIjB,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACvB,MAAM,gBAAgB,CAAC;AAmBxB,KAAK,OAAO,GAAG,WAAW,GAAG;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EACH,IAAI,CAAC,oBAAoB,EAAE,eAAe,GAAG,kBAAkB,CAAC,GAChE,SAAS,CAAC;IACd,gBAAgB,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;CACxD,CAAC;AAEF,qBAAa,wBAAyB,SAAQ,WAAW;;IACvD,QAAQ,CAAC,EAAE,iCAAiC;IAK5C,YACE,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,qBAAqB,EAuCtC;IAmLD,UAAmB,QAAQ,IAAI,IAAI,CA2BlC;IAED
|
|
1
|
+
{"version":3,"file":"change-streamer-http.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-http.ts"],"names":[],"mappings":"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"}
|
|
@@ -150,9 +150,12 @@ var ChangeStreamerHttpServer = class extends HttpService {
|
|
|
150
150
|
/**
|
|
151
151
|
* Signals (drainable) subscribers to switch over to another
|
|
152
152
|
* replication-manager, resolving when they have disconnected. Subscribers
|
|
153
|
-
* continue to be served in the meantime.
|
|
153
|
+
* continue to be served in the meantime, but new connections (e.g.
|
|
154
|
+
* switchover attempts, reconnects, and snapshot reservations) are refused,
|
|
155
|
+
* so that they are routed to another replication-manager.
|
|
154
156
|
*/
|
|
155
157
|
async drain() {
|
|
158
|
+
this.stopAccepting();
|
|
156
159
|
if (this.#changeStreamerStarted && this.#changeStreamer.drain) await this.#changeStreamer.drain();
|
|
157
160
|
}
|
|
158
161
|
async _onStop() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-streamer-http.js","names":["#lc","#opts","#changeStreamer","#changes","#reserveSnapshot","#subscribe","#receiveWebsocket","#ensureChangeStreamerStarted","#forwardReservation","#changeStreamerStarted","#shardID","#changeDB","#changeStreamerURI","#resolveChangeStreamer"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-http.ts"],"sourcesContent":["import type {IncomingMessage} from 'node:http';\nimport websocket from '@fastify/websocket';\nimport type {LogContext} from '@rocicorp/logger';\nimport WebSocket from 'ws';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport type {NormalizedZeroConfig} from '../../config/normalize.ts';\nimport type {IncomingMessageSubset} from '../../types/http.ts';\nimport {pgClient, type PostgresDB} from '../../types/pg.ts';\nimport {type Worker} from '../../types/processes.ts';\nimport {type ShardID} from '../../types/shards.ts';\nimport {\n readHead,\n streamInternal,\n streamInternalStringified,\n streamInternalWithSize,\n type Sized,\n type Source,\n} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {URLParams} from '../../types/url-params.ts';\nimport {installWebSocketReceiver} from '../../types/websocket-handoff.ts';\nimport {closeWithError, PROTOCOL_ERROR} from '../../types/ws.ts';\nimport {HttpService, type Options as HttpOptions} from '../http-service.ts';\nimport {handleProfzRequest} from '../profz.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport {\n PROTOCOL_VERSION,\n type ChangeStreamer,\n type ChangeStreamerService,\n type SubscriberContext,\n} from './change-streamer.ts';\nimport {discoverChangeStreamerAddress} from './schema/tables.ts';\nimport {type SnapshotMessage} from './snapshot-message.ts';\nimport {\n type LocalReplica,\n subscribeDownstreamSchema,\n subscribeUpstreamSchema,\n type ReservedMessage,\n type SnapshotStatus,\n type SubscribeContext,\n type SubscribeDownstream,\n type SubscribeUpstream,\n} from './subscribe.ts';\n\nconst MIN_SUPPORTED_PROTOCOL_VERSION = 4;\n\n// The merged `/subscribe` protocol was introduced at v7.\nconst MIN_SUBSCRIBE_PROTOCOL_VERSION = 7;\n\nconst SNAPSHOT_PATH_PATTERN = '/replication/:version/snapshot';\nconst CHANGES_PATH_PATTERN = '/replication/:version/changes';\nconst SUBSCRIBE_PATH_PATTERN = '/replication/:version/subscribe';\nconst PATH_REGEX =\n /\\/replication\\/v(?<version>\\d+)\\/(changes|snapshot|subscribe)$/;\n\nconst SUBSCRIBE_PATH = `/replication/v${PROTOCOL_VERSION}/subscribe`;\n\n// Max time for a client to establish the /subscribe websocket (TCP connect\n// plus upgrade) before abandoning the attempt.\nconst CONNECT_TIMEOUT_MS = 10_000;\n\ntype Options = HttpOptions & {\n startupDelayMs: number;\n config?:\n | Pick<NormalizedZeroConfig, 'adminPassword' | 'operatorPassword'>\n | undefined;\n getProfileWorker?: (() => Promise<Worker>) | undefined;\n};\n\nexport class ChangeStreamerHttpServer extends HttpService {\n readonly id = 'change-streamer-http-server';\n readonly #lc: LogContext;\n readonly #opts: Options;\n readonly #changeStreamer: ChangeStreamerService;\n\n constructor(\n lc: LogContext,\n opts: Options,\n parent: Worker,\n changeStreamer: ChangeStreamerService,\n ) {\n super('change-streamer-http-server', lc, opts, async fastify => {\n await fastify.register(websocket);\n\n fastify.get(CHANGES_PATH_PATTERN, {websocket: true}, this.#changes);\n fastify.get(\n SNAPSHOT_PATH_PATTERN,\n {websocket: true},\n this.#reserveSnapshot,\n );\n fastify.get(SUBSCRIBE_PATH_PATTERN, {websocket: true}, this.#subscribe);\n\n fastify.get('/profz', (req, res) =>\n handleProfzRequest(\n lc,\n opts.config ?? {\n adminPassword: undefined,\n operatorPassword: undefined,\n },\n req,\n res,\n opts.getProfileWorker,\n undefined,\n 'change-streamer',\n ),\n );\n\n installWebSocketReceiver<'snapshot' | 'changes' | 'subscribe'>(\n lc,\n fastify.websocketServer,\n this.#receiveWebsocket,\n parent,\n );\n });\n\n this.#lc = lc;\n this.#opts = opts;\n this.#changeStreamer = changeStreamer;\n }\n\n // Called when receiving a web socket via the main dispatcher handoff.\n readonly #receiveWebsocket = (\n ws: WebSocket,\n action: 'changes' | 'snapshot' | 'subscribe',\n msg: IncomingMessageSubset,\n ) => {\n switch (action) {\n case 'snapshot':\n return this.#reserveSnapshot(ws, msg);\n case 'changes':\n return this.#changes(ws, msg);\n case 'subscribe':\n return this.#subscribe(ws, msg);\n default:\n closeWithError(\n this._lc,\n ws,\n `invalid action \"${action}\" received in handoff`,\n );\n return;\n }\n };\n\n readonly #reserveSnapshot = async (ws: WebSocket, req: RequestHeaders) => {\n this.#ensureChangeStreamerStarted('incoming snapshot reservation');\n try {\n const url = new URL(\n req.url ?? '',\n req.headers.origin ?? 'http://localhost',\n );\n checkProtocolVersion(url.pathname);\n const taskID = url.searchParams.get('taskID');\n if (!taskID) {\n throw new Error('Missing taskID in snapshot request');\n }\n const downstream =\n await this.#changeStreamer.startSnapshotReservation(taskID);\n // Send-only for now: the reservation carries no inbound application\n // messages (only transport acks, demuxed internally).\n void streamInternal(this._lc, ws, undefined, downstream);\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n }\n };\n\n readonly #changes = async (ws: WebSocket, req: RequestHeaders) => {\n try {\n const ctx = getSubscriberContext(req);\n if (ctx.mode === 'serving') {\n this.#ensureChangeStreamerStarted('incoming subscription');\n }\n\n const downstream = await this.#changeStreamer.subscribe(ctx);\n // Send-only for now: the subscription carries no inbound application\n // messages (only transport acks, demuxed internally).\n void streamInternalStringified(this._lc, ws, undefined, downstream, {\n batched: ctx.wsBatched,\n });\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n }\n };\n\n /**\n * The merged v7 `/subscribe` handler. A single bidirectional connection\n * carries, in sequence: an optional reservation phase (`reserve-snapshot` →\n * `reserved`) and then the subscription phase (`start-subscription` → change\n * stream). This ensures that the subscriber subscribes to the same task from\n * which it restored the backup.\n */\n readonly #subscribe = async (ws: WebSocket, req: RequestHeaders) => {\n let reservation: Source<SnapshotMessage> | undefined;\n const outbound = Subscription.create<string | PreSerializedBatch>();\n\n try {\n const url = new URL(\n req.url ?? '',\n req.headers.origin ?? 'http://localhost',\n );\n const protocolVersion = checkProtocolVersion(url.pathname);\n if (protocolVersion < MIN_SUBSCRIBE_PROTOCOL_VERSION) {\n throw new Error(\n `the /subscribe endpoint requires protocol v${MIN_SUBSCRIBE_PROTOCOL_VERSION} ` +\n `(client is at v${protocolVersion})`,\n );\n }\n\n const instream = await streamInternalStringified(\n this._lc,\n ws,\n subscribeUpstreamSchema,\n outbound,\n {batched: true},\n );\n\n // Keep reading upstream control messages for the life of the connection.\n // Do not manually exit loop, as that would close the socket. The loop\n // exits naturally when the connection closes.\n for await (const msg of instream) {\n if (msg[0] === 'reserve-snapshot') {\n const {taskID, localReplica} = msg[1];\n this.#ensureChangeStreamerStarted('incoming snapshot reservation');\n reservation = await this.#changeStreamer.startSnapshotReservation(\n taskID,\n {localReplica},\n );\n void this.#forwardReservation(reservation, outbound);\n } else if (msg[0] === 'start-subscription') {\n const ctx: SubscriberContext = {\n ...msg[1],\n protocolVersion,\n wsBatched: true,\n };\n if (ctx.mode === 'serving') {\n this.#ensureChangeStreamerStarted('incoming subscription');\n }\n // subscribe() registers the subscriber and then releases any\n // reservation for that task.\n reservation = undefined;\n await this.#changeStreamer.subscribe(ctx, outbound);\n }\n }\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n } finally {\n // Covers a reservation-phase disconnect (instream ended before a\n // subscription started); idempotent with the close handler above.\n reservation?.cancel();\n }\n };\n\n /**\n * Forwards a snapshot reservation's single `['status', ...]` message onto\n * the merged connection's `outbound` sink as a `['reserved', ...]` message.\n * The loop then blocks on the held-open reservation until it is cancelled,\n * by `subscribe()` at the subscription handoff, or by the reservation-phase\n * teardown on disconnect.\n */\n async #forwardReservation(\n reservation: Source<SnapshotMessage>,\n outbound: Subscription<string | PreSerializedBatch>,\n ) {\n try {\n for await (const [, status] of reservation) {\n // Translate the /snapshot protocol's ['status', {tag: 'status', ...}]\n // message to the /subscribe protocol's ['reserved', {tag: 'snapshot', ...}].\n outbound.push(\n BigIntJSON.stringify([\n 'reserved',\n {...status, tag: 'snapshot'},\n ] satisfies ReservedMessage),\n );\n }\n } catch (e) {\n this._lc.warn?.(`error forwarding snapshot reservation`, e);\n outbound.fail(e instanceof Error ? e : new Error(String(e)));\n }\n }\n\n #changeStreamerStarted = false;\n\n #ensureChangeStreamerStarted(reason?: string) {\n if (!this.#changeStreamerStarted && this._state.shouldRun()) {\n this.#lc.info?.(\n `starting ChangeStreamerService ${reason ? `(${reason})` : ''}`,\n );\n void this.#changeStreamer\n .run()\n .catch(e =>\n this.#lc.warn?.(`ChangeStreamerService ended with error`, e),\n )\n .finally(() => this.stop());\n\n this.#changeStreamerStarted = true;\n }\n }\n\n protected override _onStart(): void {\n const {startupDelayMs, readinessGate = promiseVoid} = this.#opts;\n if (startupDelayMs === 0) {\n // In RMv2, there is no need to delay starting the change streamer\n // because RM startup is non-disruptive.\n this.#ensureChangeStreamerStarted();\n } else {\n // In RMv1, starting the change-streamer forcibly shuts down the\n // previous change-streamer, causing view-syncers to reconnect.\n // If this replication-manager has just started, the routing layer may\n // not have registered it with DNS, as that only happens after it\n // confirms health checks. To minimize downtime, the takeover is\n // delayed for the configured startupDelayMs _after_ beginning to\n // advertise readiness.\n void readinessGate.then(() => {\n this.#lc.info?.(\n `waiting ${startupDelayMs}ms before taking over the change log`,\n );\n this._state.setTimeout(\n () =>\n this.#ensureChangeStreamerStarted(\n `startup delay elapsed (${startupDelayMs} ms)`,\n ),\n startupDelayMs,\n );\n });\n }\n }\n\n /**\n * Signals (drainable) subscribers to switch over to another\n * replication-manager, resolving when they have disconnected. Subscribers\n * continue to be served in the meantime.\n */\n async drain(): Promise<void> {\n if (this.#changeStreamerStarted && this.#changeStreamer.drain) {\n await this.#changeStreamer.drain();\n }\n }\n\n protected override async _onStop(): Promise<void> {\n if (this.#changeStreamerStarted) {\n await this.#changeStreamer.stop();\n }\n }\n}\n\nexport interface SnapshotReserver {\n reserveSnapshot(\n taskID: string,\n localReplica?: LocalReplica | undefined,\n ): Promise<{\n reserved: SnapshotStatus;\n followup: ReservationFollowup;\n }>;\n}\n\nexport interface SubscriptionStarter {\n subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>>;\n}\n\nexport interface ReservationFollowup extends SubscriptionStarter {\n /**\n * A signal to indicate that the followup is no longer valid (i.e. the\n * connection to the reserving task has been severed).\n */\n readonly signal: AbortSignal;\n\n /**\n * Abandons the reservation without subscribing, closing the connection that\n * was held open to pin the change-log floor. Used when the caller cannot\n * proceed to `subscribe()` (e.g. process shutdown while a restore is in\n * progress) and must not leave the reservation's connection open forever.\n */\n cancel(reason?: Error): void;\n}\n\nexport class ChangeStreamerHttpClient\n implements ChangeStreamer, SnapshotReserver, SubscriptionStarter\n{\n readonly #lc: LogContext;\n readonly #shardID: ShardID;\n readonly #changeDB: PostgresDB;\n readonly #changeStreamerURI: string | undefined;\n\n constructor(\n lc: LogContext,\n shardID: ShardID,\n changeDB: string,\n changeStreamerURI: string | undefined,\n ) {\n this.#lc = lc;\n this.#shardID = shardID;\n // Create a pg client with a single short-lived connection for the purpose\n // of change-streamer discovery (i.e. ChangeDB as DNS).\n this.#changeDB = pgClient(lc, changeDB, 'change-streamer-discovery', {\n max: 1,\n ['idle_timeout']: 15,\n });\n this.#changeStreamerURI = changeStreamerURI;\n }\n\n async #resolveChangeStreamer(path: string) {\n let baseURL = this.#changeStreamerURI;\n if (!baseURL) {\n const address = await discoverChangeStreamerAddress(\n this.#shardID,\n this.#changeDB,\n );\n if (!address) {\n throw new Error(`no change-streamer is running`);\n }\n baseURL = address.includes('://') ? `${address}/` : `ws://${address}/`;\n }\n const uri = new URL(path, baseURL);\n this.#lc.info?.(`connecting to change-streamer@${uri}`);\n return uri;\n }\n\n async reserveSnapshot(\n taskID: string,\n localReplica?: LocalReplica | undefined,\n ): Promise<{\n reserved: SnapshotStatus;\n followup: ReservationFollowup;\n }> {\n const {instream, outbound} = await this.connect(taskID);\n outbound.push([\n 'reserve-snapshot',\n {taskID, ...(localReplica ? {localReplica} : {})},\n ]);\n\n const {head, rest} = readHead(instream);\n const item = await head.next();\n if (!item.value) {\n throw new Error('stream closed without snapshot reservation');\n }\n const {data: msg} = item.value;\n if (msg[0] !== 'reserved') {\n throw new Error(`expected \"reserved\" message but got ${msg[0]}`);\n }\n const reserved: SnapshotStatus = msg[1];\n const followup: ReservationFollowup = {\n subscribe: ctx => {\n outbound.push(['start-subscription', toStartSubscriptionContext(ctx)]);\n return Promise.resolve(rest);\n },\n signal: outbound.signal,\n cancel: reason => outbound.cancel(reason),\n };\n\n return {reserved, followup};\n }\n\n async subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>> {\n const {instream, outbound} = await this.connect(ctx.taskID);\n outbound.push(['start-subscription', toStartSubscriptionContext(ctx)]);\n return instream;\n }\n\n /**\n * Opens a merged v7 `/subscribe` connection (see subscribe.ts). Returns the\n * inbound change/reservation stream and an `outbound` sink into which the\n * caller pushes the `reserve-snapshot` / `start-subscription` control\n * messages that drive the connection's phases. The single connection spans\n * the reservation, backup restore, and subscription, so the reserving\n * replication-manager is the one that serves the subscription.\n */\n // exported for testing\n async connect(taskID: string): Promise<{\n instream: Source<Sized<SubscribeDownstream>>;\n outbound: Subscription<SubscribeUpstream>;\n }> {\n const uri = await this.#resolveChangeStreamer(SUBSCRIBE_PATH);\n\n // taskID is carried in the query for observability/routing; the server\n // reads the authoritative taskID from the control messages themselves.\n const params = new URLSearchParams({taskID});\n // Bound the TCP connect + HTTP upgrade. Without this, a SYN black-holed\n // by a stale Service endpoint (e.g. a just-terminated replication-manager\n // pod) is retransmitted by the kernel for ~2 minutes before ETIMEDOUT.\n // Failing fast lets the caller's retry open a fresh connection that is\n // routed to a live endpoint.\n const ws = new WebSocket(uri + `?${params.toString()}`, {\n handshakeTimeout: CONNECT_TIMEOUT_MS,\n });\n\n const outbound = Subscription.create<SubscribeUpstream>();\n const instream = await streamInternalWithSize(\n this.#lc,\n ws,\n subscribeDownstreamSchema,\n outbound,\n );\n return {instream, outbound};\n }\n}\n\ntype RequestHeaders = Pick<IncomingMessage, 'url' | 'headers'>;\n\nexport function getSubscriberContext(req: RequestHeaders): SubscriberContext {\n const url = new URL(req.url ?? '', req.headers.origin ?? 'http://localhost');\n const protocolVersion = checkProtocolVersion(url.pathname);\n const params = new URLParams(url);\n\n return {\n protocolVersion,\n id: params.get('id', true),\n taskID: params.get('taskID', true),\n mode: params.get('mode', false) === 'backup' ? 'backup' : 'serving',\n replicaVersion: params.get('replicaVersion', true),\n watermark: params.get('watermark', true),\n // Absent for subscribers that predate the parameter, which is the safe\n // default: the barrier falls back to polling rather than waiting on an\n // ACK that would never be attributed to a writer.\n wsBatched: params.getBoolean('wsBatched'),\n };\n}\n\nfunction checkProtocolVersion(pathname: string): number {\n const match = PATH_REGEX.exec(pathname);\n if (!match) {\n throw new Error(`invalid path: ${pathname}`);\n }\n const v = Number(match.groups?.version);\n if (\n Number.isNaN(v) ||\n v > PROTOCOL_VERSION ||\n v < MIN_SUPPORTED_PROTOCOL_VERSION\n ) {\n throw new Error(\n `Cannot service client at protocol v${v}. ` +\n `Supported protocols: [v${MIN_SUPPORTED_PROTOCOL_VERSION} ... v${PROTOCOL_VERSION}]`,\n );\n }\n return v;\n}\n\n// Projects a client-side SubscriberContext onto the start-subscription\n// message payload, dropping the fields that are implicit on the merged\n// connection: protocolVersion (from the request path), wsBatched (always on).\nfunction toStartSubscriptionContext(ctx: SubscriberContext): SubscribeContext {\n const {\n taskID,\n id,\n mode,\n replicaVersion,\n watermark,\n backfills,\n drainable,\n switchover,\n } = ctx;\n return {\n taskID,\n id,\n mode,\n replicaVersion,\n watermark,\n backfills,\n ...(drainable !== undefined ? {drainable} : {}),\n ...(switchover !== undefined ? {switchover} : {}),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"change-streamer-http.js","names":["#lc","#opts","#changeStreamer","#changes","#reserveSnapshot","#subscribe","#receiveWebsocket","#ensureChangeStreamerStarted","#forwardReservation","#changeStreamerStarted","#shardID","#changeDB","#changeStreamerURI","#resolveChangeStreamer"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-http.ts"],"sourcesContent":["import type {IncomingMessage} from 'node:http';\nimport websocket from '@fastify/websocket';\nimport type {LogContext} from '@rocicorp/logger';\nimport WebSocket from 'ws';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport type {NormalizedZeroConfig} from '../../config/normalize.ts';\nimport type {IncomingMessageSubset} from '../../types/http.ts';\nimport {pgClient, type PostgresDB} from '../../types/pg.ts';\nimport {type Worker} from '../../types/processes.ts';\nimport {type ShardID} from '../../types/shards.ts';\nimport {\n readHead,\n streamInternal,\n streamInternalStringified,\n streamInternalWithSize,\n type Sized,\n type Source,\n} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {URLParams} from '../../types/url-params.ts';\nimport {installWebSocketReceiver} from '../../types/websocket-handoff.ts';\nimport {closeWithError, PROTOCOL_ERROR} from '../../types/ws.ts';\nimport {HttpService, type Options as HttpOptions} from '../http-service.ts';\nimport {handleProfzRequest} from '../profz.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport {\n PROTOCOL_VERSION,\n type ChangeStreamer,\n type ChangeStreamerService,\n type SubscriberContext,\n} from './change-streamer.ts';\nimport {discoverChangeStreamerAddress} from './schema/tables.ts';\nimport {type SnapshotMessage} from './snapshot-message.ts';\nimport {\n type LocalReplica,\n subscribeDownstreamSchema,\n subscribeUpstreamSchema,\n type ReservedMessage,\n type SnapshotStatus,\n type SubscribeContext,\n type SubscribeDownstream,\n type SubscribeUpstream,\n} from './subscribe.ts';\n\nconst MIN_SUPPORTED_PROTOCOL_VERSION = 4;\n\n// The merged `/subscribe` protocol was introduced at v7.\nconst MIN_SUBSCRIBE_PROTOCOL_VERSION = 7;\n\nconst SNAPSHOT_PATH_PATTERN = '/replication/:version/snapshot';\nconst CHANGES_PATH_PATTERN = '/replication/:version/changes';\nconst SUBSCRIBE_PATH_PATTERN = '/replication/:version/subscribe';\nconst PATH_REGEX =\n /\\/replication\\/v(?<version>\\d+)\\/(changes|snapshot|subscribe)$/;\n\nconst SUBSCRIBE_PATH = `/replication/v${PROTOCOL_VERSION}/subscribe`;\n\n// Max time for a client to establish the /subscribe websocket (TCP connect\n// plus upgrade) before abandoning the attempt.\nconst CONNECT_TIMEOUT_MS = 10_000;\n\ntype Options = HttpOptions & {\n startupDelayMs: number;\n config?:\n | Pick<NormalizedZeroConfig, 'adminPassword' | 'operatorPassword'>\n | undefined;\n getProfileWorker?: (() => Promise<Worker>) | undefined;\n};\n\nexport class ChangeStreamerHttpServer extends HttpService {\n readonly id = 'change-streamer-http-server';\n readonly #lc: LogContext;\n readonly #opts: Options;\n readonly #changeStreamer: ChangeStreamerService;\n\n constructor(\n lc: LogContext,\n opts: Options,\n parent: Worker,\n changeStreamer: ChangeStreamerService,\n ) {\n super('change-streamer-http-server', lc, opts, async fastify => {\n await fastify.register(websocket);\n\n fastify.get(CHANGES_PATH_PATTERN, {websocket: true}, this.#changes);\n fastify.get(\n SNAPSHOT_PATH_PATTERN,\n {websocket: true},\n this.#reserveSnapshot,\n );\n fastify.get(SUBSCRIBE_PATH_PATTERN, {websocket: true}, this.#subscribe);\n\n fastify.get('/profz', (req, res) =>\n handleProfzRequest(\n lc,\n opts.config ?? {\n adminPassword: undefined,\n operatorPassword: undefined,\n },\n req,\n res,\n opts.getProfileWorker,\n undefined,\n 'change-streamer',\n ),\n );\n\n installWebSocketReceiver<'snapshot' | 'changes' | 'subscribe'>(\n lc,\n fastify.websocketServer,\n this.#receiveWebsocket,\n parent,\n );\n });\n\n this.#lc = lc;\n this.#opts = opts;\n this.#changeStreamer = changeStreamer;\n }\n\n // Called when receiving a web socket via the main dispatcher handoff.\n readonly #receiveWebsocket = (\n ws: WebSocket,\n action: 'changes' | 'snapshot' | 'subscribe',\n msg: IncomingMessageSubset,\n ) => {\n switch (action) {\n case 'snapshot':\n return this.#reserveSnapshot(ws, msg);\n case 'changes':\n return this.#changes(ws, msg);\n case 'subscribe':\n return this.#subscribe(ws, msg);\n default:\n closeWithError(\n this._lc,\n ws,\n `invalid action \"${action}\" received in handoff`,\n );\n return;\n }\n };\n\n readonly #reserveSnapshot = async (ws: WebSocket, req: RequestHeaders) => {\n this.#ensureChangeStreamerStarted('incoming snapshot reservation');\n try {\n const url = new URL(\n req.url ?? '',\n req.headers.origin ?? 'http://localhost',\n );\n checkProtocolVersion(url.pathname);\n const taskID = url.searchParams.get('taskID');\n if (!taskID) {\n throw new Error('Missing taskID in snapshot request');\n }\n const downstream =\n await this.#changeStreamer.startSnapshotReservation(taskID);\n // Send-only for now: the reservation carries no inbound application\n // messages (only transport acks, demuxed internally).\n void streamInternal(this._lc, ws, undefined, downstream);\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n }\n };\n\n readonly #changes = async (ws: WebSocket, req: RequestHeaders) => {\n try {\n const ctx = getSubscriberContext(req);\n if (ctx.mode === 'serving') {\n this.#ensureChangeStreamerStarted('incoming subscription');\n }\n\n const downstream = await this.#changeStreamer.subscribe(ctx);\n // Send-only for now: the subscription carries no inbound application\n // messages (only transport acks, demuxed internally).\n void streamInternalStringified(this._lc, ws, undefined, downstream, {\n batched: ctx.wsBatched,\n });\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n }\n };\n\n /**\n * The merged v7 `/subscribe` handler. A single bidirectional connection\n * carries, in sequence: an optional reservation phase (`reserve-snapshot` →\n * `reserved`) and then the subscription phase (`start-subscription` → change\n * stream). This ensures that the subscriber subscribes to the same task from\n * which it restored the backup.\n */\n readonly #subscribe = async (ws: WebSocket, req: RequestHeaders) => {\n let reservation: Source<SnapshotMessage> | undefined;\n const outbound = Subscription.create<string | PreSerializedBatch>();\n\n try {\n const url = new URL(\n req.url ?? '',\n req.headers.origin ?? 'http://localhost',\n );\n const protocolVersion = checkProtocolVersion(url.pathname);\n if (protocolVersion < MIN_SUBSCRIBE_PROTOCOL_VERSION) {\n throw new Error(\n `the /subscribe endpoint requires protocol v${MIN_SUBSCRIBE_PROTOCOL_VERSION} ` +\n `(client is at v${protocolVersion})`,\n );\n }\n\n const instream = await streamInternalStringified(\n this._lc,\n ws,\n subscribeUpstreamSchema,\n outbound,\n {batched: true},\n );\n\n // Keep reading upstream control messages for the life of the connection.\n // Do not manually exit loop, as that would close the socket. The loop\n // exits naturally when the connection closes.\n for await (const msg of instream) {\n if (msg[0] === 'reserve-snapshot') {\n const {taskID, localReplica} = msg[1];\n this.#ensureChangeStreamerStarted('incoming snapshot reservation');\n reservation = await this.#changeStreamer.startSnapshotReservation(\n taskID,\n {localReplica},\n );\n void this.#forwardReservation(reservation, outbound);\n } else if (msg[0] === 'start-subscription') {\n const ctx: SubscriberContext = {\n ...msg[1],\n protocolVersion,\n wsBatched: true,\n };\n if (ctx.mode === 'serving') {\n this.#ensureChangeStreamerStarted('incoming subscription');\n }\n // subscribe() registers the subscriber and then releases any\n // reservation for that task.\n reservation = undefined;\n await this.#changeStreamer.subscribe(ctx, outbound);\n }\n }\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n } finally {\n // Covers a reservation-phase disconnect (instream ended before a\n // subscription started); idempotent with the close handler above.\n reservation?.cancel();\n }\n };\n\n /**\n * Forwards a snapshot reservation's single `['status', ...]` message onto\n * the merged connection's `outbound` sink as a `['reserved', ...]` message.\n * The loop then blocks on the held-open reservation until it is cancelled,\n * by `subscribe()` at the subscription handoff, or by the reservation-phase\n * teardown on disconnect.\n */\n async #forwardReservation(\n reservation: Source<SnapshotMessage>,\n outbound: Subscription<string | PreSerializedBatch>,\n ) {\n try {\n for await (const [, status] of reservation) {\n // Translate the /snapshot protocol's ['status', {tag: 'status', ...}]\n // message to the /subscribe protocol's ['reserved', {tag: 'snapshot', ...}].\n outbound.push(\n BigIntJSON.stringify([\n 'reserved',\n {...status, tag: 'snapshot'},\n ] satisfies ReservedMessage),\n );\n }\n } catch (e) {\n this._lc.warn?.(`error forwarding snapshot reservation`, e);\n outbound.fail(e instanceof Error ? e : new Error(String(e)));\n }\n }\n\n #changeStreamerStarted = false;\n\n #ensureChangeStreamerStarted(reason?: string) {\n if (!this.#changeStreamerStarted && this._state.shouldRun()) {\n this.#lc.info?.(\n `starting ChangeStreamerService ${reason ? `(${reason})` : ''}`,\n );\n void this.#changeStreamer\n .run()\n .catch(e =>\n this.#lc.warn?.(`ChangeStreamerService ended with error`, e),\n )\n .finally(() => this.stop());\n\n this.#changeStreamerStarted = true;\n }\n }\n\n protected override _onStart(): void {\n const {startupDelayMs, readinessGate = promiseVoid} = this.#opts;\n if (startupDelayMs === 0) {\n // In RMv2, there is no need to delay starting the change streamer\n // because RM startup is non-disruptive.\n this.#ensureChangeStreamerStarted();\n } else {\n // In RMv1, starting the change-streamer forcibly shuts down the\n // previous change-streamer, causing view-syncers to reconnect.\n // If this replication-manager has just started, the routing layer may\n // not have registered it with DNS, as that only happens after it\n // confirms health checks. To minimize downtime, the takeover is\n // delayed for the configured startupDelayMs _after_ beginning to\n // advertise readiness.\n void readinessGate.then(() => {\n this.#lc.info?.(\n `waiting ${startupDelayMs}ms before taking over the change log`,\n );\n this._state.setTimeout(\n () =>\n this.#ensureChangeStreamerStarted(\n `startup delay elapsed (${startupDelayMs} ms)`,\n ),\n startupDelayMs,\n );\n });\n }\n }\n\n /**\n * Signals (drainable) subscribers to switch over to another\n * replication-manager, resolving when they have disconnected. Subscribers\n * continue to be served in the meantime, but new connections (e.g.\n * switchover attempts, reconnects, and snapshot reservations) are refused,\n * so that they are routed to another replication-manager.\n */\n async drain(): Promise<void> {\n this.stopAccepting();\n if (this.#changeStreamerStarted && this.#changeStreamer.drain) {\n await this.#changeStreamer.drain();\n }\n }\n\n protected override async _onStop(): Promise<void> {\n if (this.#changeStreamerStarted) {\n await this.#changeStreamer.stop();\n }\n }\n}\n\nexport interface SnapshotReserver {\n reserveSnapshot(\n taskID: string,\n localReplica?: LocalReplica | undefined,\n ): Promise<{\n reserved: SnapshotStatus;\n followup: ReservationFollowup;\n }>;\n}\n\nexport interface SubscriptionStarter {\n subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>>;\n}\n\nexport interface ReservationFollowup extends SubscriptionStarter {\n /**\n * A signal to indicate that the followup is no longer valid (i.e. the\n * connection to the reserving task has been severed).\n */\n readonly signal: AbortSignal;\n\n /**\n * Abandons the reservation without subscribing, closing the connection that\n * was held open to pin the change-log floor. Used when the caller cannot\n * proceed to `subscribe()` (e.g. process shutdown while a restore is in\n * progress) and must not leave the reservation's connection open forever.\n */\n cancel(reason?: Error): void;\n}\n\nexport class ChangeStreamerHttpClient\n implements ChangeStreamer, SnapshotReserver, SubscriptionStarter\n{\n readonly #lc: LogContext;\n readonly #shardID: ShardID;\n readonly #changeDB: PostgresDB;\n readonly #changeStreamerURI: string | undefined;\n\n constructor(\n lc: LogContext,\n shardID: ShardID,\n changeDB: string,\n changeStreamerURI: string | undefined,\n ) {\n this.#lc = lc;\n this.#shardID = shardID;\n // Create a pg client with a single short-lived connection for the purpose\n // of change-streamer discovery (i.e. ChangeDB as DNS).\n this.#changeDB = pgClient(lc, changeDB, 'change-streamer-discovery', {\n max: 1,\n ['idle_timeout']: 15,\n });\n this.#changeStreamerURI = changeStreamerURI;\n }\n\n async #resolveChangeStreamer(path: string) {\n let baseURL = this.#changeStreamerURI;\n if (!baseURL) {\n const address = await discoverChangeStreamerAddress(\n this.#shardID,\n this.#changeDB,\n );\n if (!address) {\n throw new Error(`no change-streamer is running`);\n }\n baseURL = address.includes('://') ? `${address}/` : `ws://${address}/`;\n }\n const uri = new URL(path, baseURL);\n this.#lc.info?.(`connecting to change-streamer@${uri}`);\n return uri;\n }\n\n async reserveSnapshot(\n taskID: string,\n localReplica?: LocalReplica | undefined,\n ): Promise<{\n reserved: SnapshotStatus;\n followup: ReservationFollowup;\n }> {\n const {instream, outbound} = await this.connect(taskID);\n outbound.push([\n 'reserve-snapshot',\n {taskID, ...(localReplica ? {localReplica} : {})},\n ]);\n\n const {head, rest} = readHead(instream);\n const item = await head.next();\n if (!item.value) {\n throw new Error('stream closed without snapshot reservation');\n }\n const {data: msg} = item.value;\n if (msg[0] !== 'reserved') {\n throw new Error(`expected \"reserved\" message but got ${msg[0]}`);\n }\n const reserved: SnapshotStatus = msg[1];\n const followup: ReservationFollowup = {\n subscribe: ctx => {\n outbound.push(['start-subscription', toStartSubscriptionContext(ctx)]);\n return Promise.resolve(rest);\n },\n signal: outbound.signal,\n cancel: reason => outbound.cancel(reason),\n };\n\n return {reserved, followup};\n }\n\n async subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>> {\n const {instream, outbound} = await this.connect(ctx.taskID);\n outbound.push(['start-subscription', toStartSubscriptionContext(ctx)]);\n return instream;\n }\n\n /**\n * Opens a merged v7 `/subscribe` connection (see subscribe.ts). Returns the\n * inbound change/reservation stream and an `outbound` sink into which the\n * caller pushes the `reserve-snapshot` / `start-subscription` control\n * messages that drive the connection's phases. The single connection spans\n * the reservation, backup restore, and subscription, so the reserving\n * replication-manager is the one that serves the subscription.\n */\n // exported for testing\n async connect(taskID: string): Promise<{\n instream: Source<Sized<SubscribeDownstream>>;\n outbound: Subscription<SubscribeUpstream>;\n }> {\n const uri = await this.#resolveChangeStreamer(SUBSCRIBE_PATH);\n\n // taskID is carried in the query for observability/routing; the server\n // reads the authoritative taskID from the control messages themselves.\n const params = new URLSearchParams({taskID});\n // Bound the TCP connect + HTTP upgrade. Without this, a SYN black-holed\n // by a stale Service endpoint (e.g. a just-terminated replication-manager\n // pod) is retransmitted by the kernel for ~2 minutes before ETIMEDOUT.\n // Failing fast lets the caller's retry open a fresh connection that is\n // routed to a live endpoint.\n const ws = new WebSocket(uri + `?${params.toString()}`, {\n handshakeTimeout: CONNECT_TIMEOUT_MS,\n });\n\n const outbound = Subscription.create<SubscribeUpstream>();\n const instream = await streamInternalWithSize(\n this.#lc,\n ws,\n subscribeDownstreamSchema,\n outbound,\n );\n return {instream, outbound};\n }\n}\n\ntype RequestHeaders = Pick<IncomingMessage, 'url' | 'headers'>;\n\nexport function getSubscriberContext(req: RequestHeaders): SubscriberContext {\n const url = new URL(req.url ?? '', req.headers.origin ?? 'http://localhost');\n const protocolVersion = checkProtocolVersion(url.pathname);\n const params = new URLParams(url);\n\n return {\n protocolVersion,\n id: params.get('id', true),\n taskID: params.get('taskID', true),\n mode: params.get('mode', false) === 'backup' ? 'backup' : 'serving',\n replicaVersion: params.get('replicaVersion', true),\n watermark: params.get('watermark', true),\n // Absent for subscribers that predate the parameter, which is the safe\n // default: the barrier falls back to polling rather than waiting on an\n // ACK that would never be attributed to a writer.\n wsBatched: params.getBoolean('wsBatched'),\n };\n}\n\nfunction checkProtocolVersion(pathname: string): number {\n const match = PATH_REGEX.exec(pathname);\n if (!match) {\n throw new Error(`invalid path: ${pathname}`);\n }\n const v = Number(match.groups?.version);\n if (\n Number.isNaN(v) ||\n v > PROTOCOL_VERSION ||\n v < MIN_SUPPORTED_PROTOCOL_VERSION\n ) {\n throw new Error(\n `Cannot service client at protocol v${v}. ` +\n `Supported protocols: [v${MIN_SUPPORTED_PROTOCOL_VERSION} ... v${PROTOCOL_VERSION}]`,\n );\n }\n return v;\n}\n\n// Projects a client-side SubscriberContext onto the start-subscription\n// message payload, dropping the fields that are implicit on the merged\n// connection: protocolVersion (from the request path), wsBatched (always on).\nfunction toStartSubscriptionContext(ctx: SubscriberContext): SubscribeContext {\n const {\n taskID,\n id,\n mode,\n replicaVersion,\n watermark,\n backfills,\n drainable,\n switchover,\n } = ctx;\n return {\n taskID,\n id,\n mode,\n replicaVersion,\n watermark,\n backfills,\n ...(drainable !== undefined ? {drainable} : {}),\n ...(switchover !== undefined ? {switchover} : {}),\n };\n}\n"],"mappings":"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"}
|
|
@@ -479,10 +479,10 @@ var ChangeStreamerImpl = class {
|
|
|
479
479
|
});
|
|
480
480
|
this.#backfills.register(subscriber, downstream);
|
|
481
481
|
try {
|
|
482
|
-
if (
|
|
483
|
-
lc.warn?.(`rejecting
|
|
482
|
+
if (this.#drainTracker.draining) {
|
|
483
|
+
lc.warn?.(`rejecting subscriber ${id}: this replication-manager is draining`);
|
|
484
484
|
subscriber.close();
|
|
485
|
-
} else if (replicaVersion !== this.#replicaVersion) if (ctx.switchover && mode === "serving" && this.#restoreAdmission &&
|
|
485
|
+
} else if (replicaVersion !== this.#replicaVersion) if (ctx.switchover && mode === "serving" && this.#restoreAdmission && replicaVersion < this.#replicaVersion) {
|
|
486
486
|
lc.info?.(`holding switchover subscriber ${subscriber.id} at replica version ${replicaVersion} for permission to restore ${this.#replicaVersion}`);
|
|
487
487
|
const ticket = this.#restoreAdmission.request({
|
|
488
488
|
onQueued: (position) => subscriber.sendControlStatus({
|