velocious 1.0.701 → 1.0.703
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 +1 -0
- package/build/background-jobs/pooled-runner-shutdown.js +1 -0
- package/build/background-jobs/types.js +2 -2
- package/build/background-jobs/worker.js +128 -2
- package/build/configuration-types.js +73 -2
- package/build/configuration.js +67 -4
- package/build/frontend-model-resource/base-resource.js +150 -2
- package/build/frontend-models/resource-definition.js +47 -0
- package/build/src/background-jobs/pooled-runner-shutdown.d.ts.map +1 -1
- package/build/src/background-jobs/pooled-runner-shutdown.js +2 -1
- package/build/src/background-jobs/types.d.ts +4 -4
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +3 -3
- package/build/src/background-jobs/worker.d.ts +90 -1
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +132 -3
- package/build/src/configuration-types.d.ts +171 -5
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +68 -3
- package/build/src/configuration.d.ts +11 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +67 -5
- package/build/src/frontend-model-resource/base-resource.d.ts +76 -3
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +127 -3
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +43 -1
- package/build/src/sync/local-mutation-log.d.ts +6 -3
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +11 -4
- package/build/src/sync/peer-mutation-bundle.d.ts +26 -0
- package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -1
- package/build/src/sync/peer-mutation-bundle.js +7 -1
- package/build/src/sync/sync-api-client.d.ts +3 -1
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +46 -8
- package/build/src/sync/sync-client-types.d.ts +24 -12
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +7 -5
- package/build/src/sync/sync-client.d.ts +101 -0
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +236 -24
- package/build/src/sync/sync-offline-session.d.ts +328 -0
- package/build/src/sync/sync-offline-session.d.ts.map +1 -0
- package/build/src/sync/sync-offline-session.js +617 -0
- package/build/src/sync/sync-resource-base.d.ts +26 -28
- package/build/src/sync/sync-resource-base.d.ts.map +1 -1
- package/build/src/sync/sync-resource-base.js +11 -45
- package/build/sync/local-mutation-log.js +10 -3
- package/build/sync/peer-mutation-bundle.js +6 -0
- package/build/sync/sync-api-client.js +53 -7
- package/build/sync/sync-client-types.js +6 -4
- package/build/sync/sync-client.js +264 -21
- package/build/sync/sync-offline-session.js +713 -0
- package/build/sync/sync-resource-base.js +10 -53
- package/package.json +1 -1
- package/src/background-jobs/pooled-runner-shutdown.js +1 -0
- package/src/background-jobs/types.js +2 -2
- package/src/background-jobs/worker.js +128 -2
- package/src/configuration-types.js +73 -2
- package/src/configuration.js +67 -4
- package/src/frontend-model-resource/base-resource.js +150 -2
- package/src/frontend-models/resource-definition.js +47 -0
- package/src/sync/local-mutation-log.js +10 -3
- package/src/sync/peer-mutation-bundle.js +6 -0
- package/src/sync/sync-api-client.js +53 -7
- package/src/sync/sync-client-types.js +6 -4
- package/src/sync/sync-client.js +264 -21
- package/src/sync/sync-offline-session.js +713 -0
- package/src/sync/sync-resource-base.js +10 -53
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, including committed counter-cache parent updates, 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))
|
|
16
16
|
* 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))
|
|
17
17
|
* Declarative client sync scopes with per-scope cursors, a reusable observable single-flight coordinator with bounded retry and deterministic teardown, generation-fenced identity replacement and selected-scope cache reset, automatic mutation tracking, opt-in durable base-version conflict replay, realtime delivery and reconnect gap closure, computed server-publish scope metadata with atomic null-safe publisher row identities, a shared public maintenance upsert helper and legacy-duplicate reconciliation, 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/sync-coordinator.md](docs/sync-coordinator.md), [docs/remote-request-context.md](docs/remote-request-context.md), and [docs/offline-sync.md](docs/offline-sync.md))
|
|
18
|
+
* SyncClient long-offline sessions: framework-owned device-identity enrollment, backend-issued offline grants with per-grant retention, signing at queue time, signed batch replay with synced/conflict/rejected mapping, and signed peer bundle export/import through `sync.client.offline` (see [docs/sync-client.md](docs/sync-client.md#long-offline-sessions-grant-lifecycle-signed-queue-signed-replay-peer-api) and [docs/offline-sync.md](docs/offline-sync.md#implemented-slice-syncclient-offline-session-grant-lifecycle-signed-queue-signed-replay-peer-api))
|
|
18
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))
|
|
19
20
|
* Server-side sync envelope replay orchestration for app-owned sync receivers, including allowlisted authoritative conflict snapshots that retain submitted aliases in conflict metadata while keying `serverModel` by canonical model attributes (see [docs/sync-envelope-replay-service.md](docs/sync-envelope-replay-service.md))
|
|
20
21
|
* Self-sustaining sync feeds: upstream imports triggered by the changes pull itself, with framework-owned coalescing and throttling (see [docs/sync-upstream-imports.md](docs/sync-upstream-imports.md))
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
/** @typedef {"queued" | "handed_off"} BackgroundJobActiveStatus */
|
|
10
10
|
/** @typedef {"cancelled" | "completed" | "failed" | "orphaned"} BackgroundJobTerminalStatus */
|
|
11
11
|
/** @typedef {BackgroundJobActiveStatus | BackgroundJobTerminalStatus} BackgroundJobStatus */
|
|
12
|
-
/** @typedef {"exit" | "process-error" | "ipc-send"} PooledRunnerFailureOrigin */
|
|
12
|
+
/** @typedef {"exit" | "process-error" | "ipc-send" | "child-silence"} PooledRunnerFailureOrigin */
|
|
13
13
|
/** @typedef {"starting" | "running" | "retiring"} PooledRunnerLifecycleState */
|
|
14
|
-
/** @typedef {"parent_retire_drained" | "job_timeout" | "worker_stop" | "signal_sigterm" | "signal_sigint" | "signal_sigkill" | "signal_other" | "ipc_disconnect" | "process_error" | "unexpected_exit"} PooledChildShutdownReason */
|
|
14
|
+
/** @typedef {"parent_retire_drained" | "job_timeout" | "worker_stop" | "child_silence" | "signal_sigterm" | "signal_sigint" | "signal_sigkill" | "signal_other" | "ipc_disconnect" | "process_error" | "unexpected_exit"} PooledChildShutdownReason */
|
|
15
15
|
/**
|
|
16
16
|
* @deprecated Use PooledChildShutdownReason for exact shutdown provenance.
|
|
17
17
|
* @typedef {"unexpected" | "job-timeout" | "worker-shutdown-timeout"} PooledRunnerTerminationReason
|
|
@@ -50,11 +50,23 @@ import { POOLED_RUNNER_INFLIGHT_JOB_ID_LIMIT, boundedPooledRunnerInflightJobIds,
|
|
|
50
50
|
* @property {ReturnType<typeof setTimeout>} [shutdownSignalTimer] - Drained-retirement fallback signal timer.
|
|
51
51
|
* @property {ReturnType<typeof setTimeout> | null} [timeoutSigkillTimer] - Pending timeout SIGKILL timer.
|
|
52
52
|
* @property {string} [timeoutJobId] - Job whose timeout initiated termination.
|
|
53
|
+
* @property {number} lastActivityAtMs - Last time the worker saw any IPC message from this child, or dispatched to it.
|
|
54
|
+
* @property {ReturnType<typeof setTimeout>} [silenceTimer] - Armed per-child IPC-silence watchdog timer.
|
|
55
|
+
* @property {ReturnType<typeof setTimeout>} [silenceSigkillTimer] - Pending SIGKILL for a child the silence watchdog declared wedged.
|
|
53
56
|
*/
|
|
54
57
|
/** Grace period after SIGTERM before a lingering process runner is SIGKILLed. */
|
|
55
58
|
const FORKED_CHILD_SIGKILL_GRACE_MS = 5000
|
|
56
59
|
/** Time a drained retirement gives the child IPC request to begin teardown before SIGTERM fallback. */
|
|
57
60
|
const POOLED_RUNNER_SHUTDOWN_REQUEST_GRACE_MS = 250
|
|
61
|
+
/**
|
|
62
|
+
* Default bound on how long a pooled child with in-flight jobs may stay silent on
|
|
63
|
+
* its IPC channel before the worker declares it wedged. A healthy child samples its
|
|
64
|
+
* memory every 10 seconds while jobs run, so silence past this bound means its event
|
|
65
|
+
* loop cannot run its own timers — the signature of the silent transport wedge. The
|
|
66
|
+
* bound is deliberately far above a healthy long job's quiet window: it only fires on
|
|
67
|
+
* a child that cannot prove it is alive, never on a genuinely busy runner.
|
|
68
|
+
*/
|
|
69
|
+
const POOLED_CHILD_SILENCE_TIMEOUT_MS = 120000
|
|
58
70
|
/**
|
|
59
71
|
* Largest delay Node's `setTimeout` accepts without overflowing to a 1ms delay
|
|
60
72
|
* (a 32-bit signed int of ms, ~24.8 days). A `jobTimeoutMs` above this — or a
|
|
@@ -198,13 +210,14 @@ export default class BackgroundJobsWorker {
|
|
|
198
210
|
* @param {number} [args.generationHandshakeTimeoutMs] - Maximum time to wait for generation acknowledgement (default: 4000).
|
|
199
211
|
* @param {number} [args.reconnectDelayMs] - Delay before reconnecting an established worker connection (default: 1000).
|
|
200
212
|
* @param {number} [args.jobTimeoutMs] - Override the wall-clock timeout for forked and pooled jobs from `configuration.getBackgroundJobsConfig()`. `0` disables it.
|
|
213
|
+
* @param {number} [args.pooledRunnerSilenceTimeoutMs] - Override the per-child IPC-silence bound for pooled children with in-flight jobs from `configuration.getBackgroundJobsConfig()` (default 120000ms). `0` disables the watchdog.
|
|
201
214
|
* @param {boolean} [args.closeDatabaseConnectionsOnStop] - Whether stop owns closing the configuration's database pools (default true).
|
|
202
215
|
* @param {() => void | Promise<void>} [args.onStopped] - Lifecycle hook invoked after the worker finishes stopping.
|
|
203
216
|
* @param {() => void} [args.onGenerationAccepted] - Explicit generation-acceptance observation hook.
|
|
204
217
|
* @param {() => void} [args.onRetireMessage] - Explicit retire-message observation hook.
|
|
205
218
|
* @param {(observation: import("./types.js").PooledChildMemoryObservation) => void | Promise<void>} [args.onPooledRunnerMemoryObservation] - Explicit pooled-child memory observation hook. Every validated observation (periodic while a child has in-flight jobs, plus on demand) is forwarded here, in addition to the worker's compact stderr log line, so an application can route memory diagnostics (e.g. to a bug reporter) without parsing logs.
|
|
206
219
|
*/
|
|
207
|
-
constructor({configuration, host, port, generationId, workerInstanceId, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, runnerMaxOldSpaceSizeMb, runnerMallocArenaMax, forkedChildSigkillGraceMs, heartbeatIntervalMs, generationHandshakeTimeoutMs = DEFAULT_GENERATION_HANDSHAKE_TIMEOUT_MS, reconnectDelayMs = 1000, jobTimeoutMs, closeDatabaseConnectionsOnStop = true, onStopped, onGenerationAccepted, onRetireMessage, onPooledRunnerMemoryObservation} = {}) {
|
|
220
|
+
constructor({configuration, host, port, generationId, workerInstanceId, maxConcurrentForkedJobs, maxConcurrentInlineJobs, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, runnerMaxOldSpaceSizeMb, runnerMallocArenaMax, forkedChildSigkillGraceMs, heartbeatIntervalMs, generationHandshakeTimeoutMs = DEFAULT_GENERATION_HANDSHAKE_TIMEOUT_MS, reconnectDelayMs = 1000, jobTimeoutMs, pooledRunnerSilenceTimeoutMs, closeDatabaseConnectionsOnStop = true, onStopped, onGenerationAccepted, onRetireMessage, onPooledRunnerMemoryObservation} = {}) {
|
|
208
221
|
/**
|
|
209
222
|
* Narrows the runtime value to the documented type.
|
|
210
223
|
* @type {Promise<import("../configuration.js").default>} */
|
|
@@ -261,6 +274,8 @@ export default class BackgroundJobsWorker {
|
|
|
261
274
|
this.pooledRunnerMaxJobs = this.pooledRunnerMaxJobsOverride || 100
|
|
262
275
|
this.pooledRunnerMaxRssBytes = this.pooledRunnerMaxRssBytesOverride || 512 * 1024 * 1024
|
|
263
276
|
this.pooledRunnerMaxLifetimeMs = this.pooledRunnerMaxLifetimeMsOverride || 60 * 60 * 1000
|
|
277
|
+
this.pooledRunnerSilenceTimeoutMsOverride = typeof pooledRunnerSilenceTimeoutMs === "number" ? pooledRunnerSilenceTimeoutMs : undefined
|
|
278
|
+
this.pooledRunnerSilenceTimeoutMs = this.pooledRunnerSilenceTimeoutMsOverride ?? POOLED_CHILD_SILENCE_TIMEOUT_MS
|
|
264
279
|
this.runnerMaxOldSpaceSizeMb = this.runnerMaxOldSpaceSizeMbOverride || 2048
|
|
265
280
|
this.runnerMallocArenaMax = this.runnerMallocArenaMaxOverride || 4
|
|
266
281
|
/**
|
|
@@ -429,6 +444,7 @@ export default class BackgroundJobsWorker {
|
|
|
429
444
|
if (typeof this.pooledRunnerMaxJobsOverride !== "number") this.pooledRunnerMaxJobs = poolConfig.pooledRunnerMaxJobs
|
|
430
445
|
if (typeof this.pooledRunnerMaxRssBytesOverride !== "number") this.pooledRunnerMaxRssBytes = poolConfig.pooledRunnerMaxRssBytes
|
|
431
446
|
if (typeof this.pooledRunnerMaxLifetimeMsOverride !== "number") this.pooledRunnerMaxLifetimeMs = poolConfig.pooledRunnerMaxLifetimeMs
|
|
447
|
+
if (typeof this.pooledRunnerSilenceTimeoutMsOverride !== "number" && typeof poolConfig.pooledRunnerSilenceTimeoutMs === "number") this.pooledRunnerSilenceTimeoutMs = poolConfig.pooledRunnerSilenceTimeoutMs
|
|
432
448
|
if (typeof this.runnerMaxOldSpaceSizeMbOverride !== "number" && typeof poolConfig.runnerMaxOldSpaceSizeMb === "number") this.runnerMaxOldSpaceSizeMb = poolConfig.runnerMaxOldSpaceSizeMb
|
|
433
449
|
if (typeof this.runnerMallocArenaMaxOverride !== "number" && typeof poolConfig.runnerMallocArenaMax === "number") this.runnerMallocArenaMax = poolConfig.runnerMallocArenaMax
|
|
434
450
|
|
|
@@ -1279,6 +1295,7 @@ export default class BackgroundJobsWorker {
|
|
|
1279
1295
|
const timeoutTimer = this._armPooledJobTimeout({child, payload})
|
|
1280
1296
|
|
|
1281
1297
|
state.inflight.set(payload.id, {payload, resolve: resolvePooledJob, pooledJob, timeoutTimer})
|
|
1298
|
+
this._notePooledChildActivity({child, state})
|
|
1282
1299
|
try {
|
|
1283
1300
|
child.send({type: "job", payload, sharedTransactionBroker: this._pooledJobSharedTransactionBrokerConfig()})
|
|
1284
1301
|
} catch (error) {
|
|
@@ -1403,7 +1420,7 @@ export default class BackgroundJobsWorker {
|
|
|
1403
1420
|
})
|
|
1404
1421
|
this.pooledChildren.add(child)
|
|
1405
1422
|
this.inflightProcessChildren.add(child)
|
|
1406
|
-
this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), lastDispatchSeq: 0, retiring: false, started: false})
|
|
1423
|
+
this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), lastDispatchSeq: 0, retiring: false, started: false, lastActivityAtMs: Date.now()})
|
|
1407
1424
|
child.on("message", (message) => this._handlePooledChildMessage({child, message}))
|
|
1408
1425
|
child.once("exit", (exitCode, signal) => this._handlePooledChildFailure({
|
|
1409
1426
|
child,
|
|
@@ -1438,6 +1455,7 @@ export default class BackgroundJobsWorker {
|
|
|
1438
1455
|
if (!message || typeof message !== "object") return
|
|
1439
1456
|
const record = /** @type {{type?: ReturnType<typeof JSON.parse>, childInstanceId?: ReturnType<typeof JSON.parse>, jobId?: ReturnType<typeof JSON.parse>, acknowledged?: ReturnType<typeof JSON.parse>, rssBytes?: ReturnType<typeof JSON.parse>, peakRssBytes?: ReturnType<typeof JSON.parse>, error?: ReturnType<typeof JSON.parse>}} */ (message)
|
|
1440
1457
|
const state = this.pooledChildStates.get(child)
|
|
1458
|
+
this._notePooledChildActivity({child, state})
|
|
1441
1459
|
if (record.type === "ready") {
|
|
1442
1460
|
if (state) {
|
|
1443
1461
|
state.started = true
|
|
@@ -1700,6 +1718,112 @@ export default class BackgroundJobsWorker {
|
|
|
1700
1718
|
return false
|
|
1701
1719
|
}
|
|
1702
1720
|
|
|
1721
|
+
/**
|
|
1722
|
+
* Records one pooled child's IPC activity and re-arms its silence watchdog while
|
|
1723
|
+
* that child still owns in-flight jobs. Every message the child sends — readiness,
|
|
1724
|
+
* acceptance, memory observation, shutdown observation, or job outcome — proves its
|
|
1725
|
+
* event loop is running, because a healthy child samples itself every 10 seconds
|
|
1726
|
+
* while jobs are in flight. Silence past the bound therefore means a wedged child
|
|
1727
|
+
* that can no longer prove it is alive, not a busy one.
|
|
1728
|
+
* @param {object} args - Activity details.
|
|
1729
|
+
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
1730
|
+
* @param {PooledChildState | undefined} args.state - Child state.
|
|
1731
|
+
* @returns {void}
|
|
1732
|
+
*/
|
|
1733
|
+
_notePooledChildActivity({child, state}) {
|
|
1734
|
+
if (!state) return
|
|
1735
|
+
state.lastActivityAtMs = Date.now()
|
|
1736
|
+
if (state.inflight.size > 0 && !state.settling) this._armPooledChildSilenceTimer({child, state})
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* Resolves the effective pooled-child silence bound in ms, or null when disabled.
|
|
1741
|
+
* A non-positive or non-finite value disables the watchdog at whichever level
|
|
1742
|
+
* supplied it, matching the job-timeout contract.
|
|
1743
|
+
* @returns {number | null} - Bound in ms, or null when disabled.
|
|
1744
|
+
*/
|
|
1745
|
+
_resolvePooledChildSilenceTimeoutMs() {
|
|
1746
|
+
const raw = this.pooledRunnerSilenceTimeoutMs
|
|
1747
|
+
|
|
1748
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return null
|
|
1749
|
+
|
|
1750
|
+
return Math.min(raw, MAX_FORKED_JOB_TIMEOUT_MS)
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Re-arms one child's silence watchdog from its last observed activity. Only
|
|
1755
|
+
* children with in-flight jobs are watched: a child with nothing in flight cannot
|
|
1756
|
+
* strand work, and its periodic sampler is inactive then, so silence is not
|
|
1757
|
+
* evidence of a wedge.
|
|
1758
|
+
* @param {object} args - Watchdog details.
|
|
1759
|
+
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
1760
|
+
* @param {PooledChildState} args.state - Child state.
|
|
1761
|
+
* @returns {void}
|
|
1762
|
+
*/
|
|
1763
|
+
_armPooledChildSilenceTimer({child, state}) {
|
|
1764
|
+
const timeoutMs = this._resolvePooledChildSilenceTimeoutMs()
|
|
1765
|
+
if (!(typeof timeoutMs === "number" && timeoutMs > 0)) return
|
|
1766
|
+
|
|
1767
|
+
if (state.silenceTimer) clearTimeout(state.silenceTimer)
|
|
1768
|
+
state.silenceTimer = setTimeout(() => this._onPooledChildSilence({child, state}), timeoutMs)
|
|
1769
|
+
if (typeof state.silenceTimer.unref === "function") state.silenceTimer.unref()
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* Fired when a pooled child with in-flight jobs stays silent past the bound. A
|
|
1774
|
+
* wedged child keeps its IPC channel open but stops running its own timers, so the
|
|
1775
|
+
* disconnect/exit path never fires and its jobs stay stranded until a human
|
|
1776
|
+
* notices. The worker declares the child dead: it requests shutdown under the exact
|
|
1777
|
+
* `child_silence` reason, reports the whole in-flight set as failed through the
|
|
1778
|
+
* established failure path (so the jobs requeue and capacity is re-advertised), and
|
|
1779
|
+
* signals the wedged process — SIGTERM, then SIGKILL after the existing grace — so
|
|
1780
|
+
* it actually goes away instead of blocking the event loop forever.
|
|
1781
|
+
* @param {object} args - Watchdog details.
|
|
1782
|
+
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
1783
|
+
* @param {PooledChildState} args.state - Child state.
|
|
1784
|
+
* @returns {void}
|
|
1785
|
+
*/
|
|
1786
|
+
_onPooledChildSilence({child, state}) {
|
|
1787
|
+
state.silenceTimer = undefined
|
|
1788
|
+
if (state.settling || state.shutdownReason) return
|
|
1789
|
+
const silentMs = Date.now() - state.lastActivityAtMs
|
|
1790
|
+
const boundMs = this._resolvePooledChildSilenceTimeoutMs()
|
|
1791
|
+
if (!(typeof boundMs === "number" && silentMs >= boundMs)) return
|
|
1792
|
+
|
|
1793
|
+
const error = new Error(`Pooled background job runner went silent for ${silentMs}ms past the ${boundMs}ms bound — treated as wedged`)
|
|
1794
|
+
this._logPooledSilenceEvent({child, state, silentMs, boundMs})
|
|
1795
|
+
this._requestPooledChildShutdown({child, reason: "child_silence", signal: "SIGTERM"})
|
|
1796
|
+
void this._handlePooledChildFailure({child, error, origin: "child-silence", signal: null})
|
|
1797
|
+
state.silenceSigkillTimer = setTimeout(() => {
|
|
1798
|
+
state.silenceSigkillTimer = undefined
|
|
1799
|
+
this._signalPooledChild({child, signal: "SIGKILL"})
|
|
1800
|
+
}, this.forkedChildSigkillGraceMs)
|
|
1801
|
+
if (typeof state.silenceSigkillTimer.unref === "function") state.silenceSigkillTimer.unref()
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Logs one compact JSON line naming a child the silence watchdog declared wedged
|
|
1806
|
+
* (the same stderr surface the memory observations use, which reaches the production
|
|
1807
|
+
* log), so the recovery is attributable after the fact instead of looking like a
|
|
1808
|
+
* random runner exit.
|
|
1809
|
+
* @param {object} args - Event details.
|
|
1810
|
+
* @param {import("node:child_process").ChildProcess} args.child - Pooled child.
|
|
1811
|
+
* @param {PooledChildState} args.state - Child state.
|
|
1812
|
+
* @param {number} args.silentMs - Observed silence in ms.
|
|
1813
|
+
* @param {number} args.boundMs - Configured bound in ms.
|
|
1814
|
+
* @returns {void}
|
|
1815
|
+
*/
|
|
1816
|
+
_logPooledSilenceEvent({child, state, silentMs, boundMs}) {
|
|
1817
|
+
console.error(JSON.stringify({
|
|
1818
|
+
event: "pooled-child-silence",
|
|
1819
|
+
childInstanceId: state.childInstanceId ?? null,
|
|
1820
|
+
childPid: child.pid ?? null,
|
|
1821
|
+
silentMs,
|
|
1822
|
+
boundMs,
|
|
1823
|
+
jobs: state.inflight.size
|
|
1824
|
+
}))
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1703
1827
|
/**
|
|
1704
1828
|
* Logs one compact pooled-capacity event to the worker's own stderr (the
|
|
1705
1829
|
* surface that reaches the production log). Used by the bounded liveness
|
|
@@ -1848,6 +1972,8 @@ export default class BackgroundJobsWorker {
|
|
|
1848
1972
|
// the SIGKILL grace from a timeout kill, and every armed per-job backstop.
|
|
1849
1973
|
if (state.timeoutSigkillTimer) clearTimeout(state.timeoutSigkillTimer)
|
|
1850
1974
|
if (state.shutdownSignalTimer) clearTimeout(state.shutdownSignalTimer)
|
|
1975
|
+
if (state.silenceTimer) clearTimeout(state.silenceTimer)
|
|
1976
|
+
if (state.silenceSigkillTimer) clearTimeout(state.silenceSigkillTimer)
|
|
1851
1977
|
for (const inflightEntry of state.inflight.values()) {
|
|
1852
1978
|
if (inflightEntry.timeoutTimer) clearTimeout(inflightEntry.timeoutTimer)
|
|
1853
1979
|
}
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
* @property {number} [pooledRunnerMaxJobs] - Number of sequential jobs a pooled child runs before it is replaced, bounding process-level resource accumulation. Default: `100`.
|
|
200
200
|
* @property {number} [pooledRunnerMaxRssBytes] - Peak RSS (the kernel `VmHWM` high-water mark reported by the child) at which a pooled child is replaced after an acknowledged job. The child's peak, not its settled RSS at job-outcome time, is the trigger, so a burst working-set that reclaims before the outcome still retires the child. Default: `536870912` (512 MiB).
|
|
201
201
|
* @property {number} [pooledRunnerMaxLifetimeMs] - Age after an acknowledged job at which a pooled child is replaced. Default: `3600000` (one hour).
|
|
202
|
+
* @property {number} [pooledRunnerSilenceTimeoutMs] - IPC-silence bound for a pooled child with in-flight jobs. A child that sends nothing (readiness, acceptance, memory observation, or job outcome) for longer than this is declared wedged: its in-flight jobs are reported failed for requeue, its capacity is re-advertised, and the wedged process is terminated. A healthy child samples itself every 10 seconds while jobs run, so the bound only fires on a child that cannot prove it is alive. Default: `120000`.
|
|
202
203
|
* @property {number} [runnerMaxOldSpaceSizeMb] - V8 heap cap (`--max-old-space-size`, MiB) applied to every background-jobs runner child process (pooled, forked, and spawned) so a single child can never commit multiple GBs of heap. Default: `2048`.
|
|
203
204
|
* @property {number} [runnerMallocArenaMax] - `MALLOC_ARENA_MAX` exported to every background-jobs runner child process, bounding glibc per-thread arenas. Defaults to an inherited `MALLOC_ARENA_MAX`, else `4`.
|
|
204
205
|
* @property {Record<string, {maxConcurrent?: number, priority?: number}>} [queues] - Per-queue
|
|
@@ -476,9 +477,19 @@
|
|
|
476
477
|
* @property {"hasOne" | "hasMany"} type - Attachment cardinality.
|
|
477
478
|
*/
|
|
478
479
|
|
|
480
|
+
/**
|
|
481
|
+
* One declarative quick-search column. A plain string is a root-table column;
|
|
482
|
+
* `{path, column}` searches a column on a joined relationship path.
|
|
483
|
+
* @typedef {object} FrontendModelQuickSearchColumn
|
|
484
|
+
* @property {string[]} [path] - Relationship path from the root model to the column's table.
|
|
485
|
+
* @property {string} column - Database column name to search.
|
|
486
|
+
*/
|
|
487
|
+
|
|
479
488
|
/**
|
|
480
489
|
* @typedef {object} FrontendModelResourceConfiguration
|
|
481
490
|
* @property {Array<string | FrontendModelAttributeConfiguration> | Record<string, FrontendModelAttributeConfiguration | import("./database/drivers/base-column.js").default | boolean>} attributes - Attributes to expose on the frontend model.
|
|
491
|
+
* @property {Array<FrontendModelQuickSearchColumn | string>} [quickSearchColumns] - Declarative quick-search text columns; an index search on the pseudo-column `quickSearch` expands to an OR of LIKE conditions over these columns.
|
|
492
|
+
* @property {Array<FrontendModelQuickSearchColumn | string>} [quickSearchNumericColumns] - Declarative quick-search numeric columns; an integer quick-search value also matches these columns exactly.
|
|
482
493
|
* @property {string[]} [abilities] - Additional camelCase ability action names to expose for per-record `record.can(action)` reads. Base CRUD actions (`read`, `create`, `update`, `destroy`) are always included and must not be listed here.
|
|
483
494
|
* @property {Record<string, FrontendModelAttachmentConfiguration>} [attachments] - Attachment helpers keyed by attachment name.
|
|
484
495
|
* @property {string[]} [commands] - Legacy built-in command names (`index`, `find`, `create`, `update`, `destroy`, `attach`, `download`, `url`).
|
|
@@ -588,17 +599,75 @@
|
|
|
588
599
|
* @property {() => boolean | Promise<boolean>} [isOnline] - Connectivity gate for pulls and replays. Defaults to always online.
|
|
589
600
|
* @property {string} [mountPath] - Mount path the server serves the sync endpoints under (match the server's `sync.api.mountPath`). Defaults to "/velocious/sync"; normalization strips trailing slashes and always fills in the default.
|
|
590
601
|
* @property {(error: Error) => void} [onError] - Reports background replay/pull failures. Defaults to rethrowing.
|
|
602
|
+
* @property {VelociousSyncClientOfflineConfiguration} [offline] - Long-offline session configuration consumed by `startOfflineSession()`: device identity, enrollment, grant scopes, signed-replay path, and the backend public key for peer import.
|
|
591
603
|
* @property {VelociousSyncClientRealtimeConfiguration} [realtime] - Realtime push configuration consumed by `subscribeRealtime(...)`.
|
|
592
604
|
* @property {VelociousSyncClientTransport} transport - Transport posting to the framework sync endpoints (e.g. the frontend-model websocket client).
|
|
593
605
|
* @property {VelociousSyncRealtimeWebsocketClient} [websocketClient] - Shared app-lifetime websocket client (the low-level form) that all sync traffic rides. Provide the same instance the frontend-model transport uses so one connection carries everything. When set, the realtime bridge subscribes channels on it and never owns its lifecycle: unsubscribing closes only channel subscriptions, leaving the socket connected.
|
|
594
606
|
* @property {string | (() => string | null | undefined)} [websocketUrl] - Shared app-lifetime websocket URL. When set (and no `websocketClient` is given), the framework builds and owns one reconnecting `VelociousWebsocketClient` for all sync traffic, connected on first use.
|
|
595
607
|
*/
|
|
596
608
|
|
|
609
|
+
/**
|
|
610
|
+
* App-owned durable storage for the device signing identity (the backend-signed
|
|
611
|
+
* device certificate plus the device private key). Native apps back this with
|
|
612
|
+
* secure storage; the framework never embeds a storage technology.
|
|
613
|
+
* @typedef {object} VelociousSyncClientDeviceIdentityStorage
|
|
614
|
+
* @property {() => import("./sync/sync-offline-session.js").SyncOfflineDeviceIdentity | null | undefined | Promise<import("./sync/sync-offline-session.js").SyncOfflineDeviceIdentity | null | undefined>} load - Reads the stored identity (null when the device has none yet).
|
|
615
|
+
* @property {(identity: import("./sync/sync-offline-session.js").SyncOfflineDeviceIdentity) => Promise<void> | void} save - Persists the identity.
|
|
616
|
+
*/
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* App-owned durable storage for retained offline grants. Pending mutations must
|
|
620
|
+
* always replay with the grant they were signed under, so a device that
|
|
621
|
+
* restarts while offline needs its issued grants back.
|
|
622
|
+
* @typedef {object} VelociousSyncClientGrantStorage
|
|
623
|
+
* @property {() => Array<import("./sync/offline-grant.js").SignedOfflineGrant> | Promise<Array<import("./sync/offline-grant.js").SignedOfflineGrant>>} load - Reads the retained grants.
|
|
624
|
+
* @property {(grants: Array<import("./sync/offline-grant.js").SignedOfflineGrant>) => Promise<void> | void} save - Persists the retained grants.
|
|
625
|
+
*/
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Client-side long-offline session configuration consumed by
|
|
629
|
+
* `SyncClient.startOfflineSession()`. The framework owns the session lifecycle
|
|
630
|
+
* (identity, grant bootstrap/refresh, signing, signed replay, peer bundles) over
|
|
631
|
+
* the built-in `/frontend-models/sync/bootstrap` and
|
|
632
|
+
* `/frontend-models/sync/replay` endpoints; the app only owns durable storage,
|
|
633
|
+
* the enrollment hook, and the grant scopes.
|
|
634
|
+
* @typedef {object} VelociousSyncClientOfflineConfiguration
|
|
635
|
+
* @property {VelociousSyncClientDeviceIdentityStorage} deviceIdentityStorage - App-owned durable device-identity storage adapter.
|
|
636
|
+
* @property {(args: {configuration: import("./configuration.js").default, publicKey: import("./sync/device-identity.js").SyncJsonWebKey}) => import("./sync/device-identity.js").DeviceCertificate | Promise<import("./sync/device-identity.js").DeviceCertificate>} enrollDevice - App-owned enrollment hook: asks the backend to register the device public key and returns the backend-signed certificate.
|
|
637
|
+
* @property {(args: {configuration: import("./configuration.js").default}) => Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue> | Promise<Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue>>} grantScopes - Resolves the scopes the bootstrap request asks the backend to materialize into the grant.
|
|
638
|
+
* @property {string} [bootstrapPath] - Signed-bootstrap endpoint path. Defaults to "/frontend-models/sync/bootstrap".
|
|
639
|
+
* @property {string} [replayPath] - Signed-replay endpoint path. Defaults to "/frontend-models/sync/replay".
|
|
640
|
+
* @property {import("./sync/device-identity.js").SyncJsonWebKey} [backendPublicKey] - Backend public key used to verify device certificates and imported peer mutations.
|
|
641
|
+
* @property {VelociousSyncClientGrantStorage} [grantStorage] - Optional durable grant retention adapter.
|
|
642
|
+
* @property {import("./sync/local-mutation-log.js").default} [mutationLog] - Durable local mutation log the offline session replays and exports. Defaults to the conflict-tracking log declared by the synced models.
|
|
643
|
+
* @property {() => Date} [now] - Session clock. Defaults to the current time.
|
|
644
|
+
*/
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Normalized client-side long-offline session configuration. Same shape as
|
|
648
|
+
* {@link VelociousSyncClientOfflineConfiguration} with the defaults filled in.
|
|
649
|
+
* @typedef {VelociousSyncClientOfflineConfiguration & {
|
|
650
|
+
* bootstrapPath: string,
|
|
651
|
+
* configuration: import("./configuration.js").default,
|
|
652
|
+
* now: () => Date,
|
|
653
|
+
* replayPath: string
|
|
654
|
+
* }} NormalizedVelociousSyncClientOfflineConfiguration
|
|
655
|
+
*/
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Normalized client-side sync configuration. Same shape as
|
|
659
|
+
* {@link VelociousSyncClientConfiguration} with the defaults filled in.
|
|
660
|
+
* @typedef {VelociousSyncClientConfiguration & {
|
|
661
|
+
* mountPath: string,
|
|
662
|
+
* offline: NormalizedVelociousSyncClientOfflineConfiguration | undefined
|
|
663
|
+
* }} NormalizedVelociousSyncClientConfiguration
|
|
664
|
+
*/
|
|
665
|
+
|
|
597
666
|
/**
|
|
598
667
|
* Velocious sync configuration.
|
|
599
668
|
* @typedef {object} VelociousSyncConfiguration
|
|
600
669
|
* @property {VelociousSyncApiConfiguration} [api] - Auto-mounts the Velocious sync changes/replay endpoints for this resource class.
|
|
601
|
-
* @property {
|
|
670
|
+
* @property {NormalizedVelociousSyncClientConfiguration | undefined} client - Normalized client-side sync configuration consumed by `SyncClient.fromConfiguration(...)`.
|
|
602
671
|
* @property {import("./sync/device-identity.js").SyncJsonWebKey | null} [deviceCertificateBackendPublicKey] - Public backend key used to verify offline device certificates for sync replay.
|
|
603
672
|
* @property {number} [changeFeedRetentionSize] - Number of accepted server changes retained before clients must refresh from snapshot.
|
|
604
673
|
* @property {Array<import("./sync/offline-grant.js").OfflineGrantSigningKey>} offlineGrantSigningKeys - Signing keys used to issue and verify offline grants. Secrets must never be exposed to clients.
|
|
@@ -617,13 +686,15 @@
|
|
|
617
686
|
*/
|
|
618
687
|
|
|
619
688
|
/**
|
|
620
|
-
* @typedef {Omit<FrontendModelResourceConfiguration, "abilities" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "sync"> & {
|
|
689
|
+
* @typedef {Omit<FrontendModelResourceConfiguration, "abilities" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "quickSearchColumns" | "quickSearchNumericColumns" | "sync"> & {
|
|
621
690
|
* abilities: FrontendModelResourceAbilitiesConfiguration
|
|
622
691
|
* builtInCollectionCommands: Record<string, string>
|
|
623
692
|
* builtInMemberCommands: Record<string, string>
|
|
624
693
|
* collectionCommands: Record<string, string>
|
|
625
694
|
* commandMetadata: Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>
|
|
626
695
|
* memberCommands: Record<string, string>
|
|
696
|
+
* quickSearchColumns: FrontendModelQuickSearchColumn[] | null
|
|
697
|
+
* quickSearchNumericColumns: FrontendModelQuickSearchColumn[] | null
|
|
627
698
|
* sync?: NormalizedFrontendModelResourceSyncConfiguration
|
|
628
699
|
* }} NormalizedFrontendModelResourceConfiguration
|
|
629
700
|
*/
|
package/build/configuration.js
CHANGED
|
@@ -900,7 +900,7 @@ export default class VelociousConfiguration {
|
|
|
900
900
|
/**
|
|
901
901
|
* Normalizes client-side sync configuration consumed by `SyncClient.fromConfiguration(...)`.
|
|
902
902
|
* @param {import("./configuration-types.js").VelociousSyncClientConfiguration | undefined} client - Client-side sync configuration.
|
|
903
|
-
* @returns {import("./configuration-types.js").
|
|
903
|
+
* @returns {import("./configuration-types.js").NormalizedVelociousSyncClientConfiguration | undefined} - Normalized client-side sync configuration.
|
|
904
904
|
*/
|
|
905
905
|
_normalizeSyncClientConfiguration(client) {
|
|
906
906
|
if (client === undefined || client === null) return undefined
|
|
@@ -909,11 +909,11 @@ export default class VelociousConfiguration {
|
|
|
909
909
|
throw new Error("sync.client must be an object with transport and authenticationToken")
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
-
const {authenticationToken, batchSize, isOnline, mountPath, onError, realtime, transport, websocketClient, websocketUrl, ...restClient} = client
|
|
912
|
+
const {authenticationToken, batchSize, isOnline, mountPath, onError, offline, realtime, transport, websocketClient, websocketUrl, ...restClient} = client
|
|
913
913
|
const restClientKeys = Object.keys(restClient)
|
|
914
914
|
|
|
915
915
|
if (restClientKeys.length > 0) {
|
|
916
|
-
throw new Error(`sync.client received unknown keys: ${restClientKeys.join(", ")} (supported: authenticationToken, batchSize, isOnline, mountPath, onError, realtime, transport, websocketClient, websocketUrl)`)
|
|
916
|
+
throw new Error(`sync.client received unknown keys: ${restClientKeys.join(", ")} (supported: authenticationToken, batchSize, isOnline, mountPath, onError, offline, realtime, transport, websocketClient, websocketUrl)`)
|
|
917
917
|
}
|
|
918
918
|
if (!transport || typeof transport !== "object" || typeof transport.post !== "function") {
|
|
919
919
|
throw new Error("sync.client.transport must be an object with a post(path, body) method (like the frontend-model websocket client)")
|
|
@@ -945,6 +945,7 @@ export default class VelociousConfiguration {
|
|
|
945
945
|
batchSize,
|
|
946
946
|
isOnline,
|
|
947
947
|
mountPath: (mountPath || "/velocious/sync").replace(/\/+$/u, "") || "/",
|
|
948
|
+
offline: this._normalizeSyncClientOfflineConfiguration(offline),
|
|
948
949
|
onError,
|
|
949
950
|
realtime,
|
|
950
951
|
transport,
|
|
@@ -953,6 +954,63 @@ export default class VelociousConfiguration {
|
|
|
953
954
|
}
|
|
954
955
|
}
|
|
955
956
|
|
|
957
|
+
/**
|
|
958
|
+
* Normalizes the client-side long-offline session configuration consumed by
|
|
959
|
+
* `SyncClient.startOfflineSession()`. The framework owns the session
|
|
960
|
+
* lifecycle; the app provides durable identity storage, the enrollment hook,
|
|
961
|
+
* and the grant scopes callback.
|
|
962
|
+
* @param {import("./configuration-types.js").VelociousSyncClientOfflineConfiguration | undefined} offline - Offline configuration block.
|
|
963
|
+
* @returns {import("./configuration-types.js").NormalizedVelociousSyncClientOfflineConfiguration | undefined} - Normalized offline configuration.
|
|
964
|
+
*/
|
|
965
|
+
_normalizeSyncClientOfflineConfiguration(offline) {
|
|
966
|
+
if (offline === undefined || offline === null) return undefined
|
|
967
|
+
|
|
968
|
+
if (typeof offline !== "object" || Array.isArray(offline)) {
|
|
969
|
+
throw new Error("sync.client.offline must be an object with deviceIdentityStorage, enrollDevice, and grantScopes")
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const {backendPublicKey, bootstrapPath, deviceIdentityStorage, enrollDevice, grantScopes, grantStorage, mutationLog, now, replayPath, ...restOffline} = offline
|
|
973
|
+
const restOfflineKeys = Object.keys(restOffline)
|
|
974
|
+
|
|
975
|
+
if (restOfflineKeys.length > 0) {
|
|
976
|
+
throw new Error(`sync.client.offline received unknown keys: ${restOfflineKeys.join(", ")} (supported: backendPublicKey, bootstrapPath, deviceIdentityStorage, enrollDevice, grantScopes, grantStorage, mutationLog, now, replayPath)`)
|
|
977
|
+
}
|
|
978
|
+
if (!deviceIdentityStorage || typeof deviceIdentityStorage !== "object" || Array.isArray(deviceIdentityStorage) || typeof deviceIdentityStorage.load !== "function" || typeof deviceIdentityStorage.save !== "function") {
|
|
979
|
+
throw new Error("sync.client.offline.deviceIdentityStorage must be an object with load() and save(identity) methods")
|
|
980
|
+
}
|
|
981
|
+
if (typeof enrollDevice !== "function") throw new Error("sync.client.offline.enrollDevice must be a function resolving the backend-signed device certificate")
|
|
982
|
+
if (typeof grantScopes !== "function") throw new Error("sync.client.offline.grantScopes must be a function resolving the bootstrap grant scopes")
|
|
983
|
+
if (backendPublicKey !== undefined && (typeof backendPublicKey !== "object" || backendPublicKey === null || Array.isArray(backendPublicKey))) {
|
|
984
|
+
throw new Error("sync.client.offline.backendPublicKey must be a public JSON Web Key object")
|
|
985
|
+
}
|
|
986
|
+
if (bootstrapPath !== undefined && (typeof bootstrapPath !== "string" || !bootstrapPath.startsWith("/"))) {
|
|
987
|
+
throw new Error(`sync.client.offline.bootstrapPath must start with '/', got: ${String(bootstrapPath)}`)
|
|
988
|
+
}
|
|
989
|
+
if (replayPath !== undefined && (typeof replayPath !== "string" || !replayPath.startsWith("/"))) {
|
|
990
|
+
throw new Error(`sync.client.offline.replayPath must start with '/', got: ${String(replayPath)}`)
|
|
991
|
+
}
|
|
992
|
+
if (grantStorage !== undefined && (typeof grantStorage !== "object" || grantStorage === null || Array.isArray(grantStorage) || typeof grantStorage.load !== "function" || typeof grantStorage.save !== "function")) {
|
|
993
|
+
throw new Error("sync.client.offline.grantStorage must be an object with load() and save(grants) methods")
|
|
994
|
+
}
|
|
995
|
+
if (mutationLog !== undefined && (typeof mutationLog !== "object" || mutationLog === null || typeof mutationLog.append !== "function")) {
|
|
996
|
+
throw new Error("sync.client.offline.mutationLog must be a LocalMutationLog")
|
|
997
|
+
}
|
|
998
|
+
if (now !== undefined && typeof now !== "function") throw new Error("sync.client.offline.now must be a function resolving the session clock")
|
|
999
|
+
|
|
1000
|
+
return {
|
|
1001
|
+
backendPublicKey,
|
|
1002
|
+
bootstrapPath: (bootstrapPath || "/frontend-models/sync/bootstrap").replace(/\/+$/u, "") || "/",
|
|
1003
|
+
configuration: this,
|
|
1004
|
+
deviceIdentityStorage,
|
|
1005
|
+
enrollDevice,
|
|
1006
|
+
grantScopes,
|
|
1007
|
+
grantStorage,
|
|
1008
|
+
mutationLog,
|
|
1009
|
+
now: now || (() => new Date()),
|
|
1010
|
+
replayPath: (replayPath || "/frontend-models/sync/replay").replace(/\/+$/u, "") || "/"
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
956
1014
|
/**
|
|
957
1015
|
* Normalizes sync API endpoint configuration.
|
|
958
1016
|
* @param {import("./configuration-types.js").VelociousSyncApiConfiguration | undefined} api - Sync API configuration.
|
|
@@ -1762,6 +1820,7 @@ export default class VelociousConfiguration {
|
|
|
1762
1820
|
const envPooledRunnerMaxJobsRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_JOBS
|
|
1763
1821
|
const envPooledRunnerMaxRssBytesRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_RSS_BYTES
|
|
1764
1822
|
const envPooledRunnerMaxLifetimeMsRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_MAX_LIFETIME_MS
|
|
1823
|
+
const envPooledRunnerSilenceTimeoutMsRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_POOLED_RUNNER_SILENCE_TIMEOUT_MS
|
|
1765
1824
|
const envRunnerMaxOldSpaceSizeMbRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_RUNNER_MAX_OLD_SPACE_SIZE_MB
|
|
1766
1825
|
const envRunnerMallocArenaMaxRaw = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_RUNNER_MALLOC_ARENA_MAX
|
|
1767
1826
|
const envDispatchStrategy = processEnvironment?.VELOCIOUS_BACKGROUND_JOBS_DISPATCH_STRATEGY
|
|
@@ -1775,6 +1834,7 @@ export default class VelociousConfiguration {
|
|
|
1775
1834
|
const envPooledRunnerMaxJobs = envPooledRunnerMaxJobsRaw ? Number(envPooledRunnerMaxJobsRaw) : undefined
|
|
1776
1835
|
const envPooledRunnerMaxRssBytes = envPooledRunnerMaxRssBytesRaw ? Number(envPooledRunnerMaxRssBytesRaw) : undefined
|
|
1777
1836
|
const envPooledRunnerMaxLifetimeMs = envPooledRunnerMaxLifetimeMsRaw ? Number(envPooledRunnerMaxLifetimeMsRaw) : undefined
|
|
1837
|
+
const envPooledRunnerSilenceTimeoutMs = envPooledRunnerSilenceTimeoutMsRaw ? Number(envPooledRunnerSilenceTimeoutMsRaw) : undefined
|
|
1778
1838
|
const envRunnerMaxOldSpaceSizeMb = envRunnerMaxOldSpaceSizeMbRaw ? Number(envRunnerMaxOldSpaceSizeMbRaw) : undefined
|
|
1779
1839
|
const envRunnerMallocArenaMax = envRunnerMallocArenaMaxRaw ? Number(envRunnerMallocArenaMaxRaw) : undefined
|
|
1780
1840
|
const envPollInterval = envPollIntervalRaw ? Number(envPollIntervalRaw) : undefined
|
|
@@ -1812,6 +1872,9 @@ export default class VelociousConfiguration {
|
|
|
1812
1872
|
const pooledRunnerMaxLifetimeMs = typeof configured.pooledRunnerMaxLifetimeMs === "number" && Number.isFinite(configured.pooledRunnerMaxLifetimeMs) && configured.pooledRunnerMaxLifetimeMs >= 1
|
|
1813
1873
|
? configured.pooledRunnerMaxLifetimeMs
|
|
1814
1874
|
: (!("pooledRunnerMaxLifetimeMs" in configured) && typeof envPooledRunnerMaxLifetimeMs === "number" && Number.isFinite(envPooledRunnerMaxLifetimeMs) && envPooledRunnerMaxLifetimeMs >= 1 ? envPooledRunnerMaxLifetimeMs : 60 * 60 * 1000)
|
|
1875
|
+
const pooledRunnerSilenceTimeoutMs = typeof configured.pooledRunnerSilenceTimeoutMs === "number" && Number.isFinite(configured.pooledRunnerSilenceTimeoutMs) && configured.pooledRunnerSilenceTimeoutMs >= 1
|
|
1876
|
+
? configured.pooledRunnerSilenceTimeoutMs
|
|
1877
|
+
: (!("pooledRunnerSilenceTimeoutMs" in configured) && typeof envPooledRunnerSilenceTimeoutMs === "number" && Number.isFinite(envPooledRunnerSilenceTimeoutMs) && envPooledRunnerSilenceTimeoutMs >= 1 ? envPooledRunnerSilenceTimeoutMs : 120000)
|
|
1815
1878
|
// Runner children get a hard V8 heap cap and a bounded glibc arena count
|
|
1816
1879
|
// so one child can never commit multiple GBs of heap or unbounded
|
|
1817
1880
|
// allocator arenas. The cap stays far above observed runner usage; an
|
|
@@ -1872,7 +1935,7 @@ export default class VelociousConfiguration {
|
|
|
1872
1935
|
|
|
1873
1936
|
const jobClasses = this.getBackgroundJobClasses()
|
|
1874
1937
|
|
|
1875
|
-
return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, mode, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, runnerMaxOldSpaceSizeMb, runnerMallocArenaMax, dispatchStrategy, pollIntervalMs, drainStoreOperationTimeoutMs, scheduledEnqueueTimeoutMs, queues, jobClasses, jobTimeoutMs, retention, generationId, initialGenerationState, lifecycleSocketPath}
|
|
1938
|
+
return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, mode, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, pooledRunnerSilenceTimeoutMs, runnerMaxOldSpaceSizeMb, runnerMallocArenaMax, dispatchStrategy, pollIntervalMs, drainStoreOperationTimeoutMs, scheduledEnqueueTimeoutMs, queues, jobClasses, jobTimeoutMs, retention, generationId, initialGenerationState, lifecycleSocketPath}
|
|
1876
1939
|
}
|
|
1877
1940
|
|
|
1878
1941
|
/**
|