velocious 1.0.504 → 1.0.506

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 (44) hide show
  1. package/build/configuration-types.js +2 -0
  2. package/build/configuration.js +11 -3
  3. package/build/database/drivers/sqlite/table-rebuilder.js +35 -0
  4. package/build/src/configuration-types.d.ts +10 -0
  5. package/build/src/configuration-types.d.ts.map +1 -1
  6. package/build/src/configuration-types.js +3 -1
  7. package/build/src/configuration.d.ts.map +1 -1
  8. package/build/src/configuration.js +12 -4
  9. package/build/src/database/drivers/sqlite/table-rebuilder.d.ts.map +1 -1
  10. package/build/src/database/drivers/sqlite/table-rebuilder.js +31 -1
  11. package/build/src/sync/query-scope.d.ts +6 -1
  12. package/build/src/sync/query-scope.d.ts.map +1 -1
  13. package/build/src/sync/query-scope.js +9 -3
  14. package/build/src/sync/sync-client-types.d.ts +18 -0
  15. package/build/src/sync/sync-client-types.d.ts.map +1 -1
  16. package/build/src/sync/sync-client-types.js +1 -1
  17. package/build/src/sync/sync-client.d.ts +61 -0
  18. package/build/src/sync/sync-client.d.ts.map +1 -1
  19. package/build/src/sync/sync-client.js +112 -2
  20. package/build/src/sync/sync-realtime-bridge.d.ts +20 -6
  21. package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
  22. package/build/src/sync/sync-realtime-bridge.js +52 -18
  23. package/build/src/sync/sync-resource-base.d.ts +20 -0
  24. package/build/src/sync/sync-resource-base.d.ts.map +1 -1
  25. package/build/src/sync/sync-resource-base.js +19 -1
  26. package/build/src/sync/sync-websocket-channel.d.ts +32 -0
  27. package/build/src/sync/sync-websocket-channel.d.ts.map +1 -1
  28. package/build/src/sync/sync-websocket-channel.js +73 -2
  29. package/build/sync/query-scope.js +9 -2
  30. package/build/sync/sync-client-types.js +10 -0
  31. package/build/sync/sync-client.js +125 -1
  32. package/build/sync/sync-realtime-bridge.js +52 -17
  33. package/build/sync/sync-resource-base.js +21 -0
  34. package/build/sync/sync-websocket-channel.js +80 -1
  35. package/package.json +1 -1
  36. package/src/configuration-types.js +2 -0
  37. package/src/configuration.js +11 -3
  38. package/src/database/drivers/sqlite/table-rebuilder.js +35 -0
  39. package/src/sync/query-scope.js +9 -2
  40. package/src/sync/sync-client-types.js +10 -0
  41. package/src/sync/sync-client.js +125 -1
  42. package/src/sync/sync-realtime-bridge.js +52 -17
  43. package/src/sync/sync-resource-base.js +21 -0
  44. package/src/sync/sync-websocket-channel.js +80 -1
@@ -120,12 +120,83 @@ export default class SyncWebsocketChannel extends VelociousWebsocketChannel {
120
120
  }
121
121
  return true;
122
122
  }
123
+ /**
124
+ * Delivers a matched broadcast. Scoped subscriptions (with explicit
125
+ * conditions) already routed through {@link SyncWebsocketChannel#matches}, so
126
+ * the change is in scope and delivers unchanged. User-scope subscriptions
127
+ * (empty conditions, "everything my ability can see") match every broadcast
128
+ * of the resource type, so each published change is re-checked against the
129
+ * subscriber's ability at fan-out through the app sync resource's
130
+ * `changeDeliverable`; only accessible changes are delivered, and a broadcast
131
+ * with no accessible change is dropped.
132
+ * @param {import("../http-server/websocket-channel.js").WebsocketJsonValue} body - Broadcast body (sync envelope).
133
+ * @param {{eventId?: string}} [meta] - Optional event metadata.
134
+ * @returns {Promise<void>}
135
+ */
136
+ async deliverBroadcast(body, meta) {
137
+ if (!this._isUserScope()) {
138
+ this.sendMessage(body, meta);
139
+ return;
140
+ }
141
+ const deliverableBody = await this._userScopeDeliverableBody(body);
142
+ if (deliverableBody !== null)
143
+ this.sendMessage(deliverableBody, meta);
144
+ }
145
+ /**
146
+ * Whether this subscription is a user scope: authorized with empty conditions
147
+ * ("everything my ability can see").
148
+ * @returns {boolean} Whether the subscription is a user scope.
149
+ */
150
+ _isUserScope() {
151
+ return Boolean(this._scope) && Object.keys(/** @type {import("./sync-resource-base.js").SerializedChangesScope} */ (this._scope).conditions).length === 0;
152
+ }
153
+ /**
154
+ * Filters a user-scope broadcast to the sync entries the subscriber's ability
155
+ * can access, re-checking each through the app sync resource's
156
+ * `changeDeliverable`. Returns the broadcast narrowed to accessible entries,
157
+ * or null when none are accessible. Non-envelope bodies and entries without a
158
+ * resource id are dropped (fail closed).
159
+ * @param {import("../http-server/websocket-channel.js").WebsocketJsonValue} body - Broadcast body.
160
+ * @returns {Promise<import("../http-server/websocket-channel.js").WebsocketJsonValue | null>} Deliverable body, or null.
161
+ */
162
+ async _userScopeDeliverableBody(body) {
163
+ if (!body || typeof body !== "object" || Array.isArray(body))
164
+ return null;
165
+ const envelope = /** @type {Record<string, ?>} */ (body);
166
+ const scope = /** @type {import("./sync-resource-base.js").SerializedChangesScope} */ (this._scope);
167
+ const syncs = Array.isArray(envelope.syncs) ? envelope.syncs : [envelope];
168
+ const configuration = this.session.configuration;
169
+ /** @type {Array<Record<string, ?>>} */
170
+ const deliverableSyncs = [];
171
+ // Broadcast fan-out runs through `withoutCurrentConnectionContexts` (see
172
+ // Configuration#_broadcastToChannelLocal), so there is no ambient database
173
+ // connection here. Resolve the resource's ability and run the per-delivery
174
+ // access query inside a checked-out connection context, mirroring how other
175
+ // broadcast-time DB work (the frontend-model channel) obtains connections.
176
+ await configuration.ensureConnections({ name: `${VELOCIOUS_SYNC_CHANNEL} user-scope delivery access check` }, async () => {
177
+ const resource = await this.buildSyncResource();
178
+ for (const sync of syncs) {
179
+ const resourceId = sync?.resourceId;
180
+ const resourceType = sync?.resourceType ?? scope.resourceType;
181
+ if (resourceId === undefined || resourceId === null)
182
+ continue;
183
+ if (await resource.changeDeliverable({ params: this.params, scope, sync: { resourceId: String(resourceId), resourceType: String(resourceType) } })) {
184
+ deliverableSyncs.push(sync);
185
+ }
186
+ }
187
+ });
188
+ if (deliverableSyncs.length === 0)
189
+ return null;
190
+ if (Array.isArray(envelope.syncs))
191
+ return { ...envelope, syncs: deliverableSyncs };
192
+ return deliverableSyncs[0];
193
+ }
123
194
  /**
124
195
  * Returns the authorized scope for debug snapshots.
125
196
  * @returns {Record<string, ?>} Debug-safe subscription details.
126
197
  */
127
198
  debugSnapshot() {
128
- return { scope: this._scope };
199
+ return { scope: this._scope, userScope: this._isUserScope() };
129
200
  }
130
201
  }
