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.
Files changed (117) hide show
  1. package/README.md +2 -0
  2. package/build/background-jobs/status-reporter.js +15 -2
  3. package/build/background-jobs/worker.js +5 -1
  4. package/build/frontend-models/base.js +105 -47
  5. package/build/frontend-models/transport-deadline.js +66 -0
  6. package/build/src/background-jobs/status-reporter.d.ts +3 -1
  7. package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
  8. package/build/src/background-jobs/status-reporter.js +16 -3
  9. package/build/src/background-jobs/worker.d.ts.map +1 -1
  10. package/build/src/background-jobs/worker.js +6 -2
  11. package/build/src/frontend-models/base.d.ts +10 -1
  12. package/build/src/frontend-models/base.d.ts.map +1 -1
  13. package/build/src/frontend-models/base.js +71 -26
  14. package/build/src/frontend-models/transport-deadline.d.ts +26 -0
  15. package/build/src/frontend-models/transport-deadline.d.ts.map +1 -0
  16. package/build/src/frontend-models/transport-deadline.js +61 -0
  17. package/build/src/testing/factory/association-declaration.d.ts +37 -0
  18. package/build/src/testing/factory/association-declaration.d.ts.map +1 -0
  19. package/build/src/testing/factory/association-declaration.js +41 -0
  20. package/build/src/testing/factory/declarations.d.ts +190 -0
  21. package/build/src/testing/factory/declarations.d.ts.map +1 -0
  22. package/build/src/testing/factory/declarations.js +98 -0
  23. package/build/src/testing/factory/definition-builder.d.ts +132 -0
  24. package/build/src/testing/factory/definition-builder.d.ts.map +1 -0
  25. package/build/src/testing/factory/definition-builder.js +417 -0
  26. package/build/src/testing/factory/errors.d.ts +62 -0
  27. package/build/src/testing/factory/errors.d.ts.map +1 -0
  28. package/build/src/testing/factory/errors.js +67 -0
  29. package/build/src/testing/factory/evaluation-context.d.ts +95 -0
  30. package/build/src/testing/factory/evaluation-context.d.ts.map +1 -0
  31. package/build/src/testing/factory/evaluation-context.js +175 -0
  32. package/build/src/testing/factory/events.d.ts +21 -0
  33. package/build/src/testing/factory/events.d.ts.map +1 -0
  34. package/build/src/testing/factory/events.js +26 -0
  35. package/build/src/testing/factory/factory-definition.d.ts +39 -0
  36. package/build/src/testing/factory/factory-definition.d.ts.map +1 -0
  37. package/build/src/testing/factory/factory-definition.js +35 -0
  38. package/build/src/testing/factory/factory-registry.d.ts +263 -0
  39. package/build/src/testing/factory/factory-registry.d.ts.map +1 -0
  40. package/build/src/testing/factory/factory-registry.js +438 -0
  41. package/build/src/testing/factory/factory-runner.d.ts +179 -0
  42. package/build/src/testing/factory/factory-runner.d.ts.map +1 -0
  43. package/build/src/testing/factory/factory-runner.js +266 -0
  44. package/build/src/testing/factory/index.d.ts +18 -0
  45. package/build/src/testing/factory/index.d.ts.map +1 -0
  46. package/build/src/testing/factory/index.js +21 -0
  47. package/build/src/testing/factory/linter.d.ts +55 -0
  48. package/build/src/testing/factory/linter.d.ts.map +1 -0
  49. package/build/src/testing/factory/linter.js +106 -0
  50. package/build/src/testing/factory/model-contract.d.ts +11 -0
  51. package/build/src/testing/factory/model-contract.d.ts.map +1 -0
  52. package/build/src/testing/factory/model-contract.js +26 -0
  53. package/build/src/testing/factory/node/load-definitions.d.ts +24 -0
  54. package/build/src/testing/factory/node/load-definitions.d.ts.map +1 -0
  55. package/build/src/testing/factory/node/load-definitions.js +87 -0
  56. package/build/src/testing/factory/sequence.d.ts +73 -0
  57. package/build/src/testing/factory/sequence.d.ts.map +1 -0
  58. package/build/src/testing/factory/sequence.js +78 -0
  59. package/build/src/testing/factory/strategies/attributes-for.d.ts +21 -0
  60. package/build/src/testing/factory/strategies/attributes-for.d.ts.map +1 -0
  61. package/build/src/testing/factory/strategies/attributes-for.js +22 -0
  62. package/build/src/testing/factory/strategies/base.d.ts +101 -0
  63. package/build/src/testing/factory/strategies/base.d.ts.map +1 -0
  64. package/build/src/testing/factory/strategies/base.js +191 -0
  65. package/build/src/testing/factory/strategies/build.d.ts +20 -0
  66. package/build/src/testing/factory/strategies/build.d.ts.map +1 -0
  67. package/build/src/testing/factory/strategies/build.js +34 -0
  68. package/build/src/testing/factory/strategies/create.d.ts +32 -0
  69. package/build/src/testing/factory/strategies/create.d.ts.map +1 -0
  70. package/build/src/testing/factory/strategies/create.js +57 -0
  71. package/build/src/testing/factory/trait-definition.d.ts +22 -0
  72. package/build/src/testing/factory/trait-definition.d.ts.map +1 -0
  73. package/build/src/testing/factory/trait-definition.js +22 -0
  74. package/build/testing/factory/association-declaration.js +50 -0
  75. package/build/testing/factory/declarations.js +110 -0
  76. package/build/testing/factory/definition-builder.js +476 -0
  77. package/build/testing/factory/errors.js +79 -0
  78. package/build/testing/factory/evaluation-context.js +199 -0
  79. package/build/testing/factory/events.js +30 -0
  80. package/build/testing/factory/factory-definition.js +41 -0
  81. package/build/testing/factory/factory-registry.js +501 -0
  82. package/build/testing/factory/factory-runner.js +296 -0
  83. package/build/testing/factory/index.js +24 -0
  84. package/build/testing/factory/linter.js +119 -0
  85. package/build/testing/factory/model-contract.js +31 -0
  86. package/build/testing/factory/node/load-definitions.js +103 -0
  87. package/build/testing/factory/sequence.js +91 -0
  88. package/build/testing/factory/strategies/attributes-for.js +24 -0
  89. package/build/testing/factory/strategies/base.js +209 -0
  90. package/build/testing/factory/strategies/build.js +42 -0
  91. package/build/testing/factory/strategies/create.js +68 -0
  92. package/build/testing/factory/trait-definition.js +24 -0
  93. package/build/tsconfig.tsbuildinfo +1 -1
  94. package/package.json +1 -1
  95. package/src/background-jobs/status-reporter.js +15 -2
  96. package/src/background-jobs/worker.js +5 -1
  97. package/src/frontend-models/base.js +105 -47
  98. package/src/frontend-models/transport-deadline.js +66 -0
  99. package/src/testing/factory/association-declaration.js +50 -0
  100. package/src/testing/factory/declarations.js +110 -0
  101. package/src/testing/factory/definition-builder.js +476 -0
  102. package/src/testing/factory/errors.js +79 -0
  103. package/src/testing/factory/evaluation-context.js +199 -0
  104. package/src/testing/factory/events.js +30 -0
  105. package/src/testing/factory/factory-definition.js +41 -0
  106. package/src/testing/factory/factory-registry.js +501 -0
  107. package/src/testing/factory/factory-runner.js +296 -0
  108. package/src/testing/factory/index.js +24 -0
  109. package/src/testing/factory/linter.js +119 -0
  110. package/src/testing/factory/model-contract.js +31 -0
  111. package/src/testing/factory/node/load-definitions.js +103 -0
  112. package/src/testing/factory/sequence.js +91 -0
  113. package/src/testing/factory/strategies/attributes-for.js +24 -0
  114. package/src/testing/factory/strategies/base.js +209 -0
  115. package/src/testing/factory/strategies/build.js +42 -0
  116. package/src/testing/factory/strategies/create.js +68 -0
  117. 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>}) => 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.
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
- if (websocketClient) {
1769
- const response = await websocketClient.post(frontendModelTransportPath(url), serializedRequestPayload, {
1770
- headers: mergedHeaders
1771
- })
1772
- const responseJson = response.json()
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
- return /** @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(responseJson))
1775
- }
1813
+ return /** @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(responseJson))
1814
+ }
1776
1815
 
1777
- const response = await fetch(url, {
1778
- body: JSON.stringify(serializedRequestPayload),
1779
- credentials: "include",
1780
- headers: mergedHeaders,
1781
- method: "POST"
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
- const responseText = await response.text()
1824
+ const responseText = await response.text()
1785
1825
 
1786
- if (!response.ok) {
1787
- throwFrontendModelHttpError({
1788
- commandLabel: "shared frontend model API",
1789
- response,
1790
- responseText
1791
- })
1792
- }
1826
+ if (!response.ok) {
1827
+ throwFrontendModelHttpError({
1828
+ commandLabel: "shared frontend model API",
1829
+ response,
1830
+ responseText
1831
+ })
1832
+ }
1793
1833
 
1794
- const json = responseText.length > 0 ? JSON.parse(responseText) : {}
1834
+ const json = responseText.length > 0 ? JSON.parse(responseText) : {}
1795
1835
 
1796
- return /** @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(json))
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
- const directResponse = await fetch(url, {
4433
- body: JSON.stringify(serializedPayload),
4434
- credentials: "include",
4435
- headers: frontendModelRequestHeaders(timeZone),
4436
- method: "POST"
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
- const directResponseText = await directResponse.text()
4496
+ const directResponseText = await directResponse.text()
4440
4497
 
4441
- if (!directResponse.ok) {
4442
- throwFrontendModelHttpError({
4443
- commandLabel: `${this.name}#${commandType}`,
4444
- response: directResponse,
4445
- responseText: directResponseText
4446
- })
4447
- }
4498
+ if (!directResponse.ok) {
4499
+ throwFrontendModelHttpError({
4500
+ commandLabel: `${this.name}#${commandType}`,
4501
+ response: directResponse,
4502
+ responseText: directResponseText
4503
+ })
4504
+ }
4448
4505
 
4449
- const directJson = directResponseText.length > 0 ? JSON.parse(directResponseText) : {}
4450
- const decodedDirectResponse = /** @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(directJson))
4506
+ const directJson = directResponseText.length > 0 ? JSON.parse(directResponseText) : {}
4507
+ const decodedDirectResponse = /** @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(directJson))
4451
4508
 
4452
- this.throwOnErrorFrontendModelResponse({
4453
- commandType,
4454
- response: decodedDirectResponse
4455
- })
4509
+ this.throwOnErrorFrontendModelResponse({
4510
+ commandType,
4511
+ response: decodedDirectResponse
4512
+ })
4456
4513
 
4457
- return decodedDirectResponse
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
+ }