velocious 1.0.620 → 1.0.622

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 (75) hide show
  1. package/README.md +6 -2
  2. package/build/background-jobs/store.js +1 -1
  3. package/build/database/drivers/base.js +20 -3
  4. package/build/database/drivers/mssql/index.js +42 -36
  5. package/build/environment-handlers/base.js +9 -0
  6. package/build/environment-handlers/node.js +16 -0
  7. package/build/frontend-model-controller.js +20 -10
  8. package/build/frontend-models/base.js +90 -25
  9. package/build/frontend-models/remote-request-context.js +29 -0
  10. package/build/http-client/websocket-client.js +32 -0
  11. package/build/http-server/client/websocket-session.js +4 -1
  12. package/build/remote-request-context.js +97 -0
  13. package/build/src/background-jobs/store.js +1 -1
  14. package/build/src/database/drivers/base.d.ts +8 -0
  15. package/build/src/database/drivers/base.d.ts.map +1 -1
  16. package/build/src/database/drivers/base.js +16 -4
  17. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  18. package/build/src/database/drivers/mssql/index.js +47 -41
  19. package/build/src/environment-handlers/base.d.ts +8 -0
  20. package/build/src/environment-handlers/base.d.ts.map +1 -1
  21. package/build/src/environment-handlers/base.js +9 -1
  22. package/build/src/environment-handlers/node.d.ts +8 -0
  23. package/build/src/environment-handlers/node.d.ts.map +1 -1
  24. package/build/src/environment-handlers/node.js +15 -1
  25. package/build/src/frontend-model-controller.d.ts +3 -1
  26. package/build/src/frontend-model-controller.d.ts.map +1 -1
  27. package/build/src/frontend-model-controller.js +11 -7
  28. package/build/src/frontend-models/base.d.ts +4 -0
  29. package/build/src/frontend-models/base.d.ts.map +1 -1
  30. package/build/src/frontend-models/base.js +78 -25
  31. package/build/src/frontend-models/remote-request-context.d.ts +15 -0
  32. package/build/src/frontend-models/remote-request-context.d.ts.map +1 -0
  33. package/build/src/frontend-models/remote-request-context.js +26 -0
  34. package/build/src/http-client/websocket-client.d.ts +8 -0
  35. package/build/src/http-client/websocket-client.d.ts.map +1 -1
  36. package/build/src/http-client/websocket-client.js +31 -1
  37. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  38. package/build/src/http-server/client/websocket-session.js +4 -2
  39. package/build/src/remote-request-context.d.ts +34 -0
  40. package/build/src/remote-request-context.d.ts.map +1 -0
  41. package/build/src/remote-request-context.js +83 -0
  42. package/build/src/sync/sync-client-types.d.ts +10 -0
  43. package/build/src/sync/sync-client-types.d.ts.map +1 -1
  44. package/build/src/sync/sync-client-types.js +3 -1
  45. package/build/src/sync/sync-client.d.ts.map +1 -1
  46. package/build/src/sync/sync-client.js +31 -7
  47. package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
  48. package/build/src/sync/sync-realtime-bridge.js +8 -2
  49. package/build/src/testing/shared-transaction-connection-coordinator.d.ts +14 -3
  50. package/build/src/testing/shared-transaction-connection-coordinator.d.ts.map +1 -1
  51. package/build/src/testing/shared-transaction-connection-coordinator.js +44 -30
  52. package/build/src/testing/test-runner.js +2 -2
  53. package/build/sync/sync-client-types.js +2 -0
  54. package/build/sync/sync-client.js +31 -6
  55. package/build/sync/sync-realtime-bridge.js +7 -1
  56. package/build/testing/shared-transaction-connection-coordinator.js +51 -27
  57. package/build/testing/test-runner.js +1 -1
  58. package/build/tsconfig.tsbuildinfo +1 -1
  59. package/package.json +1 -1
  60. package/src/background-jobs/store.js +1 -1
  61. package/src/database/drivers/base.js +20 -3
  62. package/src/database/drivers/mssql/index.js +42 -36
  63. package/src/environment-handlers/base.js +9 -0
  64. package/src/environment-handlers/node.js +16 -0
  65. package/src/frontend-model-controller.js +20 -10
  66. package/src/frontend-models/base.js +90 -25
  67. package/src/frontend-models/remote-request-context.js +29 -0
  68. package/src/http-client/websocket-client.js +32 -0
  69. package/src/http-server/client/websocket-session.js +4 -1
  70. package/src/remote-request-context.js +97 -0
  71. package/src/sync/sync-client-types.js +2 -0
  72. package/src/sync/sync-client.js +31 -6
  73. package/src/sync/sync-realtime-bridge.js +7 -1
  74. package/src/testing/shared-transaction-connection-coordinator.js +51 -27
  75. package/src/testing/test-runner.js +1 -1
