@rocicorp/zero 0.6.2024112102 → 0.7.2024120100
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/advanced.js +1 -1
- package/out/{chunk-QB7G63C6.js → chunk-4XX2NVJ7.js} +737 -738
- package/out/{chunk-QB7G63C6.js.map → chunk-4XX2NVJ7.js.map} +4 -4
- package/out/{chunk-5UY46OAF.js → chunk-C7M3BJ3Z.js} +16 -8
- package/out/chunk-C7M3BJ3Z.js.map +7 -0
- package/out/shared/src/expand.js +2 -0
- package/out/shared/src/expand.js.map +1 -0
- package/out/shared/src/immutable.js +2 -0
- package/out/shared/src/immutable.js.map +1 -0
- package/out/{zero-cache/src/config/config.d.ts → shared/src/options.d.ts} +3 -5
- package/out/shared/src/options.d.ts.map +1 -0
- package/out/{zero-cache/src/config/config.js → shared/src/options.js} +26 -26
- package/out/shared/src/options.js.map +1 -0
- package/out/shared/src/sorted-entries.js +6 -0
- package/out/shared/src/sorted-entries.js.map +1 -0
- package/out/shared/src/writable.js +2 -0
- package/out/shared/src/writable.js.map +1 -0
- package/out/solid.js +2 -2
- package/out/zero/src/build-schema.d.ts +3 -0
- package/out/zero/src/build-schema.d.ts.map +1 -0
- package/out/zero/src/build-schema.js +3 -0
- package/out/zero/src/build-schema.js.map +1 -0
- package/out/zero-cache/src/auth/load-schema.d.ts +8 -0
- package/out/zero-cache/src/auth/load-schema.d.ts.map +1 -0
- package/out/zero-cache/src/auth/load-schema.js +35 -0
- package/out/zero-cache/src/auth/load-schema.js.map +1 -0
- package/out/zero-cache/src/auth/read-authorizer.d.ts +25 -0
- package/out/zero-cache/src/auth/read-authorizer.d.ts.map +1 -0
- package/out/zero-cache/src/auth/read-authorizer.js +126 -0
- package/out/zero-cache/src/auth/read-authorizer.js.map +1 -0
- package/out/zero-cache/src/auth/write-authorizer.d.ts +20 -0
- package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -0
- package/out/zero-cache/src/auth/write-authorizer.js +320 -0
- package/out/zero-cache/src/auth/write-authorizer.js.map +1 -0
- package/out/zero-cache/src/config/zero-config.d.ts +14 -4
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +27 -14
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/server/main.js +7 -0
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +4 -3
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runtime.d.ts +3 -0
- package/out/zero-cache/src/server/runtime.d.ts.map +1 -0
- package/out/zero-cache/src/server/runtime.js +19 -0
- package/out/zero-cache/src/server/runtime.js.map +1 -0
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +7 -6
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts +7 -6
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +19 -33
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.js +2 -2
- package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.d.ts +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +11 -8
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts +17 -8
- package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +357 -94
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts +15 -10
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +42 -23
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +2 -2
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts +22 -0
- package/out/zero-cache/src/services/view-syncer/schema/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js +33 -7
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/init.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/init.js +44 -5
- package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/types.d.ts +1 -0
- package/out/zero-cache/src/services/view-syncer/schema/types.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/types.js +3 -0
- package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +5 -0
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +9 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +167 -50
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/row-key.d.ts +6 -0
- package/out/zero-cache/src/types/row-key.d.ts.map +1 -1
- package/out/zero-cache/src/types/row-key.js +16 -1
- package/out/zero-cache/src/types/row-key.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts +2 -3
- package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connection.js +4 -3
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/replicator.d.ts +3 -1
- package/out/zero-cache/src/workers/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/workers/replicator.js +2 -0
- package/out/zero-cache/src/workers/replicator.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +13 -3
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/options.d.ts +37 -8
- package/out/zero-client/src/client/options.d.ts.map +1 -1
- package/out/zero-client/src/client/reload-error-handler.d.ts +16 -1
- package/out/zero-client/src/client/reload-error-handler.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.d.ts +7 -55
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/mod.d.ts +2 -2
- package/out/zero-client/src/mod.d.ts.map +1 -1
- package/out/zero-protocol/src/ast-hash.js +14 -0
- package/out/zero-protocol/src/ast-hash.js.map +1 -0
- package/out/zero-protocol/src/ast.d.ts +9 -9
- package/out/zero-protocol/src/ast.d.ts.map +1 -1
- package/out/zero-protocol/src/ast.js +11 -12
- package/out/zero-protocol/src/ast.js.map +1 -1
- package/out/zero-protocol/src/down.d.ts +4 -4
- package/out/zero-protocol/src/poke.d.ts +16 -8
- package/out/zero-protocol/src/poke.d.ts.map +1 -1
- package/out/zero-protocol/src/poke.js +8 -2
- package/out/zero-protocol/src/poke.js.map +1 -1
- package/out/zero-schema/src/build-schema.d.ts +14 -0
- package/out/zero-schema/src/build-schema.d.ts.map +1 -0
- package/out/zero-schema/src/build-schema.js +55 -0
- package/out/zero-schema/src/build-schema.js.map +1 -0
- package/out/zero-schema/src/compiled-permissions.d.ts +480 -0
- package/out/zero-schema/src/compiled-permissions.d.ts.map +1 -0
- package/out/zero-schema/src/compiled-permissions.js +20 -0
- package/out/zero-schema/src/compiled-permissions.js.map +1 -0
- package/out/zero-schema/src/mod.d.ts +1 -1
- package/out/zero-schema/src/mod.d.ts.map +1 -1
- package/out/zero-schema/src/normalize-table-schema.d.ts +19 -17
- package/out/zero-schema/src/normalize-table-schema.d.ts.map +1 -1
- package/out/zero-schema/src/normalize-table-schema.js +92 -0
- package/out/zero-schema/src/normalize-table-schema.js.map +1 -0
- package/out/zero-schema/src/normalized-schema.d.ts +6 -1
- package/out/zero-schema/src/normalized-schema.d.ts.map +1 -1
- package/out/zero-schema/src/normalized-schema.js +31 -0
- package/out/zero-schema/src/normalized-schema.js.map +1 -0
- package/out/zero-schema/src/permissions.d.ts +32 -0
- package/out/zero-schema/src/permissions.d.ts.map +1 -0
- package/out/zero-schema/src/schema-config.d.ts +43 -0
- package/out/zero-schema/src/schema-config.d.ts.map +1 -0
- package/out/zero-schema/src/schema-config.js +79 -0
- package/out/zero-schema/src/schema-config.js.map +1 -0
- package/out/zero-schema/src/schema.js +4 -0
- package/out/zero-schema/src/schema.js.map +1 -0
- package/out/zero-schema/src/table-schema.d.ts +15 -20
- package/out/zero-schema/src/table-schema.d.ts.map +1 -1
- package/out/zero-schema/src/table-schema.js +15 -2
- package/out/zero-schema/src/table-schema.js.map +1 -1
- package/out/zero.js +4 -4
- package/out/zql/src/builder/builder.d.ts +2 -2
- package/out/zql/src/builder/builder.d.ts.map +1 -1
- package/out/zql/src/builder/builder.js +15 -17
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/builder/filter.d.ts +25 -2
- package/out/zql/src/builder/filter.d.ts.map +1 -1
- package/out/zql/src/builder/filter.js +91 -1
- package/out/zql/src/builder/filter.js.map +1 -1
- package/out/zql/src/ivm/array-view.js +70 -0
- package/out/zql/src/ivm/array-view.js.map +1 -0
- package/out/zql/src/ivm/change.d.ts +18 -6
- package/out/zql/src/ivm/change.d.ts.map +1 -1
- package/out/zql/src/ivm/change.js +1 -1
- package/out/zql/src/ivm/change.js.map +1 -1
- package/out/zql/src/ivm/constraint.d.ts +14 -0
- package/out/zql/src/ivm/constraint.d.ts.map +1 -0
- package/out/zql/src/ivm/constraint.js +60 -0
- package/out/zql/src/ivm/constraint.js.map +1 -0
- package/out/zql/src/ivm/exists.d.ts.map +1 -1
- package/out/zql/src/ivm/exists.js +19 -2
- package/out/zql/src/ivm/exists.js.map +1 -1
- package/out/zql/src/ivm/join.d.ts +11 -5
- package/out/zql/src/ivm/join.d.ts.map +1 -1
- package/out/zql/src/ivm/join.js +49 -95
- package/out/zql/src/ivm/join.js.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js +4 -13
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts +5 -22
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +61 -83
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/operator.d.ts +7 -10
- package/out/zql/src/ivm/operator.d.ts.map +1 -1
- package/out/zql/src/ivm/operator.js +1 -1
- package/out/zql/src/ivm/operator.js.map +1 -1
- package/out/zql/src/ivm/take.d.ts +3 -1
- package/out/zql/src/ivm/take.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +95 -95
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +168 -0
- package/out/zql/src/ivm/view-apply-change.js.map +1 -0
- package/out/zql/src/ivm/view.js +2 -0
- package/out/zql/src/ivm/view.js.map +1 -0
- package/out/zql/src/query/auth-query.d.ts +3 -5
- package/out/zql/src/query/auth-query.d.ts.map +1 -1
- package/out/zql/src/query/auth-query.js +30 -0
- package/out/zql/src/query/auth-query.js.map +1 -0
- package/out/zql/src/query/dnf.js +57 -0
- package/out/zql/src/query/dnf.js.map +1 -0
- package/out/zql/src/query/expression.js +155 -0
- package/out/zql/src/query/expression.js.map +1 -0
- package/out/zql/src/query/query-impl.d.ts +4 -1
- package/out/zql/src/query/query-impl.d.ts.map +1 -1
- package/out/zql/src/query/query-impl.js +353 -0
- package/out/zql/src/query/query-impl.js.map +1 -0
- package/out/zql/src/query/query-internal.js +2 -0
- package/out/zql/src/query/query-internal.js.map +1 -0
- package/out/zql/src/query/query.js +3 -0
- package/out/zql/src/query/query.js.map +1 -0
- package/out/zql/src/query/typed-view.js +2 -0
- package/out/zql/src/query/typed-view.js.map +1 -0
- package/out/zqlite/src/table-source.d.ts +2 -11
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +33 -82
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +5 -3
- package/out/chunk-5UY46OAF.js.map +0 -7
- package/out/zero-cache/src/auth/load-authorization.d.ts +0 -4
- package/out/zero-cache/src/auth/load-authorization.d.ts.map +0 -1
- package/out/zero-cache/src/auth/load-authorization.js +0 -20
- package/out/zero-cache/src/auth/load-authorization.js.map +0 -1
- package/out/zero-cache/src/config/config.d.ts.map +0 -1
- package/out/zero-cache/src/config/config.js.map +0 -1
- package/out/zero-cache/src/services/mutagen/write-authorizer.d.ts +0 -21
- package/out/zero-cache/src/services/mutagen/write-authorizer.d.ts.map +0 -1
- package/out/zero-cache/src/services/mutagen/write-authorizer.js +0 -168
- package/out/zero-cache/src/services/mutagen/write-authorizer.js.map +0 -1
- package/out/zero-schema/src/authorization.d.ts +0 -25
- package/out/zero-schema/src/authorization.d.ts.map +0 -1
- package/out/zero-schema/src/compiled-authorization.d.ts +0 -561
- package/out/zero-schema/src/compiled-authorization.d.ts.map +0 -1
- package/out/zero-schema/src/compiled-authorization.js +0 -15
- package/out/zero-schema/src/compiled-authorization.js.map +0 -1
|
@@ -3,25 +3,96 @@ import { assert } from '../../../../shared/src/asserts.js';
|
|
|
3
3
|
import { CustomKeyMap } from '../../../../shared/src/custom-key-map.js';
|
|
4
4
|
import { deepEqual, } from '../../../../shared/src/json.js';
|
|
5
5
|
import { must } from '../../../../shared/src/must.js';
|
|
6
|
+
import { promiseVoid } from '../../../../shared/src/resolved-promises.js';
|
|
7
|
+
import { sleep } from '../../../../shared/src/sleep.js';
|
|
6
8
|
import { astSchema } from '../../../../zero-protocol/src/ast.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
+
import { ErrorKind } from '../../../../zero-protocol/src/error.js';
|
|
10
|
+
import { ErrorForClient } from '../../types/error-for-client.js';
|
|
11
|
+
import { rowIDString } from '../../types/row-key.js';
|
|
9
12
|
import { rowRecordToRowsRow, rowsRowToRowRecord, } from './schema/cvr.js';
|
|
10
|
-
import { cmpVersions, versionFromString, versionString, } from './schema/types.js';
|
|
13
|
+
import { cmpVersions, EMPTY_CVR_VERSION, versionFromString, versionString, versionToNullableCookie, } from './schema/types.js';
|
|
14
|
+
/**
|
|
15
|
+
* The RowRecordCache is an in-memory cache of the `cvr.rows` tables that
|
|
16
|
+
* operates as both a write-through and write-back cache.
|
|
17
|
+
*
|
|
18
|
+
* For "small" CVR updates (i.e. zero or small numbers of rows) the
|
|
19
|
+
* RowRecordCache operates as write-through, executing commits in
|
|
20
|
+
* {@link executeRowUpdates()} before they are {@link apply}-ed to the
|
|
21
|
+
* in-memory state.
|
|
22
|
+
*
|
|
23
|
+
* For "large" CVR updates (i.e. with many rows), the cache switches to a
|
|
24
|
+
* write-back mode of operation, in which {@link executeRowUpdates()} is a
|
|
25
|
+
* no-op, and {@link apply()} initiates a background task to flush the pending
|
|
26
|
+
* row changes to the store. This allows the client poke to be completed and
|
|
27
|
+
* committed on the client without waiting for the heavyweight operation of
|
|
28
|
+
* committing the row records to the CVR store.
|
|
29
|
+
*
|
|
30
|
+
* Note that when the cache is in write-back mode, all updates become
|
|
31
|
+
* write-back (i.e. asynchronously flushed) until the pending update queue is
|
|
32
|
+
* fully flushed. This is required because updates must be applied in version
|
|
33
|
+
* order. As with all pending work systems in zero-cache, multiple pending
|
|
34
|
+
* updates are coalesced to reduce buildup of work.
|
|
35
|
+
*
|
|
36
|
+
* ### High level consistency
|
|
37
|
+
*
|
|
38
|
+
* Note that the above caching scheme only applies to the row data in `cvr.rows`
|
|
39
|
+
* and corresponding `cvr.rowsVersion` tables. CVR metadata and query
|
|
40
|
+
* information, on the other hand, are always committed before completing the
|
|
41
|
+
* client poke. In this manner, the difference between the `version` column in
|
|
42
|
+
* `cvr.instances` and the analogous column in `cvr.rowsVersion` determines
|
|
43
|
+
* whether the data in the store is consistent, or whether it is awaiting a
|
|
44
|
+
* pending update.
|
|
45
|
+
*
|
|
46
|
+
* The logic in {@link CVRStore.load()} takes this into account by loading both
|
|
47
|
+
* the `cvr.instances` version and the `cvr.rowsVersion` version and checking
|
|
48
|
+
* if they are in sync, waiting for a configurable delay until they are.
|
|
49
|
+
*
|
|
50
|
+
* ### Eventual conversion
|
|
51
|
+
*
|
|
52
|
+
* In the event of a continual stream of mutations (e.g. an animation-style
|
|
53
|
+
* app), it is conceivable that the row record data be continually behind
|
|
54
|
+
* the CVR metadata. In order to effect eventual convergence, a new view-syncer
|
|
55
|
+
* signals the current view-syncer to stop updating by writing new `owner`
|
|
56
|
+
* information to the `cvr.instances` row. This effectively stops the mutation
|
|
57
|
+
* processing (in {@link CVRStore.#checkVersionAndOwnership}) so that the row
|
|
58
|
+
* data can eventually catch up, allowing the new view-syncer to take over.
|
|
59
|
+
*
|
|
60
|
+
* Of course, there is the pathological situation in which a view-syncer
|
|
61
|
+
* process crashes before the pending row updates are flushed. In this case,
|
|
62
|
+
* the wait timeout will elapse and the CVR considered invalid.
|
|
63
|
+
*/
|
|
11
64
|
class RowRecordCache {
|
|
65
|
+
// The state in the #cache is always in sync with the CVR metadata
|
|
66
|
+
// (i.e. cvr.instances). It may contain information that has not yet
|
|
67
|
+
// been flushed to cvr.rows.
|
|
12
68
|
#cache;
|
|
69
|
+
#lc;
|
|
13
70
|
#db;
|
|
14
71
|
#cvrID;
|
|
15
|
-
|
|
72
|
+
#failService;
|
|
73
|
+
#deferredRowFlushThreshold;
|
|
74
|
+
#setTimeout;
|
|
75
|
+
// Write-back cache state.
|
|
76
|
+
#pending = new CustomKeyMap(rowIDString);
|
|
77
|
+
#pendingRowsVersion = null;
|
|
78
|
+
#flushing = null;
|
|
79
|
+
constructor(lc, db, cvrID, failService, deferredRowFlushThreshold = 100, setTimeoutFn = setTimeout) {
|
|
80
|
+
this.#lc = lc;
|
|
16
81
|
this.#db = db;
|
|
17
82
|
this.#cvrID = cvrID;
|
|
83
|
+
this.#failService = failService;
|
|
84
|
+
this.#deferredRowFlushThreshold = deferredRowFlushThreshold;
|
|
85
|
+
this.#setTimeout = setTimeoutFn;
|
|
18
86
|
}
|
|
19
87
|
async #ensureLoaded() {
|
|
20
88
|
if (this.#cache) {
|
|
21
89
|
return this.#cache;
|
|
22
90
|
}
|
|
23
91
|
const r = resolver();
|
|
24
|
-
|
|
92
|
+
// Set this.#cache immediately (before await) so that only one db
|
|
93
|
+
// query is made even if there are multiple callers.
|
|
94
|
+
this.#cache = r.promise;
|
|
95
|
+
const cache = new CustomKeyMap(rowIDString);
|
|
25
96
|
for await (const rows of this.#db `SELECT * FROM cvr.rows WHERE "clientGroupID" = ${this.#cvrID} AND "refCounts" IS NOT NULL`
|
|
26
97
|
// TODO(arv): Arbitrary page size
|
|
27
98
|
.cursor(5000)) {
|
|
@@ -31,13 +102,27 @@ class RowRecordCache {
|
|
|
31
102
|
}
|
|
32
103
|
}
|
|
33
104
|
r.resolve(cache);
|
|
34
|
-
this.#cache = r.promise;
|
|
35
105
|
return this.#cache;
|
|
36
106
|
}
|
|
37
107
|
getRowRecords() {
|
|
38
108
|
return this.#ensureLoaded();
|
|
39
109
|
}
|
|
40
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Applies the `rowRecords` corresponding to the `rowsVersion`
|
|
112
|
+
* to the cache, indicating whether the corresponding updates
|
|
113
|
+
* (generated by {@link executeRowUpdates}) were `flushed`.
|
|
114
|
+
*
|
|
115
|
+
* If `flushed` is false, the RowRecordCache will flush the records
|
|
116
|
+
* asynchronously.
|
|
117
|
+
*
|
|
118
|
+
* Note that `apply()` indicates that the CVR metadata associated with
|
|
119
|
+
* the `rowRecords` was successfully committed, which essentially means
|
|
120
|
+
* that this process has the unconditional right (and responsibility) of
|
|
121
|
+
* following up with a flush of the `rowRecords`. In particular, the
|
|
122
|
+
* commit of row records are not conditioned on the version or ownership
|
|
123
|
+
* columns of the `cvr.instances` row.
|
|
124
|
+
*/
|
|
125
|
+
async apply(rowRecords, rowsVersion, flushed) {
|
|
41
126
|
const cache = await this.#ensureLoaded();
|
|
42
127
|
for (const row of rowRecords) {
|
|
43
128
|
if (row.refCounts === null) {
|
|
@@ -46,11 +131,114 @@ class RowRecordCache {
|
|
|
46
131
|
else {
|
|
47
132
|
cache.set(row.id, row);
|
|
48
133
|
}
|
|
134
|
+
if (!flushed) {
|
|
135
|
+
this.#pending.set(row.id, row);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.#pendingRowsVersion = rowsVersion;
|
|
139
|
+
// Initiate a flush if not already flushing.
|
|
140
|
+
if (!flushed && this.#flushing === null) {
|
|
141
|
+
this.#flushing = resolver();
|
|
142
|
+
this.#setTimeout(() => this.#flush(), 0);
|
|
49
143
|
}
|
|
50
144
|
}
|
|
145
|
+
async #flush() {
|
|
146
|
+
const flushing = must(this.#flushing);
|
|
147
|
+
try {
|
|
148
|
+
while (this.#pending.size) {
|
|
149
|
+
const start = Date.now();
|
|
150
|
+
const { rows, rowsVersion } = await this.#db.begin(tx => {
|
|
151
|
+
// Note: This code block is synchronous, guaranteeing that the
|
|
152
|
+
// #pendingRowsVersion is consistent with the #pending rows.
|
|
153
|
+
const rows = this.#pending.size;
|
|
154
|
+
const rowsVersion = must(this.#pendingRowsVersion);
|
|
155
|
+
this.executeRowUpdates(tx, rowsVersion, [...this.#pending.values()], 'force');
|
|
156
|
+
this.#pending.clear();
|
|
157
|
+
return { rows, rowsVersion };
|
|
158
|
+
});
|
|
159
|
+
this.#lc.debug?.(`flushed ${rows} rows to ${versionString(rowsVersion)} (${Date.now() - start} ms)`);
|
|
160
|
+
// Note: apply() may have called while the transaction was committing,
|
|
161
|
+
// which will result in looping to commit the next #pending batch.
|
|
162
|
+
}
|
|
163
|
+
this.#lc.debug?.(`pending rows flushed to ${versionToNullableCookie(this.#pendingRowsVersion)}`);
|
|
164
|
+
flushing.resolve();
|
|
165
|
+
this.#flushing = null;
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
flushing.reject(e);
|
|
169
|
+
this.#failService(e);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Returns a promise that resolves when all outstanding row-records
|
|
174
|
+
* have been committed.
|
|
175
|
+
*/
|
|
176
|
+
flushed() {
|
|
177
|
+
return this.#flushing ? this.#flushing.promise : promiseVoid;
|
|
178
|
+
}
|
|
51
179
|
clear() {
|
|
180
|
+
// Note: Only the #cache is cleared. #pending updates, on the other hand,
|
|
181
|
+
// comprise canonical (i.e. already flushed) data and must be flushed
|
|
182
|
+
// even if the snapshot of the present state (the #cache) is cleared.
|
|
52
183
|
this.#cache = undefined;
|
|
53
184
|
}
|
|
185
|
+
async *catchupRowPatches(lc, afterVersion, upToCVR, excludeQueryHashes = []) {
|
|
186
|
+
if (cmpVersions(afterVersion, upToCVR.version) >= 0) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const startMs = Date.now();
|
|
190
|
+
const sql = this.#db;
|
|
191
|
+
const start = afterVersion ? versionString(afterVersion) : '';
|
|
192
|
+
const end = versionString(upToCVR.version);
|
|
193
|
+
lc.debug?.(`scanning row patches for clients from ${start}`);
|
|
194
|
+
// Before accessing the CVR db, pending row records must be flushed.
|
|
195
|
+
// Note that because catchupRowPatches() is called from within the
|
|
196
|
+
// view syncer lock, this flush is guaranteed to complete since no
|
|
197
|
+
// new CVR updates can happen while the lock is held.
|
|
198
|
+
await this.flushed();
|
|
199
|
+
const flushMs = Date.now() - startMs;
|
|
200
|
+
const query = excludeQueryHashes.length === 0
|
|
201
|
+
? sql `SELECT * FROM cvr.rows
|
|
202
|
+
WHERE "clientGroupID" = ${this.#cvrID}
|
|
203
|
+
AND "patchVersion" > ${start}
|
|
204
|
+
AND "patchVersion" <= ${end}`
|
|
205
|
+
: // Exclude rows that were already sent as part of query hydration.
|
|
206
|
+
sql `SELECT * FROM cvr.rows
|
|
207
|
+
WHERE "clientGroupID" = ${this.#cvrID}
|
|
208
|
+
AND "patchVersion" > ${start}
|
|
209
|
+
AND "patchVersion" <= ${end}
|
|
210
|
+
AND ("refCounts" IS NULL OR NOT "refCounts" ?| ${excludeQueryHashes})`;
|
|
211
|
+
yield* query.cursor(10000);
|
|
212
|
+
const totalMs = Date.now() - startMs;
|
|
213
|
+
lc.debug?.(`finished row catchup (flush: ${flushMs} ms, total: ${totalMs} ms)`);
|
|
214
|
+
}
|
|
215
|
+
executeRowUpdates(tx, version, rowRecordsToFlush, mode) {
|
|
216
|
+
if (mode === 'allow-defer' && // defer if there are pending updates or
|
|
217
|
+
(this.#pending.size > 0 || // the new batch is above the limit.
|
|
218
|
+
rowRecordsToFlush.length > this.#deferredRowFlushThreshold)) {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
const rowRecordRows = rowRecordsToFlush.map(r => rowRecordToRowsRow(this.#cvrID, r));
|
|
222
|
+
const rowsVersion = {
|
|
223
|
+
clientGroupID: this.#cvrID,
|
|
224
|
+
version: versionString(version),
|
|
225
|
+
};
|
|
226
|
+
const pending = [
|
|
227
|
+
tx `INSERT INTO cvr."rowsVersion" ${tx(rowsVersion)}
|
|
228
|
+
ON CONFLICT ("clientGroupID")
|
|
229
|
+
DO UPDATE SET ${tx(rowsVersion)}`.execute(),
|
|
230
|
+
];
|
|
231
|
+
let i = 0;
|
|
232
|
+
while (i < rowRecordRows.length) {
|
|
233
|
+
pending.push(tx `INSERT INTO cvr.rows ${tx(rowRecordRows.slice(i, i + ROW_RECORD_UPSERT_BATCH_SIZE))}
|
|
234
|
+
ON CONFLICT ("clientGroupID", "schema", "table", "rowKey")
|
|
235
|
+
DO UPDATE SET "rowVersion" = excluded."rowVersion",
|
|
236
|
+
"patchVersion" = excluded."patchVersion",
|
|
237
|
+
"refCounts" = excluded."refCounts"`.execute());
|
|
238
|
+
i += ROW_RECORD_UPSERT_BATCH_SIZE;
|
|
239
|
+
}
|
|
240
|
+
return pending;
|
|
241
|
+
}
|
|
54
242
|
}
|
|
55
243
|
function asQuery(row) {
|
|
56
244
|
const ast = astSchema.parse(row.clientAST);
|
|
@@ -72,56 +260,120 @@ function asQuery(row) {
|
|
|
72
260
|
transformationVersion: maybeVersion(row.transformationVersion),
|
|
73
261
|
};
|
|
74
262
|
}
|
|
263
|
+
// The time to wait between load attempts.
|
|
264
|
+
const LOAD_ATTEMPT_INTERVAL_MS = 500;
|
|
265
|
+
// The maximum number of load() attempts if the rowsVersion is behind.
|
|
266
|
+
// This currently results in a maximum catchup time of ~5 seconds, after
|
|
267
|
+
// which we give up and consider the CVR invalid.
|
|
268
|
+
//
|
|
269
|
+
// TODO: Make this configurable with something like --max-catchup-wait-ms,
|
|
270
|
+
// as it is technically application specific.
|
|
271
|
+
const MAX_LOAD_ATTEMPTS = 10;
|
|
75
272
|
export class CVRStore {
|
|
76
273
|
#lc;
|
|
274
|
+
#taskID;
|
|
77
275
|
#id;
|
|
78
276
|
#db;
|
|
79
277
|
#writes = new Set();
|
|
80
|
-
#pendingRowRecordPuts = new CustomKeyMap(
|
|
278
|
+
#pendingRowRecordPuts = new CustomKeyMap(rowIDString);
|
|
81
279
|
#rowCache;
|
|
82
|
-
|
|
280
|
+
#loadAttemptIntervalMs;
|
|
281
|
+
#maxLoadAttempts;
|
|
282
|
+
constructor(lc, db, taskID, cvrID, failService, loadAttemptIntervalMs = LOAD_ATTEMPT_INTERVAL_MS, maxLoadAttempts = MAX_LOAD_ATTEMPTS, deferredRowFlushThreshold = 100, // somewhat arbitrary
|
|
283
|
+
setTimeoutFn = setTimeout) {
|
|
83
284
|
this.#lc = lc;
|
|
84
285
|
this.#db = db;
|
|
286
|
+
this.#taskID = taskID;
|
|
85
287
|
this.#id = cvrID;
|
|
86
|
-
this.#rowCache = new RowRecordCache(db, cvrID);
|
|
288
|
+
this.#rowCache = new RowRecordCache(lc, db, cvrID, failService, deferredRowFlushThreshold, setTimeoutFn);
|
|
289
|
+
this.#loadAttemptIntervalMs = loadAttemptIntervalMs;
|
|
290
|
+
this.#maxLoadAttempts = maxLoadAttempts;
|
|
87
291
|
}
|
|
88
|
-
async load() {
|
|
292
|
+
async load(lastConnectTime) {
|
|
293
|
+
let err;
|
|
294
|
+
for (let i = 0; i < this.#maxLoadAttempts; i++) {
|
|
295
|
+
if (i > 0) {
|
|
296
|
+
await sleep(this.#loadAttemptIntervalMs);
|
|
297
|
+
}
|
|
298
|
+
const result = await this.#load(lastConnectTime);
|
|
299
|
+
if (result instanceof RowsVersionBehindError) {
|
|
300
|
+
this.#lc.info?.(`attempt ${i + 1}: ${String(result)}`);
|
|
301
|
+
err = result;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
assert(err);
|
|
307
|
+
throw new ErrorForClient([
|
|
308
|
+
'error',
|
|
309
|
+
ErrorKind.ClientNotFound,
|
|
310
|
+
`max attempts exceeded waiting for CVR@${err.cvrVersion} to catch up from ${err.rowsVersion}`,
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
async #load(lastConnectTime) {
|
|
89
314
|
const start = Date.now();
|
|
90
315
|
const id = this.#id;
|
|
91
316
|
const cvr = {
|
|
92
317
|
id,
|
|
93
|
-
version:
|
|
318
|
+
version: EMPTY_CVR_VERSION,
|
|
94
319
|
lastActive: 0,
|
|
95
320
|
replicaVersion: null,
|
|
96
321
|
clients: {},
|
|
97
322
|
queries: {},
|
|
98
323
|
};
|
|
99
324
|
const [instance, clientsRows, queryRows, desiresRows] = await this.#db.begin(tx => [
|
|
100
|
-
tx `SELECT "version",
|
|
325
|
+
tx `SELECT cvr."version",
|
|
326
|
+
"lastActive",
|
|
327
|
+
"replicaVersion",
|
|
328
|
+
"owner",
|
|
329
|
+
"grantedAt",
|
|
330
|
+
rows."version" as "rowsVersion"
|
|
331
|
+
FROM cvr.instances AS cvr
|
|
332
|
+
LEFT JOIN cvr."rowsVersion" AS rows
|
|
333
|
+
ON cvr."clientGroupID" = rows."clientGroupID"
|
|
334
|
+
WHERE cvr."clientGroupID" = ${id}`,
|
|
101
335
|
tx `SELECT "clientID", "patchVersion" FROM cvr.clients WHERE "clientGroupID" = ${id}`,
|
|
102
336
|
tx `SELECT * FROM cvr.queries WHERE "clientGroupID" = ${id} AND (deleted IS NULL OR deleted = FALSE)`,
|
|
103
337
|
tx `SELECT * FROM cvr.desires WHERE "clientGroupID" = ${id} AND (deleted IS NULL OR deleted = FALSE)`,
|
|
104
338
|
]);
|
|
105
|
-
if (instance.length
|
|
106
|
-
assert(instance.length === 1);
|
|
107
|
-
const { version, lastActive, replicaVersion } = instance[0];
|
|
108
|
-
cvr.version = versionFromString(version);
|
|
109
|
-
cvr.lastActive = lastActive;
|
|
110
|
-
cvr.replicaVersion = replicaVersion;
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
339
|
+
if (instance.length === 0) {
|
|
113
340
|
// This is the first time we see this CVR.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
version: versionString(cvr.version),
|
|
341
|
+
this.putInstance({
|
|
342
|
+
version: cvr.version,
|
|
117
343
|
lastActive: 0,
|
|
118
344
|
replicaVersion: null,
|
|
119
|
-
};
|
|
120
|
-
this.#writes.add({
|
|
121
|
-
stats: { instances: 1 },
|
|
122
|
-
write: tx => tx `INSERT INTO cvr.instances ${tx(change)}`,
|
|
123
345
|
});
|
|
124
346
|
}
|
|
347
|
+
else {
|
|
348
|
+
assert(instance.length === 1);
|
|
349
|
+
const { version, lastActive, replicaVersion, owner, grantedAt, rowsVersion, } = instance[0];
|
|
350
|
+
if (owner !== this.#taskID) {
|
|
351
|
+
if ((grantedAt ?? 0) > lastConnectTime) {
|
|
352
|
+
throw new OwnershipError(owner, grantedAt);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// Fire-and-forget an ownership change to signal the current owner.
|
|
356
|
+
// Note that the query is structured such that it only succeeds in the
|
|
357
|
+
// correct conditions (i.e. gated on `grantedAt`).
|
|
358
|
+
void this.#db `
|
|
359
|
+
UPDATE cvr.instances SET "owner" = ${this.#taskID},
|
|
360
|
+
"grantedAt" = ${lastConnectTime}
|
|
361
|
+
WHERE "clientGroupID" = ${this.#id} AND
|
|
362
|
+
("grantedAt" IS NULL OR
|
|
363
|
+
"grantedAt" <= to_timestamp(${lastConnectTime / 1000}))
|
|
364
|
+
`.execute();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (version !== (rowsVersion ?? EMPTY_CVR_VERSION.stateVersion)) {
|
|
368
|
+
// This will cause the load() method to wait for row catchup and retry.
|
|
369
|
+
// Assuming the ownership signal succeeds, the current owner will stop
|
|
370
|
+
// modifying the CVR and flush its pending row changes.
|
|
371
|
+
return new RowsVersionBehindError(version, rowsVersion);
|
|
372
|
+
}
|
|
373
|
+
cvr.version = versionFromString(version);
|
|
374
|
+
cvr.lastActive = lastActive;
|
|
375
|
+
cvr.replicaVersion = replicaVersion;
|
|
376
|
+
}
|
|
125
377
|
for (const row of clientsRows) {
|
|
126
378
|
const version = versionFromString(row.patchVersion);
|
|
127
379
|
cvr.clients[row.clientID] = {
|
|
@@ -156,15 +408,21 @@ export class CVRStore {
|
|
|
156
408
|
this.#pendingRowRecordPuts.set(row.id, row);
|
|
157
409
|
}
|
|
158
410
|
putInstance({ version, replicaVersion, lastActive, }) {
|
|
159
|
-
const change = {
|
|
160
|
-
clientGroupID: this.#id,
|
|
161
|
-
version: versionString(version),
|
|
162
|
-
lastActive,
|
|
163
|
-
replicaVersion,
|
|
164
|
-
};
|
|
165
411
|
this.#writes.add({
|
|
166
412
|
stats: { instances: 1 },
|
|
167
|
-
write: tx
|
|
413
|
+
write: (tx, lastConnectTime) => {
|
|
414
|
+
const change = {
|
|
415
|
+
clientGroupID: this.#id,
|
|
416
|
+
version: versionString(version),
|
|
417
|
+
lastActive,
|
|
418
|
+
replicaVersion,
|
|
419
|
+
owner: this.#taskID,
|
|
420
|
+
grantedAt: lastConnectTime,
|
|
421
|
+
};
|
|
422
|
+
return tx `
|
|
423
|
+
INSERT INTO cvr.instances ${tx(change)}
|
|
424
|
+
ON CONFLICT ("clientGroupID") DO UPDATE SET ${tx(change)}`;
|
|
425
|
+
},
|
|
168
426
|
});
|
|
169
427
|
}
|
|
170
428
|
markQueryAsDeleted(version, queryPatch) {
|
|
@@ -263,45 +521,18 @@ export class CVRStore {
|
|
|
263
521
|
`,
|
|
264
522
|
});
|
|
265
523
|
}
|
|
266
|
-
|
|
267
|
-
this.#
|
|
268
|
-
stats: { desires: 1 },
|
|
269
|
-
write: tx => tx `DELETE FROM cvr.desires WHERE "clientGroupID" = ${this.#id} AND "clientID" = ${client.id} AND "queryHash" = ${query.id} AND "patchVersion" = ${versionString(oldPutVersion)}`,
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
async *catchupRowPatches(lc, afterVersion, upToCVR, excludeQueryHashes = []) {
|
|
273
|
-
if (cmpVersions(afterVersion, upToCVR.version) >= 0) {
|
|
274
|
-
lc.debug?.('all clients up to date. no config catchup.');
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const startMs = Date.now();
|
|
278
|
-
const sql = this.#db;
|
|
279
|
-
const start = afterVersion ? versionString(afterVersion) : '';
|
|
280
|
-
const end = versionString(upToCVR.version);
|
|
281
|
-
lc.debug?.(`catching up clients from ${start}`);
|
|
282
|
-
const query = excludeQueryHashes.length === 0
|
|
283
|
-
? sql `SELECT * FROM cvr.rows
|
|
284
|
-
WHERE "clientGroupID" = ${this.#id}
|
|
285
|
-
AND "patchVersion" > ${start}
|
|
286
|
-
AND "patchVersion" <= ${end}`
|
|
287
|
-
: // Exclude rows that were already sent as part of query hydration.
|
|
288
|
-
sql `SELECT * FROM cvr.rows
|
|
289
|
-
WHERE "clientGroupID" = ${this.#id}
|
|
290
|
-
AND "patchVersion" > ${start}
|
|
291
|
-
AND "patchVersion" <= ${end}
|
|
292
|
-
AND ("refCounts" IS NULL OR NOT "refCounts" ?| ${excludeQueryHashes})`;
|
|
293
|
-
yield* query.cursor(10000);
|
|
294
|
-
lc.debug?.(`finished row catchup (${Date.now() - startMs} ms)`);
|
|
524
|
+
catchupRowPatches(lc, afterVersion, upToCVR, excludeQueryHashes = []) {
|
|
525
|
+
return this.#rowCache.catchupRowPatches(lc, afterVersion, upToCVR, excludeQueryHashes);
|
|
295
526
|
}
|
|
296
527
|
async catchupConfigPatches(lc, afterVersion, upToCVR) {
|
|
297
528
|
if (cmpVersions(afterVersion, upToCVR.version) >= 0) {
|
|
298
|
-
lc.debug?.('all clients up to date. no config catchup.');
|
|
299
529
|
return [];
|
|
300
530
|
}
|
|
301
531
|
const startMs = Date.now();
|
|
302
532
|
const sql = this.#db;
|
|
303
533
|
const start = afterVersion ? versionString(afterVersion) : '';
|
|
304
534
|
const end = versionString(upToCVR.version);
|
|
535
|
+
lc.debug?.(`scanning config patches for clients from ${start}`);
|
|
305
536
|
const [allDesires, clientRows, queryRows] = await Promise.all([
|
|
306
537
|
sql `SELECT * FROM cvr.desires
|
|
307
538
|
WHERE "clientGroupID" = ${this.#id}
|
|
@@ -345,21 +576,33 @@ export class CVRStore {
|
|
|
345
576
|
lc.debug?.(`${patches.length} config patches (${Date.now() - startMs} ms)`);
|
|
346
577
|
return patches;
|
|
347
578
|
}
|
|
348
|
-
async #
|
|
579
|
+
async #checkVersionAndOwnership(tx, expectedCurrentVersion, lastConnectTime) {
|
|
349
580
|
const expected = versionString(expectedCurrentVersion);
|
|
350
|
-
const result = await tx `SELECT version FROM cvr.instances
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
581
|
+
const result = await tx `SELECT "version", "owner", "grantedAt" FROM cvr.instances
|
|
582
|
+
WHERE "clientGroupID" = ${this.#id}
|
|
583
|
+
FOR UPDATE`.execute(); // Note: execute() immediately to send the query before others.
|
|
584
|
+
const { version, owner, grantedAt } = result.length > 0
|
|
585
|
+
? result[0]
|
|
586
|
+
: {
|
|
587
|
+
version: EMPTY_CVR_VERSION.stateVersion,
|
|
588
|
+
owner: null,
|
|
589
|
+
grantedAt: null,
|
|
590
|
+
};
|
|
591
|
+
if (version !== expected) {
|
|
592
|
+
throw new ConcurrentModificationException(expected, version);
|
|
593
|
+
}
|
|
594
|
+
if (owner !== this.#taskID && (grantedAt ?? 0) > lastConnectTime) {
|
|
595
|
+
throw new OwnershipError(owner, grantedAt);
|
|
354
596
|
}
|
|
355
597
|
}
|
|
356
|
-
async #flush(expectedCurrentVersion) {
|
|
598
|
+
async #flush(expectedCurrentVersion, newVersion, lastConnectTime) {
|
|
357
599
|
const stats = {
|
|
358
600
|
instances: 0,
|
|
359
601
|
queries: 0,
|
|
360
602
|
desires: 0,
|
|
361
603
|
clients: 0,
|
|
362
604
|
rows: 0,
|
|
605
|
+
rowsDeferred: 0,
|
|
363
606
|
statements: 0,
|
|
364
607
|
};
|
|
365
608
|
const existingRowRecords = await this.getRowRecords();
|
|
@@ -368,44 +611,44 @@ export class CVRStore {
|
|
|
368
611
|
return ((existing !== undefined || row.refCounts !== null) &&
|
|
369
612
|
!deepEqual(row, existing));
|
|
370
613
|
});
|
|
371
|
-
|
|
372
|
-
await this.#db.begin(tx => {
|
|
614
|
+
const rowsFlushed = await this.#db.begin(async (tx) => {
|
|
373
615
|
const pipelined = [
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
616
|
+
// #checkVersionAndOwnership() executes a `SELECT ... FOR UPDATE`
|
|
617
|
+
// query to acquire a row-level lock so that version-updating
|
|
618
|
+
// transactions are effectively serialized per cvr.instance.
|
|
619
|
+
//
|
|
620
|
+
// Note that `rowsVersion` updates, on the other hand, are not subject
|
|
621
|
+
// to this lock and can thus commit / be-committed independently of
|
|
622
|
+
// cvr.instances.
|
|
623
|
+
this.#checkVersionAndOwnership(tx, expectedCurrentVersion, lastConnectTime),
|
|
377
624
|
];
|
|
378
|
-
if (this.#pendingRowRecordPuts.size > 0) {
|
|
379
|
-
const rowRecordRows = rowRecordsToFlush.map(r => rowRecordToRowsRow(this.#id, r));
|
|
380
|
-
let i = 0;
|
|
381
|
-
while (i < rowRecordRows.length) {
|
|
382
|
-
pipelined.push(tx `INSERT INTO cvr.rows ${tx(rowRecordRows.slice(i, i + ROW_RECORD_UPSERT_BATCH_SIZE))}
|
|
383
|
-
ON CONFLICT ("clientGroupID", "schema", "table", "rowKey")
|
|
384
|
-
DO UPDATE SET "rowVersion" = excluded."rowVersion",
|
|
385
|
-
"patchVersion" = excluded."patchVersion",
|
|
386
|
-
"refCounts" = excluded."refCounts"`.execute());
|
|
387
|
-
i += ROW_RECORD_UPSERT_BATCH_SIZE;
|
|
388
|
-
stats.statements++;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
625
|
for (const write of this.#writes) {
|
|
392
626
|
stats.instances += write.stats.instances ?? 0;
|
|
393
627
|
stats.queries += write.stats.queries ?? 0;
|
|
394
628
|
stats.desires += write.stats.desires ?? 0;
|
|
395
629
|
stats.clients += write.stats.clients ?? 0;
|
|
396
|
-
pipelined.push(write.write(tx).execute());
|
|
630
|
+
pipelined.push(write.write(tx, lastConnectTime).execute());
|
|
397
631
|
stats.statements++;
|
|
398
632
|
}
|
|
633
|
+
const rowUpdates = this.#rowCache.executeRowUpdates(tx, newVersion, rowRecordsToFlush, 'allow-defer');
|
|
634
|
+
pipelined.push(...rowUpdates);
|
|
635
|
+
stats.statements += rowUpdates.length;
|
|
399
636
|
// Make sure Errors thrown by pipelined statements
|
|
400
637
|
// are propagated up the stack.
|
|
401
|
-
|
|
638
|
+
await Promise.all(pipelined);
|
|
639
|
+
if (rowUpdates.length === 0) {
|
|
640
|
+
stats.rowsDeferred = rowRecordsToFlush.length;
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
stats.rows = rowRecordsToFlush.length;
|
|
644
|
+
return true;
|
|
402
645
|
});
|
|
403
|
-
await this.#rowCache.
|
|
646
|
+
await this.#rowCache.apply(rowRecordsToFlush, newVersion, rowsFlushed);
|
|
404
647
|
return stats;
|
|
405
648
|
}
|
|
406
|
-
async flush(expectedCurrentVersion) {
|
|
649
|
+
async flush(expectedCurrentVersion, newVersion, lastConnectTime) {
|
|
407
650
|
try {
|
|
408
|
-
return await this.#flush(expectedCurrentVersion);
|
|
651
|
+
return await this.#flush(expectedCurrentVersion, newVersion, lastConnectTime);
|
|
409
652
|
}
|
|
410
653
|
catch (e) {
|
|
411
654
|
// Clear cached state if an error (e.g. ConcurrentModificationException) is encountered.
|
|
@@ -417,6 +660,10 @@ export class CVRStore {
|
|
|
417
660
|
this.#pendingRowRecordPuts.clear();
|
|
418
661
|
}
|
|
419
662
|
}
|
|
663
|
+
/** Resolves when all pending updates are flushed. */
|
|
664
|
+
flushed() {
|
|
665
|
+
return this.#rowCache.flushed();
|
|
666
|
+
}
|
|
420
667
|
}
|
|
421
668
|
// Max number of parameters for our sqlite build is 65534.
|
|
422
669
|
// Each row record has 7 parameters (1 per column).
|
|
@@ -428,4 +675,20 @@ export class ConcurrentModificationException extends Error {
|
|
|
428
675
|
super(`CVR has been concurrently modified. Expected ${expectedVersion}, got ${actualVersion}`);
|
|
429
676
|
}
|
|
430
677
|
}
|
|
678
|
+
export class OwnershipError extends Error {
|
|
679
|
+
name = 'OwnershipError';
|
|
680
|
+
constructor(owner, grantedAt) {
|
|
681
|
+
super(`CVR ownership was transferred to ${owner} at ${new Date(grantedAt ?? 0).toISOString()}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
export class RowsVersionBehindError extends Error {
|
|
685
|
+
name = 'RowsVersionBehindError';
|
|
686
|
+
cvrVersion;
|
|
687
|
+
rowsVersion;
|
|
688
|
+
constructor(cvrVersion, rowsVersion) {
|
|
689
|
+
super(`rowsVersion (${rowsVersion}) is behind CVR ${cvrVersion}`);
|
|
690
|
+
this.cvrVersion = cvrVersion;
|
|
691
|
+
this.rowsVersion = rowsVersion;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
431
694
|
//# sourceMappingURL=cvr-store.js.map
|