velocious 1.0.552 → 1.0.555
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 +2 -0
- package/build/background-jobs/status-reporter.js +15 -2
- package/build/background-jobs/worker.js +5 -1
- package/build/frontend-models/base.js +105 -47
- package/build/frontend-models/transport-deadline.js +66 -0
- package/build/src/background-jobs/status-reporter.d.ts +3 -1
- package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
- package/build/src/background-jobs/status-reporter.js +16 -3
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +6 -2
- package/build/src/frontend-models/base.d.ts +10 -1
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +71 -26
- package/build/src/frontend-models/transport-deadline.d.ts +26 -0
- package/build/src/frontend-models/transport-deadline.d.ts.map +1 -0
- package/build/src/frontend-models/transport-deadline.js +61 -0
- package/build/src/testing/factory/association-declaration.d.ts +37 -0
- package/build/src/testing/factory/association-declaration.d.ts.map +1 -0
- package/build/src/testing/factory/association-declaration.js +41 -0
- package/build/src/testing/factory/declarations.d.ts +190 -0
- package/build/src/testing/factory/declarations.d.ts.map +1 -0
- package/build/src/testing/factory/declarations.js +98 -0
- package/build/src/testing/factory/definition-builder.d.ts +132 -0
- package/build/src/testing/factory/definition-builder.d.ts.map +1 -0
- package/build/src/testing/factory/definition-builder.js +417 -0
- package/build/src/testing/factory/errors.d.ts +62 -0
- package/build/src/testing/factory/errors.d.ts.map +1 -0
- package/build/src/testing/factory/errors.js +67 -0
- package/build/src/testing/factory/evaluation-context.d.ts +95 -0
- package/build/src/testing/factory/evaluation-context.d.ts.map +1 -0
- package/build/src/testing/factory/evaluation-context.js +175 -0
- package/build/src/testing/factory/events.d.ts +21 -0
- package/build/src/testing/factory/events.d.ts.map +1 -0
- package/build/src/testing/factory/events.js +26 -0
- package/build/src/testing/factory/factory-definition.d.ts +39 -0
- package/build/src/testing/factory/factory-definition.d.ts.map +1 -0
- package/build/src/testing/factory/factory-definition.js +35 -0
- package/build/src/testing/factory/factory-registry.d.ts +263 -0
- package/build/src/testing/factory/factory-registry.d.ts.map +1 -0
- package/build/src/testing/factory/factory-registry.js +438 -0
- package/build/src/testing/factory/factory-runner.d.ts +179 -0
- package/build/src/testing/factory/factory-runner.d.ts.map +1 -0
- package/build/src/testing/factory/factory-runner.js +266 -0
- package/build/src/testing/factory/index.d.ts +18 -0
- package/build/src/testing/factory/index.d.ts.map +1 -0
- package/build/src/testing/factory/index.js +21 -0
- package/build/src/testing/factory/linter.d.ts +55 -0
- package/build/src/testing/factory/linter.d.ts.map +1 -0
- package/build/src/testing/factory/linter.js +106 -0
- package/build/src/testing/factory/model-contract.d.ts +11 -0
- package/build/src/testing/factory/model-contract.d.ts.map +1 -0
- package/build/src/testing/factory/model-contract.js +26 -0
- package/build/src/testing/factory/node/load-definitions.d.ts +24 -0
- package/build/src/testing/factory/node/load-definitions.d.ts.map +1 -0
- package/build/src/testing/factory/node/load-definitions.js +87 -0
- package/build/src/testing/factory/sequence.d.ts +73 -0
- package/build/src/testing/factory/sequence.d.ts.map +1 -0
- package/build/src/testing/factory/sequence.js +78 -0
- package/build/src/testing/factory/strategies/attributes-for.d.ts +21 -0
- package/build/src/testing/factory/strategies/attributes-for.d.ts.map +1 -0
- package/build/src/testing/factory/strategies/attributes-for.js +22 -0
- package/build/src/testing/factory/strategies/base.d.ts +101 -0
- package/build/src/testing/factory/strategies/base.d.ts.map +1 -0
- package/build/src/testing/factory/strategies/base.js +191 -0
- package/build/src/testing/factory/strategies/build.d.ts +20 -0
- package/build/src/testing/factory/strategies/build.d.ts.map +1 -0
- package/build/src/testing/factory/strategies/build.js +34 -0
- package/build/src/testing/factory/strategies/create.d.ts +32 -0
- package/build/src/testing/factory/strategies/create.d.ts.map +1 -0
- package/build/src/testing/factory/strategies/create.js +57 -0
- package/build/src/testing/factory/trait-definition.d.ts +22 -0
- package/build/src/testing/factory/trait-definition.d.ts.map +1 -0
- package/build/src/testing/factory/trait-definition.js +22 -0
- package/build/testing/factory/association-declaration.js +50 -0
- package/build/testing/factory/declarations.js +110 -0
- package/build/testing/factory/definition-builder.js +476 -0
- package/build/testing/factory/errors.js +79 -0
- package/build/testing/factory/evaluation-context.js +199 -0
- package/build/testing/factory/events.js +30 -0
- package/build/testing/factory/factory-definition.js +41 -0
- package/build/testing/factory/factory-registry.js +501 -0
- package/build/testing/factory/factory-runner.js +296 -0
- package/build/testing/factory/index.js +24 -0
- package/build/testing/factory/linter.js +119 -0
- package/build/testing/factory/model-contract.js +31 -0
- package/build/testing/factory/node/load-definitions.js +103 -0
- package/build/testing/factory/sequence.js +91 -0
- package/build/testing/factory/strategies/attributes-for.js +24 -0
- package/build/testing/factory/strategies/base.js +209 -0
- package/build/testing/factory/strategies/build.js +42 -0
- package/build/testing/factory/strategies/create.js +68 -0
- package/build/testing/factory/trait-definition.js +24 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/background-jobs/status-reporter.js +15 -2
- package/src/background-jobs/worker.js +5 -1
- package/src/frontend-models/base.js +105 -47
- package/src/frontend-models/transport-deadline.js +66 -0
- package/src/testing/factory/association-declaration.js +50 -0
- package/src/testing/factory/declarations.js +110 -0
- package/src/testing/factory/definition-builder.js +476 -0
- package/src/testing/factory/errors.js +79 -0
- package/src/testing/factory/evaluation-context.js +199 -0
- package/src/testing/factory/events.js +30 -0
- package/src/testing/factory/factory-definition.js +41 -0
- package/src/testing/factory/factory-registry.js +501 -0
- package/src/testing/factory/factory-runner.js +296 -0
- package/src/testing/factory/index.js +24 -0
- package/src/testing/factory/linter.js +119 -0
- package/src/testing/factory/model-contract.js +31 -0
- package/src/testing/factory/node/load-definitions.js +103 -0
- package/src/testing/factory/sequence.js +91 -0
- package/src/testing/factory/strategies/attributes-for.js +24 -0
- package/src/testing/factory/strategies/base.js +209 -0
- package/src/testing/factory/strategies/build.js +42 -0
- package/src/testing/factory/strategies/create.js +68 -0
- package/src/testing/factory/trait-definition.js +24 -0
|
@@ -9,6 +9,7 @@ import {normalizeDateStringForWrite} from "../database/datetime-storage.js"
|
|
|
9
9
|
import {registerFrontendModel, resolveFrontendModelClass} from "./model-registry.js"
|
|
10
10
|
import {validateFrontendModelResourceCommandName, validateFrontendModelResourcePath} from "./resource-config-validation.js"
|
|
11
11
|
import {deserializeFrontendModelTransportValue, serializeFrontendModelTransportValue} from "./transport-serialization.js"
|
|
12
|
+
import runWithTransportDeadline from "./transport-deadline.js"
|
|
12
13
|
import {REQUEST_TIME_ZONE_HEADER, validateTimeZone} from "../time-zone.js"
|
|
13
14
|
import VelociousWebsocketClient from "../http-client/websocket-client.js"
|
|
14
15
|
import {bufferOutgoingEvent, drainBufferedOutgoingEvents} from "./outgoing-event-buffer.js"
|
|
@@ -112,8 +113,10 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
|
|
|
112
113
|
* @property {string | (() => string | undefined | null)} [url] - Optional frontend-model URL. This should be the shared endpoint (for example `"/frontend-models"` or `"https://example.com/frontend-models"`).
|
|
113
114
|
* @property {boolean} [shared] - Deprecated shared-endpoint flag retained for compatibility. Frontend-model CRUD/custom commands use the shared frontend-model API envelope by default.
|
|
114
115
|
* @property {string | (() => string | undefined | null)} [websocketUrl] - Optional websocket URL. When set, Velocious creates and manages its own websocket client internally. Subscriptions use the websocket; CRUD uses HTTP and falls back gracefully. Example: `"ws://localhost:3006/websocket"`.
|
|
115
|
-
* @property {{post: (path: string, body?: ?, options?: {headers?: Record<string, string
|
|
116
|
+
* @property {{post: (path: string, body?: ?, options?: {headers?: Record<string, string>, signal?: AbortSignal}) => Promise<{json: () => ?}>, subscribe: (channel: string, options: {params?: Record<string, ?>}, callback: (payload: ?) => void) => (() => void), subscribeAndWait?: (channel: string, options: {params?: Record<string, ?>}, callback: (payload: ?) => void) => Promise<(() => void)>}} [websocketClient] - Optional websocket client for shared frontend-model API requests and subscriptions. Its `post` receives the bounded-deadline `signal` and should forward it into the underlying transport so the deadline can abort the live request and its response-body read.
|
|
116
117
|
* @property {Record<string, string> | (() => Record<string, string>)} [requestHeaders] - Extra HTTP/WS headers to attach to every frontend-model API request. Pass a function to compute them at request time (for example to include the current locale).
|
|
118
|
+
* @property {number | (() => number | undefined | null)} [timeout] - Bounded deadline in milliseconds covering connection, response headers, and response-body consumption for each frontend-model API request. On expiry the live fetch/adapter request is aborted (built on awaitery's `timeout`) and awaitery's `TimeoutError` is thrown, so callers can classify a timeout via `error instanceof TimeoutError`. Pass a function to resolve it per request. Falsy/absent means no deadline.
|
|
119
|
+
* @property {AbortSignal | (() => AbortSignal | undefined | null)} [signal] - Optional caller/session AbortSignal composed with the deadline. Aborting it cancels the live request (for example on session shutdown or offline transition); the resulting abort error stays distinguishable from a timeout. Pass a function to resolve the current signal per request.
|
|
117
120
|
* @property {{get: () => string | null | undefined | Promise<string | null | undefined>, set: (sessionId: string) => void | Promise<void>, clear: () => void | Promise<void>}} [sessionStore] - Optional sessionId persistence hook forwarded to the internal `VelociousWebsocketClient` so WS sessions can be resumed across page reloads / app restarts.
|
|
118
121
|
* @property {string | (() => string | null | undefined)} [timeZone] - IANA timezone sent with every frontend-model API request for timezone-less datetime parsing.
|
|
119
122
|
* @property {{actorDeviceId: string, actorUserId: string, clientMutationId?: () => string, enabled?: boolean, mutationLog: import("../sync/local-mutation-log.js").default, now?: () => Date, offlineGrant: {id: string}}} [offlineSync] - Offline mutation queue configuration.
|
|
@@ -1753,6 +1756,34 @@ function frontendModelRequestHeaders(timeZone = frontendModelTransportTimeZone()
|
|
|
1753
1756
|
return headers
|
|
1754
1757
|
}
|
|
1755
1758
|
|
|
1759
|
+
/**
|
|
1760
|
+
* Resolves the configured bounded transport deadline in milliseconds.
|
|
1761
|
+
* @returns {number | undefined} - Configured deadline, or undefined when no deadline is set.
|
|
1762
|
+
*/
|
|
1763
|
+
function frontendModelTransportTimeoutMs() {
|
|
1764
|
+
const configuredTimeout = typeof frontendModelTransportConfig.timeout === "function"
|
|
1765
|
+
? frontendModelTransportConfig.timeout()
|
|
1766
|
+
: frontendModelTransportConfig.timeout
|
|
1767
|
+
|
|
1768
|
+
if (typeof configuredTimeout !== "number" || !(configuredTimeout > 0)) {
|
|
1769
|
+
return undefined
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
return configuredTimeout
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/**
|
|
1776
|
+
* Resolves the configured caller/session AbortSignal composed with the deadline.
|
|
1777
|
+
* @returns {AbortSignal | undefined} - Configured caller signal, or undefined when none is set.
|
|
1778
|
+
*/
|
|
1779
|
+
function frontendModelTransportSignal() {
|
|
1780
|
+
const configuredSignal = typeof frontendModelTransportConfig.signal === "function"
|
|
1781
|
+
? frontendModelTransportConfig.signal()
|
|
1782
|
+
: frontendModelTransportConfig.signal
|
|
1783
|
+
|
|
1784
|
+
return configuredSignal || undefined
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1756
1787
|
/**
|
|
1757
1788
|
* Runs perform shared frontend model api request.
|
|
1758
1789
|
* @param {Record<string, ?>} requestPayload - Shared request payload.
|
|
@@ -1765,35 +1796,46 @@ async function performSharedFrontendModelApiRequest(requestPayload) {
|
|
|
1765
1796
|
const url = frontendModelApiUrl()
|
|
1766
1797
|
const mergedHeaders = frontendModelRequestHeaders(timeZone)
|
|
1767
1798
|
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1799
|
+
return await runWithTransportDeadline(
|
|
1800
|
+
{
|
|
1801
|
+
errorMessage: "Shared frontend model API request timed out",
|
|
1802
|
+
signal: frontendModelTransportSignal(),
|
|
1803
|
+
timeoutMs: frontendModelTransportTimeoutMs()
|
|
1804
|
+
},
|
|
1805
|
+
async (signal) => {
|
|
1806
|
+
if (websocketClient) {
|
|
1807
|
+
const response = await websocketClient.post(frontendModelTransportPath(url), serializedRequestPayload, {
|
|
1808
|
+
headers: mergedHeaders,
|
|
1809
|
+
signal
|
|
1810
|
+
})
|
|
1811
|
+
const responseJson = response.json()
|
|
1773
1812
|
|
|
1774
|
-
|
|
1775
|
-
|
|
1813
|
+
return /** @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(responseJson))
|
|
1814
|
+
}
|
|
1776
1815
|
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1816
|
+
const response = await fetch(url, {
|
|
1817
|
+
body: JSON.stringify(serializedRequestPayload),
|
|
1818
|
+
credentials: "include",
|
|
1819
|
+
headers: mergedHeaders,
|
|
1820
|
+
method: "POST",
|
|
1821
|
+
signal
|
|
1822
|
+
})
|
|
1783
1823
|
|
|
1784
|
-
|
|
1824
|
+
const responseText = await response.text()
|
|
1785
1825
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1826
|
+
if (!response.ok) {
|
|
1827
|
+
throwFrontendModelHttpError({
|
|
1828
|
+
commandLabel: "shared frontend model API",
|
|
1829
|
+
response,
|
|
1830
|
+
responseText
|
|
1831
|
+
})
|
|
1832
|
+
}
|
|
1793
1833
|
|
|
1794
|
-
|
|
1834
|
+
const json = responseText.length > 0 ? JSON.parse(responseText) : {}
|
|
1795
1835
|
|
|
1796
|
-
|
|
1836
|
+
return /** @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(json))
|
|
1837
|
+
}
|
|
1838
|
+
)
|
|
1797
1839
|
}
|
|
1798
1840
|
|
|
1799
1841
|
/**
|
|
@@ -2862,6 +2904,14 @@ export default class FrontendModelBase {
|
|
|
2862
2904
|
frontendModelTransportConfig.requestHeaders = config.requestHeaders
|
|
2863
2905
|
}
|
|
2864
2906
|
|
|
2907
|
+
if (Object.prototype.hasOwnProperty.call(config, "timeout")) {
|
|
2908
|
+
frontendModelTransportConfig.timeout = config.timeout
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
if (Object.prototype.hasOwnProperty.call(config, "signal")) {
|
|
2912
|
+
frontendModelTransportConfig.signal = config.signal
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2865
2915
|
if (Object.prototype.hasOwnProperty.call(config, "timeZone")) {
|
|
2866
2916
|
if (config.timeZone === undefined) {
|
|
2867
2917
|
delete frontendModelTransportConfig.timeZone
|
|
@@ -4428,34 +4478,42 @@ export default class FrontendModelBase {
|
|
|
4428
4478
|
return decodedBatchResponse
|
|
4429
4479
|
}
|
|
4430
4480
|
|
|
4431
|
-
return await trackFrontendModelTransportRequest(async () =>
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4481
|
+
return await trackFrontendModelTransportRequest(async () => runWithTransportDeadline(
|
|
4482
|
+
{
|
|
4483
|
+
errorMessage: `${this.name}#${commandType} request timed out`,
|
|
4484
|
+
signal: frontendModelTransportSignal(),
|
|
4485
|
+
timeoutMs: frontendModelTransportTimeoutMs()
|
|
4486
|
+
},
|
|
4487
|
+
async (signal) => {
|
|
4488
|
+
const directResponse = await fetch(url, {
|
|
4489
|
+
body: JSON.stringify(serializedPayload),
|
|
4490
|
+
credentials: "include",
|
|
4491
|
+
headers: frontendModelRequestHeaders(timeZone),
|
|
4492
|
+
method: "POST",
|
|
4493
|
+
signal
|
|
4494
|
+
})
|
|
4438
4495
|
|
|
4439
|
-
|
|
4496
|
+
const directResponseText = await directResponse.text()
|
|
4440
4497
|
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4498
|
+
if (!directResponse.ok) {
|
|
4499
|
+
throwFrontendModelHttpError({
|
|
4500
|
+
commandLabel: `${this.name}#${commandType}`,
|
|
4501
|
+
response: directResponse,
|
|
4502
|
+
responseText: directResponseText
|
|
4503
|
+
})
|
|
4504
|
+
}
|
|
4448
4505
|
|
|
4449
|
-
|
|
4450
|
-
|
|
4506
|
+
const directJson = directResponseText.length > 0 ? JSON.parse(directResponseText) : {}
|
|
4507
|
+
const decodedDirectResponse = /** @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(directJson))
|
|
4451
4508
|
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4509
|
+
this.throwOnErrorFrontendModelResponse({
|
|
4510
|
+
commandType,
|
|
4511
|
+
response: decodedDirectResponse
|
|
4512
|
+
})
|
|
4456
4513
|
|
|
4457
|
-
|
|
4458
|
-
|
|
4514
|
+
return decodedDirectResponse
|
|
4515
|
+
}
|
|
4516
|
+
))
|
|
4459
4517
|
}
|
|
4460
4518
|
|
|
4461
4519
|
/**
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import timeout from "awaitery/build/timeout.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runs an async transport operation under a bounded deadline built on awaitery's
|
|
7
|
+
* `timeout`, which owns the timer, the stable `TimeoutError`, and the timer
|
|
8
|
+
* cleanup. awaitery's deadline `AbortSignal` is handed to `operation` (composed
|
|
9
|
+
* with an optional caller/session signal) so the live request — and its
|
|
10
|
+
* response-body read — is actually aborted when the deadline expires, not merely
|
|
11
|
+
* left racing a wrapper promise.
|
|
12
|
+
*
|
|
13
|
+
* A deadline expiry rejects with awaitery's `TimeoutError`
|
|
14
|
+
* (`import {TimeoutError} from "awaitery/build/timeout.js"`), so callers can
|
|
15
|
+
* classify a timeout without parsing browser/network error text. A caller abort
|
|
16
|
+
* rejects with the caller's own reason, keeping the two distinguishable. Both the
|
|
17
|
+
* awaitery timer and the composed abort listeners are always cleaned up. With no
|
|
18
|
+
* positive `timeoutMs` the operation runs directly under the caller signal (or no
|
|
19
|
+
* signal) and no timer is armed.
|
|
20
|
+
* @template T
|
|
21
|
+
* @param {{timeoutMs?: number | null, signal?: AbortSignal | null, errorMessage?: string}} options - Deadline options.
|
|
22
|
+
* @param {(signal: AbortSignal) => Promise<T>} operation - Transport operation receiving the composed signal.
|
|
23
|
+
* @returns {Promise<T>} - The operation result.
|
|
24
|
+
*/
|
|
25
|
+
export default async function runWithTransportDeadline({timeoutMs, signal, errorMessage}, operation) {
|
|
26
|
+
if (!(typeof timeoutMs === "number" && timeoutMs > 0)) {
|
|
27
|
+
// No deadline: run under the caller signal if provided, else a never-aborting signal.
|
|
28
|
+
return await operation(signal || new AbortController().signal)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return await timeout({errorMessage: errorMessage || "Transport request timed out", timeout: timeoutMs}, async ({control}) => {
|
|
32
|
+
if (!signal) {
|
|
33
|
+
return await operation(control.signal)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A caller that has already cancelled must not start a live request.
|
|
37
|
+
if (signal.aborted) {
|
|
38
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Transport request aborted")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Compose the caller/session signal with awaitery's deadline signal: either
|
|
42
|
+
// one aborts the live operation, and the composed listeners are always removed.
|
|
43
|
+
const callerSignal = signal
|
|
44
|
+
const composed = new AbortController()
|
|
45
|
+
/**
|
|
46
|
+
* Aborts the composed operation when the deadline signal fires.
|
|
47
|
+
* @returns {void} - No return value.
|
|
48
|
+
*/
|
|
49
|
+
const onDeadlineAbort = () => composed.abort(control.signal.reason)
|
|
50
|
+
/**
|
|
51
|
+
* Aborts the composed operation when the caller signal fires.
|
|
52
|
+
* @returns {void} - No return value.
|
|
53
|
+
*/
|
|
54
|
+
const onCallerAbort = () => composed.abort(callerSignal.reason)
|
|
55
|
+
|
|
56
|
+
control.signal.addEventListener("abort", onDeadlineAbort)
|
|
57
|
+
callerSignal.addEventListener("abort", onCallerAbort)
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return await operation(composed.signal)
|
|
61
|
+
} finally {
|
|
62
|
+
control.signal.removeEventListener("abort", onDeadlineAbort)
|
|
63
|
+
callerSignal.removeEventListener("abort", onCallerAbort)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {InvalidDefinitionError} from "./errors.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A declared association. It records the relationship name, the factory to run
|
|
7
|
+
* (defaulting to the relationship name), any traits/overrides passed to that
|
|
8
|
+
* factory, and an optional explicit strategy. When no strategy is given the
|
|
9
|
+
* association follows the parent strategy at evaluation time.
|
|
10
|
+
*/
|
|
11
|
+
export default class AssociationDeclaration {
|
|
12
|
+
/**
|
|
13
|
+
* Builds an association declaration.
|
|
14
|
+
* @param {object} args - Options.
|
|
15
|
+
* @param {string} args.name - Relationship name on the owning model.
|
|
16
|
+
* @param {string} [args.factory] - Factory name to run. Defaults to the relationship name.
|
|
17
|
+
* @param {string[]} [args.traits] - Traits passed to the association factory.
|
|
18
|
+
* @param {Record<string, ?>} [args.overrides] - Overrides passed to the association factory.
|
|
19
|
+
* @param {"build" | "create" | undefined} [args.strategy] - Explicit strategy override.
|
|
20
|
+
*/
|
|
21
|
+
constructor({name, factory, traits = [], overrides = {}, strategy}) {
|
|
22
|
+
if (!name || typeof name !== "string") {
|
|
23
|
+
throw new InvalidDefinitionError(`Association name must be a non-empty string, got: ${String(name)}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (strategy !== undefined && strategy !== "build" && strategy !== "create") {
|
|
27
|
+
throw new InvalidDefinitionError(`Association strategy must be "build" or "create", got: ${String(strategy)}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @type {"association"} - Discriminant. */
|
|
31
|
+
this.kind = "association"
|
|
32
|
+
|
|
33
|
+
/** @type {string} - Relationship name on the owning model. */
|
|
34
|
+
this.name = name
|
|
35
|
+
|
|
36
|
+
/** @type {string} - Factory name to run for the association. */
|
|
37
|
+
this.factory = factory || name
|
|
38
|
+
|
|
39
|
+
/** @type {string[]} - Traits passed to the association factory. */
|
|
40
|
+
this.traits = traits
|
|
41
|
+
|
|
42
|
+
/** @type {Record<string, ?>} - Overrides passed to the association factory. */
|
|
43
|
+
this.overrides = overrides
|
|
44
|
+
|
|
45
|
+
/** @type {"build" | "create" | undefined} - Explicit strategy override. */
|
|
46
|
+
this.strategy = strategy
|
|
47
|
+
|
|
48
|
+
Object.freeze(this)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A literal or lazy attribute (or transient) declaration. A function value is the
|
|
5
|
+
* lazy form and receives the evaluator context; any other value is a literal.
|
|
6
|
+
* @typedef {object} AttributeDeclaration
|
|
7
|
+
* @property {"attribute"} kind - Discriminant.
|
|
8
|
+
* @property {string} name - Attribute name.
|
|
9
|
+
* @property {boolean} isTransient - Whether the value is transient (never assigned/returned).
|
|
10
|
+
* @property {?} value - Literal value or lazy `(context) => value` function.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A lifecycle callback declaration. The same declaration object reached through
|
|
15
|
+
* multiple trait paths runs once per record (dedup is by object identity).
|
|
16
|
+
* @typedef {object} CallbackDeclaration
|
|
17
|
+
* @property {"callback"} kind - Discriminant.
|
|
18
|
+
* @property {string} event - One of the supported callback events.
|
|
19
|
+
* @property {(args: {record: ?, context: ?, strategy: string}) => (void | Promise<void>)} fn - Callback body.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A base-trait inclusion declaration (a trait applied by default within a factory
|
|
24
|
+
* or composed inside another trait).
|
|
25
|
+
* @typedef {object} TraitIncludeDeclaration
|
|
26
|
+
* @property {"traitInclude"} kind - Discriminant.
|
|
27
|
+
* @property {string} name - Referenced trait name.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A custom-constructor declaration.
|
|
32
|
+
* @typedef {object} InitializeWithDeclaration
|
|
33
|
+
* @property {"initializeWith"} kind - Discriminant.
|
|
34
|
+
* @property {(args: {attributes: Record<string, ?>, context: ?, get: (name: string) => ?}) => (? | Promise<?>)} fn - Constructor body.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A custom-persistence declaration.
|
|
39
|
+
* @typedef {object} ToCreateDeclaration
|
|
40
|
+
* @property {"toCreate"} kind - Discriminant.
|
|
41
|
+
* @property {(args: {record: ?, context: ?}) => (void | Promise<void>)} fn - Persistence body.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A declaration that disables persistence entirely for the create strategy.
|
|
46
|
+
* @typedef {object} SkipCreateDeclaration
|
|
47
|
+
* @property {"skipCreate"} kind - Discriminant.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Union of every declaration kind stored on a factory/trait definition. Includes
|
|
52
|
+
* association declarations imported from their own module.
|
|
53
|
+
* @typedef {AttributeDeclaration | CallbackDeclaration | TraitIncludeDeclaration | InitializeWithDeclaration | ToCreateDeclaration | SkipCreateDeclaration | import("./association-declaration.js").default} Declaration
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a literal/lazy attribute declaration.
|
|
58
|
+
* @param {string} name - Attribute name.
|
|
59
|
+
* @param {?} value - Literal value or lazy function.
|
|
60
|
+
* @param {boolean} isTransient - Whether the declaration is transient.
|
|
61
|
+
* @returns {AttributeDeclaration} - The frozen declaration.
|
|
62
|
+
*/
|
|
63
|
+
export function attributeDeclaration(name, value, isTransient) {
|
|
64
|
+
return Object.freeze({kind: "attribute", name, isTransient, value})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Creates a lifecycle callback declaration.
|
|
69
|
+
* @param {string} event - Callback event name.
|
|
70
|
+
* @param {CallbackDeclaration["fn"]} fn - Callback body.
|
|
71
|
+
* @returns {CallbackDeclaration} - The frozen declaration.
|
|
72
|
+
*/
|
|
73
|
+
export function callbackDeclaration(event, fn) {
|
|
74
|
+
return Object.freeze({kind: "callback", event, fn})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates a base-trait inclusion declaration.
|
|
79
|
+
* @param {string} name - Referenced trait name.
|
|
80
|
+
* @returns {TraitIncludeDeclaration} - The frozen declaration.
|
|
81
|
+
*/
|
|
82
|
+
export function traitIncludeDeclaration(name) {
|
|
83
|
+
return Object.freeze({kind: "traitInclude", name})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a custom-constructor declaration.
|
|
88
|
+
* @param {InitializeWithDeclaration["fn"]} fn - Constructor body.
|
|
89
|
+
* @returns {InitializeWithDeclaration} - The frozen declaration.
|
|
90
|
+
*/
|
|
91
|
+
export function initializeWithDeclaration(fn) {
|
|
92
|
+
return Object.freeze({kind: "initializeWith", fn})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates a custom-persistence declaration.
|
|
97
|
+
* @param {ToCreateDeclaration["fn"]} fn - Persistence body.
|
|
98
|
+
* @returns {ToCreateDeclaration} - The frozen declaration.
|
|
99
|
+
*/
|
|
100
|
+
export function toCreateDeclaration(fn) {
|
|
101
|
+
return Object.freeze({kind: "toCreate", fn})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Creates a skip-create declaration.
|
|
106
|
+
* @returns {SkipCreateDeclaration} - The frozen declaration.
|
|
107
|
+
*/
|
|
108
|
+
export function skipCreateDeclaration() {
|
|
109
|
+
return Object.freeze({kind: "skipCreate"})
|
|
110
|
+
}
|