131
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy13ZWJzb2NrZXQtY2hhbm5lbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9zeW5jL3N5bmMtd2Vic29ja2V0LWNoYW5uZWwuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWTtBQUVaLE9BQU8seUJBQXlCLE1BQU0scUNBQXFDLENBQUE7QUFDM0UsT0FBTyxFQUFDLHNCQUFzQixFQUFDLE1BQU0sd0JBQXdCLENBQUE7QUFFN0QsK0VBQStFO0FBQy9FLE1BQU0sd0JBQXdCLEdBQUcsSUFBSSxPQUFPLEVBQUUsQ0FBQTtBQUU5Qzs7Ozs7Ozs7Ozs7R0FXRztBQUNILE1BQU0sQ0FBQyxPQUFPLE9BQU8sb0JBQXFCLFNBQVEseUJBQXlCO0lBQ3pFOzs7T0FHRztJQUNILE1BQU0sR0FBRyxJQUFJLENBQUE7SUFFYjs7Ozs7OztPQU9HO0lBQ0gsTUFBTSxDQUFDLHlCQUF5QixDQUFDLGFBQWE7UUFDNUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxvQkFBb0IsRUFBRSxDQUFDLEdBQUcsSUFBSSx3QkFBd0IsQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDO1lBQUUsT0FBTTtRQUVwRyx3QkFBd0IsQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUE7UUFDM0MsYUFBYSxDQUFDLHdCQUF3QixDQUFDLHNCQUFzQixFQUFFLElBQUksQ0FBQyxDQUFBO0lBQ3RFLENBQUM7SUFFRDs7Ozs7O09BTUc7SUFDSCxLQUFLLENBQUMsWUFBWTtRQUNoQixNQUFNLFFBQVEsR0FBRyxNQUFNLElBQUksQ0FBQyxpQkFBaUIsRUFBRSxDQUFBO1FBQy9DLE1BQU0sS0FBSyxHQUFHLFFBQVEsQ0FBQyxZQUFZLENBQUMsRUFBQyxLQUFLLEVBQUUsRUFBQyxVQUFVLEVBQUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxVQUFVLEVBQUUsWUFBWSxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsWUFBWSxFQUFDLEVBQUMsQ0FBQyxDQUFBO1FBRTFILE1BQU0s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QUUsQ0FBQztnQkFDbEMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsS0FBSyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUM7b0JBQUUsT0FBTyxLQUFLLENBQUE7WUFDM0YsQ0FBQztpQkFBTSxJQUFJLE1BQU0sQ0FBQyxjQUFjLENBQUMsS0FBSyxNQUFNLENBQUMsWUFBWSxDQUFDLEVBQUUsQ0FBQztnQkFDM0QsT0FBTyxLQUFLLENBQUE7WUFDZCxDQUFDO1FBQ0gsQ0FBQztRQUVELE9BQU8sSUFBSSxDQUFBO0lBQ2IsQ0FBQztJQUVEOzs7T0FHRztJQUNILGFBQWE7UUFDWCxPQUFPLEVBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxNQUFNLEVBQUMsQ0FBQTtJQUM3QixDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBAdHMtY2hlY2tcblxuaW1wb3J0IFZlbG9jaW91c1dlYnNvY2tldENoYW5uZWwgZnJvbSBcIi4uL2h0dHAtc2VydmVyL3dlYnNvY2tldC1jaGFubmVsLmpzXCJcbmltcG9ydCB7VkVMT0NJT1VTX1NZTkNfQ0hBTk5FTH0gZnJvbSBcIi4vc3luYy1jaGFubmVsLW5hbWUuanNcIlxuXG4vKiogQ29uZmlndXJhdGlvbnMgd2hvc2UgZnJhbWV3b3JrIHN5bmMgY2hhbm5lbCBoYXMgYWxyZWFkeSBiZWVuIHJlZ2lzdGVyZWQuICovXG5jb25zdCByZWdpc3RlcmVkQ29uZmlndXJhdGlvbnMgPSBuZXcgV2Vha1NldCgpXG5cbi8qKlxuICogRnJhbWV3b3JrLW93bmVkIHdlYnNvY2tldCBjaGFubmVsIGZvciBzeW5jZWQgcmVzb3VyY2VzXG4gKiAoe0BsaW5rIFZFTE9DSU9VU19TWU5DX0NIQU5ORUx9KS5cbiAqXG4gKiBTdWJzY3JpYmUgcGFyYW1zIG1pcnJvciBhIGRlY2xhcmVkIHB1bGwgc2NvcGUg4oCUIGB7cmVzb3VyY2VUeXBlLCBjb25kaXRpb25zfWBcbiAqIHBsdXMgdGhlIGNsaWVudC1pbmplY3RlZCBgYXV0aGVudGljYXRpb25Ub2tlbmAg4oCUIGFuZCBzdWJzY3JpYmUtdGltZVxuICogYXV0aG9yaXphdGlvbiBkZWxlZ2F0ZXMgdG8gdGhlIGFwcCBzeW5jIHJlc291cmNlJ3MgZXhpc3RpbmdcbiAqIGBhdXRob3JpemVDaGFuZ2VzKHtwYXJhbXMsIHNjb3BlfSlgICh0aGUgYHN5bmMuYXBpLnJlc291cmNlQ2xhc3NgKSwgc28gYXBwc1xuICogaG9vayBpbiB0aHJvdWdoIHRoZSBhdXRob3JpemF0aW9uIHRoZXkgYWxyZWFkeSBkZWNsYXJlZCBpbnN0ZWFkIG9mIHdyaXRpbmdcbiAqIHRoZWlyIG93biBjaGFubmVsIGNsYXNzZXMuIEJyb2FkY2FzdCByb3V0aW5nIG1hdGNoZXMgdGhlIHB1Ymxpc2hlcidzXG4gKiBzY29waW5nIHBhcmFtcyBhZ2FpbnN0IHRoZSBzdWJzY3JpcHRpb24ncyBzY29wZSBjb25kaXRpb25zLlxuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBTeW5jV2Vic29ja2V0Q2hhbm5lbCBleHRlbmRzIFZlbG9jaW91c1dlYnNvY2tldENoYW5uZWwge1xuICAvKipcbiAgICogU2NvcGUgdGhlIHN1YnNjcmlwdGlvbiB3YXMgYXV0aG9yaXplZCBmb3IsIHNldCBieSB7QGxpbmsgU3luY1dlYnNvY2tldENoYW5uZWwjY2FuU3Vic2NyaWJlfS5cbiAgICogQHR5cGUge2ltcG9ydChcIi4vc3luYy1yZXNvdXJjZS1iYXNlLmpzXCIpLlNlcmlhbGl6ZWRDaGFuZ2VzU2NvcGUgfCBudWxsfVxuICAgKi9cbiAgX3Njb3BlID0gbnVsbFxuXG4gIC8qKlxuICAgKiBSZWdpc3RlcnMgdGhlIGZyYW1ld29yayBzeW5jIGNoYW5uZWwgb24gYSBjb25maWd1cmF0aW9uIGRlY2xhcmluZyBhXG4gICAqIGBzeW5jLmFwaWAgYmxvY2sgKGd1YXJkZWQgc28gcmVwZWF0ZWQgc2VydmVyIGJvb3RzIHdpdGggdGhlIHNhbWVcbiAgICogY29uZmlndXJhdGlvbiByZWdpc3RlciBpdCBvbmx5IG9uY2UpLiBOby1vcCB3aXRob3V0IGBzeW5jLmFwaWAg4oCUIHRoZVxuICAgKiBjaGFubmVsIGF1dGhvcml6ZXMgdGhyb3VnaCB0aGUgYXBwJ3Mgc3luYyByZXNvdXJjZSBjbGFzcy5cbiAgICogQHBhcmFtIHtpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLmpzXCIpLmRlZmF1bHR9IGNvbmZpZ3VyYXRpb24gLSBDb25maWd1cmF0aW9uIGluc3RhbmNlLlxuICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICovXG4gIHN0YXRpYyByZWdpc3RlckZyb21Db25maWd1cmF0aW9uKGNvbmZpZ3VyYXRpb24pIHtcbiAgICBpZiAoIWNvbmZpZ3VyYXRpb24uZ2V0U3luY0NvbmZpZ3VyYXRpb24oKS5hcGkgfHwgcmVnaXN0ZXJlZENvbmZpZ3VyYXRpb25zLmhhcyhjb25maWd1cmF0aW9uKSkgcmV0dXJuXG5cbiAgICByZWdpc3RlcmVkQ29uZmlndXJhdGlvbnMuYWRkKGNvbmZpZ3VyYXRpb24pXG4gICAgY29uZmlndXJhdGlvbi5yZWdpc3RlcldlYnNvY2tldENoYW5uZWwoVkVMT0NJT1VTX1NZTkNfQ0hBTk5FTCwgdGhpcylcbiAgfVxuXG4gIC8qKlxuICAgKiBBdXRob3JpemVzIHRoZSBzdWJzY3JpcHRpb24gdGhyb3VnaCB0aGUgYXBwIHN5bmMgcmVzb3VyY2U6IHRoZSBzdWJzY3JpYmVcbiAgICogcGFyYW1zIGFyZSBwYXJzZWQgaW50byB0aGUgc2FtZSBzZXJpYWxpemVkIHNjb3BlIHRoZSBjaGFuZ2VzIGVuZHBvaW50XG4gICAqIGNvbnN1bWVzIGFuZCBwYXNzZWQgdG8gdGhlIHJlc291cmNlJ3MgYGF1dGhvcml6ZUNoYW5nZXMoe3BhcmFtcywgc2NvcGV9KWAuXG4gICAqIERlbmlhbHMgYW5kIG1hbGZvcm1lZCBzY29wZXMgdGhyb3csIHJlamVjdGluZyB0aGUgc3Vic2NyaXB0aW9uLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxib29sZWFuPn0gV2hldGhlciB0aGUgc3Vic2NyaXB0aW9uIGlzIGFsbG93ZWQuXG4gICAqL1xuICBhc3luYyBjYW5TdWJzY3JpYmUoKSB7XG4gICAgY29uc3QgcmVzb3VyY2UgPSBhd2FpdCB0aGlzLmJ1aWxkU3luY1Jlc291cmNlKClcbiAgICBjb25zdCBzY29wZSA9IHJlc291cmNlLmNoYW5nZXNTY29wZSh7c2NvcGU6IHtjb25kaXRpb25zOiB0aGlzLnBhcmFtcy5jb25kaXRpb25zLCByZXNvdXJjZVR5cGU6IHRoaXMucGFyYW1zLnJlc291cmNlVHlwZX19KVxuXG4gICAgYXdhaXQgcmVzb3VyY2UuYXV0aG9yaXplQ2hhbmdlcyh7cGFyYW1zOiB0aGlzLnBhcmFtcywgc2NvcGV9KVxuXG4gICAgdGhpcy5fc2NvcGUgPSBzY29wZVxuXG4gICAgcmV0dXJuIHRydWVcbiAgfVxuXG4gIC8qKlxuICAgKiBCdWlsZHMgdGhlIGFwcCBzeW5jIHJlc291cmNlIGF1dGhvcml6aW5nIHRoaXMgc3Vic2NyaXB0aW9uLCBtaXJyb3JpbmcgdGhlXG4gICAqIHN5bmMgdHJhbnNwb3J0IGNvbnRyb2xsZXIncyByZXNvdXJjZSBjb25zdHJ1Y3Rpb24gd2l0aCB0aGUgYWJpbGl0eVxuICAgKiByZXNvbHZlZCBmcm9tIHRoZSBzdWJzY3JpYmUgcGFyYW1zICh3aGljaCBjYXJyeSB0aGUgY2xpZW50LWluamVjdGVkXG4gICAqIGF1dGhlbnRpY2F0aW9uVG9rZW4pLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxpbXBvcnQoXCIuL3N5bmMtcmVzb3VyY2UtYmFzZS5qc1wiKS5kZWZhdWx0Pn0gQXBwIHN5bmMgcmVzb3VyY2UgaW5zdGFuY2UuXG4gICAqL1xuICBhc3luYyBidWlsZFN5bmNSZXNvdXJjZSgpIHtcbiAgICBjb25zdCBjb25maWd1cmF0aW9uID0gdGhpcy5zZXNzaW9uLmNvbmZpZ3VyYXRpb25cbiAgICBjb25zdCBhcGkgPSBjb25maWd1cmF0aW9uLmdldFN5bmNDb25maWd1cmF0aW9uKCkuYXBpXG5cbiAgICBpZiAoIWFwaSkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKGBUaGUgJHtWRUxPQ0lPVVNfU1lOQ19DSEFOTkVMfSBjaGFubmVsIHJlcXVpcmVzIGEgc3luYy5hcGkgY29uZmlndXJhdGlvbiBibG9jayB3aXRoIHRoZSBhcHAncyBzeW5jIHJlc291cmNlIGNsYXNzYClcbiAgICB9XG5cbiAgICAvLyBOYXJyb3dzIHRoZSBjb25maWd1cmVkIHJlc291cmNlIGNsYXNzIHRvIHRoZSBzeW5jIHJlc291cmNlIGNvbnRyYWN0XG4gICAgLy8gKGNoYW5nZXNTY29wZS9hdXRob3JpemVDaGFuZ2VzKSB0aGUgc3luYy5hcGkgdmFsaWRhdGlvbiByZXF1aXJlcy5cbiAgICBjb25zdCBSZXNvdXJjZUNsYXNzID0gLyoqIEB0eXBlIHt0eXBlb2YgaW1wb3J0KFwiLi9zeW5jLXJlc291cmNlLWJhc2UuanNcIikuZGVmYXVsdH0gKi8gKGFwaS5yZXNvdXJjZUNsYXNzKVxuICAgIC8vIE5hcnJvd3MgdGhlIHdlYnNvY2tldCBzdWJzY3JpYmUgcGFyYW1zIHRvIHRoZSByZXNvdXJjZSBwYXJhbXMgY29udHJhY3QuXG4gICAgY29uc3QgcGFyYW1zID0gLyoqIEB0eXBlIHtpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLXR5cGVzLmpzXCIpLlZlbG9jaW91c1BhcmFtc30gKi8gKC8qKiBAdHlwZSB7dW5rbm93bn0gKi8gKHRoaXMucGFyYW1zKSlcbiAgICBjb25zdCByZXF1ZXN0ID0gdGhpcy5zZXNzaW9uLnVwZ3JhZGVSZXF1ZXN0XG4gICAgY29uc3QgYWJpbGl0eSA9IGF3YWl0IGNvbmZpZ3VyYXRpb24ucmVzb2x2ZUFiaWxpdHkoe3BhcmFtcywgcmVxdWVzdH0pXG5cbiAgICByZXR1cm4gbmV3IFJlc291cmNlQ2xhc3Moe1xuICAgICAgYWJpbGl0eSxcbiAgICAgIGNvbnRleHQ6IHtcbiAgICAgICAgLi4uKGFiaWxpdHk/LmdldENvbnRleHQoKSB8fCB7fSksXG4gICAgICAgIHBhcmFtcyxcbiAgICAgICAgcmVxdWVzdFxuICAgICAgfSxcbiAgICAgIGxvY2FsczogYWJpbGl0eT8uZ2V0TG9jYWxzKCkgfHwge30sXG4gICAgICBtb2RlbENsYXNzOiBSZXNvdXJjZUNsYXNzLk1vZGVsQ2xhc3MsXG4gICAgICBtb2RlbE5hbWU6IFJlc291cmNlQ2xhc3MuTW9kZWxDbGFzcz8ubmFtZSxcbiAgICAgIHBhcmFtcyxcbiAgICAgIHJlc291cmNlQ29uZmlndXJhdGlvbjogLyoqIEB0eXBlIHtpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLXR5cGVzLmpzXCIpLkZyb250ZW5kTW9kZWxSZXNvdXJjZUNvbmZpZ3VyYXRpb259ICovICh7XG4gICAgICAgIGF0dHJpYnV0ZXM6IFJlc291cmNlQ2xhc3MuYXR0cmlidXRlcyB8fCB7fSxcbiAgICAgICAgc3luYzoge2VuYWJsZWQ6IHRydWV9XG4gICAgICB9KVxuICAgIH0pXG4gIH1cblxuICAvKipcbiAgICogUm91dGVzIGEgcHVibGlzaGVyIGJyb2FkY2FzdCB0byB0aGlzIHN1YnNjcmlwdGlvbiB3aGVuIHRoZSBwdWJsaXNoZWRcbiAgICogcmVzb3VyY2UgdHlwZSBlcXVhbHMgdGhlIHR5cGUgdGhlIHN1YnNjcmlwdGlvbiB3YXMgYXV0aG9yaXplZCBmb3IgYW5kIHRoZVxuICAgKiBzY29waW5nIHBhcmFtcyBzYXRpc2Z5IGV2ZXJ5IHNjb3BlIGNvbmRpdGlvbjogZWFjaCBjb25kaXRpb24ga2V5IG11c3QgYmVcbiAgICogcHJlc2VudCBpbiB0aGUgYnJvYWRjYXN0IHBhcmFtcyBhbmQgbWF0Y2ggYnkgc3RyaW5nIGNvbXBhcmlzb24gKGFycmF5XG4gICAqIGNvbmRpdGlvbnMgbWF0Y2ggYnkgbWVtYmVyc2hpcCkuIEJyb2FkY2FzdHMgd2l0aG91dCBhIHJlc291cmNlIHR5cGUgYW5kXG4gICAqIGNvbmRpdGlvbnMgdGhlIHB1Ymxpc2hlcidzIHNjb3BpbmcgcGFyYW1zIGRvIG5vdCBjYXJyeSBuZXZlciBtYXRjaCwgc28gYVxuICAgKiBzdWJzY3JpcHRpb24gY2Fubm90IHJlY2VpdmUgY2hhbmdlcyBvdXRzaWRlIGl0cyBhdXRob3JpemVkIHNjb3BlLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4uL2h0dHAtc2VydmVyL3dlYnNvY2tldC1jaGFubmVsLmpzXCIpLldlYnNvY2tldEpzb25WYWx1ZX0gYnJvYWRjYXN0UGFyYW1zIC0gUHVibGlzaGVyIHNjb3BpbmcgcGFyYW1zICh0aGUgcHVibGlzaGVkIHJlc291cmNlVHlwZSBwbHVzIHRoZSBjaGFuZ2UncyBzY29wZS1wYXJ0aXRpb24gdmFsdWVzKS5cbiAgICogQHJldHVybnMge2Jvb2xlYW59IFdoZXRoZXIgdGhlIGJyb2FkY2FzdCBiZWxvbmdzIHRvIHRoaXMgc3Vic2NyaXB0aW9uJ3Mgc2NvcGUuXG4gICAqL1xuICBtYXRjaGVzKGJyb2FkY2FzdFBhcmFtcykge1xuICAgIGNvbnN0IHNjb3BlID0gdGhpcy5fc2NvcGVcblxuICAgIGlmICghc2NvcGUpIHJldHVybiBmYWxzZVxuXG4gICAgY29uc3Qgc2NvcGluZ1BhcmFtcyA9IGJyb2FkY2FzdFBhcmFtcyAmJiB0eXBlb2YgYnJvYWRjYXN0UGFyYW1zID09PSBcIm9iamVjdFwiICYmICFBcnJheS5pc0FycmF5KGJyb2FkY2FzdFBhcmFtcylcbiAgICAgID8gLyoqIEB0eXBlIHtSZWNvcmQ8c3RyaW5nLCA/Pn0gKi8gKGJyb2FkY2FzdFBhcmFtcylcbiAgICAgIDoge31cblxuICAgIGlmICghT2JqZWN0Lmhhc093bihzY29waW5nUGFyYW1zLCBcInJlc291cmNlVHlwZVwiKSB8fCBTdHJpbmcoc2NvcGluZ1BhcmFtcy5yZXNvdXJjZVR5cGUpICE9PSBTdHJpbmcoc2NvcGUucmVzb3VyY2VUeXBlKSkgcmV0dXJuIGZhbHNlXG5cbiAgICBmb3IgKGNvbnN0IFtjb25kaXRpb25OYW1lLCBjb25kaXRpb25WYWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMoc2NvcGUuY29uZGl0aW9ucykpIHtcbiAgICAgIGlmICghT2JqZWN0Lmhhc093bihzY29waW5nUGFyYW1zLCBjb25kaXRpb25OYW1lKSkgcmV0dXJuIGZhbHNlXG5cbiAgICAgIGNvbnN0IHNjb3BpbmdWYWx1ZSA9IHNjb3BpbmdQYXJhbXNbY29uZGl0aW9uTmFtZV1cblxuICAgICAgaWYgKEFycmF5LmlzQXJyYXkoY29uZGl0aW9uVmFsdWUpKSB7XG4gICAgICAgIGlmICghY29uZGl0aW9uVmFsdWUuc29tZSgodmFsdWUpID0+IFN0cmluZyh2YWx1ZSkgPT09IFN0cmluZyhzY29waW5nVmFsdWUpKSkgcmV0dXJuIGZhbHNlXG4gICAgICB9IGVsc2UgaWYgKFN0cmluZyhjb25kaXRpb25WYWx1ZSkgIT09IFN0cmluZyhzY29waW5nVmFsdWUpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZVxuICAgICAgfVxuICAgIH1cblxuICAgIHJldHVybiB0cnVlXG4gIH1cblxuICAvKipcbiAgICogUmV0dXJucyB0aGUgYXV0aG9yaXplZCBzY29wZSBmb3IgZGVidWcgc25hcHNob3RzLlxuICAgKiBAcmV0dXJucyB7UmVjb3JkPHN0cmluZywgPz59IERlYnVnLXNhZmUgc3Vic2NyaXB0aW9uIGRldGFpbHMuXG4gICAqL1xuICBkZWJ1Z1NuYXBzaG90KCkge1xuICAgIHJldHVybiB7c2NvcGU6IHRoaXMuX3Njb3BlfVxuICB9XG59XG4iXX0=
202
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy13ZWJzb2NrZXQtY2hhbm5lbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9zeW5jL3N5bmMtd2Vic29ja2V0LWNoYW5uZWwuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWTtBQUVaLE9BQU8seUJBQXlCLE1BQU0scUNBQXFDLENBQUE7QUFDM0UsT0FBTyxFQUFDLHNCQUFzQixFQUFDLE1BQU0sd0JBQXdCLENBQUE7QUFFN0QsK0VBQStFO0FBQy9FLE1BQU0sd0JBQXdCLEdBQUcsSUFBSSxPQUFPLEVBQUUsQ0FBQTtBQUU5Qzs7Ozs7Ozs7Ozs7R0FXRztBQUNILE1BQU0sQ0FBQyxPQUFPLE9BQU8sb0JBQXFCLFNBQVEseUJBQXlCO0lBQ3pFOzs7T0FHRztJQUNILE1BQU0sR0FBRyxJQUFJLENBQUE7SUFFYjs7Ozs7OztPQU9HO0lBQ0gsTUFBTSxDQUFDLHlCQUF5QixDQUFDLGFBQWE7UUFDNUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxvQkFBb0IsRUFBRSxDQUFDLEdBQUcsSUFBSSx3QkFBd0IsQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDO1lBQUUsT0FBTTtRQUVwRyx3QkFBd0IsQ0FBQyxHQUFHLENBQUMsYUFBYSxDQUFDLENBQUE7UUFDM0MsYUFBYSxDQUFDLHdCQUF3QixDQUFDLHNCQUFzQixFQUFFLElBQUksQ0FBQyxDQUFBO0lBQ3RFLENBQUM7SUFFRDs7Ozs7O09BTUc7SUFDSCxLQUFLLENBQUMsWUFBWTtRQUNoQixNQUFNLFFBQVEsR0FBRyxNQUFNLElBQUksQ0FBQyxpQkFBaUIsRUFBRSxDQUFBO1FBQy9DLE1BQU0sS0FBSyxHQUFHLFFBQVEsQ0FBQyxZQUFZLENBQUMsRUFBQyxLQUFLLEVBQUUsRUFBQyxVQUFVLEVBQUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxVQUFVLEVBQUUsWUFBWSxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsWUFBWSxFQUFDLEVBQUMsQ0FBQyxDQUFBO1FBRTFILE1BQU0s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VSxFQUFFLE1BQU0sQ0FBQyxVQUFVLENBQUMsRUFBRSxZQUFZLEVBQUUsTUFBTSxDQUFDLFlBQVksQ0FBQyxFQUFDLEVBQUMsQ0FBQyxFQUFFLENBQUM7b0JBQy9JLGdCQUFnQixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQTtnQkFDN0IsQ0FBQztZQUNILENBQUM7UUFDSCxDQUFDLENBQUMsQ0FBQTtRQUVGLElBQUksZ0JBQWdCLENBQUMsTUFBTSxLQUFLLENBQUM7WUFBRSxPQUFPLElBQUksQ0FBQTtRQUM5QyxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQztZQUFFLE9BQU8sRUFBQyxHQUFHLFFBQVEsRUFBRSxLQUFLLEVBQUUsZ0JBQWdCLEVBQUMsQ0FBQTtRQUVoRixPQUFPLGdCQUFnQixDQUFDLENBQUMsQ0FBQyxDQUFBO0lBQzVCLENBQUM7SUFFRDs7O09BR0c7SUFDSCxhQUFhO1FBQ1gsT0FBTyxFQUFDLEtBQUssRUFBRSxJQUFJLENBQUMsTUFBTSxFQUFFLFNBQVMsRUFBRSxJQUFJLENBQUMsWUFBWSxFQUFFLEVBQUMsQ0FBQTtJQUM3RCxDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBAdHMtY2hlY2tcblxuaW1wb3J0IFZlbG9jaW91c1dlYnNvY2tldENoYW5uZWwgZnJvbSBcIi4uL2h0dHAtc2VydmVyL3dlYnNvY2tldC1jaGFubmVsLmpzXCJcbmltcG9ydCB7VkVMT0NJT1VTX1NZTkNfQ0hBTk5FTH0gZnJvbSBcIi4vc3luYy1jaGFubmVsLW5hbWUuanNcIlxuXG4vKiogQ29uZmlndXJhdGlvbnMgd2hvc2UgZnJhbWV3b3JrIHN5bmMgY2hhbm5lbCBoYXMgYWxyZWFkeSBiZWVuIHJlZ2lzdGVyZWQuICovXG5jb25zdCByZWdpc3RlcmVkQ29uZmlndXJhdGlvbnMgPSBuZXcgV2Vha1NldCgpXG5cbi8qKlxuICogRnJhbWV3b3JrLW93bmVkIHdlYnNvY2tldCBjaGFubmVsIGZvciBzeW5jZWQgcmVzb3VyY2VzXG4gKiAoe0BsaW5rIFZFTE9DSU9VU19TWU5DX0NIQU5ORUx9KS5cbiAqXG4gKiBTdWJzY3JpYmUgcGFyYW1zIG1pcnJvciBhIGRlY2xhcmVkIHB1bGwgc2NvcGUg4oCUIGB7cmVzb3VyY2VUeXBlLCBjb25kaXRpb25zfWBcbiAqIHBsdXMgdGhlIGNsaWVudC1pbmplY3RlZCBgYXV0aGVudGljYXRpb25Ub2tlbmAg4oCUIGFuZCBzdWJzY3JpYmUtdGltZVxuICogYXV0aG9yaXphdGlvbiBkZWxlZ2F0ZXMgdG8gdGhlIGFwcCBzeW5jIHJlc291cmNlJ3MgZXhpc3RpbmdcbiAqIGBhdXRob3JpemVDaGFuZ2VzKHtwYXJhbXMsIHNjb3BlfSlgICh0aGUgYHN5bmMuYXBpLnJlc291cmNlQ2xhc3NgKSwgc28gYXBwc1xuICogaG9vayBpbiB0aHJvdWdoIHRoZSBhdXRob3JpemF0aW9uIHRoZXkgYWxyZWFkeSBkZWNsYXJlZCBpbnN0ZWFkIG9mIHdyaXRpbmdcbiAqIHRoZWlyIG93biBjaGFubmVsIGNsYXNzZXMuIEJyb2FkY2FzdCByb3V0aW5nIG1hdGNoZXMgdGhlIHB1Ymxpc2hlcidzXG4gKiBzY29waW5nIHBhcmFtcyBhZ2FpbnN0IHRoZSBzdWJzY3JpcHRpb24ncyBzY29wZSBjb25kaXRpb25zLlxuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBTeW5jV2Vic29ja2V0Q2hhbm5lbCBleHRlbmRzIFZlbG9jaW91c1dlYnNvY2tldENoYW5uZWwge1xuICAvKipcbiAgICogU2NvcGUgdGhlIHN1YnNjcmlwdGlvbiB3YXMgYXV0aG9yaXplZCBmb3IsIHNldCBieSB7QGxpbmsgU3luY1dlYnNvY2tldENoYW5uZWwjY2FuU3Vic2NyaWJlfS5cbiAgICogQHR5cGUge2ltcG9ydChcIi4vc3luYy1yZXNvdXJjZS1iYXNlLmpzXCIpLlNlcmlhbGl6ZWRDaGFuZ2VzU2NvcGUgfCBudWxsfVxuICAgKi9cbiAgX3Njb3BlID0gbnVsbFxuXG4gIC8qKlxuICAgKiBSZWdpc3RlcnMgdGhlIGZyYW1ld29yayBzeW5jIGNoYW5uZWwgb24gYSBjb25maWd1cmF0aW9uIGRlY2xhcmluZyBhXG4gICAqIGBzeW5jLmFwaWAgYmxvY2sgKGd1YXJkZWQgc28gcmVwZWF0ZWQgc2VydmVyIGJvb3RzIHdpdGggdGhlIHNhbWVcbiAgICogY29uZmlndXJhdGlvbiByZWdpc3RlciBpdCBvbmx5IG9uY2UpLiBOby1vcCB3aXRob3V0IGBzeW5jLmFwaWAg4oCUIHRoZVxuICAgKiBjaGFubmVsIGF1dGhvcml6ZXMgdGhyb3VnaCB0aGUgYXBwJ3Mgc3luYyByZXNvdXJjZSBjbGFzcy5cbiAgICogQHBhcmFtIHtpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLmpzXCIpLmRlZmF1bHR9IGNvbmZpZ3VyYXRpb24gLSBDb25maWd1cmF0aW9uIGluc3RhbmNlLlxuICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICovXG4gIHN0YXRpYyByZWdpc3RlckZyb21Db25maWd1cmF0aW9uKGNvbmZpZ3VyYXRpb24pIHtcbiAgICBpZiAoIWNvbmZpZ3VyYXRpb24uZ2V0U3luY0NvbmZpZ3VyYXRpb24oKS5hcGkgfHwgcmVnaXN0ZXJlZENvbmZpZ3VyYXRpb25zLmhhcyhjb25maWd1cmF0aW9uKSkgcmV0dXJuXG5cbiAgICByZWdpc3RlcmVkQ29uZmlndXJhdGlvbnMuYWRkKGNvbmZpZ3VyYXRpb24pXG4gICAgY29uZmlndXJhdGlvbi5yZWdpc3RlcldlYnNvY2tldENoYW5uZWwoVkVMT0NJT1VTX1NZTkNfQ0hBTk5FTCwgdGhpcylcbiAgfVxuXG4gIC8qKlxuICAgKiBBdXRob3JpemVzIHRoZSBzdWJzY3JpcHRpb24gdGhyb3VnaCB0aGUgYXBwIHN5bmMgcmVzb3VyY2U6IHRoZSBzdWJzY3JpYmVcbiAgICogcGFyYW1zIGFyZSBwYXJzZWQgaW50byB0aGUgc2FtZSBzZXJpYWxpemVkIHNjb3BlIHRoZSBjaGFuZ2VzIGVuZHBvaW50XG4gICAqIGNvbnN1bWVzIGFuZCBwYXNzZWQgdG8gdGhlIHJlc291cmNlJ3MgYGF1dGhvcml6ZUNoYW5nZXMoe3BhcmFtcywgc2NvcGV9KWAuXG4gICAqIERlbmlhbHMgYW5kIG1hbGZvcm1lZCBzY29wZXMgdGhyb3csIHJlamVjdGluZyB0aGUgc3Vic2NyaXB0aW9uLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxib29sZWFuPn0gV2hldGhlciB0aGUgc3Vic2NyaXB0aW9uIGlzIGFsbG93ZWQuXG4gICAqL1xuICBhc3luYyBjYW5TdWJzY3JpYmUoKSB7XG4gICAgY29uc3QgcmVzb3VyY2UgPSBhd2FpdCB0aGlzLmJ1aWxkU3luY1Jlc291cmNlKClcbiAgICBjb25zdCBzY29wZSA9IHJlc291cmNlLmNoYW5nZXNTY29wZSh7c2NvcGU6IHtjb25kaXRpb25zOiB0aGlzLnBhcmFtcy5jb25kaXRpb25zLCByZXNvdXJjZVR5cGU6IHRoaXMucGFyYW1zLnJlc291cmNlVHlwZX19KVxuXG4gICAgYXdhaXQgcmVzb3VyY2UuYXV0aG9yaXplQ2hhbmdlcyh7cGFyYW1zOiB0aGlzLnBhcmFtcywgc2NvcGV9KVxuXG4gICAgdGhpcy5fc2NvcGUgPSBzY29wZVxuXG4gICAgcmV0dXJuIHRydWVcbiAgfVxuXG4gIC8qKlxuICAgKiBCdWlsZHMgdGhlIGFwcCBzeW5jIHJlc291cmNlIGF1dGhvcml6aW5nIHRoaXMgc3Vic2NyaXB0aW9uLCBtaXJyb3JpbmcgdGhlXG4gICAqIHN5bmMgdHJhbnNwb3J0IGNvbnRyb2xsZXIncyByZXNvdXJjZSBjb25zdHJ1Y3Rpb24gd2l0aCB0aGUgYWJpbGl0eVxuICAgKiByZXNvbHZlZCBmcm9tIHRoZSBzdWJzY3JpYmUgcGFyYW1zICh3aGljaCBjYXJyeSB0aGUgY2xpZW50LWluamVjdGVkXG4gICAqIGF1dGhlbnRpY2F0aW9uVG9rZW4pLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxpbXBvcnQoXCIuL3N5bmMtcmVzb3VyY2UtYmFzZS5qc1wiKS5kZWZhdWx0Pn0gQXBwIHN5bmMgcmVzb3VyY2UgaW5zdGFuY2UuXG4gICAqL1xuICBhc3luYyBidWlsZFN5bmNSZXNvdXJjZSgpIHtcbiAgICBjb25zdCBjb25maWd1cmF0aW9uID0gdGhpcy5zZXNzaW9uLmNvbmZpZ3VyYXRpb25cbiAgICBjb25zdCBhcGkgPSBjb25maWd1cmF0aW9uLmdldFN5bmNDb25maWd1cmF0aW9uKCkuYXBpXG5cbiAgICBpZiAoIWFwaSkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKGBUaGUgJHtWRUxPQ0lPVVNfU1lOQ19DSEFOTkVMfSBjaGFubmVsIHJlcXVpcmVzIGEgc3luYy5hcGkgY29uZmlndXJhdGlvbiBibG9jayB3aXRoIHRoZSBhcHAncyBzeW5jIHJlc291cmNlIGNsYXNzYClcbiAgICB9XG5cbiAgICAvLyBOYXJyb3dzIHRoZSBjb25maWd1cmVkIHJlc291cmNlIGNsYXNzIHRvIHRoZSBzeW5jIHJlc291cmNlIGNvbnRyYWN0XG4gICAgLy8gKGNoYW5nZXNTY29wZS9hdXRob3JpemVDaGFuZ2VzKSB0aGUgc3luYy5hcGkgdmFsaWRhdGlvbiByZXF1aXJlcy5cbiAgICBjb25zdCBSZXNvdXJjZUNsYXNzID0gLyoqIEB0eXBlIHt0eXBlb2YgaW1wb3J0KFwiLi9zeW5jLXJlc291cmNlLWJhc2UuanNcIikuZGVmYXVsdH0gKi8gKGFwaS5yZXNvdXJjZUNsYXNzKVxuICAgIC8vIE5hcnJvd3MgdGhlIHdlYnNvY2tldCBzdWJzY3JpYmUgcGFyYW1zIHRvIHRoZSByZXNvdXJjZSBwYXJhbXMgY29udHJhY3QuXG4gICAgY29uc3QgcGFyYW1zID0gLyoqIEB0eXBlIHtpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLXR5cGVzLmpzXCIpLlZlbG9jaW91c1BhcmFtc30gKi8gKC8qKiBAdHlwZSB7dW5rbm93bn0gKi8gKHRoaXMucGFyYW1zKSlcbiAgICBjb25zdCByZXF1ZXN0ID0gdGhpcy5zZXNzaW9uLnVwZ3JhZGVSZXF1ZXN0XG4gICAgY29uc3QgYWJpbGl0eSA9IGF3YWl0IGNvbmZpZ3VyYXRpb24ucmVzb2x2ZUFiaWxpdHkoe3BhcmFtcywgcmVxdWVzdH0pXG5cbiAgICByZXR1cm4gbmV3IFJlc291cmNlQ2xhc3Moe1xuICAgICAgYWJpbGl0eSxcbiAgICAgIGNvbnRleHQ6IHtcbiAgICAgICAgLi4uKGFiaWxpdHk/LmdldENvbnRleHQoKSB8fCB7fSksXG4gICAgICAgIHBhcmFtcyxcbiAgICAgICAgcmVxdWVzdFxuICAgICAgfSxcbiAgICAgIGxvY2FsczogYWJpbGl0eT8uZ2V0TG9jYWxzKCkgfHwge30sXG4gICAgICBtb2RlbENsYXNzOiBSZXNvdXJjZUNsYXNzLk1vZGVsQ2xhc3MsXG4gICAgICBtb2RlbE5hbWU6IFJlc291cmNlQ2xhc3MuTW9kZWxDbGFzcz8ubmFtZSxcbiAgICAgIHBhcmFtcyxcbiAgICAgIHJlc291cmNlQ29uZmlndXJhdGlvbjogLyoqIEB0eXBlIHtpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLXR5cGVzLmpzXCIpLkZyb250ZW5kTW9kZWxSZXNvdXJjZUNvbmZpZ3VyYXRpb259ICovICh7XG4gICAgICAgIGF0dHJpYnV0ZXM6IFJlc291cmNlQ2xhc3MuYXR0cmlidXRlcyB8fCB7fSxcbiAgICAgICAgc3luYzoge2VuYWJsZWQ6IHRydWV9XG4gICAgICB9KVxuICAgIH0pXG4gIH1cblxuICAvKipcbiAgICogUm91dGVzIGEgcHVibGlzaGVyIGJyb2FkY2FzdCB0byB0aGlzIHN1YnNjcmlwdGlvbiB3aGVuIHRoZSBwdWJsaXNoZWRcbiAgICogcmVzb3VyY2UgdHlwZSBlcXVhbHMgdGhlIHR5cGUgdGhlIHN1YnNjcmlwdGlvbiB3YXMgYXV0aG9yaXplZCBmb3IgYW5kIHRoZVxuICAgKiBzY29waW5nIHBhcmFtcyBzYXRpc2Z5IGV2ZXJ5IHNjb3BlIGNvbmRpdGlvbjogZWFjaCBjb25kaXRpb24ga2V5IG11c3QgYmVcbiAgICogcHJlc2VudCBpbiB0aGUgYnJvYWRjYXN0IHBhcmFtcyBhbmQgbWF0Y2ggYnkgc3RyaW5nIGNvbXBhcmlzb24gKGFycmF5XG4gICAqIGNvbmRpdGlvbnMgbWF0Y2ggYnkgbWVtYmVyc2hpcCkuIEJyb2FkY2FzdHMgd2l0aG91dCBhIHJlc291cmNlIHR5cGUgYW5kXG4gICAqIGNvbmRpdGlvbnMgdGhlIHB1Ymxpc2hlcidzIHNjb3BpbmcgcGFyYW1zIGRvIG5vdCBjYXJyeSBuZXZlciBtYXRjaCwgc28gYVxuICAgKiBzdWJzY3JpcHRpb24gY2Fubm90IHJlY2VpdmUgY2hhbmdlcyBvdXRzaWRlIGl0cyBhdXRob3JpemVkIHNjb3BlLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4uL2h0dHAtc2VydmVyL3dlYnNvY2tldC1jaGFubmVsLmpzXCIpLldlYnNvY2tldEpzb25WYWx1ZX0gYnJvYWRjYXN0UGFyYW1zIC0gUHVibGlzaGVyIHNjb3BpbmcgcGFyYW1zICh0aGUgcHVibGlzaGVkIHJlc291cmNlVHlwZSBwbHVzIHRoZSBjaGFuZ2UncyBzY29wZS1wYXJ0aXRpb24gdmFsdWVzKS5cbiAgICogQHJldHVybnMge2Jvb2xlYW59IFdoZXRoZXIgdGhlIGJyb2FkY2FzdCBiZWxvbmdzIHRvIHRoaXMgc3Vic2NyaXB0aW9uJ3Mgc2NvcGUuXG4gICAqL1xuICBtYXRjaGVzKGJyb2FkY2FzdFBhcmFtcykge1xuICAgIGNvbnN0IHNjb3BlID0gdGhpcy5fc2NvcGVcblxuICAgIGlmICghc2NvcGUpIHJldHVybiBmYWxzZVxuXG4gICAgY29uc3Qgc2NvcGluZ1BhcmFtcyA9IGJyb2FkY2FzdFBhcmFtcyAmJiB0eXBlb2YgYnJvYWRjYXN0UGFyYW1zID09PSBcIm9iamVjdFwiICYmICFBcnJheS5pc0FycmF5KGJyb2FkY2FzdFBhcmFtcylcbiAgICAgID8gLyoqIEB0eXBlIHtSZWNvcmQ8c3RyaW5nLCA/Pn0gKi8gKGJyb2FkY2FzdFBhcmFtcylcbiAgICAgIDoge31cblxuICAgIGlmICghT2JqZWN0Lmhhc093bihzY29waW5nUGFyYW1zLCBcInJlc291cmNlVHlwZVwiKSB8fCBTdHJpbmcoc2NvcGluZ1BhcmFtcy5yZXNvdXJjZVR5cGUpICE9PSBTdHJpbmcoc2NvcGUucmVzb3VyY2VUeXBlKSkgcmV0dXJuIGZhbHNlXG5cbiAgICBmb3IgKGNvbnN0IFtjb25kaXRpb25OYW1lLCBjb25kaXRpb25WYWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMoc2NvcGUuY29uZGl0aW9ucykpIHtcbiAgICAgIGlmICghT2JqZWN0Lmhhc093bihzY29waW5nUGFyYW1zLCBjb25kaXRpb25OYW1lKSkgcmV0dXJuIGZhbHNlXG5cbiAgICAgIGNvbnN0IHNjb3BpbmdWYWx1ZSA9IHNjb3BpbmdQYXJhbXNbY29uZGl0aW9uTmFtZV1cblxuICAgICAgaWYgKEFycmF5LmlzQXJyYXkoY29uZGl0aW9uVmFsdWUpKSB7XG4gICAgICAgIGlmICghY29uZGl0aW9uVmFsdWUuc29tZSgodmFsdWUpID0+IFN0cmluZyh2YWx1ZSkgPT09IFN0cmluZyhzY29waW5nVmFsdWUpKSkgcmV0dXJuIGZhbHNlXG4gICAgICB9IGVsc2UgaWYgKFN0cmluZyhjb25kaXRpb25WYWx1ZSkgIT09IFN0cmluZyhzY29waW5nVmFsdWUpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZVxuICAgICAgfVxuICAgIH1cblxuICAgIHJldHVybiB0cnVlXG4gIH1cblxuICAvKipcbiAgICogRGVsaXZlcnMgYSBtYXRjaGVkIGJyb2FkY2FzdC4gU2NvcGVkIHN1YnNjcmlwdGlvbnMgKHdpdGggZXhwbGljaXRcbiAgICogY29uZGl0aW9ucykgYWxyZWFkeSByb3V0ZWQgdGhyb3VnaCB7QGxpbmsgU3luY1dlYnNvY2tldENoYW5uZWwjbWF0Y2hlc30sIHNvXG4gICAqIHRoZSBjaGFuZ2UgaXMgaW4gc2NvcGUgYW5kIGRlbGl2ZXJzIHVuY2hhbmdlZC4gVXNlci1zY29wZSBzdWJzY3JpcHRpb25zXG4gICAqIChlbXB0eSBjb25kaXRpb25zLCBcImV2ZXJ5dGhpbmcgbXkgYWJpbGl0eSBjYW4gc2VlXCIpIG1hdGNoIGV2ZXJ5IGJyb2FkY2FzdFxuICAgKiBvZiB0aGUgcmVzb3VyY2UgdHlwZSwgc28gZWFjaCBwdWJsaXNoZWQgY2hhbmdlIGlzIHJlLWNoZWNrZWQgYWdhaW5zdCB0aGVcbiAgICogc3Vic2NyaWJlcidzIGFiaWxpdHkgYXQgZmFuLW91dCB0aHJvdWdoIHRoZSBhcHAgc3luYyByZXNvdXJjZSdzXG4gICAqIGBjaGFuZ2VEZWxpdmVyYWJsZWA7IG9ubHkgYWNjZXNzaWJsZSBjaGFuZ2VzIGFyZSBkZWxpdmVyZWQsIGFuZCBhIGJyb2FkY2FzdFxuICAgKiB3aXRoIG5vIGFjY2Vzc2libGUgY2hhbmdlIGlzIGRyb3BwZWQuXG4gICAqIEBwYXJhbSB7aW1wb3J0KFwiLi4vaHR0cC1zZXJ2ZXIvd2Vic29ja2V0LWNoYW5uZWwuanNcIikuV2Vic29ja2V0SnNvblZhbHVlfSBib2R5IC0gQnJvYWRjYXN0IGJvZHkgKHN5bmMgZW52ZWxvcGUpLlxuICAgKiBAcGFyYW0ge3tldmVudElkPzogc3RyaW5nfX0gW21ldGFdIC0gT3B0aW9uYWwgZXZlbnQgbWV0YWRhdGEuXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPHZvaWQ+fVxuICAgKi9cbiAgYXN5bmMgZGVsaXZlckJyb2FkY2FzdChib2R5LCBtZXRhKSB7XG4gICAgaWYgKCF0aGlzLl9pc1VzZXJTY29wZSgpKSB7XG4gICAgICB0aGlzLnNlbmRNZXNzYWdlKGJvZHksIG1ldGEpXG5cbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIGNvbnN0IGRlbGl2ZXJhYmxlQm9keSA9IGF3YWl0IHRoaXMuX3VzZXJTY29wZURlbGl2ZXJhYmxlQm9keShib2R5KVxuXG4gICAgaWYgKGRlbGl2ZXJhYmxlQm9keSAhPT0gbnVsbCkgdGhpcy5zZW5kTWVzc2FnZShkZWxpdmVyYWJsZUJvZHksIG1ldGEpXG4gIH1cblxuICAvKipcbiAgICogV2hldGhlciB0aGlzIHN1YnNjcmlwdGlvbiBpcyBhIHVzZXIgc2NvcGU6IGF1dGhvcml6ZWQgd2l0aCBlbXB0eSBjb25kaXRpb25zXG4gICAqIChcImV2ZXJ5dGhpbmcgbXkgYWJpbGl0eSBjYW4gc2VlXCIpLlxuICAgKiBAcmV0dXJucyB7Ym9vbGVhbn0gV2hldGhlciB0aGUgc3Vic2NyaXB0aW9uIGlzIGEgdXNlciBzY29wZS5cbiAgICovXG4gIF9pc1VzZXJTY29wZSgpIHtcbiAgICByZXR1cm4gQm9vbGVhbih0aGlzLl9zY29wZSkgJiYgT2JqZWN0LmtleXMoLyoqIEB0eXBlIHtpbXBvcnQoXCIuL3N5bmMtcmVzb3VyY2UtYmFzZS5qc1wiKS5TZXJpYWxpemVkQ2hhbmdlc1Njb3BlfSAqLyAodGhpcy5fc2NvcGUpLmNvbmRpdGlvbnMpLmxlbmd0aCA9PT0gMFxuICB9XG5cbiAgLyoqXG4gICAqIEZpbHRlcnMgYSB1c2VyLXNjb3BlIGJyb2FkY2FzdCB0byB0aGUgc3luYyBlbnRyaWVzIHRoZSBzdWJzY3JpYmVyJ3MgYWJpbGl0eVxuICAgKiBjYW4gYWNjZXNzLCByZS1jaGVja2luZyBlYWNoIHRocm91Z2ggdGhlIGFwcCBzeW5jIHJlc291cmNlJ3NcbiAgICogYGNoYW5nZURlbGl2ZXJhYmxlYC4gUmV0dXJucyB0aGUgYnJvYWRjYXN0IG5hcnJvd2VkIHRvIGFjY2Vzc2libGUgZW50cmllcyxcbiAgICogb3IgbnVsbCB3aGVuIG5vbmUgYXJlIGFjY2Vzc2libGUuIE5vbi1lbnZlbG9wZSBib2RpZXMgYW5kIGVudHJpZXMgd2l0aG91dCBhXG4gICAqIHJlc291cmNlIGlkIGFyZSBkcm9wcGVkIChmYWlsIGNsb3NlZCkuXG4gICAqIEBwYXJhbSB7aW1wb3J0KFwiLi4vaHR0cC1zZXJ2ZXIvd2Vic29ja2V0LWNoYW5uZWwuanNcIikuV2Vic29ja2V0SnNvblZhbHVlfSBib2R5IC0gQnJvYWRjYXN0IGJvZHkuXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPGltcG9ydChcIi4uL2h0dHAtc2VydmVyL3dlYnNvY2tldC1jaGFubmVsLmpzXCIpLldlYnNvY2tldEpzb25WYWx1ZSB8IG51bGw+fSBEZWxpdmVyYWJsZSBib2R5LCBvciBudWxsLlxuICAgKi9cbiAgYXN5bmMgX3VzZXJTY29wZURlbGl2ZXJhYmxlQm9keShib2R5KSB7XG4gICAgaWYgKCFib2R5IHx8IHR5cGVvZiBib2R5ICE9PSBcIm9iamVjdFwiIHx8IEFycmF5LmlzQXJyYXkoYm9keSkpIHJldHVybiBudWxsXG5cbiAgICBjb25zdCBlbnZlbG9wZSA9IC8qKiBAdHlwZSB7UmVjb3JkPHN0cmluZywgPz59ICovIChib2R5KVxuICAgIGNvbnN0IHNjb3BlID0gLyoqIEB0eXBlIHtpbXBvcnQoXCIuL3N5bmMtcmVzb3VyY2UtYmFzZS5qc1wiKS5TZXJpYWxpemVkQ2hhbmdlc1Njb3BlfSAqLyAodGhpcy5fc2NvcGUpXG4gICAgY29uc3Qgc3luY3MgPSBBcnJheS5pc0FycmF5KGVudmVsb3BlLnN5bmNzKSA/IGVudmVsb3BlLnN5bmNzIDogW2VudmVsb3BlXVxuICAgIGNvbnN0IGNvbmZpZ3VyYXRpb24gPSB0aGlzLnNlc3Npb24uY29uZmlndXJhdGlvblxuICAgIC8qKiBAdHlwZSB7QXJyYXk8UmVjb3JkPHN0cmluZywgPz4+fSAqL1xuICAgIGNvbnN0IGRlbGl2ZXJhYmxlU3luY3MgPSBbXVxuXG4gICAgLy8gQnJvYWRjYXN0IGZhbi1vdXQgcnVucyB0aHJvdWdoIGB3aXRob3V0Q3VycmVudENvbm5lY3Rpb25Db250ZXh0c2AgKHNlZVxuICAgIC8vIENvbmZpZ3VyYXRpb24jX2Jyb2FkY2FzdFRvQ2hhbm5lbExvY2FsKSwgc28gdGhlcmUgaXMgbm8gYW1iaWVudCBkYXRhYmFzZVxuICAgIC8vIGNvbm5lY3Rpb24gaGVyZS4gUmVzb2x2ZSB0aGUgcmVzb3VyY2UncyBhYmlsaXR5IGFuZCBydW4gdGhlIHBlci1kZWxpdmVyeVxuICAgIC8vIGFjY2VzcyBxdWVyeSBpbnNpZGUgYSBjaGVja2VkLW91dCBjb25uZWN0aW9uIGNvbnRleHQsIG1pcnJvcmluZyBob3cgb3RoZXJcbiAgICAvLyBicm9hZGNhc3QtdGltZSBEQiB3b3JrICh0aGUgZnJvbnRlbmQtbW9kZWwgY2hhbm5lbCkgb2J0YWlucyBjb25uZWN0aW9ucy5cbiAgICBhd2FpdCBjb25maWd1cmF0aW9uLmVuc3VyZUNvbm5lY3Rpb25zKHtuYW1lOiBgJHtWRUxPQ0lPVVNfU1lOQ19DSEFOTkVMfSB1c2VyLXNjb3BlIGRlbGl2ZXJ5IGFjY2VzcyBjaGVja2B9LCBhc3luYyAoKSA9PiB7XG4gICAgICBjb25zdCByZXNvdXJjZSA9IGF3YWl0IHRoaXMuYnVpbGRTeW5jUmVzb3VyY2UoKVxuXG4gICAgICBmb3IgKGNvbnN0IHN5bmMgb2Ygc3luY3MpIHtcbiAgICAgICAgY29uc3QgcmVzb3VyY2VJZCA9IHN5bmM/LnJlc291cmNlSWRcbiAgICAgICAgY29uc3QgcmVzb3VyY2VUeXBlID0gc3luYz8ucmVzb3VyY2VUeXBlID8/IHNjb3BlLnJlc291cmNlVHlwZVxuXG4gICAgICAgIGlmIChyZXNvdXJjZUlkID09PSB1bmRlZmluZWQgfHwgcmVzb3VyY2VJZCA9PT0gbnVsbCkgY29udGludWVcblxuICAgICAgICBpZiAoYXdhaXQgcmVzb3VyY2UuY2hhbmdlRGVsaXZlcmFibGUoe3BhcmFtczogdGhpcy5wYXJhbXMsIHNjb3BlLCBzeW5jOiB7cmVzb3VyY2VJZDogU3RyaW5nKHJlc291cmNlSWQpLCByZXNvdXJjZVR5cGU6IFN0cmluZyhyZXNvdXJjZVR5cGUpfX0pKSB7XG4gICAgICAgICAgZGVsaXZlcmFibGVTeW5jcy5wdXNoKHN5bmMpXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9KVxuXG4gICAgaWYgKGRlbGl2ZXJhYmxlU3luY3MubGVuZ3RoID09PSAwKSByZXR1cm4gbnVsbFxuICAgIGlmIChBcnJheS5pc0FycmF5KGVudmVsb3BlLnN5bmNzKSkgcmV0dXJuIHsuLi5lbnZlbG9wZSwgc3luY3M6IGRlbGl2ZXJhYmxlU3luY3N9XG5cbiAgICByZXR1cm4gZGVsaXZlcmFibGVTeW5jc1swXVxuICB9XG5cbiAgLyoqXG4gICAqIFJldHVybnMgdGhlIGF1dGhvcml6ZWQgc2NvcGUgZm9yIGRlYnVnIHNuYXBzaG90cy5cbiAgICogQHJldHVybnMge1JlY29yZDxzdHJpbmcsID8+fSBEZWJ1Zy1zYWZlIHN1YnNjcmlwdGlvbiBkZXRhaWxzLlxuICAgKi9cbiAgZGVidWdTbmFwc2hvdCgpIHtcbiAgICByZXR1cm4ge3Njb3BlOiB0aGlzLl9zY29wZSwgdXNlclNjb3BlOiB0aGlzLl9pc1VzZXJTY29wZSgpfVxuICB9XG59XG4iXX0=
@@ -44,12 +44,19 @@ export function serializedScopeFromQuery(query) {
44
44
  }
