velocious 1.0.720 → 1.0.722
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/build/background-jobs/main.js +173 -4
- package/build/configuration-types.js +1 -1
- package/build/database/advisory-lock-reentrancy-node.js +9 -0
- package/build/database/advisory-lock-reentrancy.js +66 -0
- package/build/database/advisory-lock-runner.js +9 -12
- package/build/environment-handlers/node.js +1 -0
- package/build/routes/base-route.js +3 -2
- package/build/routes/basic-route.js +33 -8
- package/build/routes/get-route.js +17 -1
- package/build/routes/post-route.js +17 -1
- package/build/routes/resolver.js +5 -3
- package/build/src/background-jobs/main.d.ts +60 -0
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +164 -6
- package/build/src/configuration-types.d.ts +2 -2
- package/build/src/configuration-types.js +2 -2
- package/build/src/database/advisory-lock-reentrancy-node.d.ts +2 -0
- package/build/src/database/advisory-lock-reentrancy-node.d.ts.map +1 -0
- package/build/src/database/advisory-lock-reentrancy-node.js +7 -0
- package/build/src/database/advisory-lock-reentrancy.d.ts +67 -0
- package/build/src/database/advisory-lock-reentrancy.d.ts.map +1 -0
- package/build/src/database/advisory-lock-reentrancy.js +60 -0
- package/build/src/database/advisory-lock-runner.d.ts +25 -33
- package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
- package/build/src/database/advisory-lock-runner.js +10 -13
- package/build/src/environment-handlers/node.d.ts +1 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +2 -1
- package/build/src/routes/base-route.d.ts +3 -2
- package/build/src/routes/base-route.d.ts.map +1 -1
- package/build/src/routes/base-route.js +4 -3
- package/build/src/routes/basic-route.d.ts +19 -6
- package/build/src/routes/basic-route.d.ts.map +1 -1
- package/build/src/routes/basic-route.js +29 -9
- package/build/src/routes/get-route.d.ts +12 -1
- package/build/src/routes/get-route.d.ts.map +1 -1
- package/build/src/routes/get-route.js +16 -2
- package/build/src/routes/post-route.d.ts +12 -1
- package/build/src/routes/post-route.d.ts.map +1 -1
- package/build/src/routes/post-route.js +16 -2
- package/build/src/routes/resolver.d.ts +2 -1
- package/build/src/routes/resolver.d.ts.map +1 -1
- package/build/src/routes/resolver.js +6 -4
- package/build/src/sync/sync-cursor.d.ts +30 -0
- package/build/src/sync/sync-cursor.d.ts.map +1 -0
- package/build/src/sync/sync-cursor.js +54 -0
- package/build/src/sync/sync-realtime-bridge.d.ts +127 -24
- package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
- package/build/src/sync/sync-realtime-bridge.js +92 -56
- package/build/src/sync/sync-websocket-channel.d.ts +49 -0
- package/build/src/sync/sync-websocket-channel.d.ts.map +1 -1
- package/build/src/sync/sync-websocket-channel.js +138 -2
- package/build/sync/sync-cursor.js +56 -0
- package/build/sync/sync-realtime-bridge.js +97 -51
- package/build/sync/sync-websocket-channel.js +153 -1
- package/package.json +1 -1
- package/src/background-jobs/main.js +173 -4
- package/src/configuration-types.js +1 -1
- package/src/database/advisory-lock-reentrancy-node.js +9 -0
- package/src/database/advisory-lock-reentrancy.js +66 -0
- package/src/database/advisory-lock-runner.js +9 -12
- package/src/environment-handlers/node.js +1 -0
- package/src/routes/base-route.js +3 -2
- package/src/routes/basic-route.js +33 -8
- package/src/routes/get-route.js +17 -1
- package/src/routes/post-route.js +17 -1
- package/src/routes/resolver.js +5 -3
- package/src/sync/sync-cursor.js +56 -0
- package/src/sync/sync-realtime-bridge.js +97 -51
- package/src/sync/sync-websocket-channel.js +153 -1
|
@@ -2,10 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
import VelociousWebsocketChannel from "../http-server/websocket-channel.js"
|
|
4
4
|
import {VELOCIOUS_SYNC_CHANNEL} from "./sync-channel-name.js"
|
|
5
|
+
import {cursorFromSyncEntry, isCursorAfter} from "./sync-cursor.js"
|
|
6
|
+
import SyncModelChangeFeedService from "./sync-model-change-feed-service.js"
|
|
5
7
|
|
|
6
8
|
/** Configurations whose framework sync channel has already been registered. */
|
|
7
9
|
const registeredConfigurations = new WeakSet()
|
|
8
10
|
|
|
11
|
+
/** Default catch-up replay page size when the subscriber sends no `limit` param. */
|
|
12
|
+
const DEFAULT_REPLAY_LIMIT = 1000
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Serializes a cursor into the change-feed request params under the given prefix.
|
|
16
|
+
* @param {"after" | "upTo"} prefix - Request field prefix.
|
|
17
|
+
* @param {import("./sync-api-client-types.js").SyncCursor} cursor - Cursor to serialize.
|
|
18
|
+
* @returns {Record<string, string | number | null>} Serialized cursor params.
|
|
19
|
+
*/
|
|
20
|
+
function cursorParams(prefix, cursor) {
|
|
21
|
+
if (!cursor) return {}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
[`${prefix}Id`]: cursor.id,
|
|
25
|
+
...(cursor.serverSequence !== null ? {[`${prefix}ServerSequence`]: cursor.serverSequence} : {}),
|
|
26
|
+
[`${prefix}UpdatedAt`]: cursor.updatedAt
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Reads the after-cursor the subscriber declared in its subscribe params, or null
|
|
32
|
+
* when it sent none (a fresh subscription replays the whole scope).
|
|
33
|
+
* @param {Record<string, ReturnType<typeof JSON.parse>>} params - Subscribe params.
|
|
34
|
+
* @returns {import("./sync-api-client-types.js").SyncCursor} Declared after-cursor, or null.
|
|
35
|
+
*/
|
|
36
|
+
function cursorFromParams(params) {
|
|
37
|
+
const updatedAt = typeof params.afterUpdatedAt === "string" ? params.afterUpdatedAt : null
|
|
38
|
+
|
|
39
|
+
if (!updatedAt) return null
|
|
40
|
+
|
|
41
|
+
const serverSequence = params.afterServerSequence === null || params.afterServerSequence === undefined
|
|
42
|
+
? null
|
|
43
|
+
: Number(params.afterServerSequence)
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: params.afterId === null || params.afterId === undefined ? null : String(params.afterId),
|
|
47
|
+
serverSequence,
|
|
48
|
+
updatedAt
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
9
52
|
/**
|
|
10
53
|
* Framework-owned websocket channel for synced resources
|
|
11
54
|
* ({@link VELOCIOUS_SYNC_CHANNEL}).
|
|
@@ -25,6 +68,14 @@ export default class SyncWebsocketChannel extends VelociousWebsocketChannel {
|
|
|
25
68
|
*/
|
|
26
69
|
_scope = null
|
|
27
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Cursor of the last sync entry delivered to this subscription, advanced by
|
|
73
|
+
* live broadcasts and catch-up replay so a resume replays only the rows missed
|
|
74
|
+
* while the socket was down.
|
|
75
|
+
* @type {import("./sync-api-client-types.js").SyncCursor}
|
|
76
|
+
*/
|
|
77
|
+
_deliveredCursor = null
|
|
78
|
+
|
|
28
79
|
/**
|
|
29
80
|
* Registers the framework sync channel on a configuration declaring a
|
|
30
81
|
* `sync.api` block (guarded so repeated server boots with the same
|
|
@@ -169,13 +220,17 @@ export default class SyncWebsocketChannel extends VelociousWebsocketChannel {
|
|
|
169
220
|
async deliverBroadcast(body, meta) {
|
|
170
221
|
if (!this._isUserScope()) {
|
|
171
222
|
this.sendMessage(body, meta)
|
|
223
|
+
this._trackDeliveredCursor(body)
|
|
172
224
|
|
|
173
225
|
return
|
|
174
226
|
}
|
|
175
227
|
|
|
176
228
|
const deliverableBody = await this._userScopeDeliverableBody(body)
|
|
177
229
|
|
|
178
|
-
if (deliverableBody !== null)
|
|
230
|
+
if (deliverableBody !== null) {
|
|
231
|
+
this.sendMessage(deliverableBody, meta)
|
|
232
|
+
this._trackDeliveredCursor(deliverableBody)
|
|
233
|
+
}
|
|
179
234
|
}
|
|
180
235
|
|
|
181
236
|
/**
|
|
@@ -245,6 +300,103 @@ export default class SyncWebsocketChannel extends VelociousWebsocketChannel {
|
|
|
245
300
|
return deliverableSyncs[0]
|
|
246
301
|
}
|
|
247
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Replays the catch-up backlog to this subscriber once its subscription is
|
|
305
|
+
* authorized: pages the scoped change feed from the cursor the client declared
|
|
306
|
+
* in its subscribe params and sends each page as a standard sync envelope, then
|
|
307
|
+
* live broadcasts continue from there. A fresh subscription with no cursor
|
|
308
|
+
* replays the whole authorized scope.
|
|
309
|
+
* @returns {Promise<void>}
|
|
310
|
+
*/
|
|
311
|
+
async subscribed() {
|
|
312
|
+
if (!this._scope) return
|
|
313
|
+
|
|
314
|
+
await this._replayFrom(cursorFromParams(this.params))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Replays rows missed while the socket was down, resuming from the cursor of
|
|
319
|
+
* the last entry this subscription delivered instead of re-reading the whole
|
|
320
|
+
* scope from the subscribe-time cursor.
|
|
321
|
+
* @returns {Promise<void>}
|
|
322
|
+
*/
|
|
323
|
+
async onResume() {
|
|
324
|
+
if (!this._scope) return
|
|
325
|
+
|
|
326
|
+
await this._replayFrom(this._deliveredCursor ?? cursorFromParams(this.params))
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Pages the scoped change feed from a cursor and delivers each page to this
|
|
331
|
+
* subscriber as a `{echoOrigin, syncs}` envelope. The snapshot high-water is
|
|
332
|
+
* resolved once and pinned across pages so rows arriving mid-replay are left to
|
|
333
|
+
* the live broadcast path instead of looping the replay. The delivered cursor
|
|
334
|
+
* advances so a later resume resumes from here.
|
|
335
|
+
* @param {import("./sync-api-client-types.js").SyncCursor} afterCursor - Cursor to replay after.
|
|
336
|
+
* @returns {Promise<void>}
|
|
337
|
+
*/
|
|
338
|
+
async _replayFrom(afterCursor) {
|
|
339
|
+
const configuration = this.session.configuration
|
|
340
|
+
const scope = this._scope
|
|
341
|
+
|
|
342
|
+
await configuration.ensureConnections({name: `${VELOCIOUS_SYNC_CHANNEL} catch-up replay`}, async () => {
|
|
343
|
+
const resource = await this.buildSyncResource()
|
|
344
|
+
const limit = this._replayLimit(resource)
|
|
345
|
+
/** @type {import("./sync-api-client-types.js").SyncCursor} */
|
|
346
|
+
let upToCursor = null
|
|
347
|
+
|
|
348
|
+
while (true) {
|
|
349
|
+
const page = await resource.changeFeedService({
|
|
350
|
+
params: {...this.params, ...cursorParams("after", afterCursor), ...(upToCursor ? cursorParams("upTo", upToCursor) : {})},
|
|
351
|
+
scope
|
|
352
|
+
}).changes()
|
|
353
|
+
|
|
354
|
+
if (!upToCursor) upToCursor = page.upToCursor
|
|
355
|
+
|
|
356
|
+
if (page.syncs.length === 0) break
|
|
357
|
+
|
|
358
|
+
await this.deliverBroadcast({echoOrigin: null, syncs: page.syncs})
|
|
359
|
+
|
|
360
|
+
afterCursor = page.nextCursor
|
|
361
|
+
if (afterCursor) this._deliveredCursor = afterCursor
|
|
362
|
+
|
|
363
|
+
if (page.syncs.length < limit) break
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Resolves the catch-up replay page size from the subscriber's `limit` param
|
|
370
|
+
* (defaulting to {@link DEFAULT_REPLAY_LIMIT}).
|
|
371
|
+
* @param {import("./sync-resource-base.js").default} resource - Authorizing sync resource.
|
|
372
|
+
* @returns {number} Replay page size.
|
|
373
|
+
*/
|
|
374
|
+
_replayLimit(resource) {
|
|
375
|
+
return new SyncModelChangeFeedService({modelClass: resource.syncModelClass(), params: this.params}).normalizedLimit(this.params.limit)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Advances the delivered-cursor watermark from a delivered envelope so a resume
|
|
380
|
+
* resumes after the newest row actually sent to this subscriber. Broadcasts may
|
|
381
|
+
* arrive out of order across resources, so each entry only advances the
|
|
382
|
+
* watermark when it is later than the current one.
|
|
383
|
+
* @param {import("../http-server/websocket-channel.js").WebsocketJsonValue} body - Delivered envelope.
|
|
384
|
+
* @returns {void}
|
|
385
|
+
*/
|
|
386
|
+
_trackDeliveredCursor(body) {
|
|
387
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return
|
|
388
|
+
|
|
389
|
+
const envelope = /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (body)
|
|
390
|
+
const syncs = Array.isArray(envelope.syncs) ? envelope.syncs : [envelope]
|
|
391
|
+
|
|
392
|
+
for (const entry of syncs) {
|
|
393
|
+
const syncEntry = /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (entry)
|
|
394
|
+
const cursor = cursorFromSyncEntry(syncEntry)
|
|
395
|
+
|
|
396
|
+
if (cursor && isCursorAfter(cursor, this._deliveredCursor)) this._deliveredCursor = cursor
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
248
400
|
/**
|
|
249
401
|
* Returns the authorized scope for debug snapshots.
|
|
250
402
|
* @returns {Record<string, ReturnType<typeof JSON.parse>>} Debug-safe subscription details.
|