@rocicorp/zero 0.7.2024120400 → 0.7.2024120700

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.
Files changed (188) hide show
  1. package/out/{chunk-WJCWI5I4.js → chunk-PFF4AHTF.js} +259 -202
  2. package/out/{chunk-WJCWI5I4.js.map → chunk-PFF4AHTF.js.map} +4 -4
  3. package/out/otel/src/noop-span-exporter.d.ts +8 -0
  4. package/out/otel/src/noop-span-exporter.d.ts.map +1 -0
  5. package/out/otel/src/noop-span-exporter.js +14 -0
  6. package/out/otel/src/noop-span-exporter.js.map +1 -0
  7. package/out/otel/src/span.d.ts +5 -0
  8. package/out/otel/src/span.d.ts.map +1 -0
  9. package/out/otel/src/span.js +30 -0
  10. package/out/otel/src/span.js.map +1 -0
  11. package/out/otel/src/version.d.ts +2 -0
  12. package/out/otel/src/version.d.ts.map +1 -0
  13. package/out/otel/src/version.js +2 -0
  14. package/out/otel/src/version.js.map +1 -0
  15. package/out/react.js +12 -6
  16. package/out/react.js.map +2 -2
  17. package/out/solid.js +1 -1
  18. package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
  19. package/out/zero-cache/src/auth/write-authorizer.js +3 -4
  20. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  21. package/out/zero-cache/src/config/zero-config.d.ts +11 -7
  22. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  23. package/out/zero-cache/src/config/zero-config.js +11 -7
  24. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  25. package/out/zero-cache/src/server/change-streamer.d.ts +1 -1
  26. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  27. package/out/zero-cache/src/server/change-streamer.js +3 -3
  28. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  29. package/out/zero-cache/src/server/main.js +6 -11
  30. package/out/zero-cache/src/server/main.js.map +1 -1
  31. package/out/zero-cache/src/server/replicator.d.ts +1 -1
  32. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  33. package/out/zero-cache/src/server/replicator.js +3 -3
  34. package/out/zero-cache/src/server/replicator.js.map +1 -1
  35. package/out/zero-cache/src/server/syncer.d.ts +1 -1
  36. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  37. package/out/zero-cache/src/server/syncer.js +34 -5
  38. package/out/zero-cache/src/server/syncer.js.map +1 -1
  39. package/out/zero-cache/src/services/change-streamer/pg/change-source.d.ts.map +1 -1
  40. package/out/zero-cache/src/services/change-streamer/pg/change-source.js +31 -21
  41. package/out/zero-cache/src/services/change-streamer/pg/change-source.js.map +1 -1
  42. package/out/zero-cache/src/services/dispatcher/connect-params.d.ts +2 -1
  43. package/out/zero-cache/src/services/dispatcher/connect-params.d.ts.map +1 -1
  44. package/out/zero-cache/src/services/dispatcher/connect-params.js +3 -2
  45. package/out/zero-cache/src/services/dispatcher/connect-params.js.map +1 -1
  46. package/out/zero-cache/src/services/dispatcher/dispatcher.d.ts.map +1 -1
  47. package/out/zero-cache/src/services/dispatcher/dispatcher.js +5 -5
  48. package/out/zero-cache/src/services/dispatcher/dispatcher.js.map +1 -1
  49. package/out/zero-cache/src/services/mutagen/mutagen.d.ts +1 -1
  50. package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
  51. package/out/zero-cache/src/services/mutagen/mutagen.js +28 -20
  52. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  53. package/out/zero-cache/src/services/runner.js +1 -4
  54. package/out/zero-cache/src/services/runner.js.map +1 -1
  55. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts +1 -1
  56. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
  57. package/out/zero-cache/src/services/view-syncer/client-handler.js +3 -2
  58. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  59. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts +2 -2
  60. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  61. package/out/zero-cache/src/services/view-syncer/cvr-store.js +41 -32
  62. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  63. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  64. package/out/zero-cache/src/services/view-syncer/cvr.js +2 -2
  65. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  66. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +11 -1
  67. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  68. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +5 -5
  69. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  70. package/out/zero-cache/src/services/view-syncer/view-syncer.js +384 -357
  71. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  72. package/out/zero-cache/src/types/error-for-client.d.ts +10 -3
  73. package/out/zero-cache/src/types/error-for-client.d.ts.map +1 -1
  74. package/out/zero-cache/src/types/error-for-client.js +4 -4
  75. package/out/zero-cache/src/types/error-for-client.js.map +1 -1
  76. package/out/zero-cache/src/types/lite.d.ts.map +1 -1
  77. package/out/zero-cache/src/types/lite.js +1 -0
  78. package/out/zero-cache/src/types/lite.js.map +1 -1
  79. package/out/zero-cache/src/types/pg.d.ts +1 -1
  80. package/out/zero-cache/src/types/pg.d.ts.map +1 -1
  81. package/out/zero-cache/src/types/pg.js +33 -7
  82. package/out/zero-cache/src/types/pg.js.map +1 -1
  83. package/out/zero-cache/src/types/processes.d.ts +5 -1
  84. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  85. package/out/zero-cache/src/types/processes.js +12 -2
  86. package/out/zero-cache/src/types/processes.js.map +1 -1
  87. package/out/zero-cache/src/types/schema-versions.d.ts.map +1 -1
  88. package/out/zero-cache/src/types/schema-versions.js +4 -5
  89. package/out/zero-cache/src/types/schema-versions.js.map +1 -1
  90. package/out/zero-cache/src/workers/connection.d.ts +14 -6
  91. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  92. package/out/zero-cache/src/workers/connection.js +78 -70
  93. package/out/zero-cache/src/workers/connection.js.map +1 -1
  94. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  95. package/out/zero-cache/src/workers/syncer.js +8 -10
  96. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  97. package/out/zero-client/src/client/options.d.ts +10 -6
  98. package/out/zero-client/src/client/options.d.ts.map +1 -1
  99. package/out/zero-client/src/client/query-manager.d.ts.map +1 -1
  100. package/out/zero-client/src/client/server-error.d.ts +5 -3
  101. package/out/zero-client/src/client/server-error.d.ts.map +1 -1
  102. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  103. package/out/zero-client/src/mod.d.ts +2 -2
  104. package/out/zero-client/src/mod.d.ts.map +1 -1
  105. package/out/zero-protocol/src/ast.d.ts +3 -0
  106. package/out/zero-protocol/src/ast.d.ts.map +1 -1
  107. package/out/zero-protocol/src/ast.js +2 -0
  108. package/out/zero-protocol/src/ast.js.map +1 -1
  109. package/out/zero-protocol/src/error.d.ts +18 -1
  110. package/out/zero-protocol/src/error.d.ts.map +1 -1
  111. package/out/zero-protocol/src/error.js +12 -2
  112. package/out/zero-protocol/src/error.js.map +1 -1
  113. package/out/zero-protocol/src/protocol-version.d.ts +20 -0
  114. package/out/zero-protocol/src/protocol-version.d.ts.map +1 -0
  115. package/out/zero-protocol/src/protocol-version.js +20 -0
  116. package/out/zero-protocol/src/protocol-version.js.map +1 -0
  117. package/out/zero-react/src/use-query.d.ts +9 -1
  118. package/out/zero-react/src/use-query.d.ts.map +1 -1
  119. package/out/zero-schema/src/compiled-permissions.d.ts +3 -3
  120. package/out/zero-schema/src/compiled-permissions.js +1 -1
  121. package/out/zero-schema/src/compiled-permissions.js.map +1 -1
  122. package/out/zero-schema/src/mod.d.ts +1 -1
  123. package/out/zero-schema/src/mod.d.ts.map +1 -1
  124. package/out/zero-schema/src/permissions.d.ts +1 -1
  125. package/out/zero-schema/src/permissions.d.ts.map +1 -1
  126. package/out/zero-schema/src/table-schema.d.ts +0 -3
  127. package/out/zero-schema/src/table-schema.d.ts.map +1 -1
  128. package/out/zero-schema/src/table-schema.js.map +1 -1
  129. package/out/zero.js +1 -1
  130. package/out/zql/src/builder/builder.d.ts.map +1 -1
  131. package/out/zql/src/builder/builder.js +9 -11
  132. package/out/zql/src/builder/builder.js.map +1 -1
  133. package/out/zql/src/ivm/array-view.d.ts +1 -1
  134. package/out/zql/src/ivm/array-view.d.ts.map +1 -1
  135. package/out/zql/src/ivm/array-view.js +16 -3
  136. package/out/zql/src/ivm/array-view.js.map +1 -1
  137. package/out/zql/src/ivm/data.d.ts +1 -1
  138. package/out/zql/src/ivm/data.d.ts.map +1 -1
  139. package/out/zql/src/ivm/data.js +3 -2
  140. package/out/zql/src/ivm/data.js.map +1 -1
  141. package/out/zql/src/ivm/exists.d.ts.map +1 -1
  142. package/out/zql/src/ivm/exists.js +13 -2
  143. package/out/zql/src/ivm/exists.js.map +1 -1
  144. package/out/zql/src/ivm/join.d.ts +3 -2
  145. package/out/zql/src/ivm/join.d.ts.map +1 -1
  146. package/out/zql/src/ivm/join.js +5 -2
  147. package/out/zql/src/ivm/join.js.map +1 -1
  148. package/out/zql/src/ivm/memory-source.d.ts +2 -12
  149. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  150. package/out/zql/src/ivm/memory-source.js +29 -63
  151. package/out/zql/src/ivm/memory-source.js.map +1 -1
  152. package/out/zql/src/ivm/operator.d.ts +3 -1
  153. package/out/zql/src/ivm/operator.d.ts.map +1 -1
  154. package/out/zql/src/ivm/schema.d.ts +2 -1
  155. package/out/zql/src/ivm/schema.d.ts.map +1 -1
  156. package/out/zql/src/ivm/skip.d.ts.map +1 -1
  157. package/out/zql/src/ivm/skip.js +60 -54
  158. package/out/zql/src/ivm/skip.js.map +1 -1
  159. package/out/zql/src/ivm/take.d.ts.map +1 -1
  160. package/out/zql/src/ivm/take.js +47 -35
  161. package/out/zql/src/ivm/take.js.map +1 -1
  162. package/out/zql/src/ivm/view.d.ts +1 -1
  163. package/out/zql/src/ivm/view.d.ts.map +1 -1
  164. package/out/zql/src/query/auth-query.d.ts +1 -0
  165. package/out/zql/src/query/auth-query.d.ts.map +1 -1
  166. package/out/zql/src/query/auth-query.js +1 -0
  167. package/out/zql/src/query/auth-query.js.map +1 -1
  168. package/out/zql/src/query/query-impl.d.ts +6 -4
  169. package/out/zql/src/query/query-impl.d.ts.map +1 -1
  170. package/out/zql/src/query/query-impl.js +18 -4
  171. package/out/zql/src/query/query-impl.js.map +1 -1
  172. package/out/zql/src/query/query.d.ts +9 -5
  173. package/out/zql/src/query/query.d.ts.map +1 -1
  174. package/out/zql/src/query/typed-view.d.ts +2 -1
  175. package/out/zql/src/query/typed-view.d.ts.map +1 -1
  176. package/out/zqlite/src/db.js +1 -1
  177. package/out/zqlite/src/table-source.d.ts.map +1 -1
  178. package/out/zqlite/src/table-source.js +31 -67
  179. package/out/zqlite/src/table-source.js.map +1 -1
  180. package/package.json +5 -1
  181. package/out/shared/src/random-values.js +0 -22
  182. package/out/shared/src/random-values.js.map +0 -1
  183. package/out/zero-client/src/client/protocol-version.d.ts +0 -2
  184. package/out/zero-client/src/client/protocol-version.d.ts.map +0 -1
  185. package/out/zql/src/ivm/lookahead-iterator.d.ts +0 -13
  186. package/out/zql/src/ivm/lookahead-iterator.d.ts.map +0 -1
  187. package/out/zql/src/ivm/lookahead-iterator.js +0 -45
  188. package/out/zql/src/ivm/lookahead-iterator.js.map +0 -1