45
45
 
46
46
  /**
47
- * Returns a stable canonical key identifying a sync scope.
47
+ * Returns a stable canonical key identifying a sync scope. When an `owner` is
48
+ * present (the authenticated identity that declared the scope locally), it
49
+ * participates in the key so the same wire scope owned by a different user gets
50
+ * its own local identity and cursor — a user scope's empty-conditions cursor
51
+ * never leaks across accounts on a shared device, while the same user
52
+ * reconnecting keeps continuity. Owner-less scopes keep their pre-owner key.
48
53
  * @param {import("./sync-client-types.js").SerializedSyncScope} scope - Serialized sync scope.
49
54
  * @returns {string} Stable scope key.
50
55
  */
51
56
  export function scopeKey(scope) {
52
- return `${scope.resourceType}:${stableJsonStringify(scope.conditions)}`
57
+ const ownerPrefix = scope.owner === undefined || scope.owner === null ? "" : `owner=${stableJsonStringify(scope.owner)}|`
58
+
59
+ return `${ownerPrefix}${scope.resourceType}:${stableJsonStringify(scope.conditions)}`
53
60
  }
54
61
 
55
62
  /**
@@ -5,6 +5,7 @@
5
5
  * @typedef {object} SerializedSyncScope
6
6
  * @property {Record<string, ?>} conditions - Plain attribute conditions from the query.
7
7
  * @property {string} resourceType - Resource/model name the scope was declared for.
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.
8
9
  */
