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
@@ -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
+ }
@@ -90,6 +90,7 @@
90
90
  * @typedef {object} SyncClientOptions
91
91
  * @property {import("../configuration.js").default} [configuration] - Configuration owning the registered models, the `sync.client` block, and the scope-store database. Defaults to the current configuration.
92
92
  * @property {(args: {scope: SerializedSyncScope}) => string | null | Promise<string | null>} [legacyCursor] - Seeds a newly declared scope's cursor (e.g. from a pre-scope cursor store) so devices don't re-pull everything.
93
+ * @property {import("../remote-request-context.js").RemoteRequestContext} [requestContext] - Immutable scalar context captured for this client and sent with every pull, replay, and realtime subscription.
93
94
  * @property {import("./sync-scope-store.js").default} [scopeStore] - Scope store override (tests).
94
95
  * @property {ReturnType<typeof JSON.parse>} [syncModel] - Pending-sync model override. Defaults to the registered "Sync" model.
95
96
  * @property {string} [databaseIdentifier] - Tenant database identifier; required with tenantHandle.
@@ -109,6 +110,7 @@
109
110
  * @property {(payload: import("./sync-api-client-types.js").SyncChangesRequest & {scope: SerializedSyncScope}) => Promise<import("./sync-api-client-types.js").SyncChangesResponse>} postChanges - Posts one changes request.
110
111
  * @property {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}) => Promise<import("./sync-api-client-types.js").SyncReplayResponse>} postReplay - Posts one replay request.
111
112
  * @property {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration} [realtime] - Realtime push configuration consumed by `subscribeRealtime(...)`.
113
+ * @property {import("../remote-request-context.js").RemoteRequestContext} requestContext - Immutable scalar context captured for this client.
112
114
  * @property {Record<string, SyncClientResourceConfig>} resources - Derived resource policies keyed by resource/model name.
113
115
  * @property {ReturnType<typeof JSON.parse>} syncModel - Local pending-sync model class.
114
116
  * @property {string} [databaseIdentifier] - Tenant-scoped database identifier.
@@ -3,6 +3,7 @@
3
3
  import Configuration from "../configuration.js"
4
4
  import {isBooleanColumnType} from "../database/column-types.js"
5
5
  import Logger from "../logger.js"
6
+ import {captureRemoteRequestContext, mergeRemoteRequestContext} from "../remote-request-context.js"
6
7
  import restArgsError from "../utils/rest-args-error.js"
7
8
  import VelociousWebsocketClient from "../http-client/websocket-client.js"
8
9
 
@@ -28,6 +29,20 @@ const DEFAULT_TRACKED_OPERATIONS = ["create", "update"]
28
29
  /** Attribute names treated as client-local sync bookkeeping when deriving localOnlyAttributes. */
29
30
  const LOCAL_BOOKKEEPING_ATTRIBUTE_NAMES = ["createdAt", "updatedAt", "lastSyncChangeAt"]
30
31
 
32
+ const SYNC_REQUEST_RESERVED_KEYS = [
33
+ "afterId",
34
+ "afterServerSequence",
35
+ "afterUpdatedAt",
36
+ "authenticationToken",
37
+ "limit",
38
+ "scope",
39
+ "syncs",
40
+ "upstreamRefresh",
41
+ "upToId",
42
+ "upToServerSequence",
43
+ "upToUpdatedAt"
44
+ ]
45
+
31
46
  /** @type {WeakMap<Configuration, SyncClient>} */
32
47
  const syncClientsByConfiguration = new WeakMap()
33
48
 