@@ -1,9 +1,14 @@
1
+ import { trace } from '@opentelemetry/api';
1
2
  import { Lock } from '@rocicorp/lock';
2
3
  import { resolver } from '@rocicorp/resolver';
4
+ import { manualSpan, startAsyncSpan, startSpan, } from '../../../../otel/src/span.js';
5
+ import { version } from '../../../../otel/src/version.js';
3
6
  import { assert, unreachable } from '../../../../shared/src/asserts.js';
4
7
  import { CustomKeyMap } from '../../../../shared/src/custom-key-map.js';
5
8
  import { must } from '../../../../shared/src/must.js';
9
+ import { randInt } from '../../../../shared/src/rand.js';
6
10
  import { ErrorKind, } from '../../../../zero-protocol/src/mod.js';
11
+ import { transformAndHashQuery } from '../../auth/read-authorizer.js';
7
12
  import { stringify } from '../../types/bigint-json.js';
8
13
  import { ErrorForClient } from '../../types/error-for-client.js';
9
14
  import { rowIDString } from '../../types/row-key.js';
@@ -15,12 +20,11 @@ import { CVRConfigDrivenUpdater, CVRQueryDrivenUpdater, } from './cvr.js';
15
20
  import { PipelineDriver } from './pipeline-driver.js';
16
21
  import { cmpVersions, EMPTY_CVR_VERSION, versionFromString, versionString, versionToCookie, } from './schema/types.js';
17
22
  import { SchemaChangeError } from './snapshotter.js';
18
- import { transformAndHashQuery } from '../../auth/read-authorizer.js';
23
+ const tracer = trace.getTracer('view-syncer', version);
19
24
  const DEFAULT_KEEPALIVE_MS = 5_000;