9
10
 
10
11
  /**
@@ -58,6 +59,13 @@
58
59
  * @typedef {boolean | ModelSyncDeclarationConfig<TModel>} ModelSyncDeclaration
59
60
  */
60
61
 
62
+ /**
63
+ * Shared app-lifetime websocket connection all sync traffic rides. Matches the
64
+ * realtime websocket client contract; the sync client rides it without owning
65
+ * its connect/disconnect lifecycle.
66
+ * @typedef {import("../configuration-types.js").VelociousSyncRealtimeWebsocketClient} SyncClientSharedConnection
67
+ */
68
+
61
69
  /**
62
70
  * Options for building a sync client. Everything else — resources, transport
63
71
  * POSTers, auth, connectivity, batch size — is derived from the configuration's
@@ -84,6 +92,8 @@
84
92
  * @property {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration} [realtime] - Realtime push configuration consumed by `subscribeRealtime(...)`.
85
93
  * @property {Record<string, SyncClientResourceConfig>} resources - Derived resource policies keyed by resource/model name.
86
94
  * @property {?} syncModel - Local pending-sync model class.
95
+ * @property {SyncClientSharedConnection} [websocketClient] - Shared app-lifetime websocket client instance (the low-level shared-connection form).
96
+ * @property {string | (() => string | null | undefined)} [websocketUrl] - Shared app-lifetime websocket URL the framework builds a client from.
87
97
  */