@@ -56,11 +71,15 @@ export default class SyncClient {
56
71
  * @param {import("./sync-client-types.js").SyncClientOptions} [options] - Optional overrides.
57
72
  */
58
73
  constructor(options = {}) {
59
- const {configuration = Configuration.current(), databaseIdentifier, legacyCursor, scopeStore, syncModel, tenantHandle, ...restOptions} = options
74
+ const {configuration = Configuration.current(), databaseIdentifier, legacyCursor, requestContext, scopeStore, syncModel, tenantHandle, ...restOptions} = options
60
75
 
61
76
  restArgsError(restOptions)
62
77
 
63
78
  const clientConfiguration = configuration.getSyncConfiguration().client
79
+ const capturedRequestContext = captureRemoteRequestContext(requestContext, {
80
+ label: "Sync client request context",
81
+ reservedKeys: SYNC_REQUEST_RESERVED_KEYS
82
+ })
64
83
 
65
84
  if (!clientConfiguration) {
66
85
  throw new Error("SyncClient requires a sync.client configuration block: new Configuration({sync: {client: {authenticationToken, transport}}})")
@@ -121,9 +140,10 @@ export default class SyncClient {
121
140
  isOnline: clientConfiguration.isOnline,
122
141
  legacyCursor,
123
142
  onError: clientConfiguration.onError,
124
- postChanges: transportPoster({path: `${clientConfiguration.mountPath}/changes`, transport: clientConfiguration.transport}),
125
- postReplay: transportPoster({path: `${clientConfiguration.mountPath}/replay`, transport: clientConfiguration.transport}),
143
+ postChanges: transportPoster({path: `${clientConfiguration.mountPath}/changes`, requestContext: capturedRequestContext, transport: clientConfiguration.transport}),
144
+ postReplay: transportPoster({path: `${clientConfiguration.mountPath}/replay`, requestContext: capturedRequestContext, transport: clientConfiguration.transport}),
126
145
  realtime: clientConfiguration.realtime,
146
+ requestContext: capturedRequestContext,
127
147
  resources,
128
148
  syncModel: resolvedSyncModel,
129
149
  tenantHandle,
@@ -1267,12 +1287,17 @@ function normalizedTrack(track) {
1267
1287
 
1268
1288
  /**
1269
1289
  * Builds a framework-owned sync endpoint POSTer over the configured transport.
1270
- * @param {{path: string, transport: import("../configuration-types.js").VelociousSyncClientTransport}} args - Poster args.
1290
+ * @param {{path: string, requestContext: import("../remote-request-context.js").RemoteRequestContext, transport: import("../configuration-types.js").VelociousSyncClientTransport}} args - Poster args.
1271
1291
  * @returns {(payload: Record<string, ReturnType<typeof JSON.parse>>) => Promise<ReturnType<typeof JSON.parse>>} Sync endpoint POSTer.
1272
1292
  */
1273
- function transportPoster({path, transport}) {
1293
+ function transportPoster({path, requestContext, transport}) {
1274
1294
  return async (payload) => {
1275
- const response = await transport.post(path, payload)
1295
+ const requestPayload = mergeRemoteRequestContext({
1296
+ context: requestContext,
1297
+ label: "Sync client request context",
1298
+ params: payload
1299
+ })
1300
+ const response = await transport.post(path, requestPayload)
1276
1301
 
1277
1302
  if (!response || typeof response.json !== "function") {
1278
1303
  throw new Error(`sync.client transport.post must resolve to a response with a json() method for ${path} (like the frontend-model websocket client)`)
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import recordChanges from "../database/record-changes.js"
4
+ import {mergeRemoteRequestContext} from "../remote-request-context.js"
4
5
 
5
6
  import SyncApiClient from "./sync-api-client.js"
6
7
  import {VELOCIOUS_SYNC_CHANNEL} from "./sync-channel-name.js"
@@ -141,10 +142,15 @@ export default class SyncRealtimeBridge {
141
142
  }
142
143
 
143
144
  const resourceType = channelDescriptor.resourceType ?? null
145
+ const params = mergeRemoteRequestContext({
146
+ context: this.syncClient.config.requestContext,
147
+ label: "Sync client request context",
148
+ params: {...channelDescriptor.params, authenticationToken}
149
+ })
144
150
  const subscription = client.subscribeChannel(channelDescriptor.channel, {
145
151
  onMessage: (body) => this.enqueueApply({body, resourceType}),
146
152
  onResume: () => this.schedulePull(),
147
- params: {...channelDescriptor.params, authenticationToken}
153
+ params
148
154
  })
149
155
 
150
156
  channels.push({channel: channelDescriptor.channel, resourceType, subscription})
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
- /** @typedef {{coordinator: (callback: () => Promise<unknown>) => Promise<unknown>, ownedQueue: Promise<void>, owner: symbol, reentrantOwners: Set<symbol>, rootOwners: Set<symbol>}} CoordinatorRegistration */
3
+ /** @typedef {{ownedQueue: Promise<void>}} CoordinatorOwnerScope */
4
+ /** @typedef {{connectionScope: CoordinatorOwnerScope, coordinator: (callback: () => Promise<unknown>) => Promise<unknown>, owner: symbol, ownerScopes: Map<symbol, CoordinatorOwnerScope>}} CoordinatorRegistration */
4
5
 
5
6
  /** @type {WeakMap<object, CoordinatorRegistration>} */
6
7
  const coordinators = new WeakMap()
@@ -21,43 +22,47 @@ async function inactiveCoordinator(callback) {
21
22
  * @template T
22
23
  * @param {import("../database/drivers/base.js").default} connection - Parent physical connection.
23
24
  * @param {CoordinatorRegistration} registration - Physical connection registration.
25
+ * @param {CoordinatorOwnerScope} ownerScope - Queue inherited from the current owner.
24
26
  * @param {() => Promise<T>} callback - Owned operation.
27
+ * @param {string} [ownerDescription] - Async owner description.
25
28
  * @returns {Promise<T>} - Operation result.
26
29
  */
27
- async function coordinateOwnedSharedTransactionConnection(connection, registration, callback) {
28
- const previous = registration.ownedQueue
30
+ async function coordinateOwnedSharedTransactionConnection(connection, registration, ownerScope, callback, ownerDescription = "shared-transaction-owned-operation") {
31
+ const previous = ownerScope.ownedQueue
29
32
  /**
30
33
  * Releases the next owned sibling operation.
31
34
  * @type {() => void}
32
35
  */
33
36
  let release = () => {}
34
37
 
35
- registration.ownedQueue = new Promise((resolve) => { release = resolve })
38
+ ownerScope.ownedQueue = new Promise((resolve) => { release = resolve })
36
39
  await previous
37
- const operationOwner = Symbol("shared-transaction-owned-operation")
40
+ const operationOwner = Symbol(ownerDescription)
38
41
  const environmentHandler = connection.configuration.getEnvironmentHandler()
42
+ const operationScope = {ownedQueue: Promise.resolve()}
39
43
 
40
- registration.reentrantOwners.add(operationOwner)
44
+ registration.ownerScopes.set(operationOwner, operationScope)
41
45
  try {
42
46
  return await environmentHandler.runWithSharedTransactionCoordinatorOwner(connection, operationOwner, callback)
43
47
  } finally {
44
- registration.reentrantOwners.delete(operationOwner)
48
+ await drainOwnedSharedTransactionConnections(operationScope)
49
+ registration.ownerScopes.delete(operationOwner)
45
50
  release()
46
51
  }
47
52
  }
48
53
 
49
54
  /**
50
55
  * Drains all inherited operations admitted before the root owner is revoked.
51
- * @param {CoordinatorRegistration} registration - Physical connection registration.
56
+ * @param {CoordinatorOwnerScope} ownerScope - Owner scope to drain.
52
57
  * @returns {Promise<void>} - Resolves when the owned queue stops advancing.
53
58
  */
54
- async function drainOwnedSharedTransactionConnections(registration) {
59
+ async function drainOwnedSharedTransactionConnections(ownerScope) {
55
60
  let tail
56
61
 
57
62
  do {
58
- tail = registration.ownedQueue
63
+ tail = ownerScope.ownedQueue
59
64
  await tail
60
- } while (tail !== registration.ownedQueue)
65
+ } while (tail !== ownerScope.ownedQueue)
61
66
  }
62
67
 
63
68
  /**
@@ -69,17 +74,13 @@ async function drainOwnedSharedTransactionConnections(registration) {
69
74
  * @returns {Promise<T>} - Operation result.
70
75
  */
71
76
  async function coordinateRootSharedTransactionConnection(connection, registration, callback) {
72
- await drainOwnedSharedTransactionConnections(registration)
73
- const rootOwner = Symbol("shared-transaction-root-operation")
74
- const environmentHandler = connection.configuration.getEnvironmentHandler()
75
-
76
- registration.rootOwners.add(rootOwner)
77
- try {
78
- return await environmentHandler.runWithSharedTransactionCoordinatorOwner(connection, rootOwner, callback)
79
- } finally {
80
- await drainOwnedSharedTransactionConnections(registration)
81
- registration.rootOwners.delete(rootOwner)
82
- }
77
+ return await coordinateOwnedSharedTransactionConnection(
78
+ connection,
79
+ registration,
80
+ registration.connectionScope,
81
+ callback,
82
+ "shared-transaction-root-operation"
83
+ )
83
84
  }
84
85
 
85
86
  /**
@@ -96,7 +97,12 @@ export function setSharedTransactionCoordinator(connection, coordinator) {
96
97
  registration.coordinator = coordinator
97
98
  registration.owner = owner
98
99
  } else {
99
- registration = {coordinator, ownedQueue: Promise.resolve(), owner, reentrantOwners: new Set(), rootOwners: new Set()}
100
+ registration = {
101
+ connectionScope: {ownedQueue: Promise.resolve()},
102
+ coordinator,
103
+ owner,
104
+ ownerScopes: new Map()
105
+ }
100
106
  connectionRegistrations.set(connection, registration)
101
107
  }
102
108
 
@@ -135,12 +141,14 @@ export async function coordinateSharedTransactionConnection(connection, callback
135
141
 
136
142
  const environmentHandler = connection.configuration.getEnvironmentHandler()
137
143
  const currentOwner = environmentHandler.getSharedTransactionCoordinatorOwner(connection)
144
+ const currentOwnerScope = currentOwner ? registration.ownerScopes.get(currentOwner) : undefined
138
145
 
139
- if (currentOwner && registration.reentrantOwners.has(currentOwner)) return await callback()
140
- if (currentOwner && registration.rootOwners.has(currentOwner)) {
141
- return await coordinateOwnedSharedTransactionConnection(connection, registration, callback)
146
+ if (currentOwnerScope) {
147
+ return await coordinateOwnedSharedTransactionConnection(connection, registration, currentOwnerScope, callback)
148
+ }
149
+ if (!activeRegistration) {
150
+ return await coordinateOwnedSharedTransactionConnection(connection, registration, registration.connectionScope, callback)
142
151
  }
143
- if (!activeRegistration) return await coordinateOwnedSharedTransactionConnection(connection, registration, callback)
144
152
  if (operationOwner === registration.owner) {
145
153
  return await coordinateRootSharedTransactionConnection(connection, registration, callback)
146
154
  }
@@ -149,3 +157,19 @@ export async function coordinateSharedTransactionConnection(connection, callback
149
157
  return await coordinateRootSharedTransactionConnection(connection, registration, callback)
150
158
  }))
151
159
  }
160
+
161
+ /**
162
+ * Runs physical query work without inheriting this connection's coordinator owner.
163
+ * Unregistered connections have no coordinator ownership to clear.
164
+ * @template T
165
+ * @param {import("../database/drivers/base.js").default} connection - Physical connection.
166
+ * @param {() => Promise<T>} callback - Physical query work.
167
+ * @returns {Promise<T>} - Callback result.
168
+ */
169
+ export async function runWithoutSharedTransactionCoordinatorOwner(connection, callback) {
170
+ if (!connectionRegistrations.has(connection)) return await callback()
171
+
172
+ const environmentHandler = connection.configuration.getEnvironmentHandler()
173
+
174
+ return await environmentHandler.runWithoutSharedTransactionCoordinatorOwner(connection, callback)
175
+ }
@@ -1436,7 +1436,7 @@ export default class TestRunner {
1436
1436
  testSharedConnectionsActive = true
1437
1437
 
1438
1438
  try {
1439
- if (testArgs.databaseCleaning?.transaction === true) {
1439
+ if (testArgs.databaseCleaning?.transaction === true || testArgs.type == "request") {
1440
1440
  sharedTransactionBrokerPreparation = await this.prepareSharedTransactionBroker()
1441
1441
  }
1442
1442