@@ -26,6 +26,8 @@ export default class VelociousWebsocketClient extends SnapReqWebSocketClient {
26
26
  this.reconnectGeneration = 0
27
27
  /** @type {Set<Promise<void>>} */
28
28
  this.runningReconnectTasks = new Set()
29
+ /** @type {Promise<void> | null} */
30
+ this.gracefulClosePromise = null
29
31
  }
30
32
 
31
33
  /**
@@ -55,6 +57,36 @@ export default class VelociousWebsocketClient extends SnapReqWebSocketClient {
55
57
  }
56
58
  }
57
59
 
60
+ /**
61
+ * Closes the WebSocket as a normal shutdown so the server permanently
62
+ * releases resumable session state.
63
+ * @returns {Promise<void>} - Resolves once closed.
64
+ */
65
+ async close() {
66
+ if (this.gracefulClosePromise) return await this.gracefulClosePromise
67
+
68
+ this.autoReconnect = false
69
+ const socket = this.socket
70
+ const closePromise = (async () => {
71
+ if (socket && socket.readyState === socket.OPEN) {
72
+ await new Promise((resolve) => {
73
+ socket.addEventListener("close", () => resolve(undefined), {once: true})
74
+ socket.close(1000)
75
+ })
76
+ }
77
+
78
+ await super.close()
79
+ })()
80
+
81
+ this.gracefulClosePromise = closePromise
82
+
83
+ try {
84
+ await closePromise
85
+ } finally {
86
+ if (this.gracefulClosePromise === closePromise) this.gracefulClosePromise = null
87
+ }
88
+ }
89
+
58
90
  /**
59
91
  * Stops reconnect, drains work that already passed SnapReq's reconnect guard,
60
92
  * and clears state changed by a stale attempt while it settled.
@@ -39,6 +39,7 @@ const WEBSOCKET_OPCODE_CLOSE = 0x8
39
39
  const WEBSOCKET_OPCODE_PING = 0x9
40
40
  const WEBSOCKET_OPCODE_PONG = 0xA
41
41
 
42
+ const WEBSOCKET_CLOSE_NORMAL = 1000
42
43
  const WEBSOCKET_CLOSE_POLICY_VIOLATION = 1008
43
44
  const WEBSOCKET_INBOUND_BACKLOG_CLOSE_REASON = "Inbound message backlog exceeded"
44
45
  const WEBSOCKET_MAX_CLOSE_REASON_BYTES = 123
@@ -785,8 +786,10 @@ export default class VelociousHttpServerClientWebsocketSession {
785
786
  }
786
787
 
787
788
  if (opcode === WEBSOCKET_OPCODE_CLOSE) {
789
+ const allowResume = payload.length < 2 || payload.readUInt16BE(0) !== WEBSOCKET_CLOSE_NORMAL
790
+
788
791
  this.sendGoodbye(this.client)
789
- this._handleClose()
792
+ this._handleClose({allowResume})
790
793
  continue
791
794
  }
792
795
 
@@ -0,0 +1,97 @@
1
+ // @ts-check
2
+
3
+ import VelociousError from "./velocious-error.js"
4
+ import isPlainObject from "./utils/plain-object.js"
5
+
6
+ /** @typedef {Readonly<Record<string, string | number | boolean>>} RemoteRequestContext */
7
+
8
+ const UNSAFE_CONTEXT_KEYS = new Set(["__proto__", "constructor", "prototype"])
9
+
10
+ /**
11
+ * Captures and validates immutable scalar context for one remote operation.
12
+ * @param {ReturnType<typeof JSON.parse> | undefined} value - Configured context value.
13
+ * @param {object} [args] - Validation options.
14
+ * @param {string} [args.label] - Context label used in errors.
15
+ * @param {Iterable<string>} [args.reservedKeys] - Framework-owned keys unavailable to context.
16
+ * @returns {RemoteRequestContext} Frozen context snapshot.
17
+ */
18
+ export function captureRemoteRequestContext(value, {label = "Remote request context", reservedKeys = []} = {}) {
19
+ if (value === undefined || value === null) return Object.freeze({})
20
+
21
+ if (!isPlainObject(value)) {
22
+ throw remoteRequestContextError(`${label} must be a plain object of scalar values`)
23
+ }
24
+
25
+ const reservedKeySet = new Set(reservedKeys)
26
+ /** @type {Record<string, string | number | boolean>} */
27
+ const context = {}
28
+
29
+ for (const key of Object.keys(value).sort()) {
30
+ if (!key.trim()) throw remoteRequestContextError(`${label} keys must be non-blank strings`)
31
+ if (UNSAFE_CONTEXT_KEYS.has(key) || reservedKeySet.has(key)) {
32
+ throw remoteRequestContextError(`${label} key ${JSON.stringify(key)} is reserved by the framework`)
33
+ }
34
+
35
+ const contextValue = value[key]
36
+
37
+ if (!remoteRequestContextScalar(contextValue)) {
38
+ throw remoteRequestContextError(`${label} key ${JSON.stringify(key)} must contain a string, finite number, or boolean scalar`)
39
+ }
40
+
41
+ context[key] = contextValue
42
+ }
43
+
44
+ return Object.freeze(context)
45
+ }
46
+
47
+ /**
48
+ * Merges captured context into framework request params without ambiguity.
49
+ * @template {Record<string, ReturnType<typeof JSON.parse>>} TParams
50
+ * @param {object} args - Merge arguments.
51
+ * @param {RemoteRequestContext} args.context - Captured context.
52
+ * @param {string} [args.label] - Context label used in errors.
53
+ * @param {TParams} args.params - Framework-owned request params.
54
+ * @returns {TParams & RemoteRequestContext} Merged params, or the original params when unscoped.
55
+ */
56
+ export function mergeRemoteRequestContext({context, label = "Remote request context", params}) {
57
+ const contextKeys = Object.keys(context)
58
+
59
+ if (contextKeys.length === 0) return params
60
+
61
+ for (const key of contextKeys) {
62
+ if (Object.hasOwn(params, key)) {
63
+ throw remoteRequestContextError(`${label} key ${JSON.stringify(key)} is reserved by the request payload`)
64
+ }
65
+ }
66
+
67
+ return {...params, ...context}
68
+ }
69
+
70
+ /**
71
+ * Returns a stable identity for an immutable captured context.
72
+ * @param {RemoteRequestContext} context - Captured context.
73
+ * @returns {string} Stable serialized key.
74
+ */
75
+ export function remoteRequestContextKey(context) {
76
+ return JSON.stringify(context)
77
+ }
78
+
79
+ /**
80
+ * Checks whether a value is a supported request-context scalar.
81
+ * @param {ReturnType<typeof JSON.parse>} value - Candidate scalar.
82
+ * @returns {value is string | number | boolean} Whether the value is supported.
83
+ */
84
+ function remoteRequestContextScalar(value) {
85
+ if (["string", "boolean"].includes(typeof value)) return true
86
+
87
+ return typeof value === "number" && Number.isFinite(value)
88
+ }
89
+
90
+ /**
91
+ * Builds a client-safe request-context validation error.
92
+ * @param {string} message - Safe validation message.
93
+ * @returns {VelociousError} Validation error.
94
+ */
95
+ function remoteRequestContextError(message) {
96
+ return VelociousError.safe(message, {code: "remote-request-context-invalid", errorType: "validation_error"})
97
+ }