88
98
 
89
99
  export {}
@@ -4,6 +4,7 @@ import Configuration from "../configuration.js"
4
4
  import {isBooleanColumnType} from "../database/column-types.js"
5
5
  import Logger from "../logger.js"
6
6
  import restArgsError from "../utils/rest-args-error.js"
7
+ import VelociousWebsocketClient from "../http-client/websocket-client.js"
7
8
 
8
9
  import {serializedScopeFromQuery} from "./query-scope.js"
9
10
  import SyncApiClient from "./sync-api-client.js"
@@ -99,11 +100,19 @@ export default class SyncClient {
99
100
  postReplay: transportPoster({path: `${clientConfiguration.mountPath}/replay`, transport: clientConfiguration.transport}),
100
101
  realtime: clientConfiguration.realtime,
101
102
  resources,
102
- syncModel: resolvedSyncModel
103
+ syncModel: resolvedSyncModel,
104
+ websocketClient: clientConfiguration.websocketClient,
105
+ websocketUrl: clientConfiguration.websocketUrl
103
106
  }
104
107
  this._clientNumber = ++clientCounter
105
108
  /** @type {SyncRealtimeBridge | null} */
106
109
  this._realtimeBridge = null
110
+ /** @type {import("./sync-client-types.js").SyncClientSharedConnection | null | undefined} Shared app-lifetime websocket connection (undefined until first resolved, null when none is configured). */
111
+ this._syncConnection = undefined
112
+ /** @type {Promise<void> | null} */
113
+ this._subscribeUserScopePromise = null
114
+ /** @type {"subscribed" | "subscribing" | "unsubscribed"} */
115
+ this._userScopeState = "unsubscribed"
107
116
  /** @type {import("./sync-scope-store.js").default | null} */
