velocious 1.0.650 → 1.0.652
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/build/database/record/counter-cache-magnitude.js +14 -3
- package/build/database/record/counter-cache-parent-updates.js +137 -0
- package/build/database/record/index.js +13 -3
- package/build/frontend-models/websocket-publishers.js +35 -12
- package/build/http-client/websocket-client.js +40 -8
- package/build/src/database/record/counter-cache-magnitude.d.ts.map +1 -1
- package/build/src/database/record/counter-cache-magnitude.js +14 -4
- package/build/src/database/record/counter-cache-parent-updates.d.ts +43 -0
- package/build/src/database/record/counter-cache-parent-updates.d.ts.map +1 -0
- package/build/src/database/record/counter-cache-parent-updates.js +125 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +14 -4
- package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-publishers.js +32 -13
- package/build/src/http-client/websocket-client.d.ts.map +1 -1
- package/build/src/http-client/websocket-client.js +41 -9
- package/package.json +2 -2
- package/src/database/record/counter-cache-magnitude.js +14 -3
- package/src/database/record/counter-cache-parent-updates.js +137 -0
- package/src/database/record/index.js +13 -3
- package/src/frontend-models/websocket-publishers.js +35 -12
- package/src/http-client/websocket-client.js +40 -8
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import Logger from "../../logger.js"
|
|
4
|
+
import {ensureError} from "typanic"
|
|
5
|
+
|
|
6
|
+
/** @typedef {(parent: import("./index.js").default, previousParent: import("./index.js").default | undefined) => void | Promise<void>} CounterCacheParentUpdateListener */
|
|
7
|
+
/** @typedef {{canonicalParentModelClass: typeof import("./index.js").default, listeners: CounterCacheParentUpdateListener[], parentId: ReturnType<typeof JSON.parse>, parentPrimaryKey: string, parentQuery: import("../query/model-class-query.js").default<typeof import("./index.js").default>, previousParent: import("./index.js").default | undefined}} PreparedCounterCacheParentUpdate */
|
|
8
|
+
|
|
9
|
+
/** @type {WeakMap<typeof import("./index.js").default, Set<CounterCacheParentUpdateListener>>} */
|
|
10
|
+
const listenersByParentModelClass = new WeakMap()
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Registers an internal listener for committed counter-cache parent updates.
|
|
14
|
+
* @param {typeof import("./index.js").default} parentModelClass - Parent model class.
|
|
15
|
+
* @param {CounterCacheParentUpdateListener} listener - Committed-parent listener.
|
|
16
|
+
* @returns {() => void} - Listener removal callback.
|
|
17
|
+
*/
|
|
18
|
+
export function registerCounterCacheParentUpdateListener(parentModelClass, listener) {
|
|
19
|
+
const canonicalParentModelClass = parentModelClass.canonicalRecordMetadataModelClass()
|
|
20
|
+
let listeners = listenersByParentModelClass.get(canonicalParentModelClass)
|
|
21
|
+
|
|
22
|
+
if (!listeners) {
|
|
23
|
+
listeners = new Set()
|
|
24
|
+
listenersByParentModelClass.set(canonicalParentModelClass, listeners)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
listeners.add(listener)
|
|
28
|
+
|
|
29
|
+
return () => listeners.delete(listener)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Captures one counter-cache parent's pre-mutation state when listeners are registered.
|
|
34
|
+
* @param {object} args - Parent update arguments.
|
|
35
|
+
* @param {ReturnType<typeof JSON.parse>} args.parentId - Parent relationship identity.
|
|
36
|
+
* @param {typeof import("./index.js").default} args.parentModelClass - Parent model class.
|
|
37
|
+
* @param {string} args.parentPrimaryKey - Parent relationship primary key.
|
|
38
|
+
* @param {import("../query/model-class-query.js").default<typeof import("./index.js").default>} args.parentQuery - Source-owned parent query.
|
|
39
|
+
* @returns {Promise<PreparedCounterCacheParentUpdate | undefined>} - Prepared delivery, or undefined when no listener is registered.
|
|
40
|
+
*/
|
|
41
|
+
export async function prepareCounterCacheParentUpdate({parentId, parentModelClass, parentPrimaryKey, parentQuery}) {
|
|
42
|
+
const canonicalParentModelClass = parentModelClass.canonicalRecordMetadataModelClass()
|
|
43
|
+
const registeredListeners = listenersByParentModelClass.get(canonicalParentModelClass)
|
|
44
|
+
|
|
45
|
+
if (!registeredListeners || registeredListeners.size == 0) return
|
|
46
|
+
const previousParent = await parentQuery.findBy({[parentPrimaryKey]: parentId}) || undefined
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
canonicalParentModelClass,
|
|
50
|
+
listeners: [...registeredListeners],
|
|
51
|
+
parentId,
|
|
52
|
+
parentPrimaryKey,
|
|
53
|
+
parentQuery,
|
|
54
|
+
previousParent
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Schedules one non-coalesced parent reload and notification on the source record's commit lifecycle.
|
|
60
|
+
* @param {object} args - Parent update arguments.
|
|
61
|
+
* @param {PreparedCounterCacheParentUpdate | undefined} args.preparedUpdate - Pre-mutation parent delivery state.
|
|
62
|
+
* @param {import("./index.js").default} args.sourceRecord - Source record that owns the transaction lifecycle.
|
|
63
|
+
* @returns {Promise<void>} - Resolves after registration or immediate delivery.
|
|
64
|
+
*/
|
|
65
|
+
export async function scheduleCounterCacheParentUpdate({preparedUpdate, sourceRecord}) {
|
|
66
|
+
if (!preparedUpdate) return
|
|
67
|
+
|
|
68
|
+
const {canonicalParentModelClass, listeners, parentId, parentPrimaryKey, parentQuery, previousParent} = preparedUpdate
|
|
69
|
+
|
|
70
|
+
await sourceRecord.connection().afterCommit(async () => {
|
|
71
|
+
let parent
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
parent = await parentQuery.findBy({[parentPrimaryKey]: parentId})
|
|
75
|
+
} catch (error) {
|
|
76
|
+
await reportCounterCacheParentUpdateError(canonicalParentModelClass._getConfiguration(), error)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!parent) return
|
|
81
|
+
|
|
82
|
+
for (const listener of listeners) {
|
|
83
|
+
try {
|
|
84
|
+
await listener(parent, previousParent)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await reportCounterCacheParentUpdateError(parent._getConfiguration(), error)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Reports a post-commit delivery failure without rejecting the durable source operation.
|
|
94
|
+
* @param {import("../../configuration.js").default} configuration - Owning configuration.
|
|
95
|
+
* @param {ReturnType<typeof JSON.parse>} caughtError - Reload or listener failure.
|
|
96
|
+
* @returns {Promise<void>} - Resolves after best-effort reporting.
|
|
97
|
+
*/
|
|
98
|
+
async function reportCounterCacheParentUpdateError(configuration, caughtError) {
|
|
99
|
+
const error = ensureError(caughtError)
|
|
100
|
+
const payload = {
|
|
101
|
+
context: {stage: "counter-cache-parent-update-after-commit"},
|
|
102
|
+
error
|
|
103
|
+
}
|
|
104
|
+
/** @type {ReturnType<typeof JSON.parse>[]} */
|
|
105
|
+
const reportingErrors = []
|
|
106
|
+
let errorEvents
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
errorEvents = configuration.getErrorEvents()
|
|
110
|
+
} catch (reportingError) {
|
|
111
|
+
reportingErrors.push(reportingError)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (errorEvents) {
|
|
115
|
+
try {
|
|
116
|
+
errorEvents.emit("framework-error", payload)
|
|
117
|
+
} catch (reportingError) {
|
|
118
|
+
reportingErrors.push(reportingError)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
|
|
123
|
+
} catch (reportingError) {
|
|
124
|
+
reportingErrors.push(reportingError)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (reportingErrors.length == 0) return
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const logger = new Logger("CounterCacheParentUpdates", {configuration})
|
|
132
|
+
|
|
133
|
+
await logger.error("Counter-cache parent update error reporting failed", {error, reportingErrors})
|
|
134
|
+
} catch {
|
|
135
|
+
console.error("Counter-cache parent update error reporting failed")
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -56,6 +56,7 @@ import {formatValue} from "../../utils/format-value.js"
|
|
|
56
56
|
import {modelPrimaryKeyCacheKey, modelPrimaryKeyConditions, readModelPrimaryKeyValue, scalarModelPrimaryKey, scalarModelPrimaryKeyValue} from "../../utils/model-primary-key.js"
|
|
57
57
|
import {captureCreateAuditChanges, captureUpdateAuditChanges, createAudit, createCreateAudit, createDestroyAudit, createUpdateAudit, initializeAuditing, registerAuditCallback, registerAuditing, withoutAudit} from "./auditing.js"
|
|
58
58
|
import {registerMagnitudeCounterCache} from "./counter-cache-magnitude.js"
|
|
59
|
+
import {prepareCounterCacheParentUpdate, scheduleCounterCacheParentUpdate} from "./counter-cache-parent-updates.js"
|
|
59
60
|
import {stateMachine} from "./state-machine.js"
|
|
60
61
|
import ValidatorsFormat from "./validators/format.js"
|
|
61
62
|
import ValidatorsLength from "./validators/length.js"
|
|
@@ -999,14 +1000,23 @@ class VelociousDatabaseRecord {
|
|
|
999
1000
|
const parentTable = ParentModel.tableName()
|
|
1000
1001
|
const childTable = ChildModel.tableName()
|
|
1001
1002
|
const pkColumn = inflection.underscore(primaryKey)
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
.driver
|
|
1003
|
+
const parentQuery = record.queryForModel(ParentModel)
|
|
1004
|
+
const connection = parentQuery.driver
|
|
1005
1005
|
const quoted = connection.quote(parentId)
|
|
1006
|
+
const preparedParentUpdate = await prepareCounterCacheParentUpdate({
|
|
1007
|
+
parentId,
|
|
1008
|
+
parentModelClass: ParentModel,
|
|
1009
|
+
parentPrimaryKey: primaryKey,
|
|
1010
|
+
parentQuery
|
|
1011
|
+
})
|
|
1006
1012
|
|
|
1007
1013
|
const sql = `UPDATE ${connection.quoteTable(parentTable)} SET ${connection.quoteColumn(counterColumn)} = (SELECT COUNT(*) FROM ${connection.quoteTable(childTable)} WHERE ${connection.quoteColumn(fk)} = ${quoted}) WHERE ${connection.quoteColumn(pkColumn)} = ${quoted}`
|
|
1008
1014
|
|
|
1009
1015
|
await connection.query(sql, {logName: `${ParentModel.name} Update`})
|
|
1016
|
+
await scheduleCounterCacheParentUpdate({
|
|
1017
|
+
preparedUpdate: preparedParentUpdate,
|
|
1018
|
+
sourceRecord: record
|
|
1019
|
+
})
|
|
1010
1020
|
}
|
|
1011
1021
|
|
|
1012
1022
|
/**
|
|
@@ -5,12 +5,14 @@ import {frontendModelResourcesWithBuiltInsForBackendProject} from "./built-in-re
|
|
|
5
5
|
import {frontendModelResourceDefinitionIsClass} from "./resource-definition.js"
|
|
6
6
|
import {serializeFrontendModelTransportValue} from "./transport-serialization.js"
|
|
7
7
|
import {modelPrimaryKeyCacheKey, readModelPrimaryKeyValue} from "../utils/model-primary-key.js"
|
|
8
|
+
import {registerCounterCacheParentUpdateListener} from "../database/record/counter-cache-parent-updates.js"
|
|
8
9
|
|
|
9
10
|
/** @typedef {{primaryKey: import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition}} FrontendModelPublisherResource */
|
|
10
11
|
/** @typedef {Record<string, import("./query.js").FrontendModelTransportValue>} FrontendModelDestroyAuthorizationRecord */
|
|
11
12
|
/** @typedef {import("../database/record/index.js").default & {__frontendModelWebsocketAction?: "create" | "update", __frontendModelWebsocketDestroyAuthorizationRecord?: FrontendModelDestroyAuthorizationRecord, __frontendModelWebsocketPreviousIds?: Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>}} FrontendModelWebsocketRecord */
|
|
12
13
|
|
|
13
14
|
const modelClassesWithRegisteredHooks = new WeakSet()
|
|
15
|
+
const modelClassesWithRegisteredCounterCacheParentListeners = new WeakSet()
|
|
14
16
|
const channelClassRegisteredConfigurations = new WeakSet()
|
|
15
17
|
/** @type {WeakMap<import("../configuration.js").default, WeakMap<typeof import("../database/record/index.js").default, Map<string, FrontendModelPublisherResource>>>} */
|
|
16
18
|
const publisherResourcesByConfiguration = new WeakMap()
|
|
@@ -127,6 +129,7 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
|
|
|
127
129
|
if (!resourceClass.ModelClass) continue
|
|
128
130
|
|
|
129
131
|
const modelClass = resourceClass.modelClass()
|
|
132
|
+
const canonicalModelClass = modelClass.canonicalRecordMetadataModelClass()
|
|
130
133
|
const resourceConfiguration = resourceClass.resourceConfig()
|
|
131
134
|
const configuredPrimaryKey = resourceConfiguration.primaryKey
|
|
132
135
|
const modelPrimaryKey = modelClass.primaryKey()
|
|
@@ -140,38 +143,47 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
|
|
|
140
143
|
publisherResourcesByConfiguration.set(configuration, publisherResourcesByModelClass)
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
let publisherResources = publisherResourcesByModelClass.get(
|
|
146
|
+
let publisherResources = publisherResourcesByModelClass.get(canonicalModelClass)
|
|
144
147
|
|
|
145
148
|
if (!publisherResources) {
|
|
146
149
|
publisherResources = new Map()
|
|
147
|
-
publisherResourcesByModelClass.set(
|
|
150
|
+
publisherResourcesByModelClass.set(canonicalModelClass, publisherResources)
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
publisherResources.set(modelName, {
|
|
151
154
|
primaryKey
|
|
152
155
|
})
|
|
153
156
|
|
|
157
|
+
if (!modelClassesWithRegisteredCounterCacheParentListeners.has(canonicalModelClass)) {
|
|
158
|
+
modelClassesWithRegisteredCounterCacheParentListeners.add(canonicalModelClass)
|
|
159
|
+
registerCounterCacheParentUpdateListener(canonicalModelClass, (parent, previousParent) => {
|
|
160
|
+
const previousIds = previousParent ? frontendModelResourceIdentities(previousParent) : undefined
|
|
161
|
+
|
|
162
|
+
broadcastFrontendModelEvents(parent, "update", previousIds)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
154
166
|
// Register lifecycle hooks once per model class, not per configuration. A model class belongs to a
|
|
155
167
|
// single backend project/config in production, so per-config registration only differs in tests where
|
|
156
168
|
// the same model class is reachable from multiple configs — there it attaches duplicate beforeCreate/
|
|
157
169
|
// afterSave/afterDestroy hooks that double-fire broadcasts (and leak across specs). The hooks read the
|
|
158
170
|
// model's runtime configuration when broadcasting, so a single registration is sufficient.
|
|
159
|
-
if (modelClassesWithRegisteredHooks.has(
|
|
171
|
+
if (modelClassesWithRegisteredHooks.has(canonicalModelClass)) continue
|
|
160
172
|
|
|
161
|
-
modelClassesWithRegisteredHooks.add(
|
|
173
|
+
modelClassesWithRegisteredHooks.add(canonicalModelClass)
|
|
162
174
|
|
|
163
|
-
|
|
175
|
+
canonicalModelClass.beforeCreate((model) => {
|
|
164
176
|
/** @type {FrontendModelWebsocketRecord} */ (model).__frontendModelWebsocketAction = "create"
|
|
165
177
|
})
|
|
166
178
|
|
|
167
|
-
|
|
179
|
+
canonicalModelClass.beforeUpdate(async (model) => {
|
|
168
180
|
const websocketModel = /** @type {FrontendModelWebsocketRecord} */ (model)
|
|
169
181
|
|
|
170
182
|
websocketModel.__frontendModelWebsocketAction = "update"
|
|
171
183
|
websocketModel.__frontendModelWebsocketPreviousIds = await frontendModelPreviousResourceIdentities(model)
|
|
172
184
|
})
|
|
173
185
|
|
|
174
|
-
|
|
186
|
+
canonicalModelClass.beforeDestroy(async (model) => {
|
|
175
187
|
const websocketModel = /** @type {FrontendModelWebsocketRecord} */ (model)
|
|
176
188
|
const persistedModel = await model
|
|
177
189
|
.queryForModel(model.getModelClass())
|
|
@@ -183,7 +195,7 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
|
|
|
183
195
|
websocketModel.__frontendModelWebsocketDestroyAuthorizationRecord = frontendModelDestroyAuthorizationRecord(persistedModel)
|
|
184
196
|
})
|
|
185
197
|
|
|
186
|
-
|
|
198
|
+
canonicalModelClass.afterSave((model) => {
|
|
187
199
|
const modelWithWebsocketAction = /** @type {FrontendModelWebsocketRecord} */ (model)
|
|
188
200
|
const action = modelWithWebsocketAction.__frontendModelWebsocketAction
|
|
189
201
|
|
|
@@ -197,7 +209,7 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
|
|
|
197
209
|
delete modelWithWebsocketAction.__frontendModelWebsocketPreviousIds
|
|
198
210
|
})
|
|
199
211
|
|
|
200
|
-
|
|
212
|
+
canonicalModelClass.afterDestroy((model) => {
|
|
201
213
|
const websocketModel = /** @type {FrontendModelWebsocketRecord} */ (model)
|
|
202
214
|
const destroyAuthorizationRecord = websocketModel.__frontendModelWebsocketDestroyAuthorizationRecord
|
|
203
215
|
const previousIds = websocketModel.__frontendModelWebsocketPreviousIds
|
|
@@ -217,7 +229,7 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
|
|
|
217
229
|
* @returns {Promise<Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>>} - Previous identities by resource name.
|
|
218
230
|
*/
|
|
219
231
|
async function frontendModelPreviousResourceIdentities(model) {
|
|
220
|
-
const publisherResources =
|
|
232
|
+
const publisherResources = publisherResourcesForModel(model)
|
|
221
233
|
/** @type {Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>} */
|
|
222
234
|
const previousIds = new Map()
|
|
223
235
|
|
|
@@ -252,7 +264,7 @@ async function frontendModelPreviousResourceIdentities(model) {
|
|
|
252
264
|
* @returns {Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>} - Identities by resource name.
|
|
253
265
|
*/
|
|
254
266
|
function frontendModelResourceIdentities(model) {
|
|
255
|
-
const publisherResources =
|
|
267
|
+
const publisherResources = publisherResourcesForModel(model)
|
|
256
268
|
/** @type {Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>} */
|
|
257
269
|
const identities = new Map()
|
|
258
270
|
|
|
@@ -267,6 +279,17 @@ function frontendModelResourceIdentities(model) {
|
|
|
267
279
|
return identities
|
|
268
280
|
}
|
|
269
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Returns publisher resources through the backing model's canonical registry owner.
|
|
284
|
+
* @param {import("../database/record/index.js").default} model - Backing model instance.
|
|
285
|
+
* @returns {Map<string, FrontendModelPublisherResource> | undefined} - Publisher resources for the model.
|
|
286
|
+
*/
|
|
287
|
+
function publisherResourcesForModel(model) {
|
|
288
|
+
const canonicalModelClass = model.getModelClass().canonicalRecordMetadataModelClass()
|
|
289
|
+
|
|
290
|
+
return publisherResourcesByConfiguration.get(model._getConfiguration())?.get(canonicalModelClass)
|
|
291
|
+
}
|
|
292
|
+
|
|
270
293
|
/**
|
|
271
294
|
* Serializes the persisted record for server-side destroy authorization. Binary values
|
|
272
295
|
* use a dedicated byte-array marker because the shared transport serializer otherwise
|
|
@@ -337,7 +360,7 @@ function frontendModelResourceIdentity({model, previous = false, primaryKey}) {
|
|
|
337
360
|
*/
|
|
338
361
|
function broadcastFrontendModelEvents(model, action, previousIds, destroyAuthorizationRecord) {
|
|
339
362
|
const configuration = model._getConfiguration()
|
|
340
|
-
const publisherResources =
|
|
363
|
+
const publisherResources = publisherResourcesForModel(model)
|
|
341
364
|
|
|
342
365
|
if (!publisherResources) return
|
|
343
366
|
|
|
@@ -118,19 +118,51 @@ export default class VelociousWebsocketClient extends SnapReqWebSocketClient {
|
|
|
118
118
|
if (this.gracefulClosePromise) return await this.gracefulClosePromise
|
|
119
119
|
|
|
120
120
|
this.autoReconnect = false
|
|
121
|
+
const channelSubscriptions = [...this._channelSubscriptions.values()]
|
|
121
122
|
const socket = this.socket
|
|
123
|
+
|
|
124
|
+
this._channelSubscriptions.clear()
|
|
125
|
+
const {promise: publishedClosePromise, reject: rejectPublishedClose, resolve: resolvePublishedClose} = Promise.withResolvers()
|
|
126
|
+
|
|
127
|
+
this.gracefulClosePromise = publishedClosePromise
|
|
128
|
+
// This internal bridge exists only for synchronous reentrancy. Its rejection
|
|
129
|
+
// duplicates closePromise, which remains the public error source below.
|
|
130
|
+
void publishedClosePromise.catch(() => {})
|
|
122
131
|
const closePromise = (async () => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
/** @type {unknown[]} */
|
|
133
|
+
const closeErrors = []
|
|
134
|
+
|
|
135
|
+
for (const subscription of channelSubscriptions) {
|
|
136
|
+
try {
|
|
137
|
+
subscription._handleClosed("client_close")
|
|
138
|
+
} catch (error) {
|
|
139
|
+
closeErrors.push(error)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
if (socket && socket.readyState === socket.OPEN) {
|
|
145
|
+
await new Promise((resolve) => {
|
|
146
|
+
socket.addEventListener("close", () => resolve(undefined), {once: true})
|
|
147
|
+
socket.close(1000)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
closeErrors.push(error)
|
|
128
152
|
}
|
|
129
153
|
|
|
130
|
-
|
|
154
|
+
try {
|
|
155
|
+
await super.close()
|
|
156
|
+
} catch (error) {
|
|
157
|
+
closeErrors.push(error)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (closeErrors.length === 1) throw closeErrors[0]
|
|
161
|
+
if (closeErrors.length > 1) throw new AggregateError(closeErrors, "Failed to close WebSocket client")
|
|
131
162
|
})()
|
|
132
163
|
|
|
133
164
|
this.gracefulClosePromise = closePromise
|
|
165
|
+
void closePromise.then(resolvePublishedClose, rejectPublishedClose)
|
|
134
166
|
|
|
135
167
|
try {
|
|
136
168
|
await closePromise
|
|
@@ -148,12 +180,12 @@ export default class VelociousWebsocketClient extends SnapReqWebSocketClient {
|
|
|
148
180
|
this.reconnectGeneration += 1
|
|
149
181
|
await super.disconnectAndStopReconnect()
|
|
150
182
|
|
|
151
|
-
if (this.runningReconnectTasks.size === 0) return
|
|
152
|
-
|
|
153
183
|
while (this.runningReconnectTasks.size > 0) {
|
|
154
184
|
await Promise.all(this.runningReconnectTasks)
|
|
155
185
|
}
|
|
156
186
|
|
|
187
|
+
// A stale attempt may have finished during the first close after changing
|
|
188
|
+
// stopped state, even when the task set is already empty here.
|
|
157
189
|
await super.disconnectAndStopReconnect()
|
|
158
190
|
}
|
|
159
191
|
}
|