@haverstack/server 0.8.0 → 0.9.0
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/dist/app.d.ts +8 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +70 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/changeStreams.d.ts +17 -0
- package/dist/lib/changeStreams.d.ts.map +1 -0
- package/{src/lib/changeStreams.ts → dist/lib/changeStreams.js} +12 -12
- package/dist/lib/changeStreams.js.map +1 -0
- package/dist/lib/frameGate.d.ts +26 -0
- package/dist/lib/frameGate.d.ts.map +1 -0
- package/dist/lib/frameGate.js +44 -0
- package/dist/lib/frameGate.js.map +1 -0
- package/dist/lib/json.d.ts +10 -0
- package/dist/lib/json.d.ts.map +1 -0
- package/dist/lib/json.js +27 -0
- package/dist/lib/json.js.map +1 -0
- package/dist/lib/nonceStore.d.ts +50 -0
- package/dist/lib/nonceStore.d.ts.map +1 -0
- package/dist/lib/nonceStore.js +96 -0
- package/dist/lib/nonceStore.js.map +1 -0
- package/dist/lib/queryLimit.d.ts +4 -0
- package/dist/lib/queryLimit.d.ts.map +1 -0
- package/{src/lib/queryLimit.ts → dist/lib/queryLimit.js} +5 -6
- package/dist/lib/queryLimit.js.map +1 -0
- package/dist/lib/queryWorker/pool.d.ts +60 -0
- package/dist/lib/queryWorker/pool.d.ts.map +1 -0
- package/dist/lib/queryWorker/pool.js +278 -0
- package/dist/lib/queryWorker/pool.js.map +1 -0
- package/{src/lib/queryWorker/protocol.ts → dist/lib/queryWorker/protocol.d.ts} +21 -18
- package/dist/lib/queryWorker/protocol.d.ts.map +1 -0
- package/dist/lib/queryWorker/protocol.js +2 -0
- package/dist/lib/queryWorker/protocol.js.map +1 -0
- package/dist/lib/queryWorker/worker.d.ts +2 -0
- package/dist/lib/queryWorker/worker.d.ts.map +1 -0
- package/{src/lib/queryWorker/worker.ts → dist/lib/queryWorker/worker.js} +19 -22
- package/dist/lib/queryWorker/worker.js.map +1 -0
- package/dist/lib/resumeBuffer.d.ts +143 -0
- package/dist/lib/resumeBuffer.d.ts.map +1 -0
- package/dist/lib/resumeBuffer.js +216 -0
- package/dist/lib/resumeBuffer.js.map +1 -0
- package/dist/lib/resumeCursor.d.ts +16 -0
- package/dist/lib/resumeCursor.d.ts.map +1 -0
- package/{src/lib/resumeCursor.ts → dist/lib/resumeCursor.js} +22 -24
- package/dist/lib/resumeCursor.js.map +1 -0
- package/dist/lib/tokenSweep.d.ts +17 -0
- package/dist/lib/tokenSweep.d.ts.map +1 -0
- package/dist/lib/tokenSweep.js +40 -0
- package/dist/lib/tokenSweep.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +46 -0
- package/dist/main.js.map +1 -0
- package/dist/middleware/auth.d.ts +25 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +74 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/errors.d.ts +17 -0
- package/dist/middleware/errors.d.ts.map +1 -0
- package/dist/middleware/errors.js +64 -0
- package/dist/middleware/errors.js.map +1 -0
- package/dist/routes/attachments.d.ts +5 -0
- package/dist/routes/attachments.d.ts.map +1 -0
- package/dist/routes/attachments.js +104 -0
- package/dist/routes/attachments.js.map +1 -0
- package/dist/routes/auth.d.ts +6 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +110 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/changes.d.ts +22 -0
- package/dist/routes/changes.d.ts.map +1 -0
- package/dist/routes/changes.js +233 -0
- package/dist/routes/changes.js.map +1 -0
- package/dist/routes/entity.d.ts +5 -0
- package/dist/routes/entity.d.ts.map +1 -0
- package/dist/routes/entity.js +30 -0
- package/dist/routes/entity.js.map +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +7 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/records.d.ts +5 -0
- package/dist/routes/records.d.ts.map +1 -0
- package/dist/routes/records.js +231 -0
- package/dist/routes/records.js.map +1 -0
- package/dist/routes/tokens.d.ts +5 -0
- package/dist/routes/tokens.d.ts.map +1 -0
- package/dist/routes/tokens.js +65 -0
- package/dist/routes/tokens.js.map +1 -0
- package/dist/routes/types.d.ts +5 -0
- package/dist/routes/types.d.ts.map +1 -0
- package/dist/routes/types.js +46 -0
- package/dist/routes/types.js.map +1 -0
- package/dist/routes/wellknown.d.ts +6 -0
- package/dist/routes/wellknown.d.ts.map +1 -0
- package/dist/routes/wellknown.js +45 -0
- package/dist/routes/wellknown.js.map +1 -0
- package/dist/shutdown.d.ts +23 -0
- package/dist/shutdown.d.ts.map +1 -0
- package/dist/shutdown.js +32 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/stack.d.ts +20 -0
- package/dist/stack.d.ts.map +1 -0
- package/dist/stack.js +50 -0
- package/dist/stack.js.map +1 -0
- package/dist/testing.d.ts +49 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +130 -0
- package/dist/testing.js.map +1 -0
- package/{src/types.ts → dist/types.d.ts} +5 -5
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/{src/wireError.ts → dist/wireError.d.ts} +2 -9
- package/dist/wireError.d.ts.map +1 -0
- package/dist/wireError.js +13 -0
- package/dist/wireError.js.map +1 -0
- package/package.json +37 -12
- package/.claude/settings.local.json +0 -22
- package/.dockerignore +0 -10
- package/.env.example +0 -90
- package/.github/pull_request_template.md +0 -23
- package/.github/workflows/ci.yml +0 -55
- package/.github/workflows/release.yml +0 -99
- package/.prettierignore +0 -3
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -91
- package/CONTRIBUTING.md +0 -234
- package/Dockerfile +0 -30
- package/SECURITY.md +0 -54
- package/docs/api.md +0 -277
- package/docs/deployment.md +0 -212
- package/eslint.config.mjs +0 -18
- package/pnpm-workspace.yaml +0 -13
- package/src/app.ts +0 -86
- package/src/config.ts +0 -176
- package/src/index.ts +0 -54
- package/src/lib/frameGate.ts +0 -42
- package/src/lib/json.ts +0 -27
- package/src/lib/nonceStore.ts +0 -103
- package/src/lib/queryWorker/pool.ts +0 -324
- package/src/lib/resumeBuffer.ts +0 -268
- package/src/lib/tokenSweep.ts +0 -43
- package/src/middleware/auth.ts +0 -77
- package/src/middleware/errors.ts +0 -73
- package/src/routes/attachments.ts +0 -126
- package/src/routes/auth.ts +0 -137
- package/src/routes/changes.ts +0 -279
- package/src/routes/entity.ts +0 -33
- package/src/routes/health.ts +0 -8
- package/src/routes/records.ts +0 -259
- package/src/routes/tokens.ts +0 -85
- package/src/routes/types.ts +0 -57
- package/src/routes/wellknown.ts +0 -55
- package/src/shutdown.ts +0 -56
- package/src/stack.ts +0 -96
- package/tests/app.test.ts +0 -86
- package/tests/changeFeedClient.ts +0 -156
- package/tests/config.test.ts +0 -112
- package/tests/conformance.test.ts +0 -2322
- package/tests/fixtures/blockingWorker.ts +0 -27
- package/tests/fixtures/brokenWorker.ts +0 -7
- package/tests/lib/changeFeedClient.test.ts +0 -126
- package/tests/lib/frameGate.test.ts +0 -93
- package/tests/lib/queryLimit.test.ts +0 -33
- package/tests/lib/queryWorker.test.ts +0 -60
- package/tests/lib/queryWorkerPool.test.ts +0 -129
- package/tests/lib/resumeBuffer.test.ts +0 -341
- package/tests/lib/resumeCursor.test.ts +0 -46
- package/tests/middleware/errors.test.ts +0 -176
- package/tests/routes/associations.test.ts +0 -170
- package/tests/routes/attachments.test.ts +0 -621
- package/tests/routes/auth.test.ts +0 -448
- package/tests/routes/changes.test.ts +0 -567
- package/tests/routes/contentQuery.test.ts +0 -68
- package/tests/routes/entity.test.ts +0 -170
- package/tests/routes/groups.test.ts +0 -69
- package/tests/routes/records.test.ts +0 -1965
- package/tests/routes/relatedToQuery.test.ts +0 -182
- package/tests/routes/tokens.test.ts +0 -157
- package/tests/routes/tombstones.test.ts +0 -93
- package/tests/routes/types.test.ts +0 -129
- package/tests/routes/versions.test.ts +0 -177
- package/tests/routes/wellknown.test.ts +0 -71
- package/tests/setup.ts +0 -154
- package/tests/shutdown.test.ts +0 -121
- package/tests/stack.test.ts +0 -198
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -12
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type { StackQuery } from '@haverstack/core';
|
|
2
|
-
|
|
3
1
|
// core's parsers (@haverstack/core/wire) report `limit` as requested and
|
|
4
2
|
// never clamp it — a ceiling is deployment policy, not wire contract. This
|
|
5
3
|
// server's ceiling, applied at the call site in records.ts.
|
|
6
4
|
export const MAX_QUERY_LIMIT = 1000;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
export function clampLimit(query) {
|
|
6
|
+
if (query.limit === undefined)
|
|
7
|
+
return query;
|
|
8
|
+
return { ...query, limit: Math.min(query.limit, MAX_QUERY_LIMIT) };
|
|
11
9
|
}
|
|
10
|
+
//# sourceMappingURL=queryLimit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryLimit.js","sourceRoot":"","sources":["../../src/lib/queryLimit.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,2EAA2E;AAC3E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AAEpC,MAAM,UAAU,UAAU,CAAC,KAAiB;IAC1C,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,eAAe,CAAC,EAAE,CAAC;AACrE,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { StackQuery, QueryResult, TokenSession } from '@haverstack/core';
|
|
2
|
+
import type { Logger } from 'pino';
|
|
3
|
+
import type { QueryWorkerInit } from './protocol.js';
|
|
4
|
+
export type QueryWorkerPoolOptions = {
|
|
5
|
+
init: QueryWorkerInit;
|
|
6
|
+
poolSize: number;
|
|
7
|
+
/** Maximum requests waiting for a slot before the pool sheds load. */
|
|
8
|
+
queueLimit: number;
|
|
9
|
+
logger: Logger;
|
|
10
|
+
/**
|
|
11
|
+
* Worker entry point. Defaults to this directory's worker; overridable
|
|
12
|
+
* so tests can drive the pool with a fixture worker whose timing they
|
|
13
|
+
* control, rather than inferring pool behavior from real query latency.
|
|
14
|
+
*/
|
|
15
|
+
workerUrl?: URL;
|
|
16
|
+
};
|
|
17
|
+
export declare class QueryWorkerPool {
|
|
18
|
+
private readonly slots;
|
|
19
|
+
private readonly pending;
|
|
20
|
+
/** FIFO of requests waiting for a slot to free up. */
|
|
21
|
+
private readonly waiting;
|
|
22
|
+
private nextId;
|
|
23
|
+
private closed;
|
|
24
|
+
private consecutiveFailures;
|
|
25
|
+
private lastFailure;
|
|
26
|
+
private readonly init;
|
|
27
|
+
private readonly queueLimit;
|
|
28
|
+
private readonly logger;
|
|
29
|
+
private readonly workerUrl;
|
|
30
|
+
constructor(opts: QueryWorkerPoolOptions);
|
|
31
|
+
/**
|
|
32
|
+
* True once every recent spawn attempt has failed — a standing fault
|
|
33
|
+
* (bad DB_PATH, unreadable file) rather than one unlucky crash.
|
|
34
|
+
*/
|
|
35
|
+
private get unhealthy();
|
|
36
|
+
private startWorker;
|
|
37
|
+
private handleMessage;
|
|
38
|
+
/**
|
|
39
|
+
* A worker crashed or exited on its own — not a deadline recycle, which
|
|
40
|
+
* is deliberate and handled by recycleSlot() without backoff.
|
|
41
|
+
*/
|
|
42
|
+
private handleWorkerDeath;
|
|
43
|
+
/** Deliberate recycle after a deadline: capacity is restored at once, no backoff. */
|
|
44
|
+
private recycleSlot;
|
|
45
|
+
private teardown;
|
|
46
|
+
private scheduleRespawn;
|
|
47
|
+
private failQueued;
|
|
48
|
+
private drainWaiting;
|
|
49
|
+
/** Hand a request to a worker and start its execution clock — see the module doc. */
|
|
50
|
+
private startExecution;
|
|
51
|
+
private onDeadline;
|
|
52
|
+
/**
|
|
53
|
+
* Run a query on the pool. `deadlineMs` bounds execution only; a request
|
|
54
|
+
* that waits for a slot waits without penalty, until the queue itself is
|
|
55
|
+
* full.
|
|
56
|
+
*/
|
|
57
|
+
query(session: TokenSession | null, stackQuery: StackQuery, deadlineMs: number): Promise<QueryResult>;
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../../src/lib/queryWorker/pool.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAG9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAA+B,eAAe,EAAE,MAAM,eAAe,CAAC;AAgClF,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB,CAAC;AAEF,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IACtD,sDAAsD;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgD;IACxE,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,WAAW,CAAsB;IAEzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAkB;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAM;gBAEpB,IAAI,EAAE,sBAAsB;IAYxC;;;OAGG;IACH,OAAO,KAAK,SAAS,GAEpB;IAED,OAAO,CAAC,WAAW;IAqBnB,OAAO,CAAC,aAAa;IAmBrB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IA4BzB,qFAAqF;IACrF,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,YAAY;IAepB,qFAAqF;IACrF,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,UAAU;IAWlB;;;;OAIG;IACH,KAAK,CACH,OAAO,EAAE,YAAY,GAAG,IAAI,EAC5B,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC;IAyBjB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAuB7B"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread side of the query worker boundary. Owns a small, fixed-size
|
|
3
|
+
* pool of workers (src/lib/queryWorker/worker.ts), each with its own
|
|
4
|
+
* LocalAdapter connection to the same stack.db, and dispatches
|
|
5
|
+
* ScopedStack.query() calls to them. The operator-facing account of why
|
|
6
|
+
* this exists is docs/deployment.md § Bounding query cost.
|
|
7
|
+
*
|
|
8
|
+
* Two bounds, deliberately not one:
|
|
9
|
+
*
|
|
10
|
+
* - `deadlineMs` bounds **execution**, and its clock starts when a
|
|
11
|
+
* request reaches a worker, not when it was accepted here. Execution
|
|
12
|
+
* is the only part that needs bounding: node:sqlite exposes no
|
|
13
|
+
* `sqlite3_interrupt` equivalent, so a query already inside the engine
|
|
14
|
+
* cannot be cancelled in place. Queueing makes a request slow, never
|
|
15
|
+
* failed.
|
|
16
|
+
*
|
|
17
|
+
* - `queueLimit` bounds **the wait**, so "slow" cannot quietly become
|
|
18
|
+
* unbounded in latency or retained memory. Past it the pool sheds load
|
|
19
|
+
* with StackTimeoutError — a retryable 503, which is honest about
|
|
20
|
+
* overload in a way a silently growing queue is not.
|
|
21
|
+
*
|
|
22
|
+
* Past its deadline the only lever left is termination: the caller is
|
|
23
|
+
* answered at once and the worker is terminated and replaced, so a stuck
|
|
24
|
+
* query costs one slot rather than wedging the pool. That recycle needs no
|
|
25
|
+
* backoff, being rate-limited by construction at `poolSize` terminations
|
|
26
|
+
* per `deadlineMs`.
|
|
27
|
+
*
|
|
28
|
+
* An *unexpected* death is the opposite case and does back off: respawning
|
|
29
|
+
* a worker that can't boot — an unreadable DB_PATH, say — as fast as the
|
|
30
|
+
* loop allows would spin threads and flood logs. After
|
|
31
|
+
* MAX_CONSECUTIVE_SPAWN_FAILURES the pool reports itself unhealthy and
|
|
32
|
+
* fails queries with the underlying spawn error, so a fatal
|
|
33
|
+
* misconfiguration surfaces as itself rather than as a pile of timeouts.
|
|
34
|
+
* Respawns continue at the capped interval, so the pool heals once the
|
|
35
|
+
* cause clears.
|
|
36
|
+
*
|
|
37
|
+
* Terminating mid-write is safe: WAL journaling leaves an uncommitted
|
|
38
|
+
* transaction simply absent once the file is reopened. Only query() routes
|
|
39
|
+
* through this pool — src/routes/records.ts holds the reason writes and
|
|
40
|
+
* by-id reads stay on the main thread.
|
|
41
|
+
*/
|
|
42
|
+
import { Worker } from 'node:worker_threads';
|
|
43
|
+
import { StackTimeoutError } from '@haverstack/core';
|
|
44
|
+
import { deserializeError } from '@haverstack/wire-types';
|
|
45
|
+
// import.meta.url ends in .ts under tsx (dev) and vitest (test), and in
|
|
46
|
+
// .js once tsc has compiled this file into dist/ — so which worker file
|
|
47
|
+
// and loader to use falls out of how *this* module was loaded, with no
|
|
48
|
+
// separate NODE_ENV check to keep in sync.
|
|
49
|
+
const RUNNING_FROM_SOURCE = import.meta.url.endsWith('.ts');
|
|
50
|
+
const WORKER_URL = new URL(RUNNING_FROM_SOURCE ? './worker.ts' : './worker.js', import.meta.url);
|
|
51
|
+
const WORKER_EXEC_ARGV = RUNNING_FROM_SOURCE ? ['--import', 'tsx'] : [];
|
|
52
|
+
/** Backoff schedule for respawning a worker that died unexpectedly. */
|
|
53
|
+
const RESPAWN_BASE_DELAY_MS = 100;
|
|
54
|
+
const RESPAWN_MAX_DELAY_MS = 30_000;
|
|
55
|
+
/** Consecutive unexpected deaths after which the pool reports itself unhealthy. */
|
|
56
|
+
const MAX_CONSECUTIVE_SPAWN_FAILURES = 5;
|
|
57
|
+
export class QueryWorkerPool {
|
|
58
|
+
slots = [];
|
|
59
|
+
pending = new Map();
|
|
60
|
+
/** FIFO of requests waiting for a slot to free up. */
|
|
61
|
+
waiting = [];
|
|
62
|
+
nextId = 1;
|
|
63
|
+
closed = false;
|
|
64
|
+
consecutiveFailures = 0;
|
|
65
|
+
lastFailure = null;
|
|
66
|
+
init;
|
|
67
|
+
queueLimit;
|
|
68
|
+
logger;
|
|
69
|
+
workerUrl;
|
|
70
|
+
constructor(opts) {
|
|
71
|
+
this.init = opts.init;
|
|
72
|
+
this.queueLimit = opts.queueLimit;
|
|
73
|
+
this.logger = opts.logger;
|
|
74
|
+
this.workerUrl = opts.workerUrl ?? WORKER_URL;
|
|
75
|
+
for (let i = 0; i < opts.poolSize; i++) {
|
|
76
|
+
const slot = { worker: null, busyWith: null, respawnTimer: null };
|
|
77
|
+
this.slots.push(slot);
|
|
78
|
+
this.startWorker(slot);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* True once every recent spawn attempt has failed — a standing fault
|
|
83
|
+
* (bad DB_PATH, unreadable file) rather than one unlucky crash.
|
|
84
|
+
*/
|
|
85
|
+
get unhealthy() {
|
|
86
|
+
return this.consecutiveFailures >= MAX_CONSECUTIVE_SPAWN_FAILURES;
|
|
87
|
+
}
|
|
88
|
+
startWorker(slot) {
|
|
89
|
+
if (this.closed)
|
|
90
|
+
return;
|
|
91
|
+
slot.respawnTimer = null;
|
|
92
|
+
const worker = new Worker(this.workerUrl, {
|
|
93
|
+
workerData: this.init,
|
|
94
|
+
execArgv: WORKER_EXEC_ARGV,
|
|
95
|
+
});
|
|
96
|
+
slot.worker = worker;
|
|
97
|
+
slot.busyWith = null;
|
|
98
|
+
worker.on('message', (msg) => this.handleMessage(slot, msg));
|
|
99
|
+
worker.on('error', (err) => this.handleWorkerDeath(slot, err));
|
|
100
|
+
worker.on('exit', (code) => {
|
|
101
|
+
if (!this.closed && code !== 0) {
|
|
102
|
+
this.handleWorkerDeath(slot, new Error(`query worker exited with code ${code}`));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// postMessage before a worker finishes booting is fine — worker_threads
|
|
106
|
+
// queues it until the thread installs its listener.
|
|
107
|
+
this.drainWaiting(slot);
|
|
108
|
+
}
|
|
109
|
+
handleMessage(slot, msg) {
|
|
110
|
+
// A worker that answers is a working worker: clear any standing
|
|
111
|
+
// failure state so an earlier rough patch can't leave the pool
|
|
112
|
+
// permanently marked unhealthy.
|
|
113
|
+
this.consecutiveFailures = 0;
|
|
114
|
+
this.lastFailure = null;
|
|
115
|
+
slot.busyWith = null;
|
|
116
|
+
const pending = this.pending.get(msg.id);
|
|
117
|
+
// Already timed out (and rejected) before the worker answered — the
|
|
118
|
+
// late reply just frees the slot, above.
|
|
119
|
+
if (pending) {
|
|
120
|
+
this.pending.delete(msg.id);
|
|
121
|
+
if (pending.timer)
|
|
122
|
+
clearTimeout(pending.timer);
|
|
123
|
+
if (msg.ok)
|
|
124
|
+
pending.resolve(msg.result);
|
|
125
|
+
else
|
|
126
|
+
pending.reject(msg.wire ? deserializeError(msg.wire.body) : new Error(msg.message));
|
|
127
|
+
}
|
|
128
|
+
this.drainWaiting(slot);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A worker crashed or exited on its own — not a deadline recycle, which
|
|
132
|
+
* is deliberate and handled by recycleSlot() without backoff.
|
|
133
|
+
*/
|
|
134
|
+
handleWorkerDeath(slot, err) {
|
|
135
|
+
const failedId = slot.busyWith;
|
|
136
|
+
if (failedId !== null) {
|
|
137
|
+
const pending = this.pending.get(failedId);
|
|
138
|
+
if (pending) {
|
|
139
|
+
this.pending.delete(failedId);
|
|
140
|
+
if (pending.timer)
|
|
141
|
+
clearTimeout(pending.timer);
|
|
142
|
+
pending.reject(err);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
this.teardown(slot);
|
|
146
|
+
this.consecutiveFailures++;
|
|
147
|
+
this.lastFailure = err;
|
|
148
|
+
const delay = Math.min(RESPAWN_BASE_DELAY_MS * 2 ** (this.consecutiveFailures - 1), RESPAWN_MAX_DELAY_MS);
|
|
149
|
+
this.logger.error({ err, consecutiveFailures: this.consecutiveFailures, retryInMs: delay }, 'Query worker died unexpectedly; respawning after backoff');
|
|
150
|
+
// Nothing is going to serve the queue any time soon — answer it with
|
|
151
|
+
// the real cause instead of letting it wait out a fault that isn't
|
|
152
|
+
// going to clear on its own.
|
|
153
|
+
if (this.unhealthy)
|
|
154
|
+
this.failQueued(err);
|
|
155
|
+
this.scheduleRespawn(slot, delay);
|
|
156
|
+
}
|
|
157
|
+
/** Deliberate recycle after a deadline: capacity is restored at once, no backoff. */
|
|
158
|
+
recycleSlot(slot) {
|
|
159
|
+
if (this.closed)
|
|
160
|
+
return;
|
|
161
|
+
this.teardown(slot);
|
|
162
|
+
this.startWorker(slot);
|
|
163
|
+
}
|
|
164
|
+
teardown(slot) {
|
|
165
|
+
const worker = slot.worker;
|
|
166
|
+
slot.worker = null;
|
|
167
|
+
slot.busyWith = null;
|
|
168
|
+
if (!worker)
|
|
169
|
+
return;
|
|
170
|
+
worker.removeAllListeners();
|
|
171
|
+
void worker.terminate().catch(() => { });
|
|
172
|
+
}
|
|
173
|
+
scheduleRespawn(slot, delay) {
|
|
174
|
+
if (this.closed)
|
|
175
|
+
return;
|
|
176
|
+
if (slot.respawnTimer)
|
|
177
|
+
clearTimeout(slot.respawnTimer);
|
|
178
|
+
slot.respawnTimer = setTimeout(() => this.startWorker(slot), delay);
|
|
179
|
+
// A pending respawn must not by itself keep the process alive.
|
|
180
|
+
slot.respawnTimer.unref();
|
|
181
|
+
}
|
|
182
|
+
failQueued(err) {
|
|
183
|
+
for (const { id } of this.waiting.splice(0)) {
|
|
184
|
+
const pending = this.pending.get(id);
|
|
185
|
+
if (!pending)
|
|
186
|
+
continue;
|
|
187
|
+
this.pending.delete(id);
|
|
188
|
+
if (pending.timer)
|
|
189
|
+
clearTimeout(pending.timer);
|
|
190
|
+
pending.reject(err);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
drainWaiting(slot) {
|
|
194
|
+
if (this.closed || !slot.worker || slot.busyWith !== null)
|
|
195
|
+
return;
|
|
196
|
+
// Looped, not recursive: a long run of already-abandoned queue entries
|
|
197
|
+
// shouldn't cost stack depth.
|
|
198
|
+
for (;;) {
|
|
199
|
+
const next = this.waiting.shift();
|
|
200
|
+
if (!next)
|
|
201
|
+
return;
|
|
202
|
+
const pending = this.pending.get(next.id);
|
|
203
|
+
// Abandoned while queued (pool went unhealthy, or close()).
|
|
204
|
+
if (!pending)
|
|
205
|
+
continue;
|
|
206
|
+
this.startExecution(slot, next.id, next.req, pending);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Hand a request to a worker and start its execution clock — see the module doc. */
|
|
211
|
+
startExecution(slot, id, req, pending) {
|
|
212
|
+
pending.timer = setTimeout(() => this.onDeadline(id), pending.deadlineMs);
|
|
213
|
+
slot.busyWith = id;
|
|
214
|
+
slot.worker.postMessage(req);
|
|
215
|
+
}
|
|
216
|
+
onDeadline(id) {
|
|
217
|
+
const pending = this.pending.get(id);
|
|
218
|
+
if (!pending)
|
|
219
|
+
return;
|
|
220
|
+
this.pending.delete(id);
|
|
221
|
+
pending.reject(new StackTimeoutError(`Query exceeded the ${pending.deadlineMs}ms execution deadline`));
|
|
222
|
+
const slot = this.slots.find((s) => s.busyWith === id);
|
|
223
|
+
if (slot)
|
|
224
|
+
this.recycleSlot(slot);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Run a query on the pool. `deadlineMs` bounds execution only; a request
|
|
228
|
+
* that waits for a slot waits without penalty, until the queue itself is
|
|
229
|
+
* full.
|
|
230
|
+
*/
|
|
231
|
+
query(session, stackQuery, deadlineMs) {
|
|
232
|
+
if (this.closed)
|
|
233
|
+
return Promise.reject(new Error('Query worker pool is closed'));
|
|
234
|
+
// A standing spawn fault is a server fault, not a slow query: report
|
|
235
|
+
// the cause rather than making every caller wait out a deadline.
|
|
236
|
+
if (this.unhealthy) {
|
|
237
|
+
return Promise.reject(this.lastFailure ?? new Error('Query worker pool is unavailable'));
|
|
238
|
+
}
|
|
239
|
+
if (this.waiting.length >= this.queueLimit) {
|
|
240
|
+
return Promise.reject(new StackTimeoutError(`Query queue is full (${this.queueLimit} waiting); the server is shedding load rather than queueing further`));
|
|
241
|
+
}
|
|
242
|
+
const id = this.nextId++;
|
|
243
|
+
const req = { id, session, query: stackQuery };
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
const pending = { resolve, reject, deadlineMs, timer: null };
|
|
246
|
+
this.pending.set(id, pending);
|
|
247
|
+
const idle = this.slots.find((s) => s.worker !== null && s.busyWith === null);
|
|
248
|
+
if (idle)
|
|
249
|
+
this.startExecution(idle, id, req, pending);
|
|
250
|
+
else
|
|
251
|
+
this.waiting.push({ id, req });
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async close() {
|
|
255
|
+
this.closed = true;
|
|
256
|
+
for (const pending of this.pending.values()) {
|
|
257
|
+
if (pending.timer)
|
|
258
|
+
clearTimeout(pending.timer);
|
|
259
|
+
pending.reject(new Error('Query worker pool is closing'));
|
|
260
|
+
}
|
|
261
|
+
this.pending.clear();
|
|
262
|
+
this.waiting.length = 0;
|
|
263
|
+
await Promise.all(this.slots.map(async (slot) => {
|
|
264
|
+
if (slot.respawnTimer) {
|
|
265
|
+
clearTimeout(slot.respawnTimer);
|
|
266
|
+
slot.respawnTimer = null;
|
|
267
|
+
}
|
|
268
|
+
const worker = slot.worker;
|
|
269
|
+
slot.worker = null;
|
|
270
|
+
slot.busyWith = null;
|
|
271
|
+
if (!worker)
|
|
272
|
+
return;
|
|
273
|
+
worker.removeAllListeners();
|
|
274
|
+
await worker.terminate();
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.js","sourceRoot":"","sources":["../../../src/lib/queryWorker/pool.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { StackQuery, QueryResult, TokenSession } from '@haverstack/core';
|
|
11
11
|
import type { WireError } from '@haverstack/wire-types';
|
|
12
|
-
|
|
13
12
|
/**
|
|
14
13
|
* Passed as `workerData` when a query worker is spawned. dbPath is the
|
|
15
14
|
* whole of it: a worker calls LocalAdapter.open(), which reads timezone
|
|
@@ -17,23 +16,27 @@ import type { WireError } from '@haverstack/wire-types';
|
|
|
17
16
|
* the initialize path takes one as input, and workers never initialize.
|
|
18
17
|
*/
|
|
19
18
|
export type QueryWorkerInit = {
|
|
20
|
-
|
|
19
|
+
dbPath: string;
|
|
21
20
|
};
|
|
22
|
-
|
|
23
21
|
export type QueryRequest = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
id: number;
|
|
23
|
+
/** null = anonymous (Stack.asEntity(null)), matching records.ts's scopeFor(). */
|
|
24
|
+
session: TokenSession | null;
|
|
25
|
+
query: StackQuery;
|
|
28
26
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
export type QueryResponse = {
|
|
28
|
+
id: number;
|
|
29
|
+
ok: true;
|
|
30
|
+
result: QueryResult;
|
|
31
|
+
} | {
|
|
32
|
+
id: number;
|
|
33
|
+
ok: false;
|
|
34
|
+
/** Set when the failure is a StackError with a wire mapping; reconstructed via deserializeError(). */
|
|
35
|
+
wire: {
|
|
36
|
+
status: number;
|
|
37
|
+
body: WireError;
|
|
38
|
+
} | null;
|
|
39
|
+
/** Always set, for logging — the only signal left when `wire` is null (an unanticipated bug). */
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/lib/queryWorker/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,iFAAiF;IACjF,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,UAAU,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAC7C;IACE,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,KAAK,CAAC;IACV,sGAAsG;IACtG,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,iGAAiG;IACjG,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/lib/queryWorker/protocol.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/lib/queryWorker/worker.ts"],"names":[],"mappings":""}
|
|
@@ -18,34 +18,31 @@ import { parentPort, workerData } from 'node:worker_threads';
|
|
|
18
18
|
import { LocalAdapter } from '@haverstack/adapter-local';
|
|
19
19
|
import { Stack } from '@haverstack/core';
|
|
20
20
|
import { serializeError } from '@haverstack/wire-types';
|
|
21
|
-
import type { QueryRequest, QueryResponse, QueryWorkerInit } from './protocol.js';
|
|
22
|
-
|
|
23
21
|
if (!parentPort) {
|
|
24
|
-
|
|
22
|
+
throw new Error('query worker must be run as a worker_thread, not the main thread');
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
const init = workerData as QueryWorkerInit;
|
|
28
|
-
|
|
24
|
+
const init = workerData;
|
|
29
25
|
// The database already exists — the main thread's initStack() created or
|
|
30
26
|
// opened it, and seeded system types, before any worker is spawned (see
|
|
31
27
|
// pool.ts). open() (not openOrInitialize()) reflects that: there is no
|
|
32
28
|
// first-run path to handle here.
|
|
33
29
|
const adapter = await LocalAdapter.open({ path: init.dbPath });
|
|
34
30
|
const stack = await Stack.create(adapter);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
parentPort.on('message', async (req) => {
|
|
32
|
+
const port = parentPort;
|
|
33
|
+
try {
|
|
34
|
+
const scoped = req.session ? stack.forSession(req.session) : stack.asEntity(null);
|
|
35
|
+
const result = await scoped.query(req.query);
|
|
36
|
+
port.postMessage({ id: req.id, ok: true, result });
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const wire = serializeError(err);
|
|
40
|
+
port.postMessage({
|
|
41
|
+
id: req.id,
|
|
42
|
+
ok: false,
|
|
43
|
+
wire,
|
|
44
|
+
message: err instanceof Error ? err.message : String(err),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
51
47
|
});
|
|
48
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../src/lib/queryWorker/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGxD,IAAI,CAAC,UAAU,EAAE,CAAC;IAChB,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;AACtF,CAAC;AAED,MAAM,IAAI,GAAG,UAA6B,CAAC;AAE3C,yEAAyE;AACzE,wEAAwE;AACxE,uEAAuE;AACvE,iCAAiC;AACjC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC/D,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAE1C,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,GAAiB,EAAE,EAAE;IACnD,MAAM,IAAI,GAAG,UAAW,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAClF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC7C,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAA0B,CAAC,CAAC;IAC7E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,WAAW,CAAC;YACf,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,EAAE,EAAE,KAAK;YACT,IAAI;YACJ,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SAClC,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-(session, filter) resume buffers for GET /changes. Sits downstream
|
|
3
|
+
* of each connection's `ScopedStack.subscribe()`, so every entry here is a
|
|
4
|
+
* frame that subscription already delivered — something this session was
|
|
5
|
+
* cleared to see at the moment it happened. That is what keeps replay a
|
|
6
|
+
* pure re-send needing nothing from core, where a single server-wide
|
|
7
|
+
* buffer would owe a permission check per frame per reader.
|
|
8
|
+
*
|
|
9
|
+
* A buffer mints its own cursors (its position is a private counter, not a
|
|
10
|
+
* slice of some global sequence) and is fed independently of any one HTTP
|
|
11
|
+
* connection: it keeps collecting for a bounded window *after* the last
|
|
12
|
+
* connection using it disconnects, which is what lets a reconnect learn
|
|
13
|
+
* about changes made while nobody was listening. Depth-bounded on top of
|
|
14
|
+
* that: a session that stays away long enough to overflow the ring gets
|
|
15
|
+
* `overflow` rather than a silently incomplete replay.
|
|
16
|
+
*/
|
|
17
|
+
import type { ChangeFilter, RecordChange, Unsubscribe } from '@haverstack/core';
|
|
18
|
+
import type { WireRecordChange } from '@haverstack/wire-types';
|
|
19
|
+
export type ResumeEntry = {
|
|
20
|
+
n: number;
|
|
21
|
+
recordId: string;
|
|
22
|
+
isPurge: boolean;
|
|
23
|
+
/** Wire-ready, seq already attached. */
|
|
24
|
+
frame: WireRecordChange;
|
|
25
|
+
};
|
|
26
|
+
export type EntriesAfterResult = {
|
|
27
|
+
status: 'ok';
|
|
28
|
+
entries: ResumeEntry[];
|
|
29
|
+
}
|
|
30
|
+
/** The buffer is the right one, but depth eviction already dropped what would be needed. */
|
|
31
|
+
| {
|
|
32
|
+
status: 'overflow';
|
|
33
|
+
}
|
|
34
|
+
/** A position this buffer never held, or no longer holds any record of. */
|
|
35
|
+
| {
|
|
36
|
+
status: 'cursor_expired';
|
|
37
|
+
};
|
|
38
|
+
/** One session+filter's resume buffer. Data and notification only — lifecycle lives in the registry. */
|
|
39
|
+
export declare class ResumeBuffer {
|
|
40
|
+
private readonly depth;
|
|
41
|
+
readonly id: string;
|
|
42
|
+
private readonly entries;
|
|
43
|
+
private currentN;
|
|
44
|
+
private evictedUpTo;
|
|
45
|
+
private readonly listeners;
|
|
46
|
+
/** Connections currently holding this buffer open. Registry-managed. */
|
|
47
|
+
liveCount: number;
|
|
48
|
+
/** Set by the registry when liveCount drops to 0; cleared on reacquire. */
|
|
49
|
+
disconnectedAt: number | null;
|
|
50
|
+
constructor(depth: number);
|
|
51
|
+
/** The cursor naming this buffer's current position — what `ready` reports. */
|
|
52
|
+
headCursor(): string;
|
|
53
|
+
/**
|
|
54
|
+
* Record one already-permission-filtered change, minting its cursor.
|
|
55
|
+
* Notifies every currently attached live connection with the same frame
|
|
56
|
+
* a replay would later produce.
|
|
57
|
+
*/
|
|
58
|
+
append(change: RecordChange): ResumeEntry;
|
|
59
|
+
/** Attach a live connection. Returns the detach function. */
|
|
60
|
+
subscribeLive(listener: (entry: ResumeEntry) => void): () => void;
|
|
61
|
+
/**
|
|
62
|
+
* Everything after position `n`, or why that can't be answered. `n` is
|
|
63
|
+
* this server's own minted position, already decoded and matched to
|
|
64
|
+
* this buffer's id by the caller.
|
|
65
|
+
*/
|
|
66
|
+
entriesAfter(n: number): EntriesAfterResult;
|
|
67
|
+
}
|
|
68
|
+
export type ResumeBufferKeyParts = {
|
|
69
|
+
/** Both null for an anonymous connection. */
|
|
70
|
+
principalId: string | null;
|
|
71
|
+
subjectId: string | null;
|
|
72
|
+
filter: ChangeFilter;
|
|
73
|
+
includeRecords: boolean;
|
|
74
|
+
includeUnlisted: boolean;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Reproducible across a reconnect that re-sends the same query params —
|
|
78
|
+
* order-independent on the parts of `filter` that don't carry order of
|
|
79
|
+
* their own (`typeId`/`kinds` are matched as sets, not sequences, so
|
|
80
|
+
* re-sending them in a different order must key the same buffer).
|
|
81
|
+
*
|
|
82
|
+
* Two filters that mean different things must never key the same buffer.
|
|
83
|
+
* A buffer opens exactly one `ScopedStack.subscribe()`, carrying the
|
|
84
|
+
* filter of whichever connection minted it, so a collision hands one of
|
|
85
|
+
* the two colliding connections the *other's* filter: either a silent gap
|
|
86
|
+
* (events it asked for and never hears about, with no `reset` to tell it
|
|
87
|
+
* so) or frames outside its filter, which "filtering is exact, not
|
|
88
|
+
* advisory" forbids. See docs/spec/wire-format.md § Change feed.
|
|
89
|
+
*
|
|
90
|
+
* Hence an object rather than a positional array: `JSON.stringify` drops
|
|
91
|
+
* an undefined *property* but renders an undefined *array element* as
|
|
92
|
+
* `null`, which would collapse "no parentId filter" onto `parentId: null`
|
|
93
|
+
* — "root records only", a filter the wire format defines. An absent part
|
|
94
|
+
* is an absent key, which no present value can imitate. Key order is fixed
|
|
95
|
+
* by construction below.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resumeBufferKey(parts: ResumeBufferKeyParts): string;
|
|
98
|
+
export type ResumeBufferRegistryOptions = {
|
|
99
|
+
/** Max entries retained per buffer before the oldest is dropped. */
|
|
100
|
+
depth: number;
|
|
101
|
+
/** How long a buffer keeps being fed after its last connection disconnects, before it's dropped entirely. */
|
|
102
|
+
retentionMs: number;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Owns every live resume buffer, keyed by (session, filter, includeRecords).
|
|
106
|
+
* A buffer's underlying `ScopedStack.subscribe()` is opened once, when the
|
|
107
|
+
* buffer is first minted, and kept open — including across a gap with zero
|
|
108
|
+
* live connections — until the buffer itself is evicted.
|
|
109
|
+
*/
|
|
110
|
+
export declare class ResumeBufferRegistry {
|
|
111
|
+
private readonly opts;
|
|
112
|
+
private readonly buffers;
|
|
113
|
+
private readonly unsubscribes;
|
|
114
|
+
private readonly evictionTimers;
|
|
115
|
+
/**
|
|
116
|
+
* Buffers whose subscription is still opening. A key lands in `buffers`
|
|
117
|
+
* only once its `ScopedStack.subscribe()` has resolved, so this is what
|
|
118
|
+
* keeps two connections arriving in the same tick from opening two
|
|
119
|
+
* subscriptions for one key — they await the same attempt and share
|
|
120
|
+
* whichever way it settles.
|
|
121
|
+
*/
|
|
122
|
+
private readonly acquiring;
|
|
123
|
+
constructor(opts: ResumeBufferRegistryOptions);
|
|
124
|
+
/**
|
|
125
|
+
* Get this key's buffer, creating one and opening its scoped subscription
|
|
126
|
+
* if none is currently retained. `subscribeToStack` is called at most
|
|
127
|
+
* once per buffer instance.
|
|
128
|
+
*
|
|
129
|
+
* A failed subscription registers nothing: the next connection on this
|
|
130
|
+
* key retries it. Registering a buffer that nothing is feeding would
|
|
131
|
+
* hand every later connection on that key a `ready` frame followed by
|
|
132
|
+
* permanent silence — a gap with no `reset` to announce it, which is
|
|
133
|
+
* the one failure the feed contract calls untrustworthy.
|
|
134
|
+
*/
|
|
135
|
+
acquire(key: string, subscribeToStack: (onChange: (change: RecordChange) => void) => Promise<Unsubscribe>): Promise<ResumeBuffer>;
|
|
136
|
+
/** Detach one connection from this key's buffer, starting its retention countdown once none remain. */
|
|
137
|
+
release(key: string): void;
|
|
138
|
+
private cancelEviction;
|
|
139
|
+
private evict;
|
|
140
|
+
/** Tear down every retained buffer immediately — for shutdown. */
|
|
141
|
+
closeAll(): void;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=resumeBuffer.d.ts.map
|