108
117
  this._scopeStore = scopeStore || null
109
118
  /** @type {Promise<void> | null} */
@@ -449,6 +458,33 @@ export default class SyncClient {
449
458
  }
450
459
  }
451
460
 
461
+ /**
462
+ * Resolves the shared app-lifetime websocket connection all sync traffic
463
+ * rides, or null when none is configured. Built once and memoized (per
464
+ * client): an app-provided `sync.client.websocketClient` instance wins (the
465
+ * frontend-model transport can pass its own client so one socket carries
466
+ * everything), else a framework-owned reconnecting {@link VelociousWebsocketClient}
467
+ * built from `sync.client.websocketUrl`. The realtime bridge rides this
468
+ * connection without owning its lifecycle; when neither is configured the
469
+ * bridge falls back to the deprecated per-cycle `realtime.createClient`.
470
+ * @returns {import("./sync-client-types.js").SyncClientSharedConnection | null} Shared websocket connection, or null.
471
+ */
472
+ syncConnection() {
473
+ if (this._syncConnection !== undefined) return this._syncConnection
474
+
475
+ if (this.config.websocketClient) {
476
+ this._syncConnection = this.config.websocketClient
477
+ } else if (this.config.websocketUrl) {
478
+ const url = typeof this.config.websocketUrl === "function" ? this.config.websocketUrl() : this.config.websocketUrl
479
+
480
+ this._syncConnection = url ? new VelociousWebsocketClient({url}) : null
481
+ } else {
482
+ this._syncConnection = null
483
+ }
484
+
485
+ return this._syncConnection
486
+ }
487
+
452
488
  /**
453
489
  * Subscribes the derived realtime channels so pushed websocket changes apply
454
490
  * through the same derived applier as pulls (idempotent, single-flighted).
@@ -459,6 +495,94 @@ export default class SyncClient {
459
495
  await this.realtimeBridge().subscribe(context)
460
496
  }
461
497
 
498
+ /**
499
+ * Subscribes the server-enumerated user scope: "everything my ability can
500
+ * see". Declares a user scope (empty conditions) for every pullable synced
501
+ * resource type, subscribes realtime so their framework sync channel
502
+ * subscriptions go live, and pulls so the device catches up. The server
503
+ * authorizes each empty-conditions scope through the app sync resource's
504
+ * `authorizeChanges` and re-checks record access per delivery, so the client
505
+ * subscribes with just its token and the server decides membership.
506
+ * Idempotent and single-flighted like {@link SyncClient#subscribeRealtime}.
507
+ * @returns {Promise<void>}
508
+ */
509
+ async subscribeUserScope() {
510
+ if (this._userScopeState === "subscribed") return
511
+
512
+ if (!this._subscribeUserScopePromise) {
513
+ this._subscribeUserScopePromise = this._subscribeUserScope().finally(() => {
514
+ this._subscribeUserScopePromise = null
515
+ })
516
+ }
517
+
518
+ await this._subscribeUserScopePromise
519
+ }
520
+
521
+ /**
522
+ * Declares and activates the user scope for every pullable resource, then
523
+ * subscribes realtime and pulls.
524
+ * @returns {Promise<void>}
525
+ */
526
+ async _subscribeUserScope() {
527
+ this._userScopeState = "subscribing"
528
+
529
+ const scopeStore = this.scopeStore()
530
+ const owner = await this.userScopeOwner()
531
+
532
+ for (const resourceType of this.userScopeResourceTypes()) {
533
+ await scopeStore.findOrCreateScope({conditions: {}, owner, resourceType})
534
+ }
535
+
536
+ await this.subscribeRealtime()
537
+ await this.pull()
538
+
539
+ this._userScopeState = "subscribed"
540
+ }
541
+
542
+ /**
543
+ * Unsubscribes the user scope: deactivates the per-resource user scopes and
544
+ * closes the realtime channel subscriptions. The shared websocket connection
545
+ * stays open when one is configured (sign-out drops subscriptions without
546
+ * disconnecting), so a subsequent sign-in resubscribes over the same socket.
547
+ * @returns {Promise<void>}
548
+ */
549
+ async unsubscribeUserScope() {
550
+ const scopeStore = this.scopeStore()
551
+ const owner = await this.userScopeOwner()
552
+
553
+ for (const resourceType of this.userScopeResourceTypes()) {
554
+ await scopeStore.deactivate({conditions: {}, owner, resourceType})
555
+ }
556
+
557
+ await this.unsubscribeRealtime()
558
+
559
+ this._userScopeState = "unsubscribed"
560
+ this._subscribeUserScopePromise = null
561
+ }
562
+
563
+ /**
564
+ * The resource types a user scope covers: every declared resource that
565
+ * receives pulled changes (has pull `attributes`).
566
+ * @returns {string[]} Pullable resource type names.
567
+ */
568
+ userScopeResourceTypes() {
569
+ return Object.keys(this.pullResourceConfigs())
570
+ }
571
+
572
+ /**
573
+ * Resolves the local partition key for the user scope: the currently
574
+ * configured authenticated identity (the sync auth token). Partitioning the
575
+ * user scope's local scope/cursor rows by this owner keeps the
576
+ * empty-conditions cursor from leaking across accounts on a shared device
577
+ * (account B signing in after account A gets a fresh cursor) while the same
578
+ * account reconnecting keeps its cursor continuity. The owner is a local
579
+ * partition key only — pulls still post empty conditions to the server.
580
+ * @returns {Promise<string>} User-scope owner partition key.
581
+ */
582
+ async userScopeOwner() {
583
+ return String(await this.config.authenticationToken())
584
+ }
585
+
462
586
  /**
463
587
  * Unsubscribes the realtime channels and disconnects the websocket client (idempotent).
464
588
  * @returns {Promise<void>}
@@ -30,6 +30,8 @@ export default class SyncRealtimeBridge {
30
30
  this._channels = []
31
31
  /** @type {VelociousSyncRealtimeWebsocketClient | null} */
