velocious 1.0.564 → 1.0.566

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.564",
6
+ "version": "1.0.566",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -255,6 +255,7 @@
255
255
  * @property {boolean} [inProcess] - Run HTTP handlers in the main thread instead of worker threads.
256
256
  * @property {number} [maxWorkers] - Backward-compatible alias for workers.
257
257
  * @property {number} [port] - Port to bind the HTTP server to.
258
+ * @property {{maxPendingBytes?: number, maxPendingMessages?: number}} [websocketInboundQueue] - Per-session retained inbound WebSocket message limits.
258
259
  * @property {{maxPendingBytes?: number, maxPendingFrames?: number}} [websocketOutboundQueue] - Per-client retained outbound WebSocket frame limits.
259
260
  * @property {number} [workers] - Worker handlers to start for the HTTP server.
260
261
  */
@@ -114,6 +114,8 @@ function resolveBeaconUnreachableReportMs(value) {
114
114
  return 30_000
115
115
  }
116
116
 
117
+ const DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_BYTES = 16 * 1024 * 1024
118
+ const DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_MESSAGES = 256
117
119
  const DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_BYTES = 16 * 1024 * 1024
118
120
  const DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_FRAMES = 256
119
121
 
@@ -228,10 +230,15 @@ export default class VelociousConfiguration {
228
230
  * @type {Promise<void> | undefined}
229
231
  */
230
232
  this._initializePromise = undefined
233
+ const websocketInboundQueue = httpServer?.websocketInboundQueue
231
234
  const websocketOutboundQueue = httpServer?.websocketOutboundQueue
232
235
 
233
236
  this.httpServer = {
234
237
  ...(httpServer || {}),
238
+ websocketInboundQueue: {
239
+ maxPendingBytes: positiveSafeInteger(websocketInboundQueue?.maxPendingBytes, "httpServer.websocketInboundQueue.maxPendingBytes", DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_BYTES),
240
+ maxPendingMessages: positiveSafeInteger(websocketInboundQueue?.maxPendingMessages, "httpServer.websocketInboundQueue.maxPendingMessages", DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_MESSAGES)
241
+ },
235
242
  websocketOutboundQueue: {
236
243
  maxPendingBytes: positiveSafeInteger(websocketOutboundQueue?.maxPendingBytes, "httpServer.websocketOutboundQueue.maxPendingBytes", DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_BYTES),
237
244
  maxPendingFrames: positiveSafeInteger(websocketOutboundQueue?.maxPendingFrames, "httpServer.websocketOutboundQueue.maxPendingFrames", DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_FRAMES)
@@ -2370,6 +2377,19 @@ export default class VelociousConfiguration {
2370
2377
  */
2371
2378
  getWebsocketSessionHeartbeatSeconds() { return this._websocketSessionHeartbeatSeconds }
2372
2379
 
2380
+ /**
2381
+ * Gets per-session WebSocket inbound message queue limits.
2382
+ * @returns {{maxBytes: number, maxMessages: number}} - Per-session inbound queue high-water marks.
2383
+ */
2384
+ getWebsocketInboundQueueLimits() {
2385
+ const queue = this.httpServer.websocketInboundQueue
2386
+
2387
+ return {
2388
+ maxBytes: queue.maxPendingBytes,
2389
+ maxMessages: queue.maxPendingMessages
2390
+ }
2391
+ }
2392
+
2373
2393
  /**
2374
2394
  * Gets per-client WebSocket outbound queue limits.
2375
2395
  * @returns {{maxBytes: number, maxFrames: number}} - Per-client outbound queue high-water marks.
@@ -50,6 +50,7 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
50
50
  const position = record.readAttribute(positionColumn)
51
51
 
52
52
  if (position != null) {
53
+ assertPositivePosition({position, positionColumn})
53
54
  await shiftPositionsUp({record, positionColumn, scope, fromPosition: position})
54
55
  } else {
55
56
  const nextPosition = await highestPositionInScope({record, positionColumn, scope})
@@ -77,12 +78,19 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
77
78
 
78
79
  if (!posChanged && !scopeChanged) return
79
80
 
81
+ assertPositivePosition({
82
+ position: rawAttributes[posColumn],
83
+ positionColumn,
84
+ persisted: true
85
+ })
86
+
80
87
  const oldPosition = posChanged ? /** @type {number} */ (rawAttributes[posColumn]) : /** @type {number} */ (record.readAttribute(positionColumn))
81
88
  const newPosition = posChanged ? /** @type {number} */ (changes[posColumn]) : /** @type {number} */ (record.readAttribute(positionColumn))
82
89
  const oldScopeValue = scopeChanged ? /** @type {number} */ (rawAttributes[scopeCol]) : /** @type {number} */ (record.readAttribute(scope))
83
90
  const newScopeValue = scopeChanged ? /** @type {number} */ (changes[scopeCol]) : /** @type {number} */ (record.readAttribute(scope))
84
91
 
85
- if (oldPosition == null || newPosition == null) return
92
+ assertPositivePosition({position: newPosition, positionColumn})
93
+ if (oldPosition == null) return
86
94
  if (newPosition === oldPosition && newScopeValue === oldScopeValue) return
87
95
 
88
96
  if (scopeChanged && oldScopeValue !== newScopeValue) {
@@ -100,7 +108,7 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
100
108
  return
101
109
  }
102
110
 
103
- await moveOutOfWay({record, positionColumn, scope, scopeValue: oldScopeValue, targetScopeValue: newScopeValue})
111
+ await moveOutOfWay({record, positionColumn, scope, scopeValue: oldScopeValue})
104
112
  setShiftingFlag(record, false)
105
113
  await shiftPositionsDown({record, positionColumn, scope, scopeValue: oldScopeValue, fromPosition: oldPosition + 1})
106
114
  await shiftPositionsUp({record, positionColumn, scope, scopeValue: newScopeValue, fromPosition: newPosition, excludeRecordId: record.id()})
@@ -121,7 +129,14 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
121
129
  modelClass.beforeDestroy(async (record) => {
122
130
  const position = record.readAttribute(positionColumn)
123
131
 
124
- if (position == null) return
132
+ if (position == null) {
133
+ const modelClass = /** @type {typeof import("./index.js").default} */ (record.constructor)
134
+ const posColumn = modelClass.getColumnNameForAttributeName(positionColumn)
135
+
136
+ if (posColumn in record._attributes) assertPositivePosition({position, positionColumn, persisted: true})
137
+ return
138
+ }
139
+ assertPositivePosition({position, positionColumn, persisted: true})
125
140
 
126
141
  await moveOutOfWay({record, positionColumn, scope})
127
142
  setShiftingFlag(record, false)
@@ -130,6 +145,22 @@ export default function registerActsAsListCallbacks(modelClass, positionColumn,
130
145
  })
131
146
  }
132
147
 
148
+ /**
149
+ * Enforces the public gap-less list position invariant before any shifting.
150
+ * @param {object} args - Arguments.
151
+ * @param {number | null | undefined} args.position - Position to validate.
152
+ * @param {string} args.positionColumn - Position attribute name.
153
+ * @param {boolean} [args.persisted] - Whether the invalid value came from persisted state.
154
+ * @returns {void}
155
+ */
156
+ function assertPositivePosition({position, positionColumn, persisted = false}) {
157
+ if (typeof position === "number" && Number.isInteger(position) && position > 0) return
158
+
159
+ const source = persisted ? "Persisted" : "Requested"
160
+
161
+ throw new Error(`${source} actsAsList ${positionColumn} must be a positive integer`)
162
+ }
163
+
133
164
  /**
134
165
  * Places a moved row after surrounding rows have shifted.
135
166
  * @param {object} args - Arguments.
@@ -380,30 +411,26 @@ function resolveScopeValue(record, scope) {
380
411
  * @param {string} args.positionColumn - camelCase position attribute.
381
412
  * @param {string} args.scope - camelCase scope attribute.
382
413
  * @param {string | number | null} [args.scopeValue] - Scope containing the record before move-out.
383
- * @param {string | number | null} [args.targetScopeValue] - Temporary scope value to assign.
384
414
  * @returns {Promise<void>}
385
415
  */
386
- async function moveOutOfWay({record, positionColumn, scope, scopeValue, targetScopeValue}) {
416
+ async function moveOutOfWay({record, positionColumn, scope, scopeValue}) {
387
417
  const modelClass = /** @type {typeof import("./index.js").default} */ (record.constructor)
388
418
  const connection = modelClass.connection()
389
419
  const tableName = modelClass._getTable().getName()
390
420
  const resolvedScopeValue = scopeValue != null ? scopeValue : resolveScopeValue(record, scope)
391
- const resolvedTargetScopeValue = targetScopeValue != null ? targetScopeValue : resolvedScopeValue
392
421
 
393
422
  if (resolvedScopeValue == null) return
394
- if (resolvedTargetScopeValue == null) return
395
423
 
396
- const tempPosition = -record.id()
397
424
  const positionColumnSql = connection.quoteColumn(modelClass.getColumnNameForAttributeName(positionColumn))
398
425
  const scopeColumnSql = connection.quoteColumn(modelClass.getColumnNameForAttributeName(scope))
399
426
  const tableSql = connection.quoteTable(tableName)
400
- const pkSql = connection.quoteColumn("id")
427
+ const pkSql = connection.quoteColumn(modelClass.primaryKey())
401
428
 
402
429
  setShiftingFlag(record, true)
403
430
 
404
431
  try {
405
432
  await connection.query(
406
- `UPDATE ${tableSql} SET ${scopeColumnSql} = ${connection.quote(resolvedTargetScopeValue)}, ${positionColumnSql} = ${connection.quote(tempPosition)} WHERE ${scopeColumnSql} = ${connection.quote(resolvedScopeValue)} AND ${pkSql} = ${connection.quote(record.id())}`
433
+ `UPDATE ${tableSql} SET ${positionColumnSql} = -${positionColumnSql} WHERE ${scopeColumnSql} = ${connection.quote(resolvedScopeValue)} AND ${pkSql} = ${connection.quote(record.id())}`
407
434
  )
408
435
  } finally {
409
436
  // Don't clear the flag here — the caller will do that after shifts
@@ -154,16 +154,12 @@ export default class DbGenerateModel extends BaseCommand {
154
154
  // --- getModelClass() override (fixes polymorphic typing in JS/JSDoc) ---
155
155
  if (await fileExists(sourceModelFullFilePath)) {
156
156
  // Model file exists (e.g. src/models/ticket.js) → return typeof Ticket
157
- fileContent += " /**\n"
158
- fileContent += ` * @returns {typeof import("${sourceModelFilePath}").default}\n`
159
- fileContent += " */\n"
157
+ fileContent += ` /** @returns {typeof import("${sourceModelFilePath}").default} */\n`
160
158
  fileContent += " // @ts-ignore - override narrows return type for better IntelliSense in generated model bases\n"
161
159
  fileContent += ` getModelClass() { return /** @type {typeof import("${sourceModelFilePath}").default} */ (this.constructor) }\n\n`
162
160
  } else {
163
161
  // No model file yet → fall back to typeof TicketBase
164
- fileContent += " /**\n"
165
- fileContent += ` * @returns {typeof ${modelNameCamelized}Base}\n`
166
- fileContent += " */\n"
162
+ fileContent += ` /** @returns {typeof ${modelNameCamelized}Base} */\n`
167
163
  fileContent += " // @ts-ignore - override narrows return type for better IntelliSense in generated model bases\n"
168
164
  fileContent += ` getModelClass() { return /** @type {typeof ${modelNameCamelized}Base} */ (this.constructor) }\n\n`
169
165
  }
@@ -181,9 +177,7 @@ export default class DbGenerateModel extends BaseCommand {
181
177
  }
182
178
 
183
179
  if (jsdocType) {
184
- fileContent += " /**\n"
185
- fileContent += ` * @returns {${jsdocType}${column.getNull() ? " | null" : ""}}\n`
186
- fileContent += " */\n"
180
+ fileContent += ` /** @returns {${jsdocType}${column.getNull() ? " | null" : ""}} */\n`
187
181
  }
188
182
 
189
183
  fileContent += ` ${camelizedColumnName}() { return this.readAttribute("${camelizedColumnName}") }\n\n`
@@ -199,9 +193,7 @@ export default class DbGenerateModel extends BaseCommand {
199
193
 
200
194
  fileContent += ` set${camelizedColumnNameBigFirst}(newValue) { return this._setColumnAttribute("${camelizedColumnName}", newValue) }\n\n`
201
195
 
202
- fileContent += " /**\n"
203
- fileContent += " * @returns {boolean}\n"
204
- fileContent += " */\n"
196
+ fileContent += " /** @returns {boolean} */\n"
205
197
  fileContent += ` has${camelizedColumnNameBigFirst}() { return this._hasAttribute(this.${camelizedColumnName}()) }\n`
206
198
 
207
199
  methodsCount++