20
- // TODO: make idle timeout more intelligent when browser-level client
21
- // management can provide signals of whether a client is likely to
22
- // reconnect.
23
- const DEFAULT_IDLE_TIMEOUT_MS = 0;
25
+ function randomID() {
26
+ return randInt(1, Number.MAX_SAFE_INTEGER).toString(36);
27
+ }
24
28
  export class ViewSyncerService {
25
29
  id;
26
30
  #shardID;
@@ -29,25 +33,21 @@ export class ViewSyncerService {
29
33
  #stateChanges;
30
34
  #drainCoordinator;
31
35
  #keepaliveMs;
32
- #idleTimeoutMs;
33
36
  // Note: It is fine to update this variable outside of the lock.
34
37
  #lastConnectTime = 0;
38
+ // Note: It is okay to add/remove clients without acquiring the lock.
39
+ #clients = new Map();
35
40
  // Serialize on this lock for:
36
41
  // (1) storage or database-dependent operations
37
42
  // (2) updating member variables.
38
- // (3) initializing a new client, to ensure that it only gets pokes after
39
- // we have processed its initConnectionMessage.
40
- //
41
- // Note that it is okay to remove/delete clients without acquiring the lock.
42
43
  #lock = new Lock();
43
- #clients = new Map();
44
44
  #cvrStore;
45
45
  #stopped = resolver();
46
46
  #permissions;
47
47
  #cvr;
48
48
  #pipelinesSynced = false;
49
49
  #authData;
50
- constructor(lc, taskID, clientGroupID, shardID, db, pipelineDriver, versionChanges, drainCoordinator, permissions, keepaliveMs = DEFAULT_KEEPALIVE_MS, idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS) {
50
+ constructor(lc, taskID, clientGroupID, shardID, db, pipelineDriver, versionChanges, drainCoordinator, permissions, keepaliveMs = DEFAULT_KEEPALIVE_MS) {
51
51
  this.id = clientGroupID;
52
52
  this.#shardID = shardID;
53
53
  this.#lc = lc;
@@ -55,7 +55,6 @@ export class ViewSyncerService {
55
55
  this.#stateChanges = versionChanges;
56
56
  this.#drainCoordinator = drainCoordinator;
57
57
  this.#keepaliveMs = keepaliveMs;
58
- this.#idleTimeoutMs = idleTimeoutMs;
59
58
  this.#cvrStore = new CVRStore(lc, db, taskID, clientGroupID,
60
59
  // On failure, cancel the #stateChanges subscription. The run()
61
60
  // loop will then await #cvrStore.flushed() which rejects if necessary.
@@ -64,11 +63,16 @@ export class ViewSyncerService {
64
63
  }
65
64
  #runInLockWithCVR(fn) {
66
65
  return this.#lock.withLock(async () => {
66
+ const lc = this.#lc.withContext('lock', randomID());
67
+ if (!this.#stateChanges.active) {
68
+ return; // view-syncer has been shutdown
69
+ }
67
70
  if (!this.#cvr) {
68
- this.#cvr = await this.#cvrStore.load(this.#lastConnectTime);
71
+ this.#cvr = await this.#cvrStore.load(lc, this.#lastConnectTime);
69
72
  }
73
+ lc.debug?.(`cvr@${versionString(this.#cvr.version)}`);
70
74
  try {
71
- return await fn(this.#cvr);
75
+ await fn(lc, this.#cvr);
72
76
  }
73
77
  catch (e) {
74
78
  // Clear cached state if an error is encountered.
@@ -89,33 +93,33 @@ export class ViewSyncerService {
89
93
  // On the first version-ready signal, connect to the replica.
90
94
  this.#pipelines.init();
91
95
  }
92
- await this.#runInLockWithCVR(async (cvr) => {
96
+ await this.#runInLockWithCVR(async (lc, cvr) => {
93
97
  if ((cvr.replicaVersion !== null ||
94
98
  cvr.version.stateVersion !== '00') &&
95
99
  cvr.replicaVersion !== this.#pipelines.replicaVersion) {
96
- const msg = `Replica Version mismatch: CVR=${cvr.replicaVersion}, DB=${this.#pipelines.replicaVersion}`;
97
- this.#lc.info?.(`resetting CVR: ${msg}`);
98
- throw new ErrorForClient(['error', ErrorKind.ClientNotFound, msg]);
100
+ const message = `Replica Version mismatch: CVR=${cvr.replicaVersion}, DB=${this.#pipelines.replicaVersion}`;
101
+ lc.info?.(`resetting CVR: ${message}`);
102
+ throw new ErrorForClient({ kind: ErrorKind.ClientNotFound, message });
99
103
  }
100
104
  if (this.#pipelinesSynced) {
101
- const result = await this.#advancePipelines(cvr);
105
+ const result = await this.#advancePipelines(lc, cvr);
102
106
  if (result === 'success') {
103
107
  return;
104
108
  }
105
- this.#lc.info?.(`resetting for schema change: ${result.message}`);
109
+ lc.info?.(`resetting for schema change: ${result.message}`);
106
110
  this.#pipelines.reset();
107
111
  }
108
112
  // Advance the snapshot to the current version.
109
113
  const version = this.#pipelines.advanceWithoutDiff();
110
114
  const cvrVer = versionString(cvr.version);
111
115
  if (version < cvr.version.stateVersion) {
112
- this.#lc.debug?.(`replica@${version} is behind cvr@${cvrVer}`);
116
+ lc.debug?.(`replica@${version} is behind cvr@${cvrVer}`);
113
117
  return; // Wait for the next advancement.
114
118
  }
115
119
  // stateVersion is at or beyond CVR version for the first time.
116
- this.#lc.info?.(`init pipelines@${version} (cvr@${cvrVer})`);
117
- this.#hydrateUnchangedQueries(cvr);
118
- await this.#syncQueryPipelineSet(cvr);
120
+ lc.info?.(`init pipelines@${version} (cvr@${cvrVer})`);
121
+ this.#hydrateUnchangedQueries(lc, cvr);
122
+ await this.#syncQueryPipelineSet(lc, cvr);
119
123
  this.#pipelinesSynced = true;
120
124
  });
121
125
  }
@@ -127,13 +131,13 @@ export class ViewSyncerService {
127
131
  this.#cleanup();
128
132
  }
129
133
  catch (e) {
130
- this.#lc.error?.(e);
134
+ this.#lc.error?.(`stopping view-syncer: ${String(e)}`, e);
131
135
  this.#cleanup(e);
132
136
  }
133
137
  finally {
134
138
  // Always wait for the cvrStore to flush, regardless of how the service
135
139
  // was stopped.
136
- await this.#cvrStore.flushed().catch(e => this.#lc.error?.(e));
140
+ await this.#cvrStore.flushed(this.#lc).catch(e => this.#lc.error?.(e));
137
141
  this.#lc.info?.('view-syncer stopped');
138
142
  this.#stopped.resolve();
139
143
  }
@@ -141,31 +145,7 @@ export class ViewSyncerService {
141
145
  #totalHydrationTimeMs() {
142
146
  return this.#pipelines.totalHydrationTimeMs();
143
147
  }
144
- // The shutdownToken is an object associated with an shutdown timeout function,
145
- // the latter of which checks the token with identity equality before
146
- // executing. Setting the #shutdownToken to a new object or to `null`
147
- // effectively cancels the previous timeout.
148
- #shutdownToken = null;
149
- #startShutdownTimer(reason) {
150
- if (this.#shutdownToken) {
151
- // Previous timeout is canceled for efficiency
152
- // (but not necessary for correctness).
153
- clearTimeout(this.#shutdownToken?.timeoutID);
154
- this.#lc.debug?.(`${reason}. resetting idle timer`);
155
- }
156
- else {
157
- this.#lc.debug?.(`${reason}. starting idle timer`);
158
- }
159
- const shutdownToken = {};
160
- this.#shutdownToken = shutdownToken;
161
- shutdownToken.timeoutID = setTimeout(() => {
162
- // If #idleToken has changed, this timeout is effectively canceled.
163
- if (this.#shutdownToken === shutdownToken) {
164
- this.#lc.info?.('shutting down after idle timeout');
165
- this.#stateChanges.cancel(); // Note: #versionChanges.active becomes false.
166
- }
167
- }, reason === 'keepalive' ? this.#keepaliveMs : this.#idleTimeoutMs);
168
- }
148
+ #keepAliveUntil = 0;
169
149
  /**
170
150
  * Guarantees that the ViewSyncer will remain running for at least
171
151
  * its configured `keepaliveMs`. This is called when establishing a
@@ -179,12 +159,25 @@ export class ViewSyncerService {
179
159
  if (!this.#stateChanges.active) {
180
160
  return false;
181
161
  }
182
- if (this.#shutdownToken) {
183
- // Resets the idle timer for another `#keepaliveMs`.
184
- this.#startShutdownTimer('keepalive');
185
- }
162
+ this.#keepAliveUntil = Date.now() + this.#keepaliveMs;
186
163
  return true;
187
164
  }
165
+ #shutdownTimer = null;
166
+ async #tryShutdown() {
167
+ // Keep the view-syncer alive if there are pending rows being flushed.
168
+ // It's better to do this before shutting down since it may take a
169
+ // while, during which new connections may come in.
170
+ await this.#cvrStore.flushed(this.#lc).catch(e => this.#lc.error?.(e));
171
+ this.#shutdownTimer = null;
172
+ if (Date.now() <= this.#keepAliveUntil) {
173
+ this.#lc.debug?.('not shutting down: keepalive');
174
+ this.#shutdownTimer = setTimeout(() => this.#tryShutdown(), this.#keepaliveMs);
175
+ }
176
+ else if (this.#clients.size === 0) {
177
+ this.#lc.info?.('shutting down');
178
+ this.#stateChanges.cancel(); // Note: #stateChanges.active becomes false.
179
+ }
180
+ }
188
181
  #deleteClient(clientID, client) {
189
182
  // Note: It is okay to delete / cleanup clients without acquiring the lock.
190
183
  // In fact, it is important to do so in order to guarantee that idle cleanup
@@ -193,30 +186,39 @@ export class ViewSyncerService {
193
186
  const c = this.#clients.get(clientID);
194
187
  if (c === client) {
195
188
  this.#clients.delete(clientID);
196
- if (this.#clients.size === 0) {
197
- this.#startShutdownTimer('no more clients');
189
+ if (this.#clients.size === 0 && this.#shutdownTimer === null) {
190
+ this.#shutdownTimer = setTimeout(() => this.#tryShutdown(), 0);
198
191
  }
199
192
  }
200
193
  }
201
- async initConnection(ctx, initConnectionMessage) {
202
- this.#lastConnectTime = Date.now();
203
- const { clientID, wsID, baseCookie, schemaVersion, tokenData } = ctx;
204
- this.#authData = pickToken(this.#authData, tokenData?.decoded);
205
- const lc = this.#lc
206
- .withContext('clientID', clientID)
207
- .withContext('wsID', wsID);
208
- // Setup the downstream connection.
209
- const downstream = Subscription.create({
210
- cleanup: (_, err) => {
211
- err
212
- ? lc.error?.(`client closed with error`, err)
213
- : lc.info?.('client closed');
214
- this.#deleteClient(clientID, newClient);
215
- },
194
+ initConnection(ctx, initConnectionMessage) {
195
+ return startSpan(tracer, 'vs.initConnection', () => {
196
+ this.#lastConnectTime = Date.now();
197
+ const { clientID, wsID, baseCookie, schemaVersion, tokenData } = ctx;
198
+ this.#authData = pickToken(this.#authData, tokenData?.decoded);
199
+ const lc = this.#lc
200
+ .withContext('clientID', clientID)
201
+ .withContext('wsID', wsID);
202
+ // Setup the downstream connection.
203
+ const downstream = Subscription.create({
204
+ cleanup: (_, err) => {
205
+ err
206
+ ? lc.error?.(`client closed with error`, err)
207
+ : lc.info?.('client closed');
208
+ this.#deleteClient(clientID, newClient);
209
+ },
210
+ });
211
+ const newClient = new ClientHandler(lc, this.id, clientID, wsID, this.#shardID, baseCookie, schemaVersion, downstream);
212
+ this.#clients.get(clientID)?.close(`replaced by wsID: ${wsID}`);
213
+ this.#clients.set(clientID, newClient);
214
+ // Note: initConnection() must be synchronous so that `downstream` is
215
+ // immediately returned to the caller (connection.ts). This ensures
216
+ // that if the connection is subsequently closed, the `downstream`
217
+ // subscription can be properly canceled even if #runInLockForClient()
218
+ // has not had a chance to run.
219
+ void this.#runInLockForClient(ctx, initConnectionMessage, this.#patchQueries, newClient).catch(e => newClient.fail(e));
220
+ return downstream;
216
221
  });
217
- const newClient = new ClientHandler(lc, this.id, clientID, wsID, this.#shardID, baseCookie, schemaVersion, downstream);
218
- await this.#runInLockForClient(ctx, initConnectionMessage, this.#patchQueries, newClient);
219
- return downstream;
220
222
  }
221
223
  async changeDesiredQueries(ctx, msg) {
222
224
  await this.#runInLockForClient(ctx, msg, this.#patchQueries);
@@ -225,30 +227,17 @@ export class ViewSyncerService {
225
227
  * Runs the given `fn` to process the `msg` from within the `#lock`,
226
228
  * optionally adding the `newClient` if supplied.
227
229
  */
228
- async #runInLockForClient(ctx, msg, fn, newClient) {
230
+ #runInLockForClient(ctx, msg, fn, newClient) {
229
231
  const { clientID, wsID } = ctx;
230
232
  const [cmd, body] = msg;
231
- const lc = this.#lc
232
- .withContext('clientID', clientID)
233
- .withContext('wsID', wsID)
234
- .withContext('cmd', cmd);
235
- // Clear and cancel any shutdown timeout.
236
- if (this.#shutdownToken) {
237
- clearTimeout(this.#shutdownToken.timeoutID);
238
- this.#shutdownToken = null;
239
- }
240
- let client;
241
- try {
242
- await this.#runInLockWithCVR(cvr => {
243
- lc.debug?.(cmd, body);
244
- if (newClient) {
245
- assert(newClient.wsID === wsID);
246
- this.#clients.get(clientID)?.close();
247
- this.#clients.set(clientID, newClient);
248
- client = newClient;
249
- checkClientAndCVRVersions(client.version(), cvr.version);
250
- }
251
- else {
233
+ return startAsyncSpan(tracer, `vs.#runInLockForClient(${cmd})`, async () => {
234
+ let client;
235
+ try {
236
+ await this.#runInLockWithCVR((lc, cvr) => {
237
+ lc = lc
238
+ .withContext('clientID', clientID)
239
+ .withContext('wsID', wsID)
240
+ .withContext('cmd', cmd);
252
241
  client = this.#clients.get(clientID);
253
242
  if (client?.wsID !== wsID) {
254
243
  // Only respond to messages of the currently connected client.
@@ -256,24 +245,39 @@ export class ViewSyncerService {
256
245
  lc.debug?.(`client no longer connected. dropping ${cmd} message`);
257
246
  return;
258
247
  }
259
- }
260
- return fn(lc, clientID, body, cvr);
261
- });
262
- }
263
- catch (e) {
264
- lc.error?.(`closing connection with error`, e);
265
- if (client) {
266
- // Ideally, propagate the exception to the client's downstream subscription ...
267
- client.fail(e);
248
+ if (newClient) {
249
+ assert(newClient === client);
250
+ checkClientAndCVRVersions(client.version(), cvr.version);
251
+ }
252
+ lc.debug?.(cmd, body);
253
+ return fn(lc, clientID, body, cvr);
254
+ });
268
255
  }
269
- else {
270
- // unless the exception happened before the client could be looked up.
271
- throw e;
256
+ catch (e) {
257
+ this.#lc
258
+ .withContext('clientID', clientID)
259
+ .withContext('wsID', wsID)
260
+ .withContext('cmd', cmd)
261
+ .error?.(`closing connection with error`, e);
262
+ if (client) {
263
+ // Ideally, propagate the exception to the client's downstream subscription ...
264
+ client.fail(e);
265
+ }
266
+ else {
267
+ // unless the exception happened before the client could be looked up.
268
+ throw e;
269
+ }
272
270
  }
273
- }
271
+ });
272
+ }
273
+ #getClients(atVersion) {
274
+ const clients = [...this.#clients.values()];
275
+ return atVersion
276
+ ? clients.filter(c => cmpVersions(c.version() ?? EMPTY_CVR_VERSION, atVersion) === 0)
277
+ : clients;
274
278
  }
275
279
  // Must be called from within #lock.
276
- #patchQueries = async (lc, clientID, { desiredQueriesPatch }, cvr) => {
280
+ #patchQueries = (lc, clientID, { desiredQueriesPatch }, cvr) => startAsyncSpan(tracer, 'vs.#patchQueries', async () => {
277
281
  // Apply requested patches.
278
282
  if (desiredQueriesPatch.length) {
279
283
  lc.debug?.(`applying ${desiredQueriesPatch.length} query patches`);
@@ -282,7 +286,9 @@ export class ViewSyncerService {
282
286
  for (const patch of desiredQueriesPatch) {
283
287
  switch (patch.op) {
284
288
  case 'put':
285
- patches.push(...updater.putDesiredQueries(clientID, { [patch.hash]: patch.ast }));
289
+ patches.push(...updater.putDesiredQueries(clientID, {
290
+ [patch.hash]: patch.ast,
291
+ }));
286
292
  break;
287
293
  case 'del':
288
294
  patches.push(...updater.deleteDesiredQueries(clientID, [patch.hash]));
@@ -298,11 +304,7 @@ export class ViewSyncerService {
298
304
  // (Clients that are behind the cvr.version need to be caught up in
299
305
  // #syncQueryPipelineSet(), as row data may be needed for catchup)
300
306
  const newCVR = this.#cvr;
301
- const pokers = [...this.#clients.values()]
302
- .filter(c =>
303
- // i.e. only clients that were caught up with the previous cvr
304
- cmpVersions(c.version() ?? EMPTY_CVR_VERSION, cvr.version) === 0)
305
- .map(c => c.startPoke(newCVR.version));
307
+ const pokers = this.#getClients(cvr.version).map(c => c.startPoke(newCVR.version));
306
308
  for (const patch of patches) {
307
309
  pokers.forEach(poker => poker.addPatch(patch));
308
310
  }
@@ -311,9 +313,9 @@ export class ViewSyncerService {
311
313
  cvr = this.#cvr; // For #syncQueryPipelineSet().
312
314
  }
313
315
  if (this.#pipelinesSynced) {
314
- await this.#syncQueryPipelineSet(cvr);
316
+ await this.#syncQueryPipelineSet(lc, cvr);
315
317
  }
316
- };
318
+ });
317
319
  /**
318
320
  * Adds and hydrates pipelines for queries whose results are already
319
321
  * recorded in the CVR. Namely:
@@ -329,12 +331,12 @@ export class ViewSyncerService {
329
331
  *
330
332
  * This must be called from within the #lock.
331
333
  */
332
- #hydrateUnchangedQueries(cvr) {
334
+ #hydrateUnchangedQueries(lc, cvr) {
333
335
  assert(this.#pipelines.initialized());
334
336
  const dbVersion = this.#pipelines.currentVersion();
335
337
  const cvrVersion = cvr.version;
336
338
  if (cvrVersion.stateVersion !== dbVersion) {
337
- this.#lc.info?.(`CVR (${versionToCookie(cvrVersion)}) is behind db ${dbVersion}`);
339
+ lc.info?.(`CVR (${versionToCookie(cvrVersion)}) is behind db ${dbVersion}`);
338
340
  return; // hydration needs to be run with the CVR updater.
339
341
  }
340
342
  const gotQueries = Object.entries(cvr.queries).filter(([_, state]) => state.transformationHash !== undefined);
@@ -351,11 +353,16 @@ export class ViewSyncerService {
351
353
  }
352
354
  const start = Date.now();
353
355
  let count = 0;
354
- for (const _ of this.#pipelines.addQuery(transformationHash, transformedAst)) {
355
- count++;
356
- }
356
+ startSpan(tracer, 'vs.#hydrateUnchangedQueries.addQuery', span => {
357
+ span.setAttribute('queryHash', hash);
358
+ span.setAttribute('transformationHash', transformationHash);
359
+ span.setAttribute('table', ast.table);
360
+ for (const _ of this.#pipelines.addQuery(transformationHash, transformedAst)) {
361
+ count++;
362
+ }
363
+ });
357
364
  const elapsed = Date.now() - start;
358
- this.#lc.debug?.(`hydrated ${count} rows for ${hash} (${elapsed} ms)`);
365
+ lc.debug?.(`hydrated ${count} rows for ${hash} (${elapsed} ms)`);
359
366
  }
360
367
  }
361
368
  /**
@@ -366,91 +373,100 @@ export class ViewSyncerService {
366
373
  *
367
374
  * This must be called from within the #lock.
368
375
  */
369
- async #syncQueryPipelineSet(cvr) {
370
- assert(this.#pipelines.initialized());
371
- const lc = this.#lc.withContext('cvrVersion', versionString(cvr.version));
372
- const hydratedQueries = this.#pipelines.addedQueries();
373
- // Convert queries to their transformed ast's and hashes
374
- const transformationHashToHash = new Map();
375
- const serverQueries = Object.entries(cvr.queries).map(([id, q]) => {
376
- const { query, hash: transformationHash } = transformAndHashQuery(q.ast, this.#permissions, this.#authData);
377
- assert(query !== undefined && transformationHash !== undefined, 'This query may not be run because the table it reads is not readable. Table: ' +
378
- q.ast.table);
379
- transformationHashToHash.set(transformationHash, id);
380
- return {
381
- id,
382
- // TODO(mlaw): follow up to handle the case where we statically determine
383
- // the query cannot be run and is `undefined`.
384
- ast: query,
385
- transformationHash,
386
- desired: q.internal || Object.keys(q.desiredBy).length > 0,
387
- };
376
+ #syncQueryPipelineSet(lc, cvr) {
377
+ return startAsyncSpan(tracer, 'vs.#syncQueryPipelineSet', async () => {
378
+ assert(this.#pipelines.initialized());
379
+ const hydratedQueries = this.#pipelines.addedQueries();
380
+ // Convert queries to their transformed ast's and hashes
381
+ const transformationHashToHash = new Map();
382
+ const serverQueries = Object.entries(cvr.queries).map(([id, q]) => {
383
+ const { query, hash: transformationHash } = transformAndHashQuery(q.ast, this.#permissions, this.#authData);
384
+ assert(query !== undefined && transformationHash !== undefined, 'This query may not be run because the table it reads is not readable. Table: ' +
385
+ q.ast.table);
386
+ transformationHashToHash.set(transformationHash, id);
387
+ return {
388
+ id,
389
+ // TODO(mlaw): follow up to handle the case where we statically determine
390
+ // the query cannot be run and is `undefined`.
391
+ ast: query,
392
+ transformationHash,
393
+ desired: q.internal || Object.keys(q.desiredBy).length > 0,
394
+ };
395
+ });
396
+ const addQueries = serverQueries.filter(q => q.desired && !hydratedQueries.has(q.transformationHash));
397
+ const removeQueries = serverQueries.filter(q => !q.desired);
398
+ const desiredQueries = new Set(serverQueries.filter(q => q.desired).map(q => q.transformationHash));
399
+ const unhydrateQueries = [...hydratedQueries].filter(transformationHash => !desiredQueries.has(transformationHash));
400
+ if (addQueries.length > 0 ||
401
+ removeQueries.length > 0 ||
402
+ unhydrateQueries.length > 0) {
403
+ await this.#addAndRemoveQueries(lc, cvr, addQueries, removeQueries, unhydrateQueries, transformationHashToHash);
404
+ }
405
+ else {
406
+ await this.#catchupClients(lc, cvr);
407
+ }
408
+ // If CVR was non-empty, then the CVR, database, and all clients
409
+ // should now be at the same version.
410
+ if (serverQueries.length > 0) {
411
+ const cvrVersion = must(this.#cvr).version;
412
+ const dbVersion = this.#pipelines.currentVersion();
413
+ assert(cvrVersion.stateVersion === dbVersion, `CVR@${versionString(cvrVersion)}" does not match DB@${dbVersion}`);
414
+ }
388
415
  });
389
- const addQueries = serverQueries.filter(q => q.desired && !hydratedQueries.has(q.transformationHash));
390
- const removeQueries = serverQueries.filter(q => !q.desired);
391
- const desiredQueries = new Set(serverQueries.filter(q => q.desired).map(q => q.transformationHash));
392
- const unhydrateQueries = [...hydratedQueries].filter(transformationHash => !desiredQueries.has(transformationHash));
393
- if (addQueries.length > 0 ||
394
- removeQueries.length > 0 ||
395
- unhydrateQueries.length > 0) {
396
- await this.#addAndRemoveQueries(lc, cvr, addQueries, removeQueries, unhydrateQueries, transformationHashToHash);
397
- }
398
- else {
399
- await this.#catchupClients(lc, cvr);
400
- }
401
- // If CVR was non-empty, then the CVR, database, and all clients
402
- // should now be at the same version.
403
- if (serverQueries.length > 0) {
404
- const cvrVersion = must(this.#cvr).version;
405
- const dbVersion = this.#pipelines.currentVersion();
406
- assert(cvrVersion.stateVersion === dbVersion, `CVR@${versionString(cvrVersion)}" does not match DB@${dbVersion}`);
407
- }
408
416
  }
409
417
  // This must be called from within the #lock.
410
- async #addAndRemoveQueries(lc, cvr, addQueries, removeQueries, unhydrateQueries, transformationHashToHash) {
411
- assert(addQueries.length > 0 ||
412
- removeQueries.length > 0 ||
413
- unhydrateQueries.length > 0);
414
- const start = Date.now();
415
- const stateVersion = this.#pipelines.currentVersion();
416
- lc = lc.withContext('stateVersion', stateVersion);
417
- lc.info?.(`hydrating ${addQueries.length} queries`);
418
- const updater = new CVRQueryDrivenUpdater(this.#cvrStore, cvr, stateVersion, this.#pipelines.replicaVersion);
419
- // Note: This kicks off background PG queries for CVR data associated with the
420
- // executed and removed queries.
421
- const { newVersion, queryPatches } = updater.trackQueries(lc, addQueries, removeQueries);
422
- const pokers = [...this.#clients.values()].map(c => c.startPoke(newVersion, this.#pipelines.currentSchemaVersions()));
423
- for (const patch of queryPatches) {
424
- pokers.forEach(poker => poker.addPatch(patch));
425
- }
426
- // Removing queries is easy. The pipelines are dropped, and the CVR
427
- // updater handles the updates and pokes.
428
- for (const q of removeQueries) {
429
- this.#pipelines.removeQuery(q.transformationHash);
430
- }
431
- for (const hash of unhydrateQueries) {
432
- this.#pipelines.removeQuery(hash);
433
- }
434
- const pipelines = this.#pipelines;
435
- function* generateRowChanges() {
436
- for (const q of addQueries) {
437
- lc.debug?.(`adding pipeline for query ${q.id}`, q.ast);
438
- yield* pipelines.addQuery(q.transformationHash, q.ast);
418
+ #addAndRemoveQueries(lc, cvr, addQueries, removeQueries, unhydrateQueries, transformationHashToHash) {
419
+ return startAsyncSpan(tracer, 'vs.#addAndRemoveQueries', async () => {
420
+ assert(addQueries.length > 0 ||
421
+ removeQueries.length > 0 ||
422
+ unhydrateQueries.length > 0);
423
+ const start = Date.now();
424
+ const stateVersion = this.#pipelines.currentVersion();
425
+ lc = lc.withContext('stateVersion', stateVersion);
426
+ lc.info?.(`hydrating ${addQueries.length} queries`);
427
+ const updater = new CVRQueryDrivenUpdater(this.#cvrStore, cvr, stateVersion, this.#pipelines.replicaVersion);
428
+ // Note: This kicks off background PG queries for CVR data associated with the
429
+ // executed and removed queries.
430
+ const { newVersion, queryPatches } = updater.trackQueries(lc, addQueries, removeQueries);
431
+ const pokers = this.#getClients().map(c => c.startPoke(newVersion, this.#pipelines.currentSchemaVersions()));
432
+ for (const patch of queryPatches) {
433
+ pokers.forEach(poker => poker.addPatch(patch));
439
434
  }
440
- }
441
- // #processChanges does batched de-duping of rows. Wrap all pipelines in
442
- // a single generator in order to maximize de-duping.
443
- await this.#processChanges(lc, generateRowChanges(), updater, pokers, transformationHashToHash);
444
- for (const patch of await updater.deleteUnreferencedRows(lc)) {
445
- pokers.forEach(poker => poker.addPatch(patch));
446
- }
447
- // Commit the changes and update the CVR snapshot.
448
- this.#cvr = (await updater.flush(lc, this.#lastConnectTime)).cvr;
449
- // Before ending the poke, catch up clients that were behind the old CVR.
450
- await this.#catchupClients(lc, cvr, addQueries.map(q => q.id), pokers);
451
- // Signal clients to commit.
452
- pokers.forEach(poker => poker.end());
453
- lc.info?.(`finished processing queries (${Date.now() - start} ms)`);
435
+ // Removing queries is easy. The pipelines are dropped, and the CVR
436
+ // updater handles the updates and pokes.
437
+ for (const q of removeQueries) {
438
+ this.#pipelines.removeQuery(q.transformationHash);
439
+ }
440
+ for (const hash of unhydrateQueries) {
441
+ this.#pipelines.removeQuery(hash);
442
+ }
443
+ const pipelines = this.#pipelines;
444
+ function* generateRowChanges() {
445
+ for (const q of addQueries) {
446
+ lc.debug?.(`adding pipeline for query ${q.id}`, q.ast);
447
+ const start = performance.now();
448
+ yield* pipelines.addQuery(q.transformationHash, q.ast);
449
+ const end = performance.now();
450
+ manualSpan(tracer, 'vs.addAndConsumeQuery', end - start, {
451
+ hash: q.id,
452
+ transformationHash: q.transformationHash,
453
+ });
454
+ }
455
+ }
456
+ // #processChanges does batched de-duping of rows. Wrap all pipelines in
457
+ // a single generator in order to maximize de-duping.
458
+ await this.#processChanges(lc, generateRowChanges(), updater, pokers, transformationHashToHash);
459
+ for (const patch of await updater.deleteUnreferencedRows(lc)) {
460
+ pokers.forEach(poker => poker.addPatch(patch));
461
+ }
462
+ // Commit the changes and update the CVR snapshot.
463
+ this.#cvr = (await updater.flush(lc, this.#lastConnectTime)).cvr;
464
+ // Before ending the poke, catch up clients that were behind the old CVR.
465
+ await this.#catchupClients(lc, cvr, addQueries.map(q => q.id), pokers);
466
+ // Signal clients to commit.
467
+ pokers.forEach(poker => poker.end());
468
+ lc.info?.(`finished processing queries (${Date.now() - start} ms)`);
469
+ });
454
470
  }
455
471
  /**
456
472
  * @param cvr The CVR to which clients should be caught up to. This does
@@ -463,105 +479,116 @@ export class ViewSyncerService {
463
479
  * using the version from the supplied `cvr`.
464
480
  */
465
481
  // Must be called within #lock
466
- async #catchupClients(lc, cvr, excludeQueryHashes = [], usePokers) {
467
- const pokers = usePokers ??
468
- [...this.#clients.values()].map(c => c.startPoke(cvr.version, this.#pipelines.currentSchemaVersions()));
469
- const catchupFrom = [...this.#clients.values()]
470
- .map(c => c.version())
471
- .reduce((a, b) => (cmpVersions(a, b) < 0 ? a : b), cvr.version);
472
- // This is an AsyncGenerator which won't execute until awaited.
473
- const rowPatches = this.#cvrStore.catchupRowPatches(lc, catchupFrom, cvr, excludeQueryHashes);
474
- // This is a plain async function that kicks off immediately.
475
- const configPatches = this.#cvrStore.catchupConfigPatches(lc, catchupFrom, cvr);
476
- // await the rowPatches first so that the AsyncGenerator kicks off.
477
- let rowPatchCount = 0;
478
- for await (const rows of rowPatches) {
479
- for (const row of rows) {
480
- const { schema, table } = row;
481
- const rowKey = row.rowKey;
482
- const toVersion = versionFromString(row.patchVersion);
483
- const id = { schema, table, rowKey };
484
- let patch;
485
- if (!row.refCounts) {
486
- patch = { type: 'row', op: 'del', id };
487
- }
488
- else {
489
- const row = must(this.#pipelines.getRow(table, rowKey), `Missing row ${table}:${stringify(rowKey)}`);
490
- const { contents } = contentsAndVersion(row);
491
- patch = { type: 'row', op: 'put', id, contents };
482
+ #catchupClients(lc, cvr, excludeQueryHashes = [], usePokers) {
483
+ return startAsyncSpan(tracer, 'vs.#catchupClients', async (span) => {
484
+ const clients = this.#getClients();
485
+ const pokers = usePokers ??
486
+ clients.map(c => c.startPoke(cvr.version, this.#pipelines.currentSchemaVersions()));
487
+ span.setAttribute('numPokers', pokers.length);
488
+ span.setAttribute('numClients', clients.length);
489
+ const catchupFrom = clients
490
+ .map(c => c.version())
491
+ .reduce((a, b) => (cmpVersions(a, b) < 0 ? a : b), cvr.version);
492
+ // This is an AsyncGenerator which won't execute until awaited.
493
+ const rowPatches = this.#cvrStore.catchupRowPatches(lc, catchupFrom, cvr, excludeQueryHashes);
494
+ // This is a plain async function that kicks off immediately.
495
+ const configPatches = this.#cvrStore.catchupConfigPatches(lc, catchupFrom, cvr);
496
+ // await the rowPatches first so that the AsyncGenerator kicks off.
497
+ let rowPatchCount = 0;
498
+ for await (const rows of rowPatches) {
499
+ for (const row of rows) {
500
+ const { schema, table } = row;
501
+ const rowKey = row.rowKey;
502
+ const toVersion = versionFromString(row.patchVersion);
503
+ const id = { schema, table, rowKey };
504
+ let patch;
505
+ if (!row.refCounts) {
506
+ patch = { type: 'row', op: 'del', id };
507
+ }
508
+ else {
509
+ const row = must(this.#pipelines.getRow(table, rowKey), `Missing row ${table}:${stringify(rowKey)}`);
510
+ const { contents } = contentsAndVersion(row);
511
+ patch = { type: 'row', op: 'put', id, contents };
512
+ }
513
+ const patchToVersion = { patch, toVersion };
514
+ pokers.forEach(poker => poker.addPatch(patchToVersion));
515
+ rowPatchCount++;
492
516
  }
493
- const patchToVersion = { patch, toVersion };
494
- pokers.forEach(poker => poker.addPatch(patchToVersion));
495
- rowPatchCount++;
496
- }
497
- }
498
- if (rowPatchCount) {
499
- lc.debug?.(`sent ${rowPatchCount} row patches`);
500
- }
501
- // Then await the config patches which were fetched in parallel.
502
- for (const patch of await configPatches) {
503
- pokers.forEach(poker => poker.addPatch(patch));
504
- }
505
- if (!usePokers) {
506
- pokers.forEach(poker => poker.end());
507
- }
508
- }
509
- async #processChanges(lc, changes, updater, pokers, transformationHashToHash) {
510
- const start = Date.now();
511
- const rows = new CustomKeyMap(rowIDString);
512
- let total = 0;
513
- const processBatch = async () => {
514
- const elapsed = Date.now() - start;
515
- total += rows.size;
516
- lc.debug?.(`processing ${rows.size} (of ${total}) rows (${elapsed} ms)`);
517
- const patches = await updater.received(this.#lc, rows);
518
- patches.forEach(patch => pokers.forEach(poker => poker.addPatch(patch)));
519
- rows.clear();
520
- };
521
- for (const change of changes) {
522
- const { type, queryHash: transformationHash, table, rowKey, row } = change;
523
- const queryHash = must(transformationHashToHash.get(transformationHash), 'could not find the original hash for the transformation hash');
524
- const rowID = { schema: '', table, rowKey: rowKey };
525
- let parsedRow = rows.get(rowID);
526
- let rc;
527
- if (!parsedRow) {
528
- parsedRow = { refCounts: {} };
529
- rows.set(rowID, parsedRow);
530
- rc = 0;
531
517
  }
532
- else {
533
- rc = parsedRow.refCounts[queryHash] ?? 0;
518
+ span.setAttribute('rowPatchCount', rowPatchCount);
519
+ if (rowPatchCount) {
520
+ lc.debug?.(`sent ${rowPatchCount} row patches`);
534
521
  }
535
- const updateVersion = (row) => {
536
- if (!parsedRow.version) {
537
- const { version, contents } = contentsAndVersion(row);
538
- parsedRow.version = version;
539
- parsedRow.contents = contents;
540
- }
541
- };
542
- switch (type) {
543
- case 'add':
544
- updateVersion(row);
545
- rc++;
546
- break;
547
- case 'edit':
548
- updateVersion(row);
549
- // No update to rc.
550
- break;
551
- case 'remove':
552
- rc--;
553
- break;
554
- default:
555
- unreachable(type);
522
+ // Then await the config patches which were fetched in parallel.
523
+ for (const patch of await configPatches) {
524
+ pokers.forEach(poker => poker.addPatch(patch));
556
525
  }
557
- parsedRow.refCounts[queryHash] = rc;
558
- if (rows.size % CURSOR_PAGE_SIZE === 0) {
559
- await processBatch();
526
+ if (!usePokers) {
527
+ pokers.forEach(poker => poker.end());
560
528
  }
561
- }
562
- if (rows.size) {
563
- await processBatch();
564
- }
529
+ });
530
+ }
531
+ #processChanges(lc, changes, updater, pokers, transformationHashToHash) {
532
+ return startAsyncSpan(tracer, 'vs.#processChanges', async () => {
533
+ const start = Date.now();
534
+ const rows = new CustomKeyMap(rowIDString);
535
+ let total = 0;
536
+ const processBatch = () => startAsyncSpan(tracer, 'processBatch', async () => {
537
+ const elapsed = Date.now() - start;
538
+ total += rows.size;
539
+ lc.debug?.(`processing ${rows.size} (of ${total}) rows (${elapsed} ms)`);
540
+ const patches = await updater.received(lc, rows);
541
+ patches.forEach(patch => pokers.forEach(poker => poker.addPatch(patch)));
542
+ rows.clear();
543
+ });
544
+ await startAsyncSpan(tracer, 'loopingChanges', async (span) => {
545
+ for (const change of changes) {
546
+ const { type, queryHash: transformationHash, table, rowKey, row, } = change;
547
+ const queryHash = must(transformationHashToHash.get(transformationHash), 'could not find the original hash for the transformation hash');
548
+ const rowID = { schema: '', table, rowKey: rowKey };
549
+ let parsedRow = rows.get(rowID);
550
+ let rc;
551
+ if (!parsedRow) {
552
+ parsedRow = { refCounts: {} };
553
+ rows.set(rowID, parsedRow);
554
+ rc = 0;
555
+ }
556
+ else {
557
+ rc = parsedRow.refCounts[queryHash] ?? 0;
558
+ }
559
+ const updateVersion = (row) => {
560
+ if (!parsedRow.version) {
561
+ const { version, contents } = contentsAndVersion(row);
562
+ parsedRow.version = version;
563
+ parsedRow.contents = contents;
564
+ }
565
+ };
566
+ switch (type) {
567
+ case 'add':
568
+ updateVersion(row);
569
+ rc++;
570
+ break;
571
+ case 'edit':
572
+ updateVersion(row);
573
+ // No update to rc.
574
+ break;
575
+ case 'remove':
576
+ rc--;
577
+ break;
578
+ default:
579
+ unreachable(type);
580
+ }
581
+ parsedRow.refCounts[queryHash] = rc;
582
+ if (rows.size % CURSOR_PAGE_SIZE === 0) {
583
+ await processBatch();
584
+ }
585
+ }
586
+ if (rows.size) {
587
+ await processBatch();
588
+ }
589
+ span.setAttribute('totalRows', total);
590
+ });
591
+ });
565
592
  }
566
593
  /**
567
594
  * Advance to the current snapshot of the replica and apply / send
@@ -571,38 +598,43 @@ export class ViewSyncerService {
571
598
  *
572
599
  * Returns false if the advancement failed due to a schema change.
573
600
  */
574
- async #advancePipelines(cvr) {
575
- assert(this.#pipelines.initialized());
576
- const start = Date.now();
577
- const { version, numChanges, changes } = this.#pipelines.advance();
578
- const lc = this.#lc.withContext('newVersion', version);
579
- // Probably need a new updater type. CVRAdvancementUpdater?
580
- const updater = new CVRQueryDrivenUpdater(this.#cvrStore, cvr, version, this.#pipelines.replicaVersion);
581
- const pokers = [...this.#clients.values()].map(c => c.startPoke(updater.updatedVersion(), this.#pipelines.currentSchemaVersions()));
582
- lc.debug?.(`applying ${numChanges} to advance to ${version}`);
583
- const transformationHashToHash = new Map();
584
- for (const query of Object.values(cvr.queries)) {
585
- if (!query.transformationHash) {
586
- continue;
601
+ #advancePipelines(lc, cvr) {
602
+ return startAsyncSpan(tracer, 'vs.#advancePipelines', async () => {
603
+ assert(this.#pipelines.initialized());
604
+ const start = Date.now();
605
+ const { version, numChanges, changes } = this.#pipelines.advance();
606
+ lc = lc.withContext('newVersion', version);
607
+ // Probably need a new updater type. CVRAdvancementUpdater?
608
+ const updater = new CVRQueryDrivenUpdater(this.#cvrStore, cvr, version, this.#pipelines.replicaVersion);
609
+ // Only poke clients that are at the cvr.version. New clients that
610
+ // are behind need to first be caught up when their initConnection
611
+ // message is processed (and #syncQueryPipelines is called).
612
+ const pokers = this.#getClients(cvr.version).map(c => c.startPoke(updater.updatedVersion(), this.#pipelines.currentSchemaVersions()));
613
+ lc.debug?.(`applying ${numChanges} to advance to ${version}`);
614
+ const transformationHashToHash = new Map();
615
+ for (const query of Object.values(cvr.queries)) {
616
+ if (!query.transformationHash) {
617
+ continue;
618
+ }
619
+ transformationHashToHash.set(query.transformationHash, query.id);
587
620
  }
588
- transformationHashToHash.set(query.transformationHash, query.id);
589
- }
590
- try {
591
- await this.#processChanges(lc, changes, updater, pokers, transformationHashToHash);
592
- }
593
- catch (e) {
594
- if (e instanceof SchemaChangeError) {
595
- pokers.forEach(poker => poker.cancel());
596
- return e;
621
+ try {
622
+ await this.#processChanges(lc, changes, updater, pokers, transformationHashToHash);
597
623
  }
598
- throw e;
599
- }
600
- // Commit the changes and update the CVR snapshot.
601
- this.#cvr = (await updater.flush(lc, this.#lastConnectTime)).cvr;
602
- // Signal clients to commit.
603
- pokers.forEach(poker => poker.end());
604
- lc.info?.(`finished processing advancement (${Date.now() - start} ms)`);
605
- return 'success';
624
+ catch (e) {
625
+ if (e instanceof SchemaChangeError) {
626
+ pokers.forEach(poker => poker.cancel());
627
+ return e;
628
+ }
629
+ throw e;
630
+ }
631
+ // Commit the changes and update the CVR snapshot.
632
+ this.#cvr = (await updater.flush(lc, this.#lastConnectTime)).cvr;
633
+ // Signal clients to commit.
634
+ pokers.forEach(poker => poker.end());
635
+ lc.info?.(`finished processing advancement (${Date.now() - start} ms)`);
636
+ return 'success';
637
+ });
606
638
  }
607
639
  stop() {
608
640
  this.#lc.info?.('stopping view syncer');
@@ -616,7 +648,7 @@ export class ViewSyncerService {
616
648
  client.fail(err);
617
649
  }
618
650
  else {
619
- client.close();
651
+ client.close('shutting down');
620
652
  }
621
653
  }
622
654
  }
@@ -634,19 +666,17 @@ function checkClientAndCVRVersions(client, cvr) {
634
666
  if (cmpVersions(cvr, NEW_CVR_VERSION) === 0 &&
635
667
  cmpVersions(client, NEW_CVR_VERSION) > 0) {
636
668
  // CVR is empty but client is not.
637
- throw new ErrorForClient([
638
- 'error',
639
- ErrorKind.ClientNotFound,
640
- 'Client not found',
641
- ]);
669
+ throw new ErrorForClient({
670
+ kind: ErrorKind.ClientNotFound,
671
+ message: 'Client not found',
672
+ });
642
673
  }
643
674
  if (cmpVersions(client, cvr) > 0) {
644
675
  // Client is ahead of a non-empty CVR.
645
- throw new ErrorForClient([
646
- 'error',
647
- ErrorKind.InvalidConnectionRequestBaseCookie,
648
- `CVR is at version ${versionString(cvr)}`,
649
- ]);
676
+ throw new ErrorForClient({
677
+ kind: ErrorKind.InvalidConnectionRequestBaseCookie,
678
+ message: `CVR is at version ${versionString(cvr)}`,
679
+ });
650
680
  }
651
681
  }
652
682
  export function pickToken(previousToken, newToken) {
@@ -655,22 +685,20 @@ export function pickToken(previousToken, newToken) {
655
685
  }
656
686
  if (newToken) {
657
687
  if (previousToken.sub !== newToken.sub) {
658
- throw new ErrorForClient([
659
- 'error',
660
- ErrorKind.Unauthorized,
661
- 'The user id in the new token does not match the previous token. Client groups are pinned to a single user.',
662
- ]);
688
+ throw new ErrorForClient({
689
+ kind: ErrorKind.Unauthorized,
690
+ message: 'The user id in the new token does not match the previous token. Client groups are pinned to a single user.',
691
+ });
663
692
  }
664
693
  if (previousToken.iat === undefined) {
665
694
  // No issued at time for the existing token? We take the most recently received token.
666
695
  return newToken;
667
696
  }
668
697
  if (newToken.iat === undefined) {
669
- throw new ErrorForClient([
670
- 'error',
671
- ErrorKind.Unauthorized,
672
- 'The new token does not have an issued at time but the prior token does. Tokens for a client group must either all have issued at times or all not have issued at times',
673
- ]);
698
+ throw new ErrorForClient({
699
+ kind: ErrorKind.Unauthorized,
700
+ message: 'The new token does not have an issued at time but the prior token does. Tokens for a client group must either all have issued at times or all not have issued at times',
701
+ });
674
702
  }
675
703
  // The new token is newer, so we take it.
676
704
  if (previousToken.iat < newToken.iat) {
@@ -680,10 +708,9 @@ export function pickToken(previousToken, newToken) {
680
708
  return previousToken;
681
709
  }
682
710
  // previousToken !== undefined but newToken is undefined
683
- throw new ErrorForClient([
684
- 'error',
685
- ErrorKind.Unauthorized,
686
- 'No token provided. An unauthenticated client cannot connect to an authenticated client group.',
687
- ]);
711
+ throw new ErrorForClient({
712
+ kind: ErrorKind.Unauthorized,
713
+ message: 'No token provided. An unauthenticated client cannot connect to an authenticated client group.',
714
+ });
688
715
  }
689
716
  //# sourceMappingURL=view-syncer.js.map