32
32
  this._client = null
33
+ /** @type {boolean} Whether the bridge created its own client (deprecated per-cycle path) and must disconnect it on unsubscribe. A shared connection is never owned. */
34
+ this._ownsClient = false
33
35
  /** @type {Promise<void>} */
34
36
  this._applyPromise = Promise.resolve()
35
37
  /** @type {number} Subscription generation - bumped by unsubscribe so in-flight subscribes detect they became stale. */
@@ -80,8 +82,13 @@ export default class SyncRealtimeBridge {
80
82
  const channels = []
81
83
  /** @type {VelociousSyncRealtimeWebsocketClient | null} */
82
84
  let client = null
85
+ /** @type {boolean} Whether this attempt created its own client and must disconnect it on teardown. */
86
+ let ownsClient = false
83
87
  /**
84
- * Tears down everything this stale/failed subscribe attempt created itself.
88
+ * Tears down everything this stale/failed subscribe attempt created itself:
89
+ * always closes its channel subscriptions, and disconnects the websocket
90
+ * only when the bridge owns it (deprecated per-cycle path); a shared
91
+ * connection stays open.
85
92
  * @returns {Promise<void>}
86
93
  */
87
94
  const teardown = async () => {
@@ -89,18 +96,28 @@ export default class SyncRealtimeBridge {
89
96
  subscription.close()
90
97
  }
91
98
 
92
- if (client) await client.disconnectAndStopReconnect()
99
+ if (client && ownsClient) await client.disconnectAndStopReconnect()
93
100
  }
94
101
 
95
102
  try {
96
- const realtime = this.realtimeConfiguration()
103
+ const sharedClient = this.syncClient.syncConnection()
104
+ const realtime = this.requireClientSource(sharedClient)
97
105
  const channelDescriptors = await this.channelDescriptors(context)
98
106
 
99
107
  if (generation !== this._generation) return
100
108
 
101
- client = await realtime.createClient()
109
+ if (sharedClient) {
110
+ client = sharedClient
111
+ } else {
112
+ // requireClientSource guaranteed realtime.createClient when there is no shared connection.
113
+ client = await /** @type {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration} */ (realtime).createClient()
114
+ ownsClient = true
115
+ }
102
116
 
103
- if (generation !== this._generation) return
117
+ if (generation !== this._generation) {
118
+ await teardown()
119
+ return
120
+ }
104
121
 
105
122
  await client.connect()
106
123
 
@@ -140,6 +157,7 @@ export default class SyncRealtimeBridge {
140
157
 
141
158
  this._channels = channels
142
159
  this._client = client
160
+ this._ownsClient = ownsClient
143
161
  this._state = "subscribed"
144
162
  this.schedulePull()
145
163
  } catch (error) {
@@ -152,9 +170,12 @@ export default class SyncRealtimeBridge {
152
170
  }
153
171
 
154
172
  /**
155
- * Closes every channel subscription and disconnects the websocket client
156
- * (idempotent). Also marks any in-flight subscribe attempt stale so it tears
157
- * itself down instead of finishing the subscription afterwards.
173
+ * Closes every channel subscription (idempotent). The websocket is
174
+ * disconnected only when the bridge owns it (deprecated per-cycle
175
+ * `realtime.createClient` path); a shared app-lifetime connection stays open
176
+ * so unsubscribing drops subscriptions without tearing down the socket. Also
177
+ * marks any in-flight subscribe attempt stale so it tears itself down instead
178
+ * of finishing the subscription afterwards.
158
179
  * @returns {Promise<void>}
159
180
  */
160
181
  async unsubscribe() {
@@ -162,16 +183,18 @@ export default class SyncRealtimeBridge {
162
183
 
163
184
  const channels = this._channels
164
185
  const client = this._client
186
+ const ownsClient = this._ownsClient
165
187
 
166
188
  this._channels = []
167
189
  this._client = null
190
+ this._ownsClient = false
168
191
  this._state = "unsubscribed"
169
192
 
170
193
  for (const {subscription} of channels) {
171
194
  subscription.close()
172
195
  }
173
196
 
174
- if (client) await client.disconnectAndStopReconnect()
197
+ if (client && ownsClient) await client.disconnectAndStopReconnect()
175
198
  }
176
199
 
177
200
  /**
@@ -195,14 +218,26 @@ export default class SyncRealtimeBridge {
195
218
  }
196
219
 
197
220
  /**
198
- * Resolves the realtime configuration, failing loudly when it is missing.
199
- * @returns {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration} Realtime configuration.
221
+ * Resolves the realtime configuration block, or null when the app declared
222
+ * none (valid when a shared connection is configured).
223
+ * @returns {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration | null} Realtime configuration, or null.
200
224
  */
201
225
  realtimeConfiguration() {
202
- const realtime = this.syncClient.config.realtime
226
+ return this.syncClient.config.realtime || null
227
+ }
228
+
229
+ /**
230
+ * Resolves the realtime configuration and asserts a websocket client source
231
+ * exists: a shared connection rides its own lifecycle, otherwise the
232
+ * deprecated per-cycle `realtime.createClient` must be configured.
233
+ * @param {import("../configuration-types.js").VelociousSyncRealtimeWebsocketClient | null} sharedClient - Shared connection, or null.
234
+ * @returns {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration | null} Realtime configuration, or null.
235
+ */
236
+ requireClientSource(sharedClient) {
237
+ const realtime = this.realtimeConfiguration()
203
238
 
204
- if (!realtime) {
205
- throw new Error("subscribeRealtime requires a sync.client.realtime configuration block with a createClient callback")
239
+ if (!sharedClient && typeof realtime?.createClient !== "function") {
240
+ throw new Error("subscribeRealtime requires a shared connection (sync.client.websocketUrl or sync.client.websocketClient) or the deprecated sync.client.realtime.createClient callback")
206
241
  }
207
242
 
208
243
  return realtime
@@ -235,7 +270,7 @@ export default class SyncRealtimeBridge {
235
270
  channelDescriptors.push({channel: resourceConfig.realtime.channel, params: resourceConfig.realtime.params, resourceType})
236
271
  }
237
272
 
238
- if (realtime.channels) {
273
+ if (realtime?.channels) {
239
274
  channelDescriptors.push(...await realtime.channels(context))
240
275
  }
241
276
 
@@ -306,7 +341,7 @@ export default class SyncRealtimeBridge {
306
341
 
307
342
  const realtime = this.realtimeConfiguration()
308
343
 
309
- if (realtime.localOrigin && body.echoOrigin !== undefined && body.echoOrigin !== null) {
344
+ if (realtime?.localOrigin && body.echoOrigin !== undefined && body.echoOrigin !== null) {
310
345
  const localOrigin = String(await realtime.localOrigin())
311
346
 
312
347
  if (String(body.echoOrigin) === localOrigin) return
@@ -329,7 +364,7 @@ export default class SyncRealtimeBridge {
329
364
  * @returns {void}
330
365
  */
331
366
  schedulePull() {
332
- if (this.realtimeConfiguration().pullOnReconnect === false) return
367
+ if (this.realtimeConfiguration()?.pullOnReconnect === false) return
333
368
 
334
369
  this._scheduledPull ||= (async () => {
335
370
  try {
@@ -215,6 +215,27 @@ export default class SyncResourceBase extends FrontendModelBaseResource {
215
215
  throw new Error("SyncResourceBase#scopeChangesQuery must be implemented")
216
216
  }
217
217
 
218
+ /**
219
+ * Decides whether one published change is deliverable to a user-scope
220
+ * subscription (the framework sync channel's per-delivery access re-check).
221
+ * The default reuses the app's ability scoping: it applies
222
+ * {@link SyncResourceBase#scopeChangesQuery} to the change-feed model — which
223
+ * for an empty-conditions user scope falls back to ability scoping — and
224
+ * checks whether the published change's feed row is visible within that
225
+ * scope. Apps get this for free from the scoping they already declared;
226
+ * override only for custom per-delivery rules.
227
+ * @param {{params: Record<string, ?>, scope: SerializedChangesScope | null, sync: {resourceId: string, resourceType: string}}} args - Request params, subscription scope, and the published change's identity.
228
+ * @returns {Promise<boolean>} Whether the change may be delivered to this subscription.
229
+ */
230
+ async changeDeliverable({params, scope, sync}) {
231
+ const query = this.syncModelClass().where({})
232
+
233
+ this.scopeChangesQuery({params, query, scope})
234
+ query.where({resource_id: String(sync.resourceId), resource_type: String(sync.resourceType)})
235
+
236
+ return Boolean(await query.first())
237
+ }
238
+
218
239
  /**
219
240
  * Resolves the replay service class handling replay mutations: the
220
241
  * declarative {@link SyncResourceBase.ReplayServiceClass} static (shared
@@ -136,11 +136,90 @@ export default class SyncWebsocketChannel extends VelociousWebsocketChannel {
136
136
  return true
137
137
  }
138
138
 
139
+ /**
140
+ * Delivers a matched broadcast. Scoped subscriptions (with explicit
141
+ * conditions) already routed through {@link SyncWebsocketChannel#matches}, so
142
+ * the change is in scope and delivers unchanged. User-scope subscriptions
143
+ * (empty conditions, "everything my ability can see") match every broadcast
144
+ * of the resource type, so each published change is re-checked against the
145
+ * subscriber's ability at fan-out through the app sync resource's
146
+ * `changeDeliverable`; only accessible changes are delivered, and a broadcast
147
+ * with no accessible change is dropped.
148
+ * @param {import("../http-server/websocket-channel.js").WebsocketJsonValue} body - Broadcast body (sync envelope).
149
+ * @param {{eventId?: string}} [meta] - Optional event metadata.
150
+ * @returns {Promise<void>}
151
+ */
152
+ async deliverBroadcast(body, meta) {
153
+ if (!this._isUserScope()) {
154
+ this.sendMessage(body, meta)
155
+
156
+ return
157
+ }
158
+
159
+ const deliverableBody = await this._userScopeDeliverableBody(body)
160
+
161
+ if (deliverableBody !== null) this.sendMessage(deliverableBody, meta)
162
+ }
163
+
164
+ /**
165
+ * Whether this subscription is a user scope: authorized with empty conditions
166
+ * ("everything my ability can see").
167
+ * @returns {boolean} Whether the subscription is a user scope.
168
+ */
169
+ _isUserScope() {
170
+ return Boolean(this._scope) && Object.keys(/** @type {import("./sync-resource-base.js").SerializedChangesScope} */ (this._scope).conditions).length === 0
171
+ }
172
+
173
+ /**
174
+ * Filters a user-scope broadcast to the sync entries the subscriber's ability
175
+ * can access, re-checking each through the app sync resource's
176
+ * `changeDeliverable`. Returns the broadcast narrowed to accessible entries,
177
+ * or null when none are accessible. Non-envelope bodies and entries without a
178
+ * resource id are dropped (fail closed).
179
+ * @param {import("../http-server/websocket-channel.js").WebsocketJsonValue} body - Broadcast body.
180
+ * @returns {Promise<import("../http-server/websocket-channel.js").WebsocketJsonValue | null>} Deliverable body, or null.
181
+ */
182
+ async _userScopeDeliverableBody(body) {
183
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null
184
+
185
+ const envelope = /** @type {Record<string, ?>} */ (body)
186
+ const scope = /** @type {import("./sync-resource-base.js").SerializedChangesScope} */ (this._scope)
187
+ const syncs = Array.isArray(envelope.syncs) ? envelope.syncs : [envelope]
188
+ const configuration = this.session.configuration
189
+ /** @type {Array<Record<string, ?>>} */
190
+ const deliverableSyncs = []
191
+
192
+ // Broadcast fan-out runs through `withoutCurrentConnectionContexts` (see
193
+ // Configuration#_broadcastToChannelLocal), so there is no ambient database
194
+ // connection here. Resolve the resource's ability and run the per-delivery
195
+ // access query inside a checked-out connection context, mirroring how other
196
+ // broadcast-time DB work (the frontend-model channel) obtains connections.
197
+ await configuration.ensureConnections({name: `${VELOCIOUS_SYNC_CHANNEL} user-scope delivery access check`}, async () => {
198
+ const resource = await this.buildSyncResource()
199
+
200
+ for (const sync of syncs) {
201
+ const resourceId = sync?.resourceId
202
+ const resourceType = sync?.resourceType ?? scope.resourceType
203
+
204
+ if (resourceId === undefined || resourceId === null) continue
205
+
206
+ if (await resource.changeDeliverable({params: this.params, scope, sync: {resourceId: String(resourceId), resourceType: String(resourceType)}})) {
207
+ deliverableSyncs.push(sync)
208
+ }
209
+ }
210
+ })
211
+
212
+ if (deliverableSyncs.length === 0) return null
213
+ if (Array.isArray(envelope.syncs)) return {...envelope, syncs: deliverableSyncs}
214
+
215
+ return deliverableSyncs[0]
216
+ }
217
+
139
218
  /**
140
219
  * Returns the authorized scope for debug snapshots.
141
220
  * @returns {Record<string, ?>} Debug-safe subscription details.
142
221
  */
143
222
  debugSnapshot() {
144
- return {scope: this._scope}
223
+ return {scope: this._scope, userScope: this._isUserScope()}
145
224
  }
146
225
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.504",
6
+ "version": "1.0.506",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [