velocious 1.0.637 → 1.0.639
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 +6 -3
- package/build/background-jobs/main.js +4 -2
- package/build/background-jobs/status-reporter.js +7 -4
- package/build/background-jobs/types.js +35 -1
- package/build/background-jobs/worker.js +104 -10
- package/build/frontend-models/base.js +61 -28
- package/build/frontend-models/query.js +20 -5
- package/build/frontend-models/remote-request-context.js +13 -1
- package/build/frontend-models/use-destroyed-event.js +8 -7
- package/build/frontend-models/use-model-class-event.js +6 -5
- package/build/frontend-models/use-updated-event.js +8 -7
- package/build/src/background-jobs/main.d.ts +3 -2
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +5 -3
- package/build/src/background-jobs/status-reporter.d.ts +6 -2
- package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
- package/build/src/background-jobs/status-reporter.js +8 -5
- package/build/src/background-jobs/types.d.ts +136 -1
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +36 -2
- package/build/src/background-jobs/worker.d.ts +94 -21
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +101 -11
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +57 -27
- package/build/src/frontend-models/query.d.ts +28 -7
- package/build/src/frontend-models/query.d.ts.map +1 -1
- package/build/src/frontend-models/query.js +20 -6
- package/build/src/frontend-models/remote-request-context.d.ts +6 -0
- package/build/src/frontend-models/remote-request-context.d.ts.map +1 -1
- package/build/src/frontend-models/remote-request-context.js +13 -2
- package/build/src/frontend-models/use-destroyed-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-destroyed-event.js +9 -8
- package/build/src/frontend-models/use-model-class-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-model-class-event.js +7 -6
- package/build/src/frontend-models/use-updated-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-updated-event.js +9 -8
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts +6 -0
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts.map +1 -1
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +72 -1
- package/build/testing/browser-frontend-model-event-hook-scenarios.js +88 -0
- package/package.json +1 -1
- package/src/background-jobs/main.js +4 -2
- package/src/background-jobs/status-reporter.js +7 -4
- package/src/background-jobs/types.js +35 -1
- package/src/background-jobs/worker.js +104 -10
- package/src/frontend-models/base.js +61 -28
- package/src/frontend-models/query.js +20 -5
- package/src/frontend-models/remote-request-context.js +13 -1
- package/src/frontend-models/use-destroyed-event.js +8 -7
- package/src/frontend-models/use-model-class-event.js +6 -5
- package/src/frontend-models/use-updated-event.js +8 -7
- package/src/testing/browser-frontend-model-event-hook-scenarios.js +88 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* External packages (engines) that contribute data models, frontend-model resources and migrations to a consuming app (see [docs/packages.md](docs/packages.md))
|
|
14
14
|
* Optional Rampway-owned durable deployment control plane mounted through the standard routes DSL on Velocious 1.0.577 or newer (see [docs/rampway-integration.md](docs/rampway-integration.md))
|
|
15
15
|
* Controllers and views for HTTP endpoints
|
|
16
|
-
* Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, with structured per-attribute validation error responses, immutable per-operation remote request context, and one-budget WebSocket startup controls (see [docs/frontend-models.md](docs/frontend-models.md), [docs/remote-request-context.md](docs/remote-request-context.md), and [docs/websocket-channels.md](docs/websocket-channels.md))
|
|
16
|
+
* Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, with structured per-attribute validation error responses, immutable per-operation remote request context, registration-local tenant subscription partitioning, and one-budget WebSocket startup controls (see [docs/frontend-models.md](docs/frontend-models.md), [docs/remote-request-context.md](docs/remote-request-context.md), and [docs/websocket-channels.md](docs/websocket-channels.md))
|
|
17
17
|
* Client-side offline sync mutation logs and frontend-model optimistic queueing primitives (see the [shared-resource sync developer guide](docs/shared-resource-sync-guide.md) and [offline sync architecture](docs/offline-sync.md))
|
|
18
18
|
* Declarative client sync scopes with per-scope cursors, automatic mutation tracking, opt-in durable base-version conflict replay, realtime delivery, and immutable-handle project clients whose local database state plus remote pull/replay/realtime request context stay tenant-bound through reconnect (see [docs/sync-client.md](docs/sync-client.md), [docs/remote-request-context.md](docs/remote-request-context.md), and [docs/offline-sync.md](docs/offline-sync.md))
|
|
19
19
|
* Reactive `useLiveQuery(Model.where(...))` queries for default databases plus immutable-handle tenant live-query sources whose committed events and refreshes stay on the captured physical tenant (see [docs/live-queries.md](docs/live-queries.md))
|
|
@@ -674,10 +674,13 @@ import useModelClassEvent from "velocious/build/src/frontend-models/use-model-cl
|
|
|
674
674
|
|
|
675
675
|
useModelClassEvent(Subscription, ["create", "update"], () => {
|
|
676
676
|
void loadSubscriptionStatus()
|
|
677
|
+
}, {
|
|
678
|
+
query: Subscription.where({workspaceId}),
|
|
679
|
+
requestContext: {workspaceId}
|
|
677
680
|
})
|
|
678
681
|
```
|
|
679
682
|
|
|
680
|
-
`useCreatedEvent`, `useUpdatedEvent`, and `useDestroyedEvent` are also available. `useUpdatedEvent` and `useDestroyedEvent` accept either a model class or model instance. Lifecycle subscriptions accept the same projection options as frontend-model queries for event records, including `select`, `preload`, `withCount`, `abilities`, and `queryData`.
|
|
683
|
+
`useCreatedEvent`, `useUpdatedEvent`, and `useDestroyedEvent` are also available. `useUpdatedEvent` and `useDestroyedEvent` accept either a model class or model instance. Lifecycle subscriptions accept the same projection options as frontend-model queries for event records, including `select`, `preload`, `withCount`, `abilities`, and `queryData`. Pass a registration-local `requestContext` when several tenant routes for the same model can be mounted concurrently. Omitting it inherits the configured transport context; passing `{}` explicitly replaces that context with an unscoped registration. Velocious captures it immutably, sends it to the tenant resolver, and partitions server subscriptions by its value: equal contexts retain multiplexing, while distinct contexts never share an event-filter request. The backend must still authorize the resolved tenant; request context is not proof of access.
|
|
681
684
|
|
|
682
685
|
Frontend-model `group(...)` is attribute/path based and does not accept raw SQL fragments. Use model/relationship shapes (for example `Task.group({project: {account: ["id"]}})`) so grouping resolves through known relationships and mapped columns.
|
|
683
686
|
Frontend-model `where(...)` supports nested relationship descriptors (for example `Task.where({project: {creatingUser: {reference: "owner-b"}}})`) and does not accept raw SQL fragments.
|
|
@@ -2392,7 +2395,7 @@ candidate without waiting for retired work to finish.
|
|
|
2392
2395
|
|
|
2393
2396
|
Jobs can opt into cross-worker durable concurrency limits by pairing a non-empty `concurrencyKey` with a positive-integer `maxConcurrency` in their background-job options, or by deriving the key in a hydrated job instance's non-static `concurrencyKey()` method. Explicit enqueue options win. The first cap registered for a key is stable; conflicting caps are rejected. See [durable concurrency limits](docs/background-jobs.md#durable-concurrency-limits).
|
|
2394
2397
|
|
|
2395
|
-
Production apps can listen for `background-job-failed` (or its `all-error` mirror) to report accepted failed attempts, including retry and terminal-state metadata, and for `background-job-orphaned` to react to a specific job the main process reclaimed after its worker died mid-run — e.g. enqueue a targeted recovery for the work it left behind, instead of only polling for the aftermath. Orphan handlers run before the sweep waits for reclaimed jobs to be dispatched, so a stalled dispatcher does not delay application recovery. See [docs/background-jobs.md](docs/background-jobs.md#failure-events).
|
|
2398
|
+
Production apps can listen for `background-job-failed` (or its `all-error` mirror) to report accepted failed attempts, including retry and terminal-state metadata. Process-level pooled-runner failures also carry one shared `context.runnerFailure` snapshot for every affected job, with active handoff identities, runner/worker lifecycle and PIDs, exit code/signal, termination reason, and an explicit nullable OOM verdict. Listen for `background-job-orphaned` to react to a specific job the main process reclaimed after its worker died mid-run — e.g. enqueue a targeted recovery for the work it left behind, instead of only polling for the aftermath. Orphan handlers run before the sweep waits for reclaimed jobs to be dispatched, so a stalled dispatcher does not delay application recovery. See [docs/background-jobs.md](docs/background-jobs.md#failure-events).
|
|
2396
2399
|
|
|
2397
2400
|
## Setup
|
|
2398
2401
|
|
|
@@ -1668,6 +1668,7 @@ export default class BackgroundJobsMain {
|
|
|
1668
1668
|
handoffId: message.handoffId,
|
|
1669
1669
|
handedOffAtMs: message.handedOffAtMs,
|
|
1670
1670
|
job: failedJob,
|
|
1671
|
+
runnerFailure: message.runnerFailure,
|
|
1671
1672
|
workerId: message.workerId
|
|
1672
1673
|
})
|
|
1673
1674
|
}
|
|
@@ -1686,10 +1687,10 @@ export default class BackgroundJobsMain {
|
|
|
1686
1687
|
|
|
1687
1688
|
/**
|
|
1688
1689
|
* Runs emit background job failed.
|
|
1689
|
-
* @param {{error: ReturnType<typeof JSON.parse>, handoffId?: string, handedOffAtMs?: number, job: import("./types.js").BackgroundJobRow, workerId?: string}} args - Failure event data.
|
|
1690
|
+
* @param {{error: ReturnType<typeof JSON.parse>, handoffId?: string, handedOffAtMs?: number, job: import("./types.js").BackgroundJobRow, runnerFailure?: import("./types.js").PooledRunnerFailure, workerId?: string}} args - Failure event data.
|
|
1690
1691
|
* @returns {void}
|
|
1691
1692
|
*/
|
|
1692
|
-
_emitBackgroundJobFailed({error, handoffId, handedOffAtMs, job, workerId}) {
|
|
1693
|
+
_emitBackgroundJobFailed({error, handoffId, handedOffAtMs, job, runnerFailure, workerId}) {
|
|
1693
1694
|
const normalizedError = this._normalizeFailureError(error)
|
|
1694
1695
|
const payload = {
|
|
1695
1696
|
context: {
|
|
@@ -1700,6 +1701,7 @@ export default class BackgroundJobsMain {
|
|
|
1700
1701
|
jobId: job.id,
|
|
1701
1702
|
jobName: job.jobName,
|
|
1702
1703
|
maxRetries: job.maxRetries,
|
|
1704
|
+
runnerFailure,
|
|
1703
1705
|
stage: "background-job-failed",
|
|
1704
1706
|
status: job.status,
|
|
1705
1707
|
terminal: job.status === "failed" || job.status === "orphaned",
|
|
@@ -47,9 +47,10 @@ export default class BackgroundJobsStatusReporter {
|
|
|
47
47
|
* @param {string} [args.handoffId] - Handoff lease id.
|
|
48
48
|
* @param {number} [args.handedOffAtMs] - Handed off timestamp.
|
|
49
49
|
* @param {string} [args.workerId] - Worker id.
|
|
50
|
+
* @param {import("./types.js").PooledRunnerFailure} [args.runnerFailure] - Pooled-child process failure provenance.
|
|
50
51
|
* @returns {Promise<void>} - Resolves when reported.
|
|
51
52
|
*/
|
|
52
|
-
async report({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId}) {
|
|
53
|
+
async report({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId, runnerFailure}) {
|
|
53
54
|
const config = this.configuration.getBackgroundJobsConfig()
|
|
54
55
|
const host = this.host || config.host
|
|
55
56
|
const port = typeof this.port === "number" ? this.port : config.port
|
|
@@ -73,7 +74,8 @@ export default class BackgroundJobsStatusReporter {
|
|
|
73
74
|
handoffId,
|
|
74
75
|
workerId,
|
|
75
76
|
handedOffAtMs,
|
|
76
|
-
error: error ? normalizeBackgroundJobError(error) : undefined
|
|
77
|
+
error: error ? normalizeBackgroundJobError(error) : undefined,
|
|
78
|
+
runnerFailure
|
|
77
79
|
})
|
|
78
80
|
},
|
|
79
81
|
onMessage: ({message, resolve, reject}) => {
|
|
@@ -100,17 +102,18 @@ export default class BackgroundJobsStatusReporter {
|
|
|
100
102
|
* @param {string} [args.handoffId] - Handoff lease id.
|
|
101
103
|
* @param {number} [args.handedOffAtMs] - Handed off timestamp.
|
|
102
104
|
* @param {string} [args.workerId] - Worker id.
|
|
105
|
+
* @param {import("./types.js").PooledRunnerFailure} [args.runnerFailure] - Pooled-child process failure provenance.
|
|
103
106
|
* @param {number} [args.maxDurationMs] - Max duration for retries.
|
|
104
107
|
* @param {boolean} [args.retryPersistErrors] - Retry a `BackgroundJobUpdateError` (main's `job-update-error`, i.e. a transient DB failure while persisting the terminal status) instead of throwing immediately. Off by default so short-lived forked/spawned runners keep failing loudly and exit non-zero to be reclaimed; on for the long-lived worker, which cannot exit-to-reclaim and would otherwise strand the job in `handed_off`.
|
|
105
108
|
* @returns {Promise<void>} - Resolves when reported.
|
|
106
109
|
*/
|
|
107
|
-
async reportWithRetry({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId, maxDurationMs, retryPersistErrors = false}) {
|
|
110
|
+
async reportWithRetry({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId, runnerFailure, maxDurationMs, retryPersistErrors = false}) {
|
|
108
111
|
let attempt = 0
|
|
109
112
|
const startTime = Date.now()
|
|
110
113
|
|
|
111
114
|
while (true) {
|
|
112
115
|
try {
|
|
113
|
-
await this.report({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId})
|
|
116
|
+
await this.report({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId, runnerFailure})
|
|
114
117
|
return
|
|
115
118
|
} catch (error) {
|
|
116
119
|
// A `BackgroundJobUpdateError` means main answered `job-update-error`, which it
|
|
@@ -6,6 +6,39 @@
|
|
|
6
6
|
/** @typedef {"candidate" | "active" | "retired"} BackgroundJobsGenerationInitialState */
|
|
7
7
|
/** @typedef {"starting" | "candidate" | "active" | "retiring" | "retired" | "stopped"} BackgroundJobsGenerationLifecycleState */
|
|
8
8
|
/** @typedef {"missing-generation" | "unexpected-generation" | "malformed-generation" | "generation-mismatch" | "worker-admission-retired" | "worker-has-no-recoverable-handoffs"} BackgroundJobsGenerationRejectionReason */
|
|
9
|
+
/** @typedef {"exit" | "process-error" | "ipc-send"} PooledRunnerFailureOrigin */
|
|
10
|
+
/** @typedef {"starting" | "running" | "retiring"} PooledRunnerLifecycleState */
|
|
11
|
+
/** @typedef {"unexpected" | "job-timeout" | "worker-shutdown-timeout"} PooledRunnerTerminationReason */
|
|
12
|
+
/** @typedef {"running" | "retiring" | "stopping"} BackgroundJobsWorkerLifecycleState */
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} PooledRunnerActiveJob
|
|
15
|
+
* @property {string | null} handoffId - Durable handoff lease id.
|
|
16
|
+
* @property {number | null} handedOffAtMs - Durable handoff timestamp.
|
|
17
|
+
* @property {string} jobId - Durable background job id.
|
|
18
|
+
* @property {string} jobName - Registered job class name.
|
|
19
|
+
* @property {string} workerId - Worker identity persisted with the handoff.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* One process-failure snapshot shared by every job lost with a pooled child.
|
|
23
|
+
* @typedef {object} PooledRunnerFailure
|
|
24
|
+
* @property {PooledRunnerActiveJob[]} activeJobs - Jobs that were in flight when the child failed, ordered by job id.
|
|
25
|
+
* @property {number | null} exitCode - Child exit code, or null for signal/process errors.
|
|
26
|
+
* @property {string | null} generationId - Release generation identity, or null in legacy mode.
|
|
27
|
+
* @property {boolean | null} oomKilled - False when the observed exit rules OOM out; null when an unexpected SIGKILL cannot be distinguished from an OOM kill without supervisor/kernel evidence.
|
|
28
|
+
* @property {PooledRunnerFailureOrigin} origin - Worker observation that initiated failure handling.
|
|
29
|
+
* @property {number} runnerAgeMs - Child age when failure handling started.
|
|
30
|
+
* @property {number} runnerCreatedAtMs - Child creation timestamp.
|
|
31
|
+
* @property {boolean} runnerDetached - Whether the runner owned a detached process group.
|
|
32
|
+
* @property {number} runnerJobsRun - Previously acknowledged jobs handled by the child.
|
|
33
|
+
* @property {PooledRunnerLifecycleState} runnerLifecycle - Child lifecycle immediately before recovery.
|
|
34
|
+
* @property {number | null} runnerPid - Child process id when available.
|
|
35
|
+
* @property {import("node:child_process").ChildProcess["signalCode"]} signal - Child termination signal when available.
|
|
36
|
+
* @property {PooledRunnerTerminationReason} terminationReason - Why the worker expected or did not expect termination.
|
|
37
|
+
* @property {string | null} timeoutJobId - Job whose timeout initiated child termination, or null.
|
|
38
|
+
* @property {string} workerId - Stable generation-qualified worker id.
|
|
39
|
+
* @property {BackgroundJobsWorkerLifecycleState} workerLifecycle - Parent worker lifecycle immediately before recovery.
|
|
40
|
+
* @property {number} workerPid - Parent worker process id.
|
|
41
|
+
*/
|
|
9
42
|
/**
|
|
10
43
|
* @typedef {object} LocalBackgroundJobsClock
|
|
11
44
|
* @property {() => number} now - Current epoch milliseconds.
|
|
@@ -140,6 +173,7 @@
|
|
|
140
173
|
* @property {string | undefined} handoffId - Handoff lease id from the worker report.
|
|
141
174
|
* @property {number | undefined} handedOffAtMs - Handoff timestamp from the worker report.
|
|
142
175
|
* @property {string | undefined} workerId - Worker id from the worker report.
|
|
176
|
+
* @property {PooledRunnerFailure | undefined} runnerFailure - Shared pooled-child process failure provenance.
|
|
143
177
|
*/
|
|
144
178
|
/**
|
|
145
179
|
* @typedef {"worker" | "client" | "reporter"} BackgroundJobSocketRole
|
|
@@ -162,7 +196,7 @@
|
|
|
162
196
|
* @typedef {{type: "cancel-scheduled-error", error?: string}} BackgroundJobCancelScheduledErrorMessage
|
|
163
197
|
* @typedef {{type: "job", payload: BackgroundJobPayload}} BackgroundJobJobMessage
|
|
164
198
|
* @typedef {{type: "job-complete", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number}} BackgroundJobCompleteMessage
|
|
165
|
-
* @typedef {{type: "job-failed", jobId: string, error?: ReturnType<typeof JSON.parse>, handoffId?: string, workerId?: string, handedOffAtMs?: number}} BackgroundJobFailedMessage
|
|
199
|
+
* @typedef {{type: "job-failed", jobId: string, error?: ReturnType<typeof JSON.parse>, handoffId?: string, workerId?: string, handedOffAtMs?: number, runnerFailure?: PooledRunnerFailure}} BackgroundJobFailedMessage
|
|
166
200
|
* @typedef {{type: "job-reschedule", jobId: string, delayMs: number, handoffId?: string, workerId?: string, handedOffAtMs?: number}} BackgroundJobRescheduleMessage
|
|
167
201
|
* @typedef {{type: "job-updated", jobId: string}} BackgroundJobUpdatedMessage
|
|
168
202
|
* @typedef {{type: "job-update-error", jobId: string, error?: string}} BackgroundJobUpdateErrorMessage
|
|
@@ -22,6 +22,26 @@ import BackgroundJobsGenerationHandshakeTimeoutError, { DEFAULT_GENERATION_HANDS
|
|
|
22
22
|
* @property {ReturnType<typeof setTimeout> | null} timer - The pending timeout timer, cleared on exit.
|
|
23
23
|
* @property {ReturnType<typeof setTimeout> | null} sigkillTimer - The pending SIGKILL grace timer, cleared on exit.
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} PooledJobEntry
|
|
27
|
+
* @property {import("./types.js").BackgroundJobPayload & {id: string}} payload - Durable job payload.
|
|
28
|
+
* @property {(value: void) => void} [resolve] - Completion resolver.
|
|
29
|
+
* @property {Promise<void>} [pooledJob] - Tracked pooled-job promise.
|
|
30
|
+
* @property {ReturnType<typeof setTimeout> | null} [timeoutTimer] - Per-job timeout timer.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} PooledChildState
|
|
34
|
+
* @property {number} createdAtMs - Child creation timestamp.
|
|
35
|
+
* @property {number} jobsRun - Acknowledged jobs completed by this child.
|
|
36
|
+
* @property {Map<string, PooledJobEntry>} inflight - Jobs currently owned by this child.
|
|
37
|
+
* @property {number} lastDispatchSeq - Round-robin dispatch sequence.
|
|
38
|
+
* @property {boolean} retiring - Whether this child is draining before retirement.
|
|
39
|
+
* @property {boolean} [started] - Whether the child completed its startup handshake.
|
|
40
|
+
* @property {boolean} [settling] - Whether failure handling already owns this child.
|
|
41
|
+
* @property {ReturnType<typeof setTimeout> | null} [timeoutSigkillTimer] - Pending timeout SIGKILL timer.
|
|
42
|
+
* @property {import("./types.js").PooledRunnerTerminationReason} [terminationReason] - Expected termination reason.
|
|
43
|
+
* @property {string} [timeoutJobId] - Job whose timeout initiated termination.
|
|
44
|
+
*/
|
|
25
45
|
/** Grace period after SIGTERM before a lingering process runner is SIGKILLed. */
|
|
26
46
|
const FORKED_CHILD_SIGKILL_GRACE_MS = 5000
|
|
27
47
|
/**
|
|
@@ -234,7 +254,7 @@ export default class BackgroundJobsWorker {
|
|
|
234
254
|
this.pooledJobQueueTrackers = new Map()
|
|
235
255
|
/** @type {Set<import("node:child_process").ChildProcess>} */
|
|
236
256
|
this.pooledChildren = new Set()
|
|
237
|
-
/** @type {Map<import("node:child_process").ChildProcess,
|
|
257
|
+
/** @type {Map<import("node:child_process").ChildProcess, PooledChildState>} */
|
|
238
258
|
this.pooledChildStates = new Map()
|
|
239
259
|
/** @type {WeakSet<Promise<void>>} */
|
|
240
260
|
this._pooledStartupFailureJobs = new WeakSet()
|
|
@@ -511,6 +531,11 @@ export default class BackgroundJobsWorker {
|
|
|
511
531
|
if (this.inflightProcessChildren.size === 0) return
|
|
512
532
|
|
|
513
533
|
for (const child of this.inflightProcessChildren) {
|
|
534
|
+
const pooledState = this.pooledChildStates.get(child)
|
|
535
|
+
if (pooledState && pooledState.inflight.size > 0 && !pooledState.terminationReason) {
|
|
536
|
+
pooledState.terminationReason = "worker-shutdown-timeout"
|
|
537
|
+
}
|
|
538
|
+
|
|
514
539
|
try {
|
|
515
540
|
child.kill("SIGTERM")
|
|
516
541
|
} catch {
|
|
@@ -1046,7 +1071,7 @@ export default class BackgroundJobsWorker {
|
|
|
1046
1071
|
try {
|
|
1047
1072
|
child.send({type: "job", payload, sharedTransactionBroker: this._pooledJobSharedTransactionBrokerConfig()})
|
|
1048
1073
|
} catch (error) {
|
|
1049
|
-
void this._handlePooledChildFailure({child, error})
|
|
1074
|
+
void this._handlePooledChildFailure({child, error, origin: "ipc-send"})
|
|
1050
1075
|
}
|
|
1051
1076
|
|
|
1052
1077
|
return pooledJob
|
|
@@ -1131,7 +1156,10 @@ export default class BackgroundJobsWorker {
|
|
|
1131
1156
|
const state = this.pooledChildStates.get(child)
|
|
1132
1157
|
|
|
1133
1158
|
// Already settling/gone, or the job finished in the race with this timer.
|
|
1134
|
-
if (!state || state.settling || !state.inflight.has(jobId)) return
|
|
1159
|
+
if (!state || state.settling || state.terminationReason || !state.inflight.has(jobId)) return
|
|
1160
|
+
|
|
1161
|
+
state.terminationReason = "job-timeout"
|
|
1162
|
+
state.timeoutJobId = jobId
|
|
1135
1163
|
|
|
1136
1164
|
try {
|
|
1137
1165
|
child.kill("SIGTERM")
|
|
@@ -1163,8 +1191,20 @@ export default class BackgroundJobsWorker {
|
|
|
1163
1191
|
this.inflightProcessChildren.add(child)
|
|
1164
1192
|
this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), lastDispatchSeq: 0, retiring: false, started: false})
|
|
1165
1193
|
child.on("message", (message) => this._handlePooledChildMessage({child, message}))
|
|
1166
|
-
child.once("exit", (
|
|
1167
|
-
|
|
1194
|
+
child.once("exit", (exitCode, signal) => this._handlePooledChildFailure({
|
|
1195
|
+
child,
|
|
1196
|
+
error: new Error(`Pooled background job runner exited: code=${exitCode} signal=${signal || "none"}`),
|
|
1197
|
+
exitCode,
|
|
1198
|
+
origin: "exit",
|
|
1199
|
+
signal
|
|
1200
|
+
}))
|
|
1201
|
+
child.once("error", (error) => this._handlePooledChildFailure({
|
|
1202
|
+
child,
|
|
1203
|
+
error,
|
|
1204
|
+
exitCode: child.exitCode,
|
|
1205
|
+
origin: "process-error",
|
|
1206
|
+
signal: child.signalCode
|
|
1207
|
+
}))
|
|
1168
1208
|
return child
|
|
1169
1209
|
}
|
|
1170
1210
|
|
|
@@ -1271,9 +1311,12 @@ export default class BackgroundJobsWorker {
|
|
|
1271
1311
|
* @param {object} args - Failure details.
|
|
1272
1312
|
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
1273
1313
|
* @param {ReturnType<typeof JSON.parse>} args.error - Failure.
|
|
1314
|
+
* @param {number | null} [args.exitCode] - Child exit code when observed.
|
|
1315
|
+
* @param {import("./types.js").PooledRunnerFailureOrigin} [args.origin] - Worker observation that initiated recovery.
|
|
1316
|
+
* @param {import("node:child_process").ChildProcess["signalCode"]} [args.signal] - Child termination signal when observed.
|
|
1274
1317
|
* @returns {Promise<void>}
|
|
1275
1318
|
*/
|
|
1276
|
-
async _handlePooledChildFailure({child, error}) {
|
|
1319
|
+
async _handlePooledChildFailure({child, error, exitCode = null, origin = "process-error", signal = null}) {
|
|
1277
1320
|
const state = this.pooledChildStates.get(child)
|
|
1278
1321
|
if (state?.settling) return
|
|
1279
1322
|
if (state) {
|
|
@@ -1289,6 +1332,9 @@ export default class BackgroundJobsWorker {
|
|
|
1289
1332
|
this.inflightProcessChildren.delete(child)
|
|
1290
1333
|
|
|
1291
1334
|
const entries = state ? [...state.inflight.values()] : []
|
|
1335
|
+
const runnerFailure = state
|
|
1336
|
+
? this._pooledRunnerFailure({child, exitCode, origin, signal, state})
|
|
1337
|
+
: undefined
|
|
1292
1338
|
if (state) state.inflight.clear()
|
|
1293
1339
|
this.pooledChildStates.delete(child)
|
|
1294
1340
|
|
|
@@ -1299,6 +1345,7 @@ export default class BackgroundJobsWorker {
|
|
|
1299
1345
|
error,
|
|
1300
1346
|
handoffId: entry.payload.handoffId,
|
|
1301
1347
|
handedOffAtMs: entry.payload.handedOffAtMs,
|
|
1348
|
+
runnerFailure,
|
|
1302
1349
|
workerId: entry.payload.workerId || this.workerId
|
|
1303
1350
|
})
|
|
1304
1351
|
if (entry.resolve) entry.resolve(undefined)
|
|
@@ -1326,6 +1373,51 @@ export default class BackgroundJobsWorker {
|
|
|
1326
1373
|
await Promise.allSettled(failureReports)
|
|
1327
1374
|
}
|
|
1328
1375
|
|
|
1376
|
+
/**
|
|
1377
|
+
* Captures one stable process snapshot before the failed child's state is removed.
|
|
1378
|
+
* @param {object} args - Failure details.
|
|
1379
|
+
* @param {import("node:child_process").ChildProcess} args.child - Failed pooled child.
|
|
1380
|
+
* @param {number | null} args.exitCode - Child exit code when observed.
|
|
1381
|
+
* @param {import("./types.js").PooledRunnerFailureOrigin} args.origin - Worker observation that initiated recovery.
|
|
1382
|
+
* @param {import("node:child_process").ChildProcess["signalCode"]} args.signal - Child termination signal when observed.
|
|
1383
|
+
* @param {PooledChildState} args.state - Child state immediately before recovery.
|
|
1384
|
+
* @returns {import("./types.js").PooledRunnerFailure} - Shared failure provenance.
|
|
1385
|
+
*/
|
|
1386
|
+
_pooledRunnerFailure({child, exitCode, origin, signal, state}) {
|
|
1387
|
+
const terminationReason = state.terminationReason ?? "unexpected"
|
|
1388
|
+
const workerLifecycle = this.shouldStop ? "stopping" : this.isRetiring ? "retiring" : "running"
|
|
1389
|
+
const runnerLifecycle = state.started === false ? "starting" : state.retiring ? "retiring" : "running"
|
|
1390
|
+
const activeJobs = [...state.inflight.values()]
|
|
1391
|
+
.map((entry) => ({
|
|
1392
|
+
handoffId: entry.payload.handoffId ?? null,
|
|
1393
|
+
handedOffAtMs: entry.payload.handedOffAtMs ?? null,
|
|
1394
|
+
jobId: entry.payload.id,
|
|
1395
|
+
jobName: entry.payload.jobName,
|
|
1396
|
+
workerId: entry.payload.workerId ?? this.workerId
|
|
1397
|
+
}))
|
|
1398
|
+
.sort((left, right) => left.jobId.localeCompare(right.jobId))
|
|
1399
|
+
|
|
1400
|
+
return Object.freeze({
|
|
1401
|
+
activeJobs,
|
|
1402
|
+
exitCode,
|
|
1403
|
+
generationId: this.generationId ?? null,
|
|
1404
|
+
oomKilled: signal === "SIGKILL" && terminationReason === "unexpected" ? null : false,
|
|
1405
|
+
origin,
|
|
1406
|
+
runnerAgeMs: Math.max(0, Date.now() - state.createdAtMs),
|
|
1407
|
+
runnerCreatedAtMs: state.createdAtMs,
|
|
1408
|
+
runnerDetached: false,
|
|
1409
|
+
runnerJobsRun: state.jobsRun,
|
|
1410
|
+
runnerLifecycle,
|
|
1411
|
+
runnerPid: child.pid ?? null,
|
|
1412
|
+
signal,
|
|
1413
|
+
terminationReason,
|
|
1414
|
+
timeoutJobId: state.timeoutJobId ?? null,
|
|
1415
|
+
workerId: this.workerId,
|
|
1416
|
+
workerLifecycle,
|
|
1417
|
+
workerPid: process.pid
|
|
1418
|
+
})
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1329
1421
|
/**
|
|
1330
1422
|
* Runs run job inline.
|
|
1331
1423
|
* @param {import("./types.js").BackgroundJobPayload} payload - Payload.
|
|
@@ -1655,9 +1747,10 @@ export default class BackgroundJobsWorker {
|
|
|
1655
1747
|
* @param {string} [args.handoffId] - Handoff lease id.
|
|
1656
1748
|
* @param {number} [args.handedOffAtMs] - Handed off timestamp.
|
|
1657
1749
|
* @param {string} [args.workerId] - Worker id.
|
|
1750
|
+
* @param {import("./types.js").PooledRunnerFailure} [args.runnerFailure] - Pooled-child process failure provenance.
|
|
1658
1751
|
* @returns {Promise<void>} - Resolves when reported.
|
|
1659
1752
|
*/
|
|
1660
|
-
async _reportJobResult({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId}) {
|
|
1753
|
+
async _reportJobResult({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId, runnerFailure}) {
|
|
1661
1754
|
if (!this.statusReporter) return
|
|
1662
1755
|
|
|
1663
1756
|
try {
|
|
@@ -1665,7 +1758,7 @@ export default class BackgroundJobsWorker {
|
|
|
1665
1758
|
// long-lived and cannot exit to trigger orphan reclaim, so dropping the
|
|
1666
1759
|
// completion here would strand the job in `handed_off` forever — fatal for a
|
|
1667
1760
|
// `max_concurrency: 1` job (a stranded row blocks every future run).
|
|
1668
|
-
await this.statusReporter.reportWithRetry({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId, retryPersistErrors: true})
|
|
1761
|
+
await this.statusReporter.reportWithRetry({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId, runnerFailure, retryPersistErrors: true})
|
|
1669
1762
|
} catch (reportError) {
|
|
1670
1763
|
console.error("Background job status reporting failed:", reportError)
|
|
1671
1764
|
}
|
|
@@ -1683,15 +1776,16 @@ export default class BackgroundJobsWorker {
|
|
|
1683
1776
|
* @param {string} [args.handoffId] - Handoff lease id.
|
|
1684
1777
|
* @param {number} [args.handedOffAtMs] - Handed off timestamp.
|
|
1685
1778
|
* @param {string} [args.workerId] - Worker id.
|
|
1779
|
+
* @param {import("./types.js").PooledRunnerFailure} [args.runnerFailure] - Pooled-child process failure provenance.
|
|
1686
1780
|
* @returns {void}
|
|
1687
1781
|
*/
|
|
1688
|
-
_reportJobResultInBackground({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId}) {
|
|
1782
|
+
_reportJobResultInBackground({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId, runnerFailure}) {
|
|
1689
1783
|
/**
|
|
1690
1784
|
* Defines report.
|
|
1691
1785
|
* @type {Promise<void>} */
|
|
1692
1786
|
let report
|
|
1693
1787
|
|
|
1694
|
-
report = this._reportJobResult({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId}).finally(() => {
|
|
1788
|
+
report = this._reportJobResult({jobId, status, delayMs, error, handoffId, handedOffAtMs, workerId, runnerFailure}).finally(() => {
|
|
1695
1789
|
this.inflightReports.delete(report)
|
|
1696
1790
|
})
|
|
1697
1791
|
|
|
@@ -1779,6 +1779,17 @@ function frontendModelRequestContext() {
|
|
|
1779
1779
|
return captureFrontendModelRemoteRequestContext(configuredContext)
|
|
1780
1780
|
}
|
|
1781
1781
|
|
|
1782
|
+
/**
|
|
1783
|
+
* Captures the explicit lifecycle context or falls back to the configured transport context.
|
|
1784
|
+
* @param {import("../remote-request-context.js").RemoteRequestContext | undefined} requestContext - Registration-local context.
|
|
1785
|
+
* @returns {import("../remote-request-context.js").RemoteRequestContext} Frozen context snapshot.
|
|
1786
|
+
*/
|
|
1787
|
+
function frontendModelEventRequestContext(requestContext) {
|
|
1788
|
+
if (requestContext === undefined) return frontendModelRequestContext()
|
|
1789
|
+
|
|
1790
|
+
return captureFrontendModelRemoteRequestContext(requestContext)
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1782
1793
|
/**
|
|
1783
1794
|
* Runs ensure frontend model instance listener.
|
|
1784
1795
|
* @param {FrontendModelEventSubscription} sub - Event subscription bucket.
|
|
@@ -1799,6 +1810,27 @@ function ensureFrontendModelInstanceListener(sub, id, instance) {
|
|
|
1799
1810
|
return listener
|
|
1800
1811
|
}
|
|
1801
1812
|
|
|
1813
|
+
/**
|
|
1814
|
+
* Removes one instance callback entry and tears down an empty listener/subscription bucket.
|
|
1815
|
+
* @param {FrontendModelEventSubscription} sub - Event subscription bucket.
|
|
1816
|
+
* @param {string} id - Model id.
|
|
1817
|
+
* @param {(listener: {instance: FrontendModelBase, updateCallbacks: Set<FrontendModelModelEventCallbackEntry>, destroyCallbacks: Set<FrontendModelDestroyEventCallbackEntry>}) => void} removeEntry - Callback entry removal.
|
|
1818
|
+
* @returns {void}
|
|
1819
|
+
*/
|
|
1820
|
+
function removeFrontendModelInstanceListenerEntry(sub, id, removeEntry) {
|
|
1821
|
+
const current = sub.instanceListeners.get(id)
|
|
1822
|
+
|
|
1823
|
+
if (!current) return
|
|
1824
|
+
|
|
1825
|
+
removeEntry(current)
|
|
1826
|
+
|
|
1827
|
+
if (current.updateCallbacks.size === 0 && current.destroyCallbacks.size === 0) {
|
|
1828
|
+
sub.instanceListeners.delete(id)
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
sub.maybeTeardown()
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1802
1834
|
/**
|
|
1803
1835
|
* Runs frontend model command url.
|
|
1804
1836
|
* @param {string} resourcePath - Resource path prefix.
|
|
@@ -3723,8 +3755,9 @@ export default class FrontendModelBase {
|
|
|
3723
3755
|
* @returns {Promise<() => void>} - Unsubscribe callback.
|
|
3724
3756
|
*/
|
|
3725
3757
|
static async onCreate(callback, options = {}) {
|
|
3726
|
-
const
|
|
3727
|
-
const
|
|
3758
|
+
const {requestContext, ...eventOptionsPayload} = frontendModelEventOptionsPayload(this, options)
|
|
3759
|
+
const sub = ensureFrontendModelEventSubscription(this, frontendModelEventRequestContext(requestContext))
|
|
3760
|
+
const entry = {callback, ...eventOptionsPayload}
|
|
3728
3761
|
|
|
3729
3762
|
return await sub.registerClassCallback(sub.classCreateCallbacks, entry)
|
|
3730
3763
|
}
|
|
@@ -3737,8 +3770,9 @@ export default class FrontendModelBase {
|
|
|
3737
3770
|
* @returns {Promise<() => void>} - Unsubscribe callback.
|
|
3738
3771
|
*/
|
|
3739
3772
|
static async onUpdate(callback, options = {}) {
|
|
3740
|
-
const
|
|
3741
|
-
const
|
|
3773
|
+
const {requestContext, ...eventOptionsPayload} = frontendModelEventOptionsPayload(this, options)
|
|
3774
|
+
const sub = ensureFrontendModelEventSubscription(this, frontendModelEventRequestContext(requestContext))
|
|
3775
|
+
const entry = {callback, ...eventOptionsPayload}
|
|
3742
3776
|
|
|
3743
3777
|
return await sub.registerClassCallback(sub.classUpdateCallbacks, entry)
|
|
3744
3778
|
}
|
|
@@ -3753,7 +3787,8 @@ export default class FrontendModelBase {
|
|
|
3753
3787
|
static async onDestroy(callback, options = {}) {
|
|
3754
3788
|
assertNoDestroyEventFilter(this, options)
|
|
3755
3789
|
|
|
3756
|
-
const
|
|
3790
|
+
const {requestContext} = frontendModelEventOptionsPayload(this, options)
|
|
3791
|
+
const sub = ensureFrontendModelEventSubscription(this, frontendModelEventRequestContext(requestContext))
|
|
3757
3792
|
const entry = {callback}
|
|
3758
3793
|
|
|
3759
3794
|
return await sub.registerClassCallback(sub.classDestroyCallbacks, entry)
|
|
@@ -3771,24 +3806,23 @@ export default class FrontendModelBase {
|
|
|
3771
3806
|
async onUpdate(callback, options = {}) {
|
|
3772
3807
|
const self = /** @type {ReturnType<typeof JSON.parse>} */ (this)
|
|
3773
3808
|
const ModelClass = frontendModelClassFor(this)
|
|
3774
|
-
const
|
|
3809
|
+
const {requestContext, ...eventOptionsPayload} = frontendModelEventOptionsPayload(ModelClass, options)
|
|
3810
|
+
const sub = ensureFrontendModelEventSubscription(ModelClass, frontendModelEventRequestContext(requestContext))
|
|
3775
3811
|
const id = String(self.id())
|
|
3776
|
-
const entry = {callback, ...
|
|
3812
|
+
const entry = {callback, ...eventOptionsPayload}
|
|
3777
3813
|
const listener = ensureFrontendModelInstanceListener(sub, id, this)
|
|
3778
3814
|
|
|
3779
3815
|
listener.updateCallbacks.add(entry)
|
|
3780
|
-
await sub.ensureSubscribed()
|
|
3781
|
-
|
|
3782
|
-
return () => {
|
|
3783
|
-
const current = sub.instanceListeners.get(id)
|
|
3784
3816
|
|
|
3785
|
-
|
|
3786
|
-
|
|
3817
|
+
try {
|
|
3818
|
+
await sub.ensureSubscribed()
|
|
3819
|
+
} catch (error) {
|
|
3820
|
+
removeFrontendModelInstanceListenerEntry(sub, id, (current) => current.updateCallbacks.delete(entry))
|
|
3821
|
+
throw error
|
|
3822
|
+
}
|
|
3787
3823
|
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
}
|
|
3791
|
-
sub.maybeTeardown()
|
|
3824
|
+
return () => {
|
|
3825
|
+
removeFrontendModelInstanceListenerEntry(sub, id, (current) => current.updateCallbacks.delete(entry))
|
|
3792
3826
|
}
|
|
3793
3827
|
}
|
|
3794
3828
|
|
|
@@ -3804,24 +3838,23 @@ export default class FrontendModelBase {
|
|
|
3804
3838
|
|
|
3805
3839
|
assertNoDestroyEventFilter(ModelClass, options)
|
|
3806
3840
|
|
|
3807
|
-
const
|
|
3841
|
+
const {requestContext} = frontendModelEventOptionsPayload(ModelClass, options)
|
|
3842
|
+
const sub = ensureFrontendModelEventSubscription(ModelClass, frontendModelEventRequestContext(requestContext))
|
|
3808
3843
|
const id = String(self.id())
|
|
3809
3844
|
const entry = {callback}
|
|
3810
3845
|
const listener = ensureFrontendModelInstanceListener(sub, id, this)
|
|
3811
3846
|
|
|
3812
3847
|
listener.destroyCallbacks.add(entry)
|
|
3813
|
-
await sub.ensureSubscribed()
|
|
3814
3848
|
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3849
|
+
try {
|
|
3850
|
+
await sub.ensureSubscribed()
|
|
3851
|
+
} catch (error) {
|
|
3852
|
+
removeFrontendModelInstanceListenerEntry(sub, id, (current) => current.destroyCallbacks.delete(entry))
|
|
3853
|
+
throw error
|
|
3854
|
+
}
|
|
3820
3855
|
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
}
|
|
3824
|
-
sub.maybeTeardown()
|
|
3856
|
+
return () => {
|
|
3857
|
+
removeFrontendModelInstanceListenerEntry(sub, id, (current) => current.destroyCallbacks.delete(entry))
|
|
3825
3858
|
}
|
|
3826
3859
|
}
|
|
3827
3860
|
|
|
@@ -39,9 +39,15 @@ import isPlainObject from "../utils/plain-object.js"
|
|
|
39
39
|
* @property {string[] | Record<string, string[]>} [abilities] - Ability actions to compute per record.
|
|
40
40
|
* @property {string | Array<string | Record<string, FrontendModelTransportValue>> | Record<string, FrontendModelTransportValue>} [queryData] - Backend query data names/spec.
|
|
41
41
|
*/
|
|
42
|
+
/**
|
|
43
|
+
* FrontendModelEventRoutingOptions type.
|
|
44
|
+
* @typedef {object} FrontendModelEventRoutingOptions
|
|
45
|
+
* @property {FrontendModelQuery<import("./base.js").FrontendModelClass>} [query] - Query whose filters match events and whose projections shape event records.
|
|
46
|
+
* @property {import("../remote-request-context.js").RemoteRequestContext} [requestContext] - Registration-local remote routing context. Its captured value partitions lifecycle server subscriptions and replaces the transport-wide context for this registration.
|
|
47
|
+
*/
|
|
42
48
|
/**
|
|
43
49
|
* Defines this typedef.
|
|
44
|
-
* @typedef {FrontendModelProjectionOptions &
|
|
50
|
+
* @typedef {FrontendModelProjectionOptions & FrontendModelEventRoutingOptions} FrontendModelEventOptionsObject
|
|
45
51
|
*/
|
|
46
52
|
/**
|
|
47
53
|
* FrontendModelEventOptions type.
|
|
@@ -69,12 +75,16 @@ import isPlainObject from "../utils/plain-object.js"
|
|
|
69
75
|
* @typedef {FrontendModelEventFilterPayload & {key: string}} FrontendModelEventFilterPayloadEntry
|
|
70
76
|
*/
|
|
71
77
|
/**
|
|
72
|
-
*
|
|
73
|
-
* @typedef {object}
|
|
78
|
+
* FrontendModelEventQueryPayload type.
|
|
79
|
+
* @typedef {object} FrontendModelEventQueryPayload
|
|
74
80
|
* @property {string | null} eventFilterKey - Stable event filter key, or null when no filter is present.
|
|
75
81
|
* @property {FrontendModelEventFilterPayload | null} eventFilterPayload - Normalized event filter payload, or null when unfiltered.
|
|
76
82
|
* @property {FrontendModelProjectionPayload} projectionPayload - Normalized event serialization projection payload.
|
|
77
83
|
*/
|
|
84
|
+
/**
|
|
85
|
+
* FrontendModelEventOptionsPayload type.
|
|
86
|
+
* @typedef {FrontendModelEventQueryPayload & {requestContext: import("../remote-request-context.js").RemoteRequestContext | undefined}} FrontendModelEventOptionsPayload
|
|
87
|
+
*/
|
|
78
88
|
/**
|
|
79
89
|
* FrontendModelSort type.
|
|
80
90
|
* @typedef {object} FrontendModelSort
|
|
@@ -1829,7 +1839,7 @@ export default class FrontendModelQuery {
|
|
|
1829
1839
|
|
|
1830
1840
|
/**
|
|
1831
1841
|
* Returns the eventOptionsPayload result.
|
|
1832
|
-
* @returns {
|
|
1842
|
+
* @returns {FrontendModelEventQueryPayload} - Combined event filter and projection payload.
|
|
1833
1843
|
*/
|
|
1834
1844
|
eventOptionsPayload() {
|
|
1835
1845
|
const eventFilterPayload = this.eventFilterPayload()
|
|
@@ -2236,5 +2246,10 @@ function frontendModelEventQuery(modelClass, options = {}) {
|
|
|
2236
2246
|
* @returns {FrontendModelEventOptionsPayload} - Normalized event subscription payload.
|
|
2237
2247
|
*/
|
|
2238
2248
|
export function frontendModelEventOptionsPayload(modelClass, options = {}) {
|
|
2239
|
-
|
|
2249
|
+
const requestContext = options instanceof FrontendModelQuery ? undefined : options.requestContext
|
|
2250
|
+
|
|
2251
|
+
return {
|
|
2252
|
+
...frontendModelEventQuery(modelClass, options).eventOptionsPayload(),
|
|
2253
|
+
requestContext
|
|
2254
|
+
}
|
|
2240
2255
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {captureRemoteRequestContext, mergeRemoteRequestContext} from "../remote-request-context.js"
|
|
3
|
+
import {captureRemoteRequestContext, mergeRemoteRequestContext, remoteRequestContextKey} from "../remote-request-context.js"
|
|
4
4
|
|
|
5
5
|
const RESERVED_KEYS = ["commandType", "customPath", "model", "payload", "requestContext", "requestId", "requests"]
|
|
6
6
|
const REQUEST_CONTEXT_LABEL = "Frontend model request context"
|
|
@@ -17,6 +17,18 @@ export function captureFrontendModelRemoteRequestContext(value) {
|
|
|
17
17
|
})
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Builds the canonical identity for one frontend-model remote request context.
|
|
22
|
+
* @param {ReturnType<typeof JSON.parse> | undefined} value - Context value.
|
|
23
|
+
* @returns {string} Stable serialized context key.
|
|
24
|
+
*/
|
|
25
|
+
export function frontendModelRemoteRequestContextKey(value) {
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
context: remoteRequestContextKey(captureFrontendModelRemoteRequestContext(value)),
|
|
28
|
+
present: value !== undefined
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
/**
|
|
21
33
|
* Merges captured context into frontend-model command or subscription params.
|
|
22
34
|
* @template {Record<string, ReturnType<typeof JSON.parse>>} TParams
|