velocious 1.0.514 → 1.0.516
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/README.md +4 -3
- package/build/background-jobs/cron-expression.js +9 -9
- package/build/background-jobs/forked-runner-child.js +1 -1
- package/build/background-jobs/job-runner.js +21 -15
- package/build/background-jobs/json-socket.js +4 -0
- package/build/background-jobs/main.js +180 -18
- package/build/background-jobs/status-reporter.js +12 -5
- package/build/background-jobs/store.js +132 -50
- package/build/background-jobs/types.js +11 -3
- package/build/background-jobs/worker.js +9 -5
- package/build/beacon/client.js +13 -6
- package/build/configuration.js +54 -29
- package/build/controller.js +9 -3
- package/build/database/drivers/sqlite/base.js +1 -1
- package/build/database/drivers/sqlite/index.js +15 -14
- package/build/database/migrations-ledger.js +16 -16
- package/build/database/pool/async-tracked-multi-connection.js +16 -0
- package/build/database/pool/base.js +26 -0
- package/build/database/query/where-model-class-hash.js +2 -0
- package/build/database/query/with-count.js +1 -1
- package/build/database/record/acts-as-list.js +2 -2
- package/build/database/record/auditing.js +23 -6
- package/build/database/record/index.js +7 -7
- package/build/database/record/validators/uniqueness.js +3 -3
- package/build/database/tenants/data-copier.js +23 -23
- package/build/database/tenants/schema-cloner.js +27 -27
- package/build/environment-handlers/browser.js +0 -1
- package/build/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
- package/build/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
- package/build/environment-handlers/node.js +1 -1
- package/build/frontend-model-controller.js +26 -18
- package/build/frontend-model-resource/base-resource.js +4 -4
- package/build/frontend-models/base.js +5 -5
- package/build/frontend-models/query.js +12 -12
- package/build/http-server/client/index.js +61 -15
- package/build/http-server/client/request-runner.js +7 -1
- package/build/http-server/client/response.js +17 -1
- package/build/http-server/client/websocket-session.js +13 -13
- package/build/http-server/server-client.js +96 -0
- package/build/http-server/websocket-channel.js +12 -11
- package/build/http-server/websocket-connection.js +12 -11
- package/build/http-server/websocket-events-host.js +1 -1
- package/build/http-server/worker-handler/in-process.js +48 -9
- package/build/http-server/worker-handler/index.js +67 -7
- package/build/http-server/worker-handler/worker-thread.js +68 -0
- package/build/src/background-jobs/cron-expression.d.ts +2 -2
- package/build/src/background-jobs/cron-expression.js +10 -10
- package/build/src/background-jobs/forked-runner-child.js +2 -2
- package/build/src/background-jobs/job-runner.d.ts +5 -1
- package/build/src/background-jobs/job-runner.d.ts.map +1 -1
- package/build/src/background-jobs/job-runner.js +22 -16
- package/build/src/background-jobs/json-socket.d.ts +4 -0
- package/build/src/background-jobs/json-socket.d.ts.map +1 -1
- package/build/src/background-jobs/json-socket.js +5 -1
- package/build/src/background-jobs/main.d.ts +62 -10
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +171 -19
- package/build/src/background-jobs/status-reporter.d.ts +6 -2
- package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
- package/build/src/background-jobs/status-reporter.js +13 -6
- package/build/src/background-jobs/store.d.ts +48 -25
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +123 -50
- package/build/src/background-jobs/types.d.ts +36 -3
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +12 -4
- package/build/src/background-jobs/worker.d.ts +7 -5
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +10 -6
- package/build/src/beacon/client.d.ts +6 -3
- package/build/src/beacon/client.d.ts.map +1 -1
- package/build/src/beacon/client.js +14 -7
- package/build/src/configuration-types.d.ts +1 -1
- package/build/src/configuration.d.ts +42 -29
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +52 -30
- package/build/src/controller.d.ts +3 -0
- package/build/src/controller.d.ts.map +1 -1
- package/build/src/controller.js +9 -4
- package/build/src/database/drivers/sqlite/base.js +2 -2
- package/build/src/database/drivers/sqlite/index.d.ts +7 -6
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.js +16 -15
- package/build/src/database/migrations-ledger.d.ts +16 -16
- package/build/src/database/migrations-ledger.js +17 -17
- package/build/src/database/pool/async-tracked-multi-connection.d.ts +0 -11
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +16 -1
- package/build/src/database/pool/base.d.ts +21 -0
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +24 -1
- package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
- package/build/src/database/query/where-model-class-hash.js +3 -1
- package/build/src/database/query/with-count.js +2 -2
- package/build/src/database/record/acts-as-list.js +3 -3
- package/build/src/database/record/auditing.d.ts +1 -1
- package/build/src/database/record/auditing.d.ts.map +1 -1
- package/build/src/database/record/auditing.js +24 -7
- package/build/src/database/record/index.d.ts +7 -7
- package/build/src/database/record/index.js +8 -8
- package/build/src/database/record/validators/uniqueness.d.ts +3 -3
- package/build/src/database/record/validators/uniqueness.js +4 -4
- package/build/src/database/tenants/data-copier.d.ts +18 -18
- package/build/src/database/tenants/data-copier.js +24 -24
- package/build/src/database/tenants/schema-cloner.d.ts +27 -27
- package/build/src/database/tenants/schema-cloner.js +28 -28
- package/build/src/environment-handlers/browser.d.ts +9 -0
- package/build/src/environment-handlers/browser.d.ts.map +1 -1
- package/build/src/environment-handlers/browser.js +1 -2
- package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.js +3 -2
- package/build/src/environment-handlers/node/cli/commands/lint/relationships.js +3 -3
- package/build/src/environment-handlers/node.js +2 -2
- package/build/src/frontend-model-controller.d.ts +9 -8
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +26 -19
- package/build/src/frontend-model-resource/base-resource.js +5 -5
- package/build/src/frontend-models/base.d.ts +4 -4
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +6 -6
- package/build/src/frontend-models/query.d.ts +7 -7
- package/build/src/frontend-models/query.js +13 -13
- package/build/src/http-server/client/index.d.ts +23 -1
- package/build/src/http-server/client/index.d.ts.map +1 -1
- package/build/src/http-server/client/index.js +55 -17
- package/build/src/http-server/client/request-runner.d.ts.map +1 -1
- package/build/src/http-server/client/request-runner.js +8 -2
- package/build/src/http-server/client/response.d.ts +11 -1
- package/build/src/http-server/client/response.d.ts.map +1 -1
- package/build/src/http-server/client/response.js +16 -2
- package/build/src/http-server/client/websocket-session.d.ts +13 -13
- package/build/src/http-server/client/websocket-session.js +14 -14
- package/build/src/http-server/server-client.d.ts +13 -0
- package/build/src/http-server/server-client.d.ts.map +1 -1
- package/build/src/http-server/server-client.js +94 -1
- package/build/src/http-server/websocket-channel.d.ts +12 -11
- package/build/src/http-server/websocket-channel.d.ts.map +1 -1
- package/build/src/http-server/websocket-channel.js +13 -12
- package/build/src/http-server/websocket-connection.d.ts +12 -11
- package/build/src/http-server/websocket-connection.d.ts.map +1 -1
- package/build/src/http-server/websocket-connection.js +13 -12
- package/build/src/http-server/websocket-events-host.d.ts +1 -1
- package/build/src/http-server/websocket-events-host.js +2 -2
- package/build/src/http-server/worker-handler/in-process.d.ts +2 -0
- package/build/src/http-server/worker-handler/in-process.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/in-process.js +42 -11
- package/build/src/http-server/worker-handler/index.d.ts +21 -0
- package/build/src/http-server/worker-handler/index.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/index.js +61 -7
- package/build/src/http-server/worker-handler/worker-thread.d.ts +30 -0
- package/build/src/http-server/worker-handler/worker-thread.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/worker-thread.js +61 -1
- package/build/src/sync/device-identity.d.ts +2 -2
- package/build/src/sync/device-identity.d.ts.map +1 -1
- package/build/src/sync/device-identity.js +4 -4
- package/build/src/sync/local-mutation-log.d.ts +0 -1
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +1 -2
- package/build/src/sync/query-scope.d.ts +5 -0
- package/build/src/sync/query-scope.d.ts.map +1 -1
- package/build/src/sync/query-scope.js +7 -2
- package/build/src/sync/sync-client-types.d.ts +2 -2
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +1 -1
- package/build/src/sync/sync-client.d.ts +17 -2
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +28 -14
- package/build/src/sync/sync-realtime-bridge.d.ts +6 -2
- package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
- package/build/src/sync/sync-realtime-bridge.js +14 -3
- package/build/src/sync/sync-resource-base.d.ts +14 -3
- package/build/src/sync/sync-resource-base.d.ts.map +1 -1
- package/build/src/sync/sync-resource-base.js +20 -4
- package/build/src/sync/sync-scope-store.d.ts +3 -3
- package/build/src/sync/sync-scope-store.d.ts.map +1 -1
- package/build/src/sync/sync-scope-store.js +7 -5
- package/build/src/sync/sync-websocket-channel.d.ts +10 -0
- package/build/src/sync/sync-websocket-channel.d.ts.map +1 -1
- package/build/src/sync/sync-websocket-channel.js +20 -3
- package/build/src/tenants/tenant-iterator.d.ts +8 -8
- package/build/src/tenants/tenant-iterator.js +9 -9
- package/build/src/tenants/tenant.d.ts +6 -6
- package/build/src/tenants/tenant.js +7 -7
- package/build/src/testing/test-runner.d.ts +16 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +79 -19
- package/build/sync/device-identity.js +3 -3
- package/build/sync/local-mutation-log.js +0 -1
- package/build/sync/query-scope.js +6 -1
- package/build/sync/sync-client-types.js +1 -1
- package/build/sync/sync-client.js +28 -15
- package/build/sync/sync-realtime-bridge.js +13 -2
- package/build/sync/sync-resource-base.js +20 -3
- package/build/sync/sync-scope-store.js +6 -4
- package/build/sync/sync-websocket-channel.js +18 -2
- package/build/tenants/tenant-iterator.js +8 -8
- package/build/tenants/tenant.js +6 -6
- package/build/testing/test-runner.js +86 -20
- package/package.json +2 -2
- package/src/background-jobs/cron-expression.js +9 -9
- package/src/background-jobs/forked-runner-child.js +1 -1
- package/src/background-jobs/job-runner.js +21 -15
- package/src/background-jobs/json-socket.js +4 -0
- package/src/background-jobs/main.js +180 -18
- package/src/background-jobs/status-reporter.js +12 -5
- package/src/background-jobs/store.js +132 -50
- package/src/background-jobs/types.js +11 -3
- package/src/background-jobs/worker.js +9 -5
- package/src/beacon/client.js +13 -6
- package/src/configuration.js +54 -29
- package/src/controller.js +9 -3
- package/src/database/drivers/sqlite/base.js +1 -1
- package/src/database/drivers/sqlite/index.js +15 -14
- package/src/database/migrations-ledger.js +16 -16
- package/src/database/pool/async-tracked-multi-connection.js +16 -0
- package/src/database/pool/base.js +26 -0
- package/src/database/query/where-model-class-hash.js +2 -0
- package/src/database/query/with-count.js +1 -1
- package/src/database/record/acts-as-list.js +2 -2
- package/src/database/record/auditing.js +23 -6
- package/src/database/record/index.js +7 -7
- package/src/database/record/validators/uniqueness.js +3 -3
- package/src/database/tenants/data-copier.js +23 -23
- package/src/database/tenants/schema-cloner.js +27 -27
- package/src/environment-handlers/browser.js +0 -1
- package/src/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
- package/src/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
- package/src/environment-handlers/node.js +1 -1
- package/src/frontend-model-controller.js +26 -18
- package/src/frontend-model-resource/base-resource.js +4 -4
- package/src/frontend-models/base.js +5 -5
- package/src/frontend-models/query.js +12 -12
- package/src/http-server/client/index.js +61 -15
- package/src/http-server/client/request-runner.js +7 -1
- package/src/http-server/client/response.js +17 -1
- package/src/http-server/client/websocket-session.js +13 -13
- package/src/http-server/server-client.js +96 -0
- package/src/http-server/websocket-channel.js +12 -11
- package/src/http-server/websocket-connection.js +12 -11
- package/src/http-server/websocket-events-host.js +1 -1
- package/src/http-server/worker-handler/in-process.js +48 -9
- package/src/http-server/worker-handler/index.js +67 -7
- package/src/http-server/worker-handler/worker-thread.js +68 -0
- package/src/sync/device-identity.js +3 -3
- package/src/sync/local-mutation-log.js +0 -1
- package/src/sync/query-scope.js +6 -1
- package/src/sync/sync-client-types.js +1 -1
- package/src/sync/sync-client.js +28 -15
- package/src/sync/sync-realtime-bridge.js +13 -2
- package/src/sync/sync-resource-base.js +20 -3
- package/src/sync/sync-scope-store.js +6 -4
- package/src/sync/sync-websocket-channel.js +18 -2
- package/src/tenants/tenant-iterator.js +8 -8
- package/src/tenants/tenant.js +6 -6
- package/src/testing/test-runner.js +86 -20
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Client-declared sync scope serialized from a model query.
|
|
5
5
|
* @typedef {object} SerializedSyncScope
|
|
6
6
|
* @property {Record<string, ?>} conditions - Plain attribute conditions from the query.
|
|
7
|
-
* @property {string} resourceType - Resource/model name the scope was declared for.
|
|
7
|
+
* @property {string | null} resourceType - Resource/model name the scope was declared for, or null for the all-types (user) scope covering every type the server authorizes for the caller.
|
|
8
8
|
* @property {string} [owner] - Local partition key: the authenticated identity that declared the scope. Used only to partition the local scope/cursor store (never sent to the server as scope conditions), so a user scope's empty-conditions cursor does not leak across accounts on a shared device.
|
|
9
9
|
*/
|
|
10
10
|
|
package/src/sync/sync-client.js
CHANGED
|
@@ -428,7 +428,12 @@ export default class SyncClient {
|
|
|
428
428
|
}) : undefined,
|
|
429
429
|
postChanges: async (payload) => await this.config.postChanges({
|
|
430
430
|
...payload,
|
|
431
|
-
scope
|
|
431
|
+
// Only the all-types scope carries the type list; a type-declared scope needs none.
|
|
432
|
+
scope: {
|
|
433
|
+
conditions: scopeRow.conditions,
|
|
434
|
+
resourceType: scopeRow.resourceType,
|
|
435
|
+
...(scopeRow.resourceType === null ? {resourceTypes: this.userScopeResourceTypes()} : {})
|
|
436
|
+
}
|
|
432
437
|
}),
|
|
433
438
|
saveCursor: async (cursor) => await scopeStore.saveCursor(scopeRow, cursor)
|
|
434
439
|
})
|
|
@@ -543,12 +548,7 @@ export default class SyncClient {
|
|
|
543
548
|
async _subscribeUserScope() {
|
|
544
549
|
this._userScopeState = "subscribing"
|
|
545
550
|
|
|
546
|
-
|
|
547
|
-
const owner = await this.userScopeOwner()
|
|
548
|
-
|
|
549
|
-
for (const resourceType of this.userScopeResourceTypes()) {
|
|
550
|
-
await scopeStore.findOrCreateScope({conditions: {}, owner, resourceType})
|
|
551
|
-
}
|
|
551
|
+
await this.scopeStore().findOrCreateScope(await this.userScope())
|
|
552
552
|
|
|
553
553
|
await this.subscribeRealtime()
|
|
554
554
|
await this.pull()
|
|
@@ -564,12 +564,7 @@ export default class SyncClient {
|
|
|
564
564
|
* @returns {Promise<void>}
|
|
565
565
|
*/
|
|
566
566
|
async unsubscribeUserScope() {
|
|
567
|
-
|
|
568
|
-
const owner = await this.userScopeOwner()
|
|
569
|
-
|
|
570
|
-
for (const resourceType of this.userScopeResourceTypes()) {
|
|
571
|
-
await scopeStore.deactivate({conditions: {}, owner, resourceType})
|
|
572
|
-
}
|
|
567
|
+
await this.scopeStore().deactivate(await this.userScope())
|
|
573
568
|
|
|
574
569
|
await this.unsubscribeRealtime()
|
|
575
570
|
|
|
@@ -578,8 +573,26 @@ export default class SyncClient {
|
|
|
578
573
|
}
|
|
579
574
|
|
|
580
575
|
/**
|
|
581
|
-
* The
|
|
582
|
-
*
|
|
576
|
+
* The user scope: a single all-types scope (null resourceType) with empty
|
|
577
|
+
* conditions, partitioned locally by owner. One scope - not one per resource
|
|
578
|
+
* type - so the server authorizes the caller once per sync and per subscribe,
|
|
579
|
+
* however many resource types it serves. The server decides which types the
|
|
580
|
+
* caller may see; the client applies each pulled row by the resource type on
|
|
581
|
+
* its own envelope.
|
|
582
|
+
* @returns {Promise<import("./sync-client-types.js").SerializedSyncScope>} The user scope.
|
|
583
|
+
*/
|
|
584
|
+
async userScope() {
|
|
585
|
+
return {conditions: {}, owner: await this.userScopeOwner(), resourceType: null}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* The resource types the user scope covers: every declared resource that
|
|
590
|
+
* receives pulled changes (has pull `attributes`), so the client can apply
|
|
591
|
+
* them. Sent with the scope as a delivery/type filter - it narrows, never
|
|
592
|
+
* widens, what the server's authorization already allows, and it keeps a
|
|
593
|
+
* broadcast of a type this client cannot apply from reaching the server's
|
|
594
|
+
* per-delivery access re-check (a database query per matched broadcast, per
|
|
595
|
+
* subscribed device).
|
|
583
596
|
* @returns {string[]} Pullable resource type names.
|
|
584
597
|
*/
|
|
585
598
|
userScopeResourceTypes() {
|
|
@@ -262,7 +262,12 @@ export default class SyncRealtimeBridge {
|
|
|
262
262
|
for (const scopeRow of await this.syncClient.scopeStore().activeScopes()) {
|
|
263
263
|
channelDescriptors.push({
|
|
264
264
|
channel: VELOCIOUS_SYNC_CHANNEL,
|
|
265
|
-
params: {
|
|
265
|
+
params: {
|
|
266
|
+
conditions: this.attributeNamedConditions(scopeRow),
|
|
267
|
+
resourceType: scopeRow.resourceType,
|
|
268
|
+
// Only the all-types scope carries the type list; a type-declared scope needs none.
|
|
269
|
+
...(scopeRow.resourceType === null ? {resourceTypes: this.syncClient.userScopeResourceTypes()} : {})
|
|
270
|
+
}
|
|
266
271
|
})
|
|
267
272
|
}
|
|
268
273
|
|
|
@@ -292,10 +297,16 @@ export default class SyncRealtimeBridge {
|
|
|
292
297
|
* without a column mapping are already attribute names and pass through;
|
|
293
298
|
* scopes on models without a declared sync resource fail loudly because no
|
|
294
299
|
* attribute mapping exists for them.
|
|
295
|
-
*
|
|
300
|
+
*
|
|
301
|
+
* The all-types (user) scope has no resource type and no conditions - it
|
|
302
|
+
* covers everything the server authorizes for the caller - so there is
|
|
303
|
+
* nothing to map.
|
|
304
|
+
* @param {{conditions: Record<string, ?>, resourceType: string | null}} scopeRow - Active scope row.
|
|
296
305
|
* @returns {Record<string, ?>} Attribute-named scope conditions.
|
|
297
306
|
*/
|
|
298
307
|
attributeNamedConditions(scopeRow) {
|
|
308
|
+
if (scopeRow.resourceType === null) return {}
|
|
309
|
+
|
|
299
310
|
const resourceConfig = this.syncClient.config.resources[scopeRow.resourceType]
|
|
300
311
|
|
|
301
312
|
if (!resourceConfig) {
|
|
@@ -13,7 +13,8 @@ const QUICK_SEARCH_COLUMN = "quickSearch"
|
|
|
13
13
|
* Optional client-declared sync scope carried on a changes request.
|
|
14
14
|
* @typedef {object} SerializedChangesScope
|
|
15
15
|
* @property {Record<string, ?>} conditions - Plain attribute conditions from the client query.
|
|
16
|
-
* @property {string} resourceType - Client resource/model name the scope was declared for.
|
|
16
|
+
* @property {string | null} resourceType - Client resource/model name the scope was declared for, or null for the all-types (user) scope: one scope covering every resource type this resource authorizes for the caller, so a sync authorizes once however many types it serves.
|
|
17
|
+
* @property {string[] | null} resourceTypes - For the all-types scope, the resource types the client can apply. A cheap delivery/type filter only - it narrows, never widens, what the app's authorization already allows. Null for a type-declared scope.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -135,14 +136,30 @@ export default class SyncResourceBase extends FrontendModelBaseResource {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
const scopeParams = /** @type {Record<string, ?>} */ (scope)
|
|
138
|
-
const resourceType =
|
|
139
|
+
const resourceType = scopeParams.resourceType === null || scopeParams.resourceType === undefined
|
|
140
|
+
? null
|
|
141
|
+
: forcedNonBlankString(scopeParams.resourceType, "resourceType")
|
|
142
|
+
const resourceTypes = this.changesScopeResourceTypes(scopeParams.resourceTypes)
|
|
139
143
|
const conditions = scopeParams.conditions
|
|
140
144
|
|
|
141
145
|
if (!conditions || typeof conditions !== "object" || Array.isArray(conditions)) {
|
|
142
146
|
throw new Error(`Sync changes scope.conditions must be an object, got: ${String(conditions)}`)
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
return {conditions: /** @type {Record<string, ?>} */ (conditions), resourceType}
|
|
149
|
+
return {conditions: /** @type {Record<string, ?>} */ (conditions), resourceType, resourceTypes}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parses the optional resource-type list an all-types scope declares.
|
|
154
|
+
* @param {?} value - Raw `scope.resourceTypes` param.
|
|
155
|
+
* @returns {string[] | null} Declared resource types, or null when the client sent none.
|
|
156
|
+
*/
|
|
157
|
+
changesScopeResourceTypes(value) {
|
|
158
|
+
if (value === undefined || value === null) return null
|
|
159
|
+
|
|
160
|
+
if (!Array.isArray(value)) throw new Error(`Sync changes scope.resourceTypes must be an array, got: ${String(value)}`)
|
|
161
|
+
|
|
162
|
+
return value.map((resourceType) => forcedNonBlankString(resourceType, "resourceTypes"))
|
|
146
163
|
}
|
|
147
164
|
|
|
148
165
|
/**
|
|
@@ -24,7 +24,7 @@ function scopeDigestForScope(scope) {
|
|
|
24
24
|
* @property {Record<string, ?>} conditions - Scope attribute conditions.
|
|
25
25
|
* @property {string | null} cursorPayload - Persisted cursor JSON payload.
|
|
26
26
|
* @property {string} id - Scope row id.
|
|
27
|
-
* @property {string} resourceType - Scope resource/model name.
|
|
27
|
+
* @property {string | null} resourceType - Scope resource/model name, or null for the all-types (user) scope.
|
|
28
28
|
* @property {string} scopeDigest - Fixed-size deterministic digest of the canonical scope key.
|
|
29
29
|
* @property {string} state - Scope state ("active" or "removed").
|
|
30
30
|
*/
|
|
@@ -102,7 +102,7 @@ export default class SyncScopeStore {
|
|
|
102
102
|
conditions: scope.conditions,
|
|
103
103
|
cursorPayload: null,
|
|
104
104
|
id: new UUID(4).format(),
|
|
105
|
-
resourceType: scope.resourceType,
|
|
105
|
+
resourceType: scope.resourceType ?? null,
|
|
106
106
|
scopeDigest: digest,
|
|
107
107
|
state: "active"
|
|
108
108
|
}
|
|
@@ -131,7 +131,9 @@ export default class SyncScopeStore {
|
|
|
131
131
|
created_at: new Date(),
|
|
132
132
|
cursor_json: null,
|
|
133
133
|
id: new UUID(4).format(),
|
|
134
|
-
|
|
134
|
+
// The all-types (user) scope has no resource type; the column is non-null, so it
|
|
135
|
+
// stores as an empty string and normalizes back to null on read.
|
|
136
|
+
resource_type: scope.resourceType ?? "",
|
|
135
137
|
scope_digest: digest,
|
|
136
138
|
state: "active",
|
|
137
139
|
updated_at: new Date()
|
|
@@ -319,7 +321,7 @@ export default class SyncScopeStore {
|
|
|
319
321
|
conditions: /** @type {Record<string, ?>} */ (JSON.parse(String(row.conditions_json))),
|
|
320
322
|
cursorPayload: row.cursor_json === null || row.cursor_json === undefined ? null : String(row.cursor_json),
|
|
321
323
|
id: String(row.id),
|
|
322
|
-
resourceType: String(row.resource_type),
|
|
324
|
+
resourceType: String(row.resource_type) === "" ? null : String(row.resource_type),
|
|
323
325
|
scopeDigest: String(row.scope_digest),
|
|
324
326
|
state: String(row.state)
|
|
325
327
|
}
|
|
@@ -49,7 +49,7 @@ export default class SyncWebsocketChannel extends VelociousWebsocketChannel {
|
|
|
49
49
|
*/
|
|
50
50
|
async canSubscribe() {
|
|
51
51
|
const resource = await this.buildSyncResource()
|
|
52
|
-
const scope = resource.changesScope({scope: {conditions: this.params.conditions, resourceType: this.params.resourceType}})
|
|
52
|
+
const scope = resource.changesScope({scope: {conditions: this.params.conditions, resourceType: this.params.resourceType, resourceTypes: this.params.resourceTypes}})
|
|
53
53
|
|
|
54
54
|
await resource.authorizeChanges({params: this.params, scope})
|
|
55
55
|
|
|
@@ -107,6 +107,16 @@ export default class SyncWebsocketChannel extends VelociousWebsocketChannel {
|
|
|
107
107
|
* conditions match by membership). Broadcasts without a resource type and
|
|
108
108
|
* conditions the publisher's scoping params do not carry never match, so a
|
|
109
109
|
* subscription cannot receive changes outside its authorized scope.
|
|
110
|
+
*
|
|
111
|
+
* The all-types (user) scope declares no single resource type - it subscribes
|
|
112
|
+
* once for every type it can apply, listed in `scope.resourceTypes` - so it
|
|
113
|
+
* matches a broadcast of any of those types. Types outside that list are
|
|
114
|
+
* dropped here, by this cheap check, and never reach the per-delivery access
|
|
115
|
+
* re-check in {@link SyncWebsocketChannel#_userScopeDeliverableBody}: that
|
|
116
|
+
* re-check checks out a database connection and runs a query per matched
|
|
117
|
+
* broadcast, so matching every type would put DB work on every broadcast for
|
|
118
|
+
* every subscribed device. A broadcast carrying no resource type at all still
|
|
119
|
+
* never matches.
|
|
110
120
|
* @param {import("../http-server/websocket-channel.js").WebsocketJsonValue} broadcastParams - Publisher scoping params (the published resourceType plus the change's scope-partition values).
|
|
111
121
|
* @returns {boolean} Whether the broadcast belongs to this subscription's scope.
|
|
112
122
|
*/
|
|
@@ -119,7 +129,13 @@ export default class SyncWebsocketChannel extends VelociousWebsocketChannel {
|
|
|
119
129
|
? /** @type {Record<string, ?>} */ (broadcastParams)
|
|
120
130
|
: {}
|
|
121
131
|
|
|
122
|
-
if (!Object.hasOwn(scopingParams, "resourceType")
|
|
132
|
+
if (!Object.hasOwn(scopingParams, "resourceType")) return false
|
|
133
|
+
|
|
134
|
+
if (scope.resourceType === null) {
|
|
135
|
+
if (scope.resourceTypes && !scope.resourceTypes.some((resourceType) => resourceType === String(scopingParams.resourceType))) return false
|
|
136
|
+
} else if (String(scopingParams.resourceType) !== String(scope.resourceType)) {
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
123
139
|
|
|
124
140
|
for (const [conditionName, conditionValue] of Object.entries(scope.conditions)) {
|
|
125
141
|
if (!Object.hasOwn(scopingParams, conditionName)) return false
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* against the wrong connection. When iterating in parallel the per-tenant failures are
|
|
8
8
|
* collected and rethrown together as an `AggregateError` so one bad tenant does not hide the
|
|
9
9
|
* others. This is the iteration engine shared by the `db:tenants:*` CLI commands and the
|
|
10
|
-
* runtime
|
|
10
|
+
* runtime tenant façade; the caller is responsible for producing the tenant list.
|
|
11
11
|
*/
|
|
12
12
|
export default class TenantIterator {
|
|
13
13
|
/**
|
|
14
14
|
* Creates an iterator bound to a configuration and tenant database identifier.
|
|
15
|
-
* @param {{configuration: import("../configuration.js").default, identifier: string, parallelCount?: number}} args
|
|
15
|
+
* @param {{configuration: import("../configuration.js").default, identifier: string, parallelCount?: number}} args - Tenant configuration, database identifier, and concurrency limit.
|
|
16
16
|
*/
|
|
17
17
|
constructor({configuration, identifier, parallelCount = 1}) {
|
|
18
18
|
this.configuration = configuration
|
|
@@ -22,9 +22,9 @@ export default class TenantIterator {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Runs `callback` within each tenant's context and returns how many tenants were processed.
|
|
25
|
-
* @param {Array<?>} tenants
|
|
26
|
-
* @param {function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>} callback
|
|
27
|
-
* @returns {Promise<number>}
|
|
25
|
+
* @param {Array<?>} tenants - Tenant descriptors to enter and process.
|
|
26
|
+
* @param {function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>} callback - Per-tenant operation receiving the active database configuration.
|
|
27
|
+
* @returns {Promise<number>} - Number of processed tenants.
|
|
28
28
|
*/
|
|
29
29
|
async run(tenants, callback) {
|
|
30
30
|
if (this.parallelCount <= 1) {
|
|
@@ -76,7 +76,7 @@ export default class TenantIterator {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Enters one tenant's context and runs the callback, asserting the database is active first.
|
|
79
|
-
* @param {{callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, tenant: ?}} args
|
|
79
|
+
* @param {{callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, tenant: ?}} args - Tenant descriptor and operation to run in its context.
|
|
80
80
|
* @returns {Promise<void>}
|
|
81
81
|
*/
|
|
82
82
|
async runTenantCallback({callback, tenant}) {
|
|
@@ -94,8 +94,8 @@ export default class TenantIterator {
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Builds a human-readable label for a tenant for use in error messages.
|
|
97
|
-
* @param {?} tenant
|
|
98
|
-
* @returns {string}
|
|
97
|
+
* @param {?} tenant - Tenant descriptor to identify in an error.
|
|
98
|
+
* @returns {string} - Human-readable tenant label.
|
|
99
99
|
*/
|
|
100
100
|
static tenantLabel(tenant) {
|
|
101
101
|
if (tenant && typeof tenant === "object") {
|
package/src/tenants/tenant.js
CHANGED
|
@@ -26,8 +26,8 @@ export default class Tenant {
|
|
|
26
26
|
* connections keyed by identifier, the same as `ensureConnections`.
|
|
27
27
|
* @template T
|
|
28
28
|
* @param {object} tenant Descriptor understood by the app's tenantDatabaseResolver.
|
|
29
|
-
* @param {(connections: Record<string, import("../database/drivers/base.js").default>) => Promise<T>} callback
|
|
30
|
-
* @returns {Promise<T>}
|
|
29
|
+
* @param {(connections: Record<string, import("../database/drivers/base.js").default>) => Promise<T>} callback - Operation to run with the tenant's active connections.
|
|
30
|
+
* @returns {Promise<T>} - Callback result from within the tenant context.
|
|
31
31
|
*/
|
|
32
32
|
static async with(tenant, callback) {
|
|
33
33
|
const configuration = Current.configuration()
|
|
@@ -37,7 +37,7 @@ export default class Tenant {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* The current tenant descriptor, or undefined when running outside any tenant context.
|
|
40
|
-
* @returns {Record<string, unknown> | undefined}
|
|
40
|
+
* @returns {Record<string, unknown> | undefined} - Current async-context tenant descriptor, if any.
|
|
41
41
|
*/
|
|
42
42
|
static current() {
|
|
43
43
|
return Current.tenant()
|
|
@@ -49,8 +49,8 @@ export default class Tenant {
|
|
|
49
49
|
* {@link Tenant.with}, the callback runs inside `ensureConnections` so each tenant's
|
|
50
50
|
* database is queryable without the caller wiring up connections. Returns how many tenants
|
|
51
51
|
* the callback ran for (after filtering).
|
|
52
|
-
* @param {{identifier: string, callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, parallel?: number, filter?: (tenant: ?) => boolean, configuration?: import("../configuration.js").default}} args
|
|
53
|
-
* @returns {Promise<number>}
|
|
52
|
+
* @param {{identifier: string, callback: function({databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}) : Promise<void>, parallel?: number, filter?: (tenant: ?) => boolean, configuration?: import("../configuration.js").default}} args - Tenant database identifier, per-tenant operation, filtering, and concurrency settings.
|
|
53
|
+
* @returns {Promise<number>} - Number of processed tenants.
|
|
54
54
|
*/
|
|
55
55
|
static async each({identifier, callback, parallel = 1, filter, configuration = Current.configuration()}) {
|
|
56
56
|
const provider = configuration.getTenantDatabaseProvider(identifier)
|
|
@@ -76,7 +76,7 @@ export default class Tenant {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Drops one tenant's database/schema through the provider's `dropDatabase` hook.
|
|
79
|
-
* @param {{identifier: string, tenant: object, configuration?: import("../configuration.js").default}} args
|
|
79
|
+
* @param {{identifier: string, tenant: object, configuration?: import("../configuration.js").default}} args - Tenant descriptor and database identifier to drop.
|
|
80
80
|
* @returns {Promise<void>}
|
|
81
81
|
*/
|
|
82
82
|
static async drop({identifier, tenant, configuration = Current.configuration()}) {
|
|
@@ -520,7 +520,12 @@ export default class TestRunner {
|
|
|
520
520
|
if (!this._application) {
|
|
521
521
|
this._application = new Application({
|
|
522
522
|
configuration: this.getConfiguration(),
|
|
523
|
-
|
|
523
|
+
// Run request handlers in the main thread (not worker threads) so they
|
|
524
|
+
// resolve DB work to the per-test shared connection set by
|
|
525
|
+
// {@link activateTestSharedConnections}. This lets request-type specs use
|
|
526
|
+
// transaction-based cleaning (their writes land inside the test's
|
|
527
|
+
// transaction and roll back) instead of truncating every table.
|
|
528
|
+
httpServer: {inProcess: true, port: 31006},
|
|
524
529
|
type: "test-runner"
|
|
525
530
|
})
|
|
526
531
|
|
|
@@ -531,6 +536,46 @@ export default class TestRunner {
|
|
|
531
536
|
return this._application
|
|
532
537
|
}
|
|
533
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Pins each per-test connection as its pool's in-process test shared connection so
|
|
541
|
+
* HTTP request handlers dispatched during the test (which run in the main thread but
|
|
542
|
+
* in a fresh async context without the connection pin) resolve to the SAME connection
|
|
543
|
+
* — and therefore the same open transaction — as the test body. Pools that predate the
|
|
544
|
+
* shared-connection hook are skipped. Pair with {@link clearTestSharedConnections} in a
|
|
545
|
+
* finally.
|
|
546
|
+
* @returns {void}
|
|
547
|
+
*/
|
|
548
|
+
activateTestSharedConnections() {
|
|
549
|
+
const configuration = this.getConfiguration()
|
|
550
|
+
const currentConnections = configuration.getCurrentConnections()
|
|
551
|
+
|
|
552
|
+
for (const identifier of Object.keys(currentConnections)) {
|
|
553
|
+
const pool = configuration.getDatabasePool(identifier)
|
|
554
|
+
|
|
555
|
+
// Tenant-scoped pools resolve a different connection per request tenant
|
|
556
|
+
// (via runWithTenant), so forcing a single shared connection would break
|
|
557
|
+
// per-request tenant resolution. Only share non-tenant pools; the tenant
|
|
558
|
+
// pool keeps resolving its own connection per request.
|
|
559
|
+
if (pool.getConfiguration().tenantOnly) {
|
|
560
|
+
continue
|
|
561
|
+
}
|
|
562
|
+
pool.setTestSharedConnection(currentConnections[identifier])
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Clears the in-process test shared connection on every configured pool. Idempotent and
|
|
568
|
+
* safe to call when none was set.
|
|
569
|
+
* @returns {void}
|
|
570
|
+
*/
|
|
571
|
+
clearTestSharedConnections() {
|
|
572
|
+
const configuration = this.getConfiguration()
|
|
573
|
+
|
|
574
|
+
for (const identifier of configuration.getDatabaseIdentifiers()) {
|
|
575
|
+
configuration.getDatabasePool(identifier).clearTestSharedConnection()
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
534
579
|
/**
|
|
535
580
|
* Runs request client.
|
|
536
581
|
* @returns {Promise<RequestClient>} - Resolves with the request client.
|
|
@@ -941,28 +986,40 @@ export default class TestRunner {
|
|
|
941
986
|
// stale async-context pin and force every later test onto a fresh checkout,
|
|
942
987
|
// breaking isolation).
|
|
943
988
|
await this.getConfiguration().ensureConnections({name: `Test: ${testDescription}`}, async () => {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
989
|
+
// Only request-type tests dispatch through the in-process HTTP handler,
|
|
990
|
+
// so only they need the pinned connection shared with it. Sharing it for
|
|
991
|
+
// every test would let unrelated specs observe a test shared connection on
|
|
992
|
+
// pools they inspect directly. Cleared after afterEach either way.
|
|
993
|
+
if (testArgs.type == "request") {
|
|
994
|
+
this.activateTestSharedConnections()
|
|
995
|
+
}
|
|
949
996
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
this.
|
|
997
|
+
try {
|
|
998
|
+
try {
|
|
999
|
+
clearDeliveries()
|
|
1000
|
+
for (const beforeEachData of newBeforeEaches) {
|
|
1001
|
+
await beforeEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Record which test is running so an async crash (an unhandled
|
|
1005
|
+
// rejection detached from any await) that fires during or shortly
|
|
1006
|
+
// after this test can be attributed to it in run()'s handler.
|
|
1007
|
+
this._lastTestContext = {
|
|
1008
|
+
fullDescription: this.buildFullDescription(descriptions, testDescription),
|
|
1009
|
+
filePath: testData.filePath ?? "<unknown>",
|
|
1010
|
+
line: testData.line ?? 0
|
|
1011
|
+
}
|
|
1012
|
+
await testData.function(testArgs)
|
|
1013
|
+
if (!timeoutState.timedOut) {
|
|
1014
|
+
this._successfulTests++
|
|
1015
|
+
}
|
|
1016
|
+
} finally {
|
|
1017
|
+
for (const afterEachData of newAfterEaches) {
|
|
1018
|
+
await afterEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
|
|
1019
|
+
}
|
|
961
1020
|
}
|
|
962
1021
|
} finally {
|
|
963
|
-
|
|
964
|
-
await afterEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
|
|
965
|
-
}
|
|
1022
|
+
this.clearTestSharedConnections()
|
|
966
1023
|
}
|
|
967
1024
|
})
|
|
968
1025
|
})
|
|
@@ -996,6 +1053,15 @@ export default class TestRunner {
|
|
|
996
1053
|
if (timedOut && testLifecycle) {
|
|
997
1054
|
timeoutState.timedOut = true
|
|
998
1055
|
await awaitSettledOrGrace(testLifecycle, timeoutMs ?? 60000)
|
|
1056
|
+
|
|
1057
|
+
// If the abandoned lifecycle never settled within the grace, its
|
|
1058
|
+
// `finally` (which clears the shared connections) has not run, so
|
|
1059
|
+
// this test's shared connection — sitting on a still-open, timed-out
|
|
1060
|
+
// transaction — would otherwise be reused by the next test's outer
|
|
1061
|
+
// ensureConnections before activateTestSharedConnections() replaces
|
|
1062
|
+
// it. Clear it here so the next test checks out a fresh connection.
|
|
1063
|
+
// Idempotent when the lifecycle did settle and already cleared.
|
|
1064
|
+
this.clearTestSharedConnections()
|
|
999
1065
|
}
|
|
1000
1066
|
|
|
1001
1067
|
willRetry = retriesUsed < retryCount
|