velocious 1.0.667 → 1.0.668
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 +27 -7
- package/build/configuration.js +23 -1
- package/build/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
- package/build/environment-handlers/node/cli/commands/test.js +17 -69
- package/build/frontend-models/websocket-channel.js +18 -0
- package/build/http-server/client/websocket-session.js +41 -4
- package/build/http-server/websocket-channel.js +14 -0
- package/build/http-server/websocket-event-log-store.js +85 -12
- package/build/http-server/websocket-events-host.js +14 -6
- package/build/src/configuration.d.ts +18 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +22 -2
- package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +3 -47
- package/build/src/environment-handlers/node/cli/commands/test.d.ts +2 -28
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +10 -61
- package/build/src/frontend-models/websocket-channel.d.ts +9 -0
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +16 -1
- package/build/src/http-server/client/websocket-session.d.ts +25 -2
- package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
- package/build/src/http-server/client/websocket-session.js +40 -5
- package/build/src/http-server/websocket-channel.d.ts +11 -0
- package/build/src/http-server/websocket-channel.d.ts.map +1 -1
- package/build/src/http-server/websocket-channel.js +14 -1
- package/build/src/http-server/websocket-event-log-store.d.ts +60 -41
- package/build/src/http-server/websocket-event-log-store.d.ts.map +1 -1
- package/build/src/http-server/websocket-event-log-store.js +79 -12
- package/build/src/http-server/websocket-events-host.d.ts +10 -3
- package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
- package/build/src/http-server/websocket-events-host.js +14 -7
- package/build/src/testing/test-files-finder.d.ts +13 -116
- package/build/src/testing/test-files-finder.d.ts.map +1 -1
- package/build/src/testing/test-files-finder.js +26 -296
- package/build/src/testing/test-filter-parser.d.ts +32 -22
- package/build/src/testing/test-filter-parser.d.ts.map +1 -1
- package/build/src/testing/test-filter-parser.js +25 -217
- package/build/src/testing/test-profile-activity.d.ts +1 -6
- package/build/src/testing/test-profile-activity.d.ts.map +1 -1
- package/build/src/testing/test-profile-activity.js +2 -12
- package/build/src/testing/test-profile-output.d.ts +1 -40
- package/build/src/testing/test-profile-output.d.ts.map +1 -1
- package/build/src/testing/test-profile-output.js +2 -217
- package/build/src/testing/test-profiler.d.ts +15 -659
- package/build/src/testing/test-profiler.d.ts.map +1 -1
- package/build/src/testing/test-profiler.js +35 -833
- package/build/src/testing/test-runner.d.ts +10 -1
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +26 -4
- package/build/src/testing/test-suite-splitter.d.ts +1 -128
- package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
- package/build/src/testing/test-suite-splitter.js +2 -259
- package/build/src/testing/timing-manifest.d.ts +5 -93
- package/build/src/testing/timing-manifest.d.ts.map +1 -1
- package/build/src/testing/timing-manifest.js +4 -283
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
- package/build/src/testing/velocious-attempt-executor.js +8 -2
- package/build/testing/test-files-finder.js +29 -345
- package/build/testing/test-filter-parser.js +29 -248
- package/build/testing/test-profile-activity.js +1 -12
- package/build/testing/test-profile-output.js +8 -253
- package/build/testing/test-profiler.js +38 -895
- package/build/testing/test-runner.js +33 -9
- package/build/testing/test-suite-splitter.js +1 -301
- package/build/testing/timing-manifest.js +10 -344
- package/build/testing/velocious-attempt-executor.js +7 -1
- package/package.json +3 -3
- package/scripts/test-browser.js +29 -6
- package/scripts/verify-docker-dev-environment.js +13 -1
- package/src/configuration.js +23 -1
- package/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
- package/src/environment-handlers/node/cli/commands/test.js +17 -69
- package/src/frontend-models/websocket-channel.js +18 -0
- package/src/http-server/client/websocket-session.js +41 -4
- package/src/http-server/websocket-channel.js +14 -0
- package/src/http-server/websocket-event-log-store.js +85 -12
- package/src/http-server/websocket-events-host.js +14 -6
- package/src/testing/test-files-finder.js +29 -345
- package/src/testing/test-filter-parser.js +29 -248
- package/src/testing/test-profile-activity.js +1 -12
- package/src/testing/test-profile-output.js +8 -253
- package/src/testing/test-profiler.js +38 -895
- package/src/testing/test-runner.js +33 -9
- package/src/testing/test-suite-splitter.js +1 -301
- package/src/testing/timing-manifest.js +10 -344
- package/src/testing/velocious-attempt-executor.js +7 -1
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import BaseCommand from "../../../../cli/base-command.js"
|
|
4
|
-
import fs from "fs/promises"
|
|
5
4
|
import path from "node:path"
|
|
6
5
|
import picocolors from "picocolors"
|
|
7
6
|
import TestFilesFinder from "../../../../testing/test-files-finder.js"
|
|
8
7
|
import TestProfiler from "../../../../testing/test-profiler.js"
|
|
9
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
formatTestProfileSummary,
|
|
10
|
+
loadTimingManifest,
|
|
11
|
+
resolveTestProfileOptions,
|
|
12
|
+
writeTestProfileOutputs
|
|
13
|
+
} from "../../../../testing/test-profile-output.js"
|
|
10
14
|
import TestRunner from "../../../../testing/test-runner.js"
|
|
11
15
|
import TestSuiteSplitter from "../../../../testing/test-suite-splitter.js"
|
|
12
16
|
import { normalizeExamplePatterns, parseFilters } from "../../../../testing/test-filter-parser.js"
|
|
13
17
|
import {
|
|
14
18
|
canonicalTimingManifestPath,
|
|
15
|
-
timingManifestFileSetHash
|
|
16
|
-
validateTimingManifest
|
|
19
|
+
timingManifestFileSetHash
|
|
17
20
|
} from "../../../../testing/timing-manifest.js"
|
|
18
21
|
import { prepareSourcePeerPackage } from "../../source-peer-package.js"
|
|
19
22
|
|
|
@@ -44,8 +47,11 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
44
47
|
groupNumber,
|
|
45
48
|
profile,
|
|
46
49
|
profileJsonPath,
|
|
50
|
+
retries,
|
|
51
|
+
setupFiles,
|
|
47
52
|
timingManifestPath,
|
|
48
|
-
timingManifestOutputPath
|
|
53
|
+
timingManifestOutputPath,
|
|
54
|
+
timeoutMs
|
|
49
55
|
} = parseFilters(this.processArgs || [])
|
|
50
56
|
const profileOptions = resolveTestProfileOptions({
|
|
51
57
|
cwd: process.cwd(),
|
|
@@ -70,7 +76,7 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
70
76
|
|
|
71
77
|
/**
|
|
72
78
|
* Finalizes requested outputs once for every command outcome.
|
|
73
|
-
* @param {
|
|
79
|
+
* @param {import("@velocious/testing/node").TestProfileStatus} status - Run status.
|
|
74
80
|
* @returns {Promise<void>} - Resolves after requested outputs are written.
|
|
75
81
|
*/
|
|
76
82
|
const finalizeProfile = async (status) => {
|
|
@@ -157,7 +163,10 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
157
163
|
testFiles,
|
|
158
164
|
lineFilters: testFilesFinder.getLineFiltersByFile(),
|
|
159
165
|
examplePatterns: normalizeExamplePatterns(examplePatterns),
|
|
160
|
-
profiler
|
|
166
|
+
profiler,
|
|
167
|
+
retries,
|
|
168
|
+
setupFiles: setupFiles.map((setupFile) => path.resolve(process.cwd(), setupFile)),
|
|
169
|
+
timeoutMs
|
|
161
170
|
})
|
|
162
171
|
const activeTestRunner = testRunner
|
|
163
172
|
let signalHandled = false
|
|
@@ -272,68 +281,7 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
272
281
|
}
|
|
273
282
|
}
|
|
274
283
|
|
|
275
|
-
|
|
276
|
-
* Resolves and validates profiling paths before test discovery starts.
|
|
277
|
-
* @param {object} args - Raw profiling options.
|
|
278
|
-
* @param {string} args.cwd - Command working directory.
|
|
279
|
-
* @param {boolean} args.profile - Whether console profiling was requested.
|
|
280
|
-
* @param {string} [args.profileJsonPath] - Rich profile output path.
|
|
281
|
-
* @param {string} [args.timingManifestPath] - Timing manifest input path.
|
|
282
|
-
* @param {string} [args.timingManifestOutputPath] - Timing manifest output path.
|
|
283
|
-
* @returns {{profile: boolean, profileJsonPath: string | undefined, timingManifestPath: string | undefined, timingManifestOutputPath: string | undefined}} - Resolved profiling options.
|
|
284
|
-
*/
|
|
285
|
-
export function resolveTestProfileOptions({cwd, profile, profileJsonPath, timingManifestPath, timingManifestOutputPath}) {
|
|
286
|
-
const resolvedProfileJsonPath = profileJsonPath ? path.resolve(cwd, profileJsonPath) : undefined
|
|
287
|
-
const resolvedTimingManifestPath = timingManifestPath ? path.resolve(cwd, timingManifestPath) : undefined
|
|
288
|
-
const resolvedTimingManifestOutputPath = timingManifestOutputPath
|
|
289
|
-
? path.resolve(cwd, timingManifestOutputPath)
|
|
290
|
-
: undefined
|
|
291
|
-
|
|
292
|
-
if (resolvedProfileJsonPath && resolvedTimingManifestOutputPath && resolvedProfileJsonPath === resolvedTimingManifestOutputPath) {
|
|
293
|
-
throw new Error("Test profiling output paths must be different")
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (resolvedTimingManifestPath && (
|
|
297
|
-
resolvedProfileJsonPath === resolvedTimingManifestPath ||
|
|
298
|
-
resolvedTimingManifestOutputPath === resolvedTimingManifestPath
|
|
299
|
-
)) {
|
|
300
|
-
throw new Error("Test profiling outputs must not overwrite --timing-manifest input")
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
profile: profile || Boolean(resolvedProfileJsonPath || resolvedTimingManifestOutputPath),
|
|
305
|
-
profileJsonPath: resolvedProfileJsonPath,
|
|
306
|
-
timingManifestPath: resolvedTimingManifestPath,
|
|
307
|
-
timingManifestOutputPath: resolvedTimingManifestOutputPath
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Loads and validates an explicitly supplied plain JSON timing manifest.
|
|
313
|
-
* @param {string | undefined} timingManifestPath - Timing manifest path.
|
|
314
|
-
* @returns {Promise<Record<string, number> | undefined>} - Canonical manifest, or undefined when not requested.
|
|
315
|
-
*/
|
|
316
|
-
export async function loadTimingManifest(timingManifestPath) {
|
|
317
|
-
if (!timingManifestPath) return undefined
|
|
318
|
-
|
|
319
|
-
let content
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
content = await fs.readFile(timingManifestPath, "utf8")
|
|
323
|
-
} catch (error) {
|
|
324
|
-
throw new Error(`Failed to read timing manifest: ${timingManifestPath}`, {cause: error})
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
let parsed
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
parsed = JSON.parse(content)
|
|
331
|
-
} catch (error) {
|
|
332
|
-
throw new Error(`Failed to parse timing manifest: ${timingManifestPath}`, {cause: error})
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return validateTimingManifest(parsed, {source: `Timing manifest ${timingManifestPath}`})
|
|
336
|
-
}
|
|
284
|
+
export { loadTimingManifest, resolveTestProfileOptions }
|
|
337
285
|
|
|
338
286
|
/**
|
|
339
287
|
* Resolves how many slowest tests to report from the `VELOCIOUS_SLOW_TEST_COUNT`
|
|
@@ -389,6 +389,24 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
389
389
|
return broadcastParams?.model === this._modelName()
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Drops the server-only destroy-authorization snapshot before replay
|
|
394
|
+
* persistence. The snapshot is what makes replayed destroy events
|
|
395
|
+
* require a client resync, and the pre-delete row it captures must
|
|
396
|
+
* never be stored.
|
|
397
|
+
* @param {Record<string, import("./query.js").FrontendModelTransportValue> | null | undefined} broadcastParams - Params from `broadcastToChannel`.
|
|
398
|
+
* @returns {Record<string, import("./query.js").FrontendModelTransportValue> | null} - Persistable routing params.
|
|
399
|
+
*/
|
|
400
|
+
static replayableBroadcastParams(broadcastParams) {
|
|
401
|
+
if (!broadcastParams) return null
|
|
402
|
+
|
|
403
|
+
const replayableParams = {...broadcastParams}
|
|
404
|
+
|
|
405
|
+
delete replayableParams.destroyAuthorizationRecord
|
|
406
|
+
|
|
407
|
+
return replayableParams
|
|
408
|
+
}
|
|
409
|
+
|
|
392
410
|
/**
|
|
393
411
|
* Runs debug snapshot.
|
|
394
412
|
* @returns {Record<string, ReturnType<typeof JSON.parse>>} Debug-safe subscription details.
|
|
@@ -1843,8 +1843,13 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1843
1843
|
|
|
1844
1844
|
/**
|
|
1845
1845
|
* Replays missed events from the persistent event-log store for a
|
|
1846
|
-
* channel subscription that provided `lastEventId`.
|
|
1847
|
-
*
|
|
1846
|
+
* channel subscription that provided `lastEventId`. Delivery is
|
|
1847
|
+
* stream-scoped: each persisted event's broadcast params are
|
|
1848
|
+
* re-applied through the subscription's `matches()` — the same routing
|
|
1849
|
+
* decision live delivery uses — so the subscription only replays events
|
|
1850
|
+
* from its own stream of the channel. A checkpoint that belongs to a
|
|
1851
|
+
* different stream (or is no longer retained) yields
|
|
1852
|
+
* `channel-replay-gap` instead of cross-stream replay.
|
|
1848
1853
|
* @param {object} args - Options.
|
|
1849
1854
|
* @param {string} args.channelType - Channel type name (event-log key).
|
|
1850
1855
|
* @param {string} args.lastEventId - Client's last-seen event id.
|
|
@@ -1858,7 +1863,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1858
1863
|
|
|
1859
1864
|
const checkpoint = await store.getEventById({channel: channelType, id: lastEventId})
|
|
1860
1865
|
|
|
1861
|
-
if (!checkpoint) {
|
|
1866
|
+
if (!checkpoint || !this._replayEventMatchesSubscription({channelType, event: checkpoint, subscription})) {
|
|
1862
1867
|
this.sendJson({
|
|
1863
1868
|
type: "channel-replay-gap",
|
|
1864
1869
|
subscriptionId: subscription.subscriptionId,
|
|
@@ -1880,6 +1885,8 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1880
1885
|
for (const event of events) {
|
|
1881
1886
|
if (subscription.isClosed()) break
|
|
1882
1887
|
|
|
1888
|
+
if (!this._replayEventMatchesSubscription({channelType, event, subscription})) continue
|
|
1889
|
+
|
|
1883
1890
|
if (await subscription._requiresReplayGap(event.payload)) {
|
|
1884
1891
|
this.sendJson({
|
|
1885
1892
|
type: "channel-replay-gap",
|
|
@@ -1891,11 +1898,41 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1891
1898
|
|
|
1892
1899
|
await subscription.deliverBroadcast(
|
|
1893
1900
|
/** @type {import("../websocket-channel.js").WebsocketJsonValue} */ (event.payload),
|
|
1894
|
-
{
|
|
1901
|
+
{
|
|
1902
|
+
...(event.params !== null ? {broadcastParams: event.params} : {}),
|
|
1903
|
+
eventId: event.id
|
|
1904
|
+
}
|
|
1895
1905
|
)
|
|
1896
1906
|
}
|
|
1897
1907
|
}
|
|
1898
1908
|
|
|
1909
|
+
/**
|
|
1910
|
+
* Whether a persisted replay event belongs to this subscription's
|
|
1911
|
+
* stream. A `matches()` failure means the stream membership cannot be
|
|
1912
|
+
* proven, so the event is treated as not matching — the same isolation
|
|
1913
|
+
* live delivery applies to a broken `matches()`.
|
|
1914
|
+
* @param {object} args - Options.
|
|
1915
|
+
* @param {string} args.channelType - Channel type name.
|
|
1916
|
+
* @param {{params: Record<string, ReturnType<typeof JSON.parse>> | null}} args.event - Persisted replay event.
|
|
1917
|
+
* @param {import("../websocket-channel.js").default} args.subscription - Live subscription.
|
|
1918
|
+
* @returns {boolean} - Whether the event belongs to the subscription's stream.
|
|
1919
|
+
*/
|
|
1920
|
+
_replayEventMatchesSubscription({channelType, event, subscription}) {
|
|
1921
|
+
try {
|
|
1922
|
+
return Boolean(subscription.matches(event.params || {}))
|
|
1923
|
+
} catch (caughtError) {
|
|
1924
|
+
const error = this._reportUnexpectedDispatchError(caughtError, {
|
|
1925
|
+
channelType,
|
|
1926
|
+
stage: "websocket-channel-replay",
|
|
1927
|
+
subscriptionId: subscription.subscriptionId
|
|
1928
|
+
})
|
|
1929
|
+
|
|
1930
|
+
this.logger.error(() => [`Websocket replay subscription ${subscription.subscriptionId} matches() threw`, error])
|
|
1931
|
+
|
|
1932
|
+
return false
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1899
1936
|
/**
|
|
1900
1937
|
* Handles `{type: "channel-unsubscribe"}` from the client — calls
|
|
1901
1938
|
* `unsubscribed()` and sends `channel-unsubscribed`.
|
|
@@ -94,6 +94,20 @@ export default class VelociousWebsocketChannel {
|
|
|
94
94
|
*/
|
|
95
95
|
matches(..._broadcastArgs) { return true }
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Returns the broadcast params that may be persisted for replay.
|
|
99
|
+
* Persisted params are re-applied through `matches(broadcastParams)`
|
|
100
|
+
* when replaying missed events, so they must be JSON-serializable and
|
|
101
|
+
* safe to store. Override when `broadcastParams` carries server-only
|
|
102
|
+
* values that must never reach the event log (e.g. authorization
|
|
103
|
+
* snapshots).
|
|
104
|
+
* @param {WebsocketParams | null | undefined} broadcastParams - Params passed to `broadcastToChannel`.
|
|
105
|
+
* @returns {WebsocketParams | null} - Params to persist, or null to store none.
|
|
106
|
+
*/
|
|
107
|
+
static replayableBroadcastParams(broadcastParams) {
|
|
108
|
+
return broadcastParams ?? null
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
/**
|
|
98
112
|
* Whether replaying a persisted broadcast would require a client resync.
|
|
99
113
|
* Subclasses override this when replay storage deliberately omits metadata
|
|
@@ -11,6 +11,7 @@ import Logger from "../logger.js"
|
|
|
11
11
|
* @property {Date | string} created_at - Creation time.
|
|
12
12
|
* @property {string} id - Event id.
|
|
13
13
|
* @property {string} payload_json - Serialized payload.
|
|
14
|
+
* @property {string | null} params_json - Serialized broadcast params, or null when the publish carried none.
|
|
14
15
|
* @property {number | string} sequence - Sequence number.
|
|
15
16
|
*/
|
|
16
17
|
/**
|
|
@@ -18,6 +19,16 @@ import Logger from "../logger.js"
|
|
|
18
19
|
* @typedef {object} WebsocketReplayChannelRow
|
|
19
20
|
* @property {string} channel - Channel name.
|
|
20
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* Normalized persisted websocket event.
|
|
24
|
+
* @typedef {object} WebsocketPersistedEvent
|
|
25
|
+
* @property {string} channel - Channel name.
|
|
26
|
+
* @property {string} createdAt - ISO creation time.
|
|
27
|
+
* @property {string} id - Event id.
|
|
28
|
+
* @property {Record<string, ReturnType<typeof JSON.parse>> | null} params - Persisted broadcast params, or null when the publish carried none.
|
|
29
|
+
* @property {ReturnType<typeof JSON.parse>} payload - Event payload.
|
|
30
|
+
* @property {number} sequence - Sequence number.
|
|
31
|
+
*/
|
|
21
32
|
const EVENTS_TABLE = "websocket_channel_events"
|
|
22
33
|
const REPLAY_CHANNELS_TABLE = "websocket_replay_channels"
|
|
23
34
|
const DEFAULT_RETENTION_MS = 10 * 60 * 1000
|
|
@@ -100,22 +111,40 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
100
111
|
|
|
101
112
|
/**
|
|
102
113
|
* Runs schema present.
|
|
103
|
-
* @returns {Promise<boolean>} - Whether both event-log tables
|
|
114
|
+
* @returns {Promise<boolean>} - Whether both event-log tables exist and the events table carries every required column.
|
|
104
115
|
*/
|
|
105
116
|
async _schemaPresent() {
|
|
106
117
|
return await this._withDb(async (db) =>
|
|
107
|
-
await db.tableExists(EVENTS_TABLE) && await db.tableExists(REPLAY_CHANNELS_TABLE)
|
|
118
|
+
(await db.tableExists(EVENTS_TABLE) && await db.tableExists(REPLAY_CHANNELS_TABLE))
|
|
119
|
+
&& (await this._columnPresent(db, EVENTS_TABLE, "params_json"))
|
|
108
120
|
)
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Runs column present.
|
|
125
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
126
|
+
* @param {string} tableName - Table name.
|
|
127
|
+
* @param {string} columnName - Column name.
|
|
128
|
+
* @returns {Promise<boolean>} - Whether the column exists on the table.
|
|
129
|
+
*/
|
|
130
|
+
async _columnPresent(db, tableName, columnName) {
|
|
131
|
+
const tables = await db.getTables()
|
|
132
|
+
const table = tables.find((candidate) => candidate.getName() == tableName)
|
|
133
|
+
|
|
134
|
+
if (!table) return false
|
|
135
|
+
|
|
136
|
+
return (await table.getColumnByName(columnName)) !== undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
111
139
|
/**
|
|
112
140
|
* Runs append event.
|
|
113
141
|
* @param {object} args - Options.
|
|
114
142
|
* @param {string} args.channel - Channel name.
|
|
115
143
|
* @param {ReturnType<typeof JSON.parse>} args.payload - Event payload.
|
|
116
|
-
* @
|
|
144
|
+
* @param {Record<string, ReturnType<typeof JSON.parse>> | null} [args.params] - Broadcast params to persist for stream-scoped replay, or null when the publish carried none.
|
|
145
|
+
* @returns {Promise<WebsocketPersistedEvent>} - Persisted event row.
|
|
117
146
|
*/
|
|
118
|
-
async appendEvent({channel, payload}) {
|
|
147
|
+
async appendEvent({channel, params = null, payload}) {
|
|
119
148
|
await this.ensureReady()
|
|
120
149
|
|
|
121
150
|
const id = randomUUID()
|
|
@@ -128,10 +157,11 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
128
157
|
channel,
|
|
129
158
|
created_at: createdAt,
|
|
130
159
|
id,
|
|
160
|
+
params_json: params === null ? null : JSON.stringify(params),
|
|
131
161
|
payload_json: JSON.stringify(payload)
|
|
132
162
|
}
|
|
133
163
|
})
|
|
134
|
-
return {channel, createdAt: createdAt.toISOString(), id, payload}
|
|
164
|
+
return {channel, createdAt: createdAt.toISOString(), id, params, payload}
|
|
135
165
|
})
|
|
136
166
|
}
|
|
137
167
|
|
|
@@ -139,8 +169,13 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
139
169
|
* Runs mark channel interested.
|
|
140
170
|
* @param {string} channel - Channel name.
|
|
141
171
|
* @returns {Promise<void>} - Resolves when the channel interest was persisted.
|
|
172
|
+
* @throws {Error} When the channel is registered live-only, which forbids replay persistence.
|
|
142
173
|
*/
|
|
143
174
|
async markChannelInterested(channel) {
|
|
175
|
+
if (this.configuration.isWebsocketChannelLiveOnly(channel)) {
|
|
176
|
+
throw new Error(`Websocket channel "${channel}" is registered live-only and cannot be marked interested in replay persistence`)
|
|
177
|
+
}
|
|
178
|
+
|
|
144
179
|
await this.ensureReady()
|
|
145
180
|
|
|
146
181
|
const interestedUntil = new Date(Date.now() + this.retentionMs)
|
|
@@ -158,6 +193,12 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
158
193
|
* @returns {Promise<boolean>} - Whether the channel should be persisted for replay.
|
|
159
194
|
*/
|
|
160
195
|
async shouldPersistChannel(channel) {
|
|
196
|
+
// A channel re-registered live-only can still hold cached or durable
|
|
197
|
+
// interest state from before the re-registration; the live-only
|
|
198
|
+
// contract must hold at the persistence decision, not only at
|
|
199
|
+
// interest marking.
|
|
200
|
+
if (this.configuration.isWebsocketChannelLiveOnly(channel)) return false
|
|
201
|
+
|
|
161
202
|
if (this._channelInterestCached(channel)) return true
|
|
162
203
|
if (this._interestedChannels.size === 0) return false
|
|
163
204
|
|
|
@@ -197,7 +238,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
197
238
|
* @param {object} args - Options.
|
|
198
239
|
* @param {string} args.channel - Channel name.
|
|
199
240
|
* @param {string} args.id - Event id.
|
|
200
|
-
* @returns {Promise<
|
|
241
|
+
* @returns {Promise<WebsocketPersistedEvent | null>} - Event row or null.
|
|
201
242
|
*/
|
|
202
243
|
async getEventById({channel, id}) {
|
|
203
244
|
await this.ensureReady()
|
|
@@ -237,7 +278,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
237
278
|
* @param {string} args.channel - Channel name.
|
|
238
279
|
* @param {number} args.sequence - Lower bound sequence.
|
|
239
280
|
* @param {number | null | undefined} [args.upToSequence] - Inclusive ceiling sequence.
|
|
240
|
-
* @returns {Promise<
|
|
281
|
+
* @returns {Promise<WebsocketPersistedEvent[]>} - Ordered events.
|
|
241
282
|
*/
|
|
242
283
|
async getEventsAfter({channel, sequence, upToSequence}) {
|
|
243
284
|
await this.ensureReady()
|
|
@@ -312,24 +353,55 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
312
353
|
* @returns {Promise<void>} - Resolves when complete.
|
|
313
354
|
*/
|
|
314
355
|
async _ensureEventsTable(db) {
|
|
315
|
-
this.logger.info("Applying websocket event-log schema")
|
|
316
|
-
|
|
317
356
|
if (await db.tableExists(EVENTS_TABLE)) {
|
|
318
|
-
this.
|
|
357
|
+
await this._ensureEventsTableColumns(db)
|
|
319
358
|
return
|
|
320
359
|
}
|
|
321
360
|
|
|
361
|
+
this.logger.info("Applying websocket event-log schema")
|
|
362
|
+
|
|
322
363
|
const eventTable = new TableData(EVENTS_TABLE, {ifNotExists: true})
|
|
323
364
|
|
|
324
365
|
eventTable.integer("sequence", {autoIncrement: true, null: false, primaryKey: true})
|
|
325
366
|
eventTable.string("id", {index: true, null: false})
|
|
326
367
|
eventTable.string("channel", {index: true, null: false})
|
|
368
|
+
eventTable.text("params_json", {null: true})
|
|
327
369
|
eventTable.text("payload_json", {null: false})
|
|
328
370
|
eventTable.datetime("created_at", {index: true, null: false})
|
|
329
371
|
|
|
330
372
|
await db.createTable(eventTable)
|
|
331
373
|
}
|
|
332
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Adds columns the current schema requires to an events table created by an
|
|
377
|
+
* older framework version, so upgrades keep working without a manual ALTER.
|
|
378
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
379
|
+
* @returns {Promise<void>} - Resolves when the events table schema is current.
|
|
380
|
+
*/
|
|
381
|
+
async _ensureEventsTableColumns(db) {
|
|
382
|
+
if (await this._columnPresent(db, EVENTS_TABLE, "params_json")) return
|
|
383
|
+
|
|
384
|
+
this.logger.info("Adding params_json column to websocket event-log table")
|
|
385
|
+
|
|
386
|
+
const tableData = new TableData(EVENTS_TABLE)
|
|
387
|
+
|
|
388
|
+
tableData.addColumn("params_json", {isNewColumn: true, null: true, type: "text"})
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
for (const sql of await db.alterTableSQLs(tableData)) {
|
|
392
|
+
await db.query(sql)
|
|
393
|
+
}
|
|
394
|
+
} catch (error) {
|
|
395
|
+
// A concurrent process can add the column between the presence check
|
|
396
|
+
// and the ALTER (multi-worker or rolling upgrade); the
|
|
397
|
+
// duplicate-column failure is then the expected outcome, not a real
|
|
398
|
+
// error. Anything else re-checks as absent and rethrows.
|
|
399
|
+
if (await this._columnPresent(db, EVENTS_TABLE, "params_json")) return
|
|
400
|
+
|
|
401
|
+
throw error
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
333
405
|
/**
|
|
334
406
|
* Runs ensure replay channels table.
|
|
335
407
|
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
@@ -352,7 +424,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
352
424
|
* @param {string} args.channel - Channel name.
|
|
353
425
|
* @param {import("../database/drivers/base.js").default} args.db - Database connection.
|
|
354
426
|
* @param {string} args.id - Event id.
|
|
355
|
-
* @returns {Promise<
|
|
427
|
+
* @returns {Promise<WebsocketPersistedEvent | null>} - Event row or null.
|
|
356
428
|
*/
|
|
357
429
|
async _getEventById({channel, db, id}) {
|
|
358
430
|
const rows = /** @type {WebsocketEventRow[]} */ (await db
|
|
@@ -370,7 +442,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
370
442
|
/**
|
|
371
443
|
* Runs normalize event row.
|
|
372
444
|
* @param {WebsocketEventRow} row - Raw row.
|
|
373
|
-
* @returns {
|
|
445
|
+
* @returns {WebsocketPersistedEvent} - Normalized row.
|
|
374
446
|
*/
|
|
375
447
|
_normalizeEventRow(row) {
|
|
376
448
|
const createdAtValue = row.created_at
|
|
@@ -379,6 +451,7 @@ export default class VelociousHttpServerWebsocketEventLogStore {
|
|
|
379
451
|
channel: row.channel,
|
|
380
452
|
createdAt: createdAtValue instanceof Date ? createdAtValue.toISOString() : new Date(createdAtValue).toISOString(),
|
|
381
453
|
id: row.id,
|
|
454
|
+
params: row.params_json === null || row.params_json === undefined ? null : JSON.parse(row.params_json),
|
|
382
455
|
payload: JSON.parse(row.payload_json),
|
|
383
456
|
sequence: Number(row.sequence)
|
|
384
457
|
}
|
|
@@ -116,7 +116,7 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
116
116
|
// Other channels chain onto their own tails and are not delayed.
|
|
117
117
|
this._queuePublish({
|
|
118
118
|
callback: async () => {
|
|
119
|
-
const persistedEvent = await this._persistV2EventIfNeeded({body, channel, configuration})
|
|
119
|
+
const persistedEvent = await this._persistV2EventIfNeeded({body, broadcastParams, channel, configuration})
|
|
120
120
|
const dispatchedTargets = new Set()
|
|
121
121
|
|
|
122
122
|
for (const handler of this.broadcastHandlersByConfiguration.get(configuration) || []) {
|
|
@@ -193,12 +193,13 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
193
193
|
* Runs persist v2 event if needed.
|
|
194
194
|
* @param {object} args - Options.
|
|
195
195
|
* @param {ReturnType<typeof JSON.parse>} args.body - Event body.
|
|
196
|
+
* @param {Record<string, ReturnType<typeof JSON.parse>>} args.broadcastParams - Routing filter params.
|
|
196
197
|
* @param {string} args.channel - Channel name.
|
|
197
198
|
* @param {import("../configuration.js").default} args.configuration - Originating configuration.
|
|
198
199
|
* @returns {Promise<{createdAt: string, id: string} | null>} - Persisted event metadata when storage is enabled.
|
|
199
200
|
*/
|
|
200
|
-
async _persistV2EventIfNeeded({body, channel, configuration}) {
|
|
201
|
-
return await this._persistChannelEventIfNeeded({channel, payload: body
|
|
201
|
+
async _persistV2EventIfNeeded({body, broadcastParams, channel, configuration}) {
|
|
202
|
+
return await this._persistChannelEventIfNeeded({broadcastParams, channel, configuration, payload: body})
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
/**
|
|
@@ -213,14 +214,18 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
/**
|
|
216
|
-
* Runs persist channel event if needed.
|
|
217
|
+
* Runs persist channel event if needed. Persists the broadcast's routing
|
|
218
|
+
* params (sanitized through the channel class's
|
|
219
|
+
* `replayableBroadcastParams`) so replay can re-apply `matches()` per
|
|
220
|
+
* stream instead of delivering the whole channel's log.
|
|
217
221
|
* @param {object} args - Options object.
|
|
222
|
+
* @param {Record<string, ReturnType<typeof JSON.parse>> | null} [args.broadcastParams] - Broadcast params to persist for stream-scoped replay.
|
|
218
223
|
* @param {string} args.channel - Channel name.
|
|
219
224
|
* @param {ReturnType<typeof JSON.parse>} args.payload - Payload data.
|
|
220
225
|
* @param {import("../configuration.js").default} [args.configuration] - Configuration owning the event store.
|
|
221
226
|
* @returns {Promise<{createdAt: string, id: string} | null>} - Persisted event metadata.
|
|
222
227
|
*/
|
|
223
|
-
async _persistChannelEventIfNeeded({channel,
|
|
228
|
+
async _persistChannelEventIfNeeded({broadcastParams = null, channel, configuration, payload}) {
|
|
224
229
|
const handler = this.handlers.values().next().value
|
|
225
230
|
const eventConfiguration = configuration || handler?.configuration
|
|
226
231
|
|
|
@@ -231,7 +236,10 @@ export class VelociousHttpServerWebsocketEventsHost {
|
|
|
231
236
|
|
|
232
237
|
if (!shouldPersist) return null
|
|
233
238
|
|
|
234
|
-
const
|
|
239
|
+
const ChannelClass = eventConfiguration.getWebsocketChannelClass(channel)
|
|
240
|
+
const params = ChannelClass ? ChannelClass.replayableBroadcastParams(broadcastParams) : broadcastParams
|
|
241
|
+
|
|
242
|
+
const persistedEvent = await websocketEventLogStore.appendEvent({channel, params, payload})
|
|
235
243
|
|
|
236
244
|
return {
|
|
237
245
|
createdAt: persistedEvent.createdAt,
|