velocious 1.0.596 → 1.0.598
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 +18 -2
- package/build/background-jobs/main.js +42 -32
- package/build/background-jobs/pooled-runner-broker-identity.js +22 -4
- package/build/background-jobs/store.js +383 -9
- package/build/background-jobs/types.js +1 -0
- package/build/cli/commands/db/tenants/migrations/pending.js +45 -0
- package/build/configuration-types.js +3 -0
- package/build/configuration.js +5 -2
- package/build/database/drivers/sqlite/base.js +3 -3
- package/build/database/tenants/migration-pending-inspector.js +77 -0
- package/build/environment-handlers/base.js +8 -0
- package/build/environment-handlers/node.js +14 -0
- package/build/mailer/backends/resend-smtp.js +121 -0
- package/build/mailer/base.js +52 -22
- package/build/mailer/delivery-operation-store.js +152 -0
- package/build/mailer/delivery-operation.js +205 -0
- package/build/mailer/delivery.js +9 -4
- package/build/mailer/index.js +5 -1
- package/build/mailer.js +6 -1
- package/build/src/background-jobs/main.d.ts +15 -10
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +42 -34
- package/build/src/background-jobs/pooled-runner-broker-identity.d.ts +10 -1
- package/build/src/background-jobs/pooled-runner-broker-identity.d.ts.map +1 -1
- package/build/src/background-jobs/pooled-runner-broker-identity.js +23 -5
- package/build/src/background-jobs/store.d.ts +154 -0
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +350 -10
- package/build/src/background-jobs/types.d.ts +5 -0
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +2 -1
- package/build/src/cli/commands/db/tenants/migrations/pending.d.ts +15 -0
- package/build/src/cli/commands/db/tenants/migrations/pending.d.ts.map +1 -0
- package/build/src/cli/commands/db/tenants/migrations/pending.js +40 -0
- package/build/src/configuration-types.d.ts +20 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +4 -1
- package/build/src/configuration.d.ts +2 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +6 -3
- package/build/src/database/drivers/sqlite/base.js +4 -4
- package/build/src/database/tenants/migration-pending-inspector.d.ts +38 -0
- package/build/src/database/tenants/migration-pending-inspector.d.ts.map +1 -0
- package/build/src/database/tenants/migration-pending-inspector.js +70 -0
- package/build/src/environment-handlers/base.d.ts +10 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +8 -1
- package/build/src/environment-handlers/node.d.ts +11 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +14 -1
- package/build/src/mailer/backends/resend-smtp.d.ts +43 -0
- package/build/src/mailer/backends/resend-smtp.d.ts.map +1 -0
- package/build/src/mailer/backends/resend-smtp.js +106 -0
- package/build/src/mailer/base.d.ts +9 -2
- package/build/src/mailer/base.d.ts.map +1 -1
- package/build/src/mailer/base.js +41 -19
- package/build/src/mailer/delivery-operation-store.d.ts +52 -0
- package/build/src/mailer/delivery-operation-store.d.ts.map +1 -0
- package/build/src/mailer/delivery-operation-store.js +130 -0
- package/build/src/mailer/delivery-operation.d.ts +73 -0
- package/build/src/mailer/delivery-operation.d.ts.map +1 -0
- package/build/src/mailer/delivery-operation.js +185 -0
- package/build/src/mailer/delivery.d.ts +4 -2
- package/build/src/mailer/delivery.d.ts.map +1 -1
- package/build/src/mailer/delivery.js +9 -5
- package/build/src/mailer/index.d.ts +24 -1
- package/build/src/mailer/index.d.ts.map +1 -1
- package/build/src/mailer/index.js +6 -2
- package/build/src/mailer.d.ts +11 -13
- package/build/src/mailer.d.ts.map +1 -1
- package/build/src/mailer.js +7 -2
- package/build/src/sync/local-mutation-log.d.ts +5 -5
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +34 -10
- package/build/src/sync/stable-json.d.ts +1 -8
- package/build/src/sync/stable-json.d.ts.map +1 -1
- package/build/src/sync/stable-json.js +2 -27
- package/build/src/sync/sync-envelope-replay-service.d.ts +3 -2
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +12 -8
- package/build/src/testing/factory/node/definition-reload-policy.d.ts +97 -0
- package/build/src/testing/factory/node/definition-reload-policy.d.ts.map +1 -0
- package/build/src/testing/factory/node/definition-reload-policy.js +133 -0
- package/build/src/testing/factory/node/load-definitions.d.ts +4 -1
- package/build/src/testing/factory/node/load-definitions.d.ts.map +1 -1
- package/build/src/testing/factory/node/load-definitions.js +27 -4
- package/build/src/testing/test-runner.js +7 -7
- package/build/src/utils/stable-json.d.ts +7 -0
- package/build/src/utils/stable-json.d.ts.map +1 -0
- package/build/src/utils/stable-json.js +25 -0
- package/build/sync/local-mutation-log.js +37 -6
- package/build/sync/stable-json.js +1 -28
- package/build/sync/sync-envelope-replay-service.js +11 -7
- package/build/testing/factory/node/definition-reload-policy.js +149 -0
- package/build/testing/factory/node/load-definitions.js +29 -5
- package/build/testing/test-runner.js +6 -6
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/stable-json.js +26 -0
- package/package.json +2 -1
- package/src/background-jobs/main.js +42 -32
- package/src/background-jobs/pooled-runner-broker-identity.js +22 -4
- package/src/background-jobs/store.js +383 -9
- package/src/background-jobs/types.js +1 -0
- package/src/cli/commands/db/tenants/migrations/pending.js +45 -0
- package/src/configuration-types.js +3 -0
- package/src/configuration.js +5 -2
- package/src/database/drivers/sqlite/base.js +3 -3
- package/src/database/tenants/migration-pending-inspector.js +77 -0
- package/src/environment-handlers/base.js +8 -0
- package/src/environment-handlers/node.js +14 -0
- package/src/mailer/backends/resend-smtp.js +121 -0
- package/src/mailer/base.js +52 -22
- package/src/mailer/delivery-operation-store.js +152 -0
- package/src/mailer/delivery-operation.js +205 -0
- package/src/mailer/delivery.js +9 -4
- package/src/mailer/index.js +5 -1
- package/src/mailer.js +6 -1
- package/src/sync/local-mutation-log.js +37 -6
- package/src/sync/stable-json.js +1 -28
- package/src/sync/sync-envelope-replay-service.js +11 -7
- package/src/testing/factory/node/definition-reload-policy.js +149 -0
- package/src/testing/factory/node/load-definitions.js +29 -5
- package/src/testing/test-runner.js +6 -6
- package/src/utils/stable-json.js +26 -0
package/src/mailer.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
/** @typedef {
|
|
3
|
+
/** @typedef {import("./mailer/index.js").MailerDeliveryOperationRequest} MailerDeliveryOperationRequest */
|
|
4
|
+
/** @typedef {import("./mailer/index.js").MailerDeliveryOperation} MailerDeliveryOperation */
|
|
5
|
+
/** @typedef {import("./mailer/index.js").MailerDeliveryIdempotencyCapability} MailerDeliveryIdempotencyCapability */
|
|
6
|
+
/** @typedef {import("./mailer/index.js").MailerDeliveryLaterOptions} MailerDeliveryLaterOptions */
|
|
7
|
+
/** @typedef {import("./mailer/index.js").MailerDeliveryPayload} MailerDeliveryPayload */
|
|
4
8
|
|
|
5
9
|
export {
|
|
6
10
|
VelociousMailerBase,
|
|
@@ -12,4 +16,5 @@ export {
|
|
|
12
16
|
setDeliveryHandler
|
|
13
17
|
} from "./mailer/index.js"
|
|
14
18
|
export {default as SmtpMailerBackend} from "./mailer/backends/smtp.js"
|
|
19
|
+
export {default as ResendSmtpMailerBackend} from "./mailer/backends/resend-smtp.js"
|
|
15
20
|
export {default} from "./mailer/index.js"
|
|
@@ -37,11 +37,12 @@
|
|
|
37
37
|
* @property {import("./device-identity.js").SignedSyncMutation} [signedMutation] - Original signed mutation envelope, retained for peer-forwarded mutations.
|
|
38
38
|
* @property {number} sequence - Monotonic local sequence.
|
|
39
39
|
* @property {LocalMutationStatus} status - Local replay/apply status.
|
|
40
|
-
* @property {Record<string, import("../
|
|
40
|
+
* @property {Record<string, import("../frontend-models/base.js").FrontendModelTransportValue>} [syncResult] - Backend replay/result metadata. Stored as transport markers so Date (and other typed) values survive the durable JSON round trip and are restored on readback.
|
|
41
41
|
* @property {string} updatedAt - ISO timestamp when the record was last changed.
|
|
42
42
|
*/
|
|
43
43
|
// @ts-check
|
|
44
44
|
|
|
45
|
+
import {deserializeFrontendModelTransportValue, serializeFrontendModelTransportValue} from "../frontend-models/transport-serialization.js"
|
|
45
46
|
import stableJsonStringify from "./stable-json.js"
|
|
46
47
|
|
|
47
48
|
const DEFAULT_STORAGE_KEY = "velocious.sync.localMutationLog"
|
|
@@ -126,7 +127,7 @@ export default class LocalMutationLog {
|
|
|
126
127
|
* @param {object} args - Arguments.
|
|
127
128
|
* @param {string} args.id - Record id.
|
|
128
129
|
* @param {LocalMutationStatus} args.status - New status.
|
|
129
|
-
* @param {Record<string, import("../
|
|
130
|
+
* @param {Record<string, import("../frontend-models/base.js").FrontendModelTransportValue>} [args.syncResult] - Result metadata (may carry transport-restored typed values).
|
|
130
131
|
* @returns {Promise<LocalMutationLogRecord>} - Updated record.
|
|
131
132
|
*/
|
|
132
133
|
async updateStatus({id, status, syncResult}) {
|
|
@@ -140,11 +141,16 @@ export default class LocalMutationLog {
|
|
|
140
141
|
const record = normalizeRecord(rawRecord)
|
|
141
142
|
|
|
142
143
|
record.status = /** @type {LocalMutationStatus} */ (status)
|
|
143
|
-
if (syncResult !== undefined)
|
|
144
|
+
if (syncResult !== undefined) {
|
|
145
|
+
// Encode transport-restored typed values (e.g. Date attributes in a
|
|
146
|
+
// conflict serverModel) as markers before the JSON clone so the
|
|
147
|
+
// durable persistence round trip cannot stringify them.
|
|
148
|
+
record.syncResult = cloneJsonObject(serializeFrontendModelTransportValue(syncResult), "syncResult")
|
|
149
|
+
}
|
|
144
150
|
record.updatedAt = this.currentTimestamp()
|
|
145
151
|
await this.storage.updateRecord(this.storageKey, cloneRecord(record))
|
|
146
152
|
|
|
147
|
-
return cloneRecord(record)
|
|
153
|
+
return restoreSyncResultTypes(cloneRecord(record))
|
|
148
154
|
})
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -168,7 +174,7 @@ export default class LocalMutationLog {
|
|
|
168
174
|
record.updatedAt = this.currentTimestamp()
|
|
169
175
|
await this.storage.updateRecord(this.storageKey, cloneRecord(record))
|
|
170
176
|
|
|
171
|
-
return cloneRecord(record)
|
|
177
|
+
return restoreSyncResultTypes(cloneRecord(record))
|
|
172
178
|
})
|
|
173
179
|
}
|
|
174
180
|
|
|
@@ -270,6 +276,25 @@ function normalizeRecordList(records) {
|
|
|
270
276
|
.map(normalizeRecord)
|
|
271
277
|
.sort((left, right) => left.sequence - right.sequence)
|
|
272
278
|
.map((record) => cloneRecord(record))
|
|
279
|
+
.map(restoreSyncResultTypes)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Restores transport-restored typed values in a record's syncResult after the
|
|
284
|
+
* final JSON clone, so callers see Date (and other typed) values on durable
|
|
285
|
+
* readback instead of marker-encoded ISO strings.
|
|
286
|
+
* @param {LocalMutationLogRecord} record - Cloned record.
|
|
287
|
+
* @returns {LocalMutationLogRecord} - Record with restored syncResult types.
|
|
288
|
+
*/
|
|
289
|
+
function restoreSyncResultTypes(record) {
|
|
290
|
+
if (record.syncResult === undefined) return record
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
...record,
|
|
294
|
+
syncResult: /** @type {Record<string, import("../frontend-models/base.js").FrontendModelTransportValue>} */ (
|
|
295
|
+
deserializeFrontendModelTransportValue(record.syncResult)
|
|
296
|
+
)
|
|
297
|
+
}
|
|
273
298
|
}
|
|
274
299
|
|
|
275
300
|
/**
|
|
@@ -328,7 +353,13 @@ function normalizeRecord(value) {
|
|
|
328
353
|
if (record.signedMutation !== undefined) {
|
|
329
354
|
normalizedRecord.signedMutation = /** @type {import("./device-identity.js").SignedSyncMutation} */ (cloneJsonObject(record.signedMutation, "signedMutation"))
|
|
330
355
|
}
|
|
331
|
-
if (record.syncResult !== undefined)
|
|
356
|
+
if (record.syncResult !== undefined) {
|
|
357
|
+
// Persisted syncResult is marker-encoded so it survives the JSON clone;
|
|
358
|
+
// typed values are restored by restoreSyncResultTypes at the read boundary.
|
|
359
|
+
normalizedRecord.syncResult = /** @type {Record<string, import("../frontend-models/base.js").FrontendModelTransportValue>} */ (
|
|
360
|
+
cloneJsonObject(record.syncResult, "syncResult")
|
|
361
|
+
)
|
|
362
|
+
}
|
|
332
363
|
|
|
333
364
|
return normalizedRecord
|
|
334
365
|
}
|
package/src/sync/stable-json.js
CHANGED
|
@@ -1,28 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Serializes a JSON-compatible value with recursively sorted object keys, so
|
|
5
|
-
* equal values always produce byte-identical strings (used for sync scope and
|
|
6
|
-
* change-feed identity comparisons).
|
|
7
|
-
* @param {ReturnType<typeof JSON.parse>} value - JSON-compatible value.
|
|
8
|
-
* @returns {string} - Stable JSON string.
|
|
9
|
-
*/
|
|
10
|
-
export default function stableJsonStringify(value) {
|
|
11
|
-
return JSON.stringify(stableJsonValue(value))
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Produces a recursively key-sorted JSON value.
|
|
16
|
-
* @param {ReturnType<typeof JSON.parse>} value - JSON-compatible value.
|
|
17
|
-
* @returns {ReturnType<typeof JSON.parse>} - Stable JSON-compatible value.
|
|
18
|
-
*/
|
|
19
|
-
function stableJsonValue(value) {
|
|
20
|
-
if (Array.isArray(value)) return value.map((item) => stableJsonValue(item))
|
|
21
|
-
if (!value || typeof value !== "object") return value
|
|
22
|
-
|
|
23
|
-
return Object.keys(value).sort().reduce((memo, key) => {
|
|
24
|
-
memo[key] = stableJsonValue(value[key])
|
|
25
|
-
|
|
26
|
-
return memo
|
|
27
|
-
}, /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ ({}))
|
|
28
|
-
}
|
|
1
|
+
export {default} from "../utils/stable-json.js"
|
|
@@ -841,8 +841,9 @@ export default class SyncEnvelopeReplayService {
|
|
|
841
841
|
* Projects affected mutation fields through the resource's readable
|
|
842
842
|
* attribute contract. Writable-but-hidden fields are omitted, while custom
|
|
843
843
|
* `<attribute>Attribute(model)` serializers and model accessors remain the
|
|
844
|
-
* source of frontend-visible values
|
|
845
|
-
*
|
|
844
|
+
* source of frontend-visible values (Date values are kept raw so the normal
|
|
845
|
+
* frontend-model transport serializer can emit its date marker). The full
|
|
846
|
+
* model attribute hash is never exposed.
|
|
846
847
|
* @param {object} args - Projection args.
|
|
847
848
|
* @param {Record<string, ReturnType<typeof JSON.parse>>} args.attributes - Permitted affected mutation attributes.
|
|
848
849
|
* @param {import("../database/record/index.js").default} args.existingRecord - Authorized server record.
|
|
@@ -885,7 +886,7 @@ export default class SyncEnvelopeReplayService {
|
|
|
885
886
|
const resourceAttribute = resource.resourceMethod(`${attributeName}Attribute`)
|
|
886
887
|
|
|
887
888
|
if (resourceAttribute) {
|
|
888
|
-
serializedAttributes[affectedField] =
|
|
889
|
+
serializedAttributes[affectedField] = await resourceAttribute.method.call(resourceAttribute.resource, existingRecord)
|
|
889
890
|
continue
|
|
890
891
|
}
|
|
891
892
|
|
|
@@ -893,9 +894,9 @@ export default class SyncEnvelopeReplayService {
|
|
|
893
894
|
const attributeMethod = recordMethods[attributeName]
|
|
894
895
|
|
|
895
896
|
if (typeof attributeMethod === "function") {
|
|
896
|
-
serializedAttributes[affectedField] =
|
|
897
|
+
serializedAttributes[affectedField] = await attributeMethod.call(existingRecord)
|
|
897
898
|
} else {
|
|
898
|
-
serializedAttributes[affectedField] =
|
|
899
|
+
serializedAttributes[affectedField] = existingRecord.readAttribute(attributeName)
|
|
899
900
|
}
|
|
900
901
|
}
|
|
901
902
|
|
|
@@ -1166,8 +1167,11 @@ export function syncReplayConflictLockName({resourceId, resourceType}) {
|
|
|
1166
1167
|
}
|
|
1167
1168
|
|
|
1168
1169
|
/**
|
|
1169
|
-
* Normalizes
|
|
1170
|
-
*
|
|
1170
|
+
* Normalizes a version value for deterministic comparison and transport.
|
|
1171
|
+
* Only version values participate in stable-JSON comparison against client
|
|
1172
|
+
* `baseVersion` strings; resource serializer/accessor results must stay raw so
|
|
1173
|
+
* the frontend-model transport serializer can retain Date markers.
|
|
1174
|
+
* @param {ReturnType<typeof JSON.parse>} value - Raw version value from a database record.
|
|
1171
1175
|
* @returns {ReturnType<typeof JSON.parse>} - Normalized value (Date values become ISO strings).
|
|
1172
1176
|
*/
|
|
1173
1177
|
function normalizeConflictValue(value) {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default maximum number of cache-busted factory definition import attempts a
|
|
5
|
+
* single Node process may perform. Chosen conservatively from the
|
|
6
|
+
* `factory-esm-reload-retention` benchmark evidence: each cache-busted import
|
|
7
|
+
* retains roughly 6 KB of heap in Node's ESM module map, so the default bounds
|
|
8
|
+
* retained definition modules to a few tens of MB before the owning process
|
|
9
|
+
* must be recycled. This module is intentionally Node-only; browser-safe factory
|
|
10
|
+
* code must never import it.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_DEFINITION_RELOAD_BUDGET = 4096
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rejected when code tries to configure the process-global reload budget more
|
|
16
|
+
* than once or after any valid cache-busted import reservation was attempted.
|
|
17
|
+
* Retained ESM modules and their accounting live for the process lifetime, so
|
|
18
|
+
* changing the budget can never begin a new in-process policy epoch.
|
|
19
|
+
*/
|
|
20
|
+
export class DefinitionReloadConfigurationError extends Error {
|
|
21
|
+
/**
|
|
22
|
+
* Creates the error.
|
|
23
|
+
* @param {object} args - Details.
|
|
24
|
+
* @param {number} args.current - Cache-busted imports already reserved.
|
|
25
|
+
* @param {number} args.budget - Active process-global import budget.
|
|
26
|
+
* @param {number} args.requestedBudget - Rejected replacement budget.
|
|
27
|
+
* @param {boolean} args.configured - Whether an explicit budget was already configured.
|
|
28
|
+
*/
|
|
29
|
+
constructor({budget, configured, current, requestedBudget}) {
|
|
30
|
+
const reason = configured
|
|
31
|
+
? "the process-global definition reload budget was already configured"
|
|
32
|
+
: "a definition reload reservation was already attempted"
|
|
33
|
+
|
|
34
|
+
super(`Cannot configure definition reload budget to ${requestedBudget}: ${reason} (current=${current}, budget=${budget}). Configuration is allowed exactly once before the first reservation; only process exit resets retained-module accounting.`)
|
|
35
|
+
this.name = "DefinitionReloadConfigurationError"
|
|
36
|
+
this.budget = budget
|
|
37
|
+
this.configured = configured
|
|
38
|
+
this.current = current
|
|
39
|
+
this.requestedBudget = requestedBudget
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Rejected when a reload would push the process over its cache-busted import
|
|
45
|
+
* budget. The rejection happens synchronously before any registry reset or
|
|
46
|
+
* import, so the currently loaded registry stays usable. Node never evicts
|
|
47
|
+
* retained ESM module instances, so only recycling/restarting the owning Node
|
|
48
|
+
* process reclaims the memory and refreshes edited dependency modules.
|
|
49
|
+
*/
|
|
50
|
+
export class DefinitionRecycleRequiredError extends Error {
|
|
51
|
+
/**
|
|
52
|
+
* Creates the error.
|
|
53
|
+
* @param {object} args - Details.
|
|
54
|
+
* @param {number} args.current - Cache-busted import attempts already reserved in this process.
|
|
55
|
+
* @param {number} args.budget - Process-global import budget.
|
|
56
|
+
* @param {number} args.requested - Import attempts the rejected reload needed.
|
|
57
|
+
*/
|
|
58
|
+
constructor({current, budget, requested}) {
|
|
59
|
+
super(`Factory definition reload import budget exhausted (current=${current}, budget=${budget}, requested=${requested}). Recycle or restart the owning Node process: every reload imports a fresh cache-busted module instance and Node never evicts them, so process recycling is the only reclamation boundary.`)
|
|
60
|
+
this.name = "DefinitionRecycleRequiredError"
|
|
61
|
+
this.current = current
|
|
62
|
+
this.budget = budget
|
|
63
|
+
this.requested = requested
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @type {number} - The single process-global import budget. */
|
|
68
|
+
let importBudget = DEFAULT_DEFINITION_RELOAD_BUDGET
|
|
69
|
+
|
|
70
|
+
/** @type {number} - Cache-busted import attempts reserved so far across every registry and target. */
|
|
71
|
+
let reservedImports = 0
|
|
72
|
+
|
|
73
|
+
/** @type {boolean} - Whether the process-global budget was explicitly configured. */
|
|
74
|
+
let budgetConfigured = false
|
|
75
|
+
|
|
76
|
+
/** @type {boolean} - Whether any valid complete reload batch reservation was attempted. */
|
|
77
|
+
let reservationStarted = false
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns the process-global cache-busted import budget.
|
|
81
|
+
* @returns {number} - The budget.
|
|
82
|
+
*/
|
|
83
|
+
export function getDefinitionReloadBudget() {
|
|
84
|
+
return importBudget
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reads the cache-busted import attempts reserved so far in this process,
|
|
89
|
+
* across every registry and target. Combined with {@link getDefinitionReloadBudget}
|
|
90
|
+
* this is the deterministic process-global census for the recycle policy.
|
|
91
|
+
* @returns {number} - Reserved count.
|
|
92
|
+
*/
|
|
93
|
+
export function peekDefinitionReloadBudget() {
|
|
94
|
+
return reservedImports
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Configures the one process-global import budget exactly once and only before
|
|
99
|
+
* the first valid reservation attempt. There is exactly one budget for the whole
|
|
100
|
+
* process, so no combination of registries or targets can create independent
|
|
101
|
+
* budgets that defeat the global limit. Retained-import accounting is never
|
|
102
|
+
* reset in-process.
|
|
103
|
+
* @param {number} budget - New budget.
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
export function setDefinitionReloadBudget(budget) {
|
|
107
|
+
if (!Number.isInteger(budget) || budget < 1) {
|
|
108
|
+
throw new TypeError(`Definition reload budget must be a positive integer, got ${JSON.stringify(budget)}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (budgetConfigured || reservationStarted) {
|
|
112
|
+
throw new DefinitionReloadConfigurationError({
|
|
113
|
+
budget: importBudget,
|
|
114
|
+
configured: budgetConfigured,
|
|
115
|
+
current: reservedImports,
|
|
116
|
+
requestedBudget: budget
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
importBudget = budget
|
|
121
|
+
budgetConfigured = true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Preflights and reserves a whole reload batch synchronously. Malformed counts
|
|
126
|
+
* are rejected before configuration is sealed or accounting changes. Every valid
|
|
127
|
+
* request, including zero and a rejected over-budget request, seals configuration
|
|
128
|
+
* before capacity is evaluated. Throws {@link DefinitionRecycleRequiredError}
|
|
129
|
+
* when the requested batch would push the process over its budget. The check and
|
|
130
|
+
* reservation run in one synchronous step, so concurrent reloads cannot race past
|
|
131
|
+
* the budget. The reservation is deliberately conservative: it covers every
|
|
132
|
+
* import attempt in the batch, so a mid-batch import failure still counts its
|
|
133
|
+
* attempts as retained modules.
|
|
134
|
+
* @param {number} requested - Cache-busted import attempts the reload will perform.
|
|
135
|
+
* @returns {void}
|
|
136
|
+
*/
|
|
137
|
+
export function reserveDefinitionReloadBudget(requested) {
|
|
138
|
+
if (!Number.isInteger(requested) || requested < 0) {
|
|
139
|
+
throw new TypeError(`Definition reload reservation must be a non-negative integer, got ${typeof requested} ${String(requested)}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
reservationStarted = true
|
|
143
|
+
|
|
144
|
+
if (reservedImports + requested > importBudget) {
|
|
145
|
+
throw new DefinitionRecycleRequiredError({current: reservedImports, budget: importBudget, requested})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
reservedImports += requested
|
|
149
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {pathToFileURL} from "node:url"
|
|
4
|
-
import {readdir, stat} from "node:fs/promises"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
4
|
+
import { readdir, stat } from "node:fs/promises"
|
|
5
5
|
import path from "node:path"
|
|
6
|
+
import { reserveDefinitionReloadBudget } from "./definition-reload-policy.js"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Monotonic cache-busting counter shared by reloads. Kept module-local so a reload
|
|
@@ -68,6 +69,26 @@ async function resolveFiles(target) {
|
|
|
68
69
|
export async function loadDefinitions(registry, target, {reload = false} = {}) {
|
|
69
70
|
const files = await resolveFiles(target)
|
|
70
71
|
|
|
72
|
+
return await loadResolvedDefinitionFiles({files, registry, reload})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Loads definition files that have already been resolved into a deterministic
|
|
77
|
+
* sorted list. When `reload` is set, the whole batch is preflighted and reserved
|
|
78
|
+
* against the process-global import budget before any registry reset or import
|
|
79
|
+
* attempt, so a rejected reload never mutates the registry.
|
|
80
|
+
* @param {object} args - Options object.
|
|
81
|
+
* @param {string[]} args.files - Resolved, sorted definition file paths.
|
|
82
|
+
* @param {import("../factory-registry.js").default} args.registry - Registry to define into.
|
|
83
|
+
* @param {boolean} args.reload - Whether to cache-bust the imports.
|
|
84
|
+
* @param {boolean} [args.reset] - Whether to reset the registry first.
|
|
85
|
+
* @returns {Promise<string[]>} - The loaded file paths, in load order.
|
|
86
|
+
*/
|
|
87
|
+
async function loadResolvedDefinitionFiles({files, registry, reload, reset = false}) {
|
|
88
|
+
if (reload) reserveDefinitionReloadBudget(files.length)
|
|
89
|
+
|
|
90
|
+
if (reset) registry.reset()
|
|
91
|
+
|
|
71
92
|
for (const file of files) {
|
|
72
93
|
let href = pathToFileURL(file).href
|
|
73
94
|
|
|
@@ -91,13 +112,16 @@ export async function loadDefinitions(registry, target, {reload = false} = {}) {
|
|
|
91
112
|
/**
|
|
92
113
|
* Fully reloads definitions: resets the registry (dropping every factory, trait,
|
|
93
114
|
* sequence, callback and default) and re-imports the target files with cache
|
|
94
|
-
* busting so edited definitions take effect.
|
|
115
|
+
* busting so edited definitions take effect. The resolved batch is preflighted
|
|
116
|
+
* against the process-global import budget before the reset, and a
|
|
117
|
+
* `DefinitionRecycleRequiredError` is raised before any mutation when the batch
|
|
118
|
+
* would exceed the budget.
|
|
95
119
|
* @param {import("../factory-registry.js").default} registry - Registry to reload.
|
|
96
120
|
* @param {string | string[]} target - File path, directory, or list of paths.
|
|
97
121
|
* @returns {Promise<string[]>} - The reloaded file paths, in load order.
|
|
98
122
|
*/
|
|
99
123
|
export async function reloadDefinitions(registry, target) {
|
|
100
|
-
|
|
124
|
+
const files = await resolveFiles(target)
|
|
101
125
|
|
|
102
|
-
return await
|
|
126
|
+
return await loadResolvedDefinitionFiles({files, registry, reload: true, reset: true})
|
|
103
127
|
}
|
|
@@ -1058,12 +1058,12 @@ export default class TestRunner {
|
|
|
1058
1058
|
// back). Releasing the lease after each lifecycle also runs the pool's
|
|
1059
1059
|
// session cleanup before another test can reuse the connection.
|
|
1060
1060
|
await this.getConfiguration().ensureConnections({name: `Test: ${testDescription}`}, async () => {
|
|
1061
|
-
// Register dynamic candidates before
|
|
1062
|
-
//
|
|
1063
|
-
//
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1061
|
+
// Register dynamic candidates before hooks so transaction state changes
|
|
1062
|
+
// made during a hook are immediately visible to any in-process work.
|
|
1063
|
+
// Long-lived services such as a background-jobs main can dispatch DB
|
|
1064
|
+
// work between a transaction-starting hook and broker activation just
|
|
1065
|
+
// as an HTTP request can dispatch work from inside the hook itself.
|
|
1066
|
+
testSharedConnectionRegistrations = this.activateTestSharedConnections()
|
|
1067
1067
|
|
|
1068
1068
|
try {
|
|
1069
1069
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serializes a JSON-compatible value with recursively sorted object keys.
|
|
5
|
+
* @param {ReturnType<typeof JSON.parse>} value - JSON-compatible value.
|
|
6
|
+
* @returns {string} - Stable JSON string.
|
|
7
|
+
*/
|
|
8
|
+
export default function stableJsonStringify(value) {
|
|
9
|
+
return JSON.stringify(stableJsonValue(value))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Produces a recursively key-sorted JSON value.
|
|
14
|
+
* @param {ReturnType<typeof JSON.parse>} value - JSON-compatible value.
|
|
15
|
+
* @returns {ReturnType<typeof JSON.parse>} - Stable JSON-compatible value.
|
|
16
|
+
*/
|
|
17
|
+
function stableJsonValue(value) {
|
|
18
|
+
if (Array.isArray(value)) return value.map((item) => stableJsonValue(item))
|
|
19
|
+
if (!value || typeof value !== "object") return value
|
|
20
|
+
|
|
21
|
+
return Object.keys(value).sort().reduce((memo, key) => {
|
|
22
|
+
memo[key] = stableJsonValue(value[key])
|
|
23
|
+
|
|
24
|
+
return memo
|
|
25
|
+
}, /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ ({}))
|
|
26
|
+
}
|