@spooky-sync/core 0.0.1-canary.32 → 0.0.1-canary.320
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/AGENTS.md +81 -0
- package/README.md +56 -16
- package/dist/index.d.ts +2034 -137
- package/dist/index.js +15137 -2434
- package/dist/live.d.ts +141 -0
- package/dist/live.js +709 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +337 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +379 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +439 -0
- package/dist/sw.d.ts +207 -0
- package/dist/sw.js +393 -0
- package/dist/tabs-broker-worker.d.ts +8 -0
- package/dist/tabs-broker-worker.js +472 -0
- package/dist/types.d.ts +1811 -38
- package/dist/worker-edges.js +439 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +46 -0
- package/dist/worker-types.d.ts +112 -0
- package/dist/worker-types.js +73 -0
- package/package.json +27 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +172 -0
- package/skills/sp00ky-core/SKILL.md +31 -6
- package/skills/sp00ky-core/references/config.md +6 -0
- package/src/boot/auth-flip.saga.test.ts +135 -0
- package/src/boot/auth-flip.saga.ts +77 -0
- package/src/boot/boot.saga.test.ts +180 -0
- package/src/boot/boot.saga.ts +142 -0
- package/src/boot/bucket-switch.saga.test.ts +187 -0
- package/src/boot/bucket-switch.saga.ts +133 -0
- package/src/boot/preload.saga.test.ts +51 -0
- package/src/boot/preload.saga.ts +31 -0
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/client/bucket-handle.test.ts +117 -0
- package/src/client/bucket-handle.ts +227 -0
- package/src/client/router.test.ts +58 -0
- package/src/client/router.ts +100 -0
- package/src/client/runtime.test.ts +195 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.test.ts +51 -0
- package/src/client/services.ts +404 -0
- package/src/client/sp00ky-client.test.ts +303 -0
- package/src/client/sp00ky-client.ts +586 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +69 -1
- package/src/kernel/constants.test.ts +19 -0
- package/src/kernel/constants.ts +80 -0
- package/src/kernel/effects.test.ts +40 -0
- package/src/kernel/effects.ts +163 -0
- package/src/kernel/events.ts +57 -0
- package/src/kernel/interpreter.test.ts +116 -0
- package/src/kernel/interpreter.ts +148 -0
- package/src/kernel/saga.test.ts +89 -0
- package/src/kernel/saga.ts +73 -0
- package/src/live/index.test.ts +478 -0
- package/src/live/index.ts +1013 -0
- package/src/modules/app-release/index.test.ts +125 -0
- package/src/modules/app-release/index.ts +209 -0
- package/src/modules/auth/auth.local-first.test.ts +169 -0
- package/src/modules/auth/events/index.ts +9 -2
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.integration.test.ts +205 -0
- package/src/modules/auth/impersonation.test.ts +377 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +466 -19
- package/src/modules/crdt/crdt-field.ts +294 -0
- package/src/modules/crdt/crdt-hydration.test.ts +210 -0
- package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
- package/src/modules/crdt/index.ts +463 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +746 -95
- package/src/modules/devtools/mutations.test.ts +92 -0
- package/src/modules/devtools/mutations.ts +160 -0
- package/src/modules/devtools/notify-throttle.test.ts +166 -0
- package/src/modules/devtools/state-shape.test.ts +263 -0
- package/src/modules/devtools/storage-info.test.ts +79 -0
- package/src/modules/devtools/storage-info.ts +168 -0
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +110 -0
- package/src/modules/feature-flag/index.test.ts +298 -0
- package/src/modules/feature-flag/index.ts +316 -0
- package/src/modules/ref-tables.test.ts +91 -0
- package/src/modules/ref-tables.ts +88 -0
- package/src/modules/web-push/index.test.ts +824 -0
- package/src/modules/web-push/index.ts +1027 -0
- package/src/mutation/jobs.test.ts +25 -0
- package/src/mutation/jobs.ts +41 -0
- package/src/mutation/mutation-id.test.ts +37 -0
- package/src/mutation/mutation-id.ts +35 -0
- package/src/mutation/pending-writes.integration.test.ts +175 -0
- package/src/mutation/push.saga.test.ts +285 -0
- package/src/mutation/push.saga.ts +217 -0
- package/src/mutation/rows.test.ts +231 -0
- package/src/mutation/rows.ts +427 -0
- package/src/mutation/tray.saga.test.ts +63 -0
- package/src/mutation/tray.saga.ts +48 -0
- package/src/mutation/write.saga.test.ts +316 -0
- package/src/mutation/write.saga.ts +268 -0
- package/src/otel/index.ts +10 -7
- package/src/pure.ts +65 -0
- package/src/push/types.ts +174 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +56 -0
- package/src/query/fetch.saga.test.ts +199 -0
- package/src/query/fetch.saga.ts +120 -0
- package/src/query/hash.test.ts +14 -0
- package/src/query/hash.ts +18 -0
- package/src/query/id-set-plan.test.ts +132 -0
- package/src/query/lifecycle.saga.test.ts +274 -0
- package/src/query/lifecycle.saga.ts +159 -0
- package/src/query/materialize.saga.test.ts +115 -0
- package/src/query/materialize.saga.ts +84 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +350 -0
- package/src/query/membership.saga.ts +278 -0
- package/src/query/membership.test.ts +118 -0
- package/src/query/membership.ts +201 -0
- package/src/query/register.saga.test.ts +311 -0
- package/src/query/register.saga.ts +229 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +81 -0
- package/src/query/sql.ts +112 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +195 -0
- package/src/services/blobs/blob-cache.test.ts +359 -0
- package/src/services/blobs/blob-cache.ts +603 -0
- package/src/services/blobs/blob-manifest.ts +227 -0
- package/src/services/blobs/blob-store.test.ts +77 -0
- package/src/services/blobs/blob-store.ts +359 -0
- package/src/services/blobs/blob.fixture.ts +90 -0
- package/src/services/blobs/index.ts +70 -0
- package/src/services/database/cache-engine.ts +199 -0
- package/src/services/database/connection-supervisor.test.ts +318 -0
- package/src/services/database/connection-supervisor.ts +422 -0
- package/src/services/database/database.query-timeout.test.ts +140 -0
- package/src/services/database/database.responses.test.ts +43 -0
- package/src/services/database/database.ts +146 -61
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/errors.ts +34 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +7 -0
- package/src/services/database/landed-body-clear.integration.test.ts +236 -0
- package/src/services/database/local-migrator.integration.test.ts +110 -0
- package/src/services/database/local-migrator.ts +59 -16
- package/src/services/database/local.test.ts +64 -0
- package/src/services/database/local.ts +487 -63
- package/src/services/database/plan-render.test.ts +159 -0
- package/src/services/database/plan-render.ts +108 -0
- package/src/services/database/relation-resolver.test.ts +484 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +255 -6
- package/src/services/database/sqlite-cache-engine.test.ts +617 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1372 -0
- package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
- package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
- package/src/services/database/sqlite-lock-verify.test.ts +33 -0
- package/src/services/database/sqlite-lock-verify.ts +45 -0
- package/src/services/database/sqlite-open.test.ts +150 -0
- package/src/services/database/sqlite-open.ts +164 -0
- package/src/services/database/sqlite-plan-sql.test.ts +145 -0
- package/src/services/database/sqlite-plan-sql.ts +182 -0
- package/src/services/database/sqlite-projection.test.ts +121 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +261 -0
- package/src/services/database/sqlite-select.ts +131 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +224 -0
- package/src/services/database/sqlite-worker.ts +437 -0
- package/src/services/database/surql-translate.ts +416 -0
- package/src/services/database/surreal-cache-engine.ts +161 -0
- package/src/services/logger/index.test.ts +132 -0
- package/src/services/logger/index.ts +189 -7
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +10 -3
- package/src/services/persistence/surrealdb.ts +4 -4
- package/src/services/stream-processor/index.ts +783 -68
- package/src/services/stream-processor/permissions.test.ts +47 -0
- package/src/services/stream-processor/permissions.ts +53 -0
- package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
- package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +58 -2
- package/src/services/tabs/broker-client.ts +283 -0
- package/src/services/tabs/broker.test.ts +327 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +638 -0
- package/src/services/tabs/fake-ports.fixture.ts +112 -0
- package/src/services/tabs/leader-locks.ts +75 -0
- package/src/services/tabs/protocol.ts +262 -0
- package/src/services/tabs/support.ts +36 -0
- package/src/services/tabs/tabs-broker-worker.ts +640 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +200 -0
- package/src/state/lifecycle.test.ts +141 -0
- package/src/state/lifecycle.ts +99 -0
- package/src/state/reducers.test.ts +304 -0
- package/src/state/reducers.ts +526 -0
- package/src/state/selectors.test.ts +175 -0
- package/src/state/selectors.ts +222 -0
- package/src/sw/index.test.ts +582 -0
- package/src/sw/index.ts +607 -0
- package/src/sw/store.ts +96 -0
- package/src/sync/connection.saga.test.ts +86 -0
- package/src/sync/connection.saga.ts +85 -0
- package/src/sync/live.saga.test.ts +178 -0
- package/src/sync/live.saga.ts +107 -0
- package/src/sync/policy.health.test.ts +56 -0
- package/src/sync/policy.test.ts +659 -0
- package/src/sync/policy.ts +502 -0
- package/src/sync/poll.saga.test.ts +51 -0
- package/src/sync/poll.saga.ts +36 -0
- package/src/sync/tabs.saga.test.ts +78 -0
- package/src/sync/tabs.saga.ts +96 -0
- package/src/testing/adapters.ts +133 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +48 -0
- package/src/testing/run-pure.test.ts +92 -0
- package/src/testing/run-pure.ts +128 -0
- package/src/testing/services.ts +17 -0
- package/src/types.ts +624 -10
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/edges.ts +39 -0
- package/src/utils/error-classification.test.ts +54 -0
- package/src/utils/error-classification.ts +25 -0
- package/src/utils/index.ts +78 -12
- package/src/utils/parser.test.ts +67 -102
- package/src/utils/parser.ts +64 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/sha256.ts +15 -0
- package/src/utils/surql.ts +30 -18
- package/src/utils/utils.test.ts +3 -2
- package/tsdown.config.ts +117 -3
- package/vitest.config.ts +48 -0
- package/src/modules/cache/index.ts +0 -241
- package/src/modules/cache/types.ts +0 -19
- package/src/modules/data/data.test.ts +0 -58
- package/src/modules/data/index.ts +0 -879
- package/src/modules/sync/engine.ts +0 -173
- package/src/modules/sync/events/index.ts +0 -77
- package/src/modules/sync/index.ts +0 -3
- package/src/modules/sync/queue/index.ts +0 -2
- package/src/modules/sync/queue/queue-down.ts +0 -89
- package/src/modules/sync/queue/queue-up.ts +0 -223
- package/src/modules/sync/scheduler.ts +0 -84
- package/src/modules/sync/sync.ts +0 -421
- package/src/modules/sync/utils.test.ts +0 -311
- package/src/modules/sync/utils.ts +0 -167
package/dist/live.js
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { B as queryHashInput, C as heartbeatRowGone, M as registerVars, N as singleSnapshotSelect, S as heartbeatBatch, a as metaFromRow, b as bodySelect, c as parseDuration, d as parseQueryParams, f as ANON_USER_ID, h as listRefTableFor, j as registerSelect, l as parseRecordIdString, n as rowOfEdge, p as DEFAULT_REF_MODE, s as encodeRecordId, t as hashOfEdge, u as withTimeout, z as sha256Hex } from "./worker-edges.js";
|
|
2
|
+
import { n as decodeTokenClaims } from "./worker-impersonation.js";
|
|
3
|
+
import { RecordId, Surreal } from "surrealdb";
|
|
4
|
+
|
|
5
|
+
//#region src/live/index.ts
|
|
6
|
+
/**
|
|
7
|
+
* `@spooky-sync/core/live`: a wasm-free live subscriber.
|
|
8
|
+
*
|
|
9
|
+
* Speaks the exact server contract the full client uses, over the pure-JS
|
|
10
|
+
* `surrealdb` SDK and one WebSocket, with no local store, no circuit, no
|
|
11
|
+
* logger and no `window`: it runs in a service worker, a dedicated worker and
|
|
12
|
+
* Node.
|
|
13
|
+
*
|
|
14
|
+
* Per query:
|
|
15
|
+
* - `fn::query::register($config)` plus the membership read-back in one
|
|
16
|
+
* request (`registerSelect` / `registerVars`, the same builders the full
|
|
17
|
+
* client's `registerRemote` uses; `$config = { id: _00_query:<key>, surql,
|
|
18
|
+
* params, ttl }`, key = sha256(JSON.stringify({ surql, params, sessionId })));
|
|
19
|
+
* - bodies with `SELECT * FROM $ids`;
|
|
20
|
+
* - one `LIVE SELECT * FROM <the user's _00_list_ref table> FETCH out` per
|
|
21
|
+
* feed, so a changed row usually arrives inline with its edge; an edge whose
|
|
22
|
+
* `out` is a bare id (a server that ignores FETCH) costs a body read, one
|
|
23
|
+
* whose `out` is null (unreadable or deleted row) a membership re-read;
|
|
24
|
+
* - `fn::query::heartbeat` at half the ttl, re-registering a reclaimed view;
|
|
25
|
+
* - `fn::query::unsubscribe` for every view on `unsubscribe()` / `close()`.
|
|
26
|
+
*
|
|
27
|
+
* Rows are the SDK's decoded values (`id` is a `RecordId`), unordered: apply
|
|
28
|
+
* the query's order yourself if it matters. Related children (`.related()`)
|
|
29
|
+
* are not joined in; subscribe to them separately when you need them.
|
|
30
|
+
*/
|
|
31
|
+
var LiveFeedError = class extends Error {
|
|
32
|
+
constructor(code, message, cause) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.cause = cause;
|
|
36
|
+
this.name = "LiveFeedError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function createLiveFeed(options) {
|
|
40
|
+
return new Feed(options);
|
|
41
|
+
}
|
|
42
|
+
const MATERIALIZE_RETRIES = 20;
|
|
43
|
+
const MATERIALIZE_RETRY_MS = 150;
|
|
44
|
+
const FLUSH_MS = 10;
|
|
45
|
+
const FETCH_BATCH_MS = 15;
|
|
46
|
+
const REREAD_MS = 50;
|
|
47
|
+
const RELEASE_TIMEOUT_MS = 2e3;
|
|
48
|
+
function deferred() {
|
|
49
|
+
let resolve;
|
|
50
|
+
let reject;
|
|
51
|
+
const promise = new Promise((res, rej) => {
|
|
52
|
+
resolve = res;
|
|
53
|
+
reject = rej;
|
|
54
|
+
});
|
|
55
|
+
promise.catch(() => void 0);
|
|
56
|
+
return {
|
|
57
|
+
promise,
|
|
58
|
+
resolve,
|
|
59
|
+
reject
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
var SubState = class {
|
|
63
|
+
rid;
|
|
64
|
+
payload;
|
|
65
|
+
rows = /* @__PURE__ */ new Map();
|
|
66
|
+
listeners = /* @__PURE__ */ new Set();
|
|
67
|
+
ready = deferred();
|
|
68
|
+
loaded = false;
|
|
69
|
+
failed = false;
|
|
70
|
+
registered = false;
|
|
71
|
+
/** A LIVE event hit this view before its first set landed. */
|
|
72
|
+
dirtyDuringLoad = false;
|
|
73
|
+
pending = {
|
|
74
|
+
added: /* @__PURE__ */ new Map(),
|
|
75
|
+
updated: /* @__PURE__ */ new Map(),
|
|
76
|
+
removed: /* @__PURE__ */ new Map()
|
|
77
|
+
};
|
|
78
|
+
flushTimer = null;
|
|
79
|
+
rereadTimer = null;
|
|
80
|
+
constructor(key, surql, params, ttl) {
|
|
81
|
+
this.key = key;
|
|
82
|
+
this.rid = new RecordId("_00_query", key);
|
|
83
|
+
this.payload = {
|
|
84
|
+
id: this.rid,
|
|
85
|
+
surql,
|
|
86
|
+
params,
|
|
87
|
+
ttl
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
list() {
|
|
91
|
+
return [...this.rows.values()].map((h) => h.row);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
function isBuilt(query) {
|
|
95
|
+
const q = query;
|
|
96
|
+
return !!(q && (q.innerQuery?.selectQuery || q.selectQuery));
|
|
97
|
+
}
|
|
98
|
+
/** The register input the full client derives from the same query. */
|
|
99
|
+
function liveQueryInput(query, schema) {
|
|
100
|
+
if (isBuilt(query)) {
|
|
101
|
+
const inner = "innerQuery" in query ? query.innerQuery : query;
|
|
102
|
+
const info = inner.selectQuery;
|
|
103
|
+
const table = inner.tableName;
|
|
104
|
+
const columns = (schema ?? inner.schema ?? query.schema)?.tables?.find((t) => t.name === table)?.columns;
|
|
105
|
+
const vars = info.vars ?? {};
|
|
106
|
+
return {
|
|
107
|
+
table,
|
|
108
|
+
surql: info.query,
|
|
109
|
+
params: columns ? parseQueryParams(columns, vars) : { ...vars }
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const raw = query;
|
|
113
|
+
if (typeof raw?.surql !== "string") throw new TypeError("live feed: expected a built query or { surql, params }");
|
|
114
|
+
return {
|
|
115
|
+
table: raw.table ?? /\bFROM\s+([A-Za-z0-9_]+)/i.exec(raw.surql)?.[1] ?? "",
|
|
116
|
+
surql: raw.surql,
|
|
117
|
+
params: { ...raw.params }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** `http(s)://` becomes `ws(s)://`; a bare host gets `/rpc`. */
|
|
121
|
+
function liveEndpoint(endpoint) {
|
|
122
|
+
try {
|
|
123
|
+
const url = new URL(endpoint);
|
|
124
|
+
if (url.protocol === "http:") url.protocol = "ws:";
|
|
125
|
+
else if (url.protocol === "https:") url.protocol = "wss:";
|
|
126
|
+
if (url.pathname === "" || url.pathname === "/") url.pathname = "/rpc";
|
|
127
|
+
return url.toString();
|
|
128
|
+
} catch {
|
|
129
|
+
return endpoint;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function asRecordId(value) {
|
|
133
|
+
if (value instanceof RecordId) return value;
|
|
134
|
+
if (typeof value === "string" && value.includes(":")) return parseRecordIdString(value);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function keyOf(rid) {
|
|
138
|
+
return encodeRecordId(rid);
|
|
139
|
+
}
|
|
140
|
+
/** `SELECT out, version ...` rows into edges, one per id at its highest version. */
|
|
141
|
+
function parseEdges(result) {
|
|
142
|
+
if (!Array.isArray(result)) return null;
|
|
143
|
+
const out = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const row of result) {
|
|
145
|
+
const rid = asRecordId(row?.out);
|
|
146
|
+
if (!rid) continue;
|
|
147
|
+
const version = typeof row.version === "number" ? row.version : 0;
|
|
148
|
+
const key = keyOf(rid);
|
|
149
|
+
const held = out.get(key);
|
|
150
|
+
if (!held || held.version < version) out.set(key, {
|
|
151
|
+
rid,
|
|
152
|
+
version
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
/** The three membership statements starting at `offset`: edges, meta, children. */
|
|
158
|
+
function snapshotAt(results, offset) {
|
|
159
|
+
if (!Array.isArray(results)) return null;
|
|
160
|
+
const edges = parseEdges(results[offset]);
|
|
161
|
+
if (!edges) return null;
|
|
162
|
+
return {
|
|
163
|
+
edges,
|
|
164
|
+
state: metaFromRow(results[offset + 1]).state
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
168
|
+
function randomId() {
|
|
169
|
+
const c = globalThis.crypto;
|
|
170
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
171
|
+
return `s${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
172
|
+
}
|
|
173
|
+
async function resolveToken(source) {
|
|
174
|
+
if (!source) return null;
|
|
175
|
+
const value = typeof source === "function" ? await source() : source;
|
|
176
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
177
|
+
}
|
|
178
|
+
var Feed = class {
|
|
179
|
+
db = null;
|
|
180
|
+
connecting = null;
|
|
181
|
+
uid = null;
|
|
182
|
+
table = null;
|
|
183
|
+
live = null;
|
|
184
|
+
liveStarting = null;
|
|
185
|
+
subs = /* @__PURE__ */ new Map();
|
|
186
|
+
sessionId;
|
|
187
|
+
ttl;
|
|
188
|
+
timeoutMs;
|
|
189
|
+
isClosed = false;
|
|
190
|
+
lastActivity = Date.now();
|
|
191
|
+
busy = 0;
|
|
192
|
+
starting = 0;
|
|
193
|
+
heartbeatTimer = null;
|
|
194
|
+
fetchTimer = null;
|
|
195
|
+
fetchQueue = /* @__PURE__ */ new Map();
|
|
196
|
+
offs = [];
|
|
197
|
+
dropped = false;
|
|
198
|
+
constructor(options) {
|
|
199
|
+
this.options = options;
|
|
200
|
+
this.sessionId = options.sessionId ?? randomId();
|
|
201
|
+
this.ttl = options.ttl ?? "10m";
|
|
202
|
+
this.timeoutMs = options.timeoutMs ?? 15e3;
|
|
203
|
+
}
|
|
204
|
+
get userId() {
|
|
205
|
+
return this.uid;
|
|
206
|
+
}
|
|
207
|
+
get listRefTable() {
|
|
208
|
+
return this.table;
|
|
209
|
+
}
|
|
210
|
+
get closed() {
|
|
211
|
+
return this.isClosed;
|
|
212
|
+
}
|
|
213
|
+
connect() {
|
|
214
|
+
if (this.isClosed) return Promise.reject(new LiveFeedError("closed", "live feed is closed"));
|
|
215
|
+
if (this.db) return Promise.resolve();
|
|
216
|
+
if (!this.connecting) this.connecting = this.open().catch((error) => {
|
|
217
|
+
this.connecting = null;
|
|
218
|
+
throw error;
|
|
219
|
+
});
|
|
220
|
+
return this.connecting;
|
|
221
|
+
}
|
|
222
|
+
makeSurreal() {
|
|
223
|
+
const given = this.options.surreal;
|
|
224
|
+
if (typeof given === "function") return given();
|
|
225
|
+
if (given) return given;
|
|
226
|
+
return new Surreal();
|
|
227
|
+
}
|
|
228
|
+
async open() {
|
|
229
|
+
const token = await resolveToken(this.options.token);
|
|
230
|
+
this.uid = token ? decodeTokenClaims(token).userId : null;
|
|
231
|
+
this.table = listRefTableFor(this.options.refMode ?? DEFAULT_REF_MODE, this.uid ?? ANON_USER_ID);
|
|
232
|
+
const db = this.makeSurreal();
|
|
233
|
+
try {
|
|
234
|
+
await db.connect(liveEndpoint(this.options.endpoint), { reconnect: {
|
|
235
|
+
enabled: true,
|
|
236
|
+
attempts: 5,
|
|
237
|
+
retryDelay: 500,
|
|
238
|
+
retryDelayMax: 5e3,
|
|
239
|
+
retryDelayMultiplier: 2,
|
|
240
|
+
retryDelayJitter: .1
|
|
241
|
+
} });
|
|
242
|
+
await db.use({
|
|
243
|
+
namespace: this.options.namespace,
|
|
244
|
+
database: this.options.database
|
|
245
|
+
});
|
|
246
|
+
} catch (error) {
|
|
247
|
+
await db.close().catch(() => void 0);
|
|
248
|
+
throw new LiveFeedError("connect", `live feed: could not connect to ${this.options.endpoint}`, error);
|
|
249
|
+
}
|
|
250
|
+
if (token) try {
|
|
251
|
+
await db.authenticate(token);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
await db.close().catch(() => void 0);
|
|
254
|
+
try {
|
|
255
|
+
await this.options.onUnauthorized?.(error);
|
|
256
|
+
} catch {}
|
|
257
|
+
throw new LiveFeedError("unauthorized", "live feed: the server refused the token", error);
|
|
258
|
+
}
|
|
259
|
+
if (this.isClosed) {
|
|
260
|
+
await db.close().catch(() => void 0);
|
|
261
|
+
throw new LiveFeedError("closed", "live feed is closed");
|
|
262
|
+
}
|
|
263
|
+
if (db.subscribe) {
|
|
264
|
+
const drop = () => {
|
|
265
|
+
this.dropped = true;
|
|
266
|
+
this.live?.off();
|
|
267
|
+
this.live = null;
|
|
268
|
+
};
|
|
269
|
+
this.offs.push(db.subscribe("disconnected", drop), db.subscribe("reconnecting", drop), db.subscribe("connected", () => {
|
|
270
|
+
if (!this.dropped || this.isClosed) return;
|
|
271
|
+
this.dropped = false;
|
|
272
|
+
this.recover();
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
this.db = db;
|
|
276
|
+
}
|
|
277
|
+
/** After a reconnect: LIVE again, then re-read every view (events in the gap are lost). */
|
|
278
|
+
async recover() {
|
|
279
|
+
try {
|
|
280
|
+
await this.ensureLive();
|
|
281
|
+
for (const sub of this.subs.values()) if (sub.loaded) this.scheduleReread(sub);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
this.report(error);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async q(sql, vars) {
|
|
287
|
+
await this.connect();
|
|
288
|
+
const db = this.db;
|
|
289
|
+
const result = await withTimeout(Promise.resolve(db.query(sql, vars)), this.timeoutMs, "live feed statement timed out");
|
|
290
|
+
return Array.isArray(result) ? result : [result];
|
|
291
|
+
}
|
|
292
|
+
async query(surql, vars) {
|
|
293
|
+
try {
|
|
294
|
+
return await this.q(surql, vars);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error instanceof LiveFeedError) throw error;
|
|
297
|
+
throw new LiveFeedError("query", error instanceof Error ? error.message : String(error), error);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
report(error) {
|
|
301
|
+
try {
|
|
302
|
+
this.options.onError?.(error);
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
touch() {
|
|
306
|
+
this.lastActivity = Date.now();
|
|
307
|
+
}
|
|
308
|
+
/** Start the feed's one LIVE query. `true` when this call started it. */
|
|
309
|
+
async ensureLive() {
|
|
310
|
+
if (this.live || this.isClosed) return false;
|
|
311
|
+
if (!this.liveStarting) this.liveStarting = (async () => {
|
|
312
|
+
try {
|
|
313
|
+
let id;
|
|
314
|
+
try {
|
|
315
|
+
[id] = await this.q(`LIVE SELECT * FROM ${this.table} FETCH out`);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
this.report(error);
|
|
318
|
+
[id] = await this.q(`LIVE SELECT * FROM ${this.table}`);
|
|
319
|
+
}
|
|
320
|
+
const stream = await this.db.liveOf(id);
|
|
321
|
+
const off = stream.subscribe((message) => this.onLive(message));
|
|
322
|
+
if (this.isClosed) {
|
|
323
|
+
off();
|
|
324
|
+
await (stream.kill?.() ?? Promise.resolve()).catch(() => void 0);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
this.live = {
|
|
328
|
+
id,
|
|
329
|
+
stream,
|
|
330
|
+
off
|
|
331
|
+
};
|
|
332
|
+
return true;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
this.report(error);
|
|
335
|
+
return false;
|
|
336
|
+
} finally {
|
|
337
|
+
this.liveStarting = null;
|
|
338
|
+
}
|
|
339
|
+
})();
|
|
340
|
+
return this.liveStarting;
|
|
341
|
+
}
|
|
342
|
+
onLive(message) {
|
|
343
|
+
if (message.action === "KILLED" || this.isClosed) return;
|
|
344
|
+
const edge = message.value;
|
|
345
|
+
const hash = hashOfEdge(edge);
|
|
346
|
+
const sub = hash ? this.subs.get(hash) : void 0;
|
|
347
|
+
if (!sub || !edge) return;
|
|
348
|
+
if (edge.parent !== void 0 && edge.parent !== null) return;
|
|
349
|
+
this.touch();
|
|
350
|
+
if (!sub.loaded) {
|
|
351
|
+
sub.dirtyDuringLoad = true;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const out = edge.out;
|
|
355
|
+
if (message.action === "DELETE") {
|
|
356
|
+
const rid = asRecordId(out) ?? asRecordId(out?.id);
|
|
357
|
+
if (rid) this.remove(sub, keyOf(rid));
|
|
358
|
+
else this.scheduleReread(sub);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const inline = rowOfEdge(edge);
|
|
362
|
+
if (inline) {
|
|
363
|
+
this.upsert(sub, inline.id, inline.version, inline.record);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const rid = asRecordId(out);
|
|
367
|
+
if (rid) {
|
|
368
|
+
this.enqueueFetch(sub, rid, typeof edge.version === "number" ? edge.version : 0);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.scheduleReread(sub);
|
|
372
|
+
}
|
|
373
|
+
upsert(sub, key, version, row) {
|
|
374
|
+
const held = sub.rows.get(key);
|
|
375
|
+
if (held && held.version >= version) return;
|
|
376
|
+
sub.rows.set(key, {
|
|
377
|
+
version,
|
|
378
|
+
row
|
|
379
|
+
});
|
|
380
|
+
if (sub.pending.removed.has(key)) {
|
|
381
|
+
sub.pending.removed.delete(key);
|
|
382
|
+
sub.pending.updated.set(key, row);
|
|
383
|
+
} else if (held || sub.pending.updated.has(key)) if (sub.pending.added.has(key)) sub.pending.added.set(key, row);
|
|
384
|
+
else sub.pending.updated.set(key, row);
|
|
385
|
+
else sub.pending.added.set(key, row);
|
|
386
|
+
this.scheduleFlush(sub);
|
|
387
|
+
}
|
|
388
|
+
remove(sub, key) {
|
|
389
|
+
const held = sub.rows.get(key);
|
|
390
|
+
if (!held) return;
|
|
391
|
+
sub.rows.delete(key);
|
|
392
|
+
if (sub.pending.added.has(key)) sub.pending.added.delete(key);
|
|
393
|
+
else {
|
|
394
|
+
sub.pending.updated.delete(key);
|
|
395
|
+
sub.pending.removed.set(key, held.row);
|
|
396
|
+
}
|
|
397
|
+
this.scheduleFlush(sub);
|
|
398
|
+
}
|
|
399
|
+
scheduleFlush(sub) {
|
|
400
|
+
this.touch();
|
|
401
|
+
if (sub.flushTimer) return;
|
|
402
|
+
sub.flushTimer = setTimeout(() => {
|
|
403
|
+
sub.flushTimer = null;
|
|
404
|
+
this.flush(sub);
|
|
405
|
+
}, FLUSH_MS);
|
|
406
|
+
}
|
|
407
|
+
flush(sub) {
|
|
408
|
+
const { added, updated, removed } = sub.pending;
|
|
409
|
+
if (added.size + updated.size + removed.size === 0) return;
|
|
410
|
+
sub.pending = {
|
|
411
|
+
added: /* @__PURE__ */ new Map(),
|
|
412
|
+
updated: /* @__PURE__ */ new Map(),
|
|
413
|
+
removed: /* @__PURE__ */ new Map()
|
|
414
|
+
};
|
|
415
|
+
const change = {
|
|
416
|
+
added: [...added.values()],
|
|
417
|
+
updated: [...updated.values()],
|
|
418
|
+
removed: [...removed.values()]
|
|
419
|
+
};
|
|
420
|
+
const rows = sub.list();
|
|
421
|
+
this.touch();
|
|
422
|
+
for (const l of [...sub.listeners]) try {
|
|
423
|
+
l.handlers.onChange?.(change);
|
|
424
|
+
l.handlers.onSet(rows);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
this.report(error);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
enqueueFetch(sub, rid, version) {
|
|
430
|
+
const key = keyOf(rid);
|
|
431
|
+
const entry = this.fetchQueue.get(key) ?? {
|
|
432
|
+
rid,
|
|
433
|
+
waiters: []
|
|
434
|
+
};
|
|
435
|
+
entry.waiters.push({
|
|
436
|
+
sub,
|
|
437
|
+
version
|
|
438
|
+
});
|
|
439
|
+
this.fetchQueue.set(key, entry);
|
|
440
|
+
if (this.fetchTimer) return;
|
|
441
|
+
this.busy++;
|
|
442
|
+
this.fetchTimer = setTimeout(() => {
|
|
443
|
+
this.fetchTimer = null;
|
|
444
|
+
this.drainFetches().finally(() => {
|
|
445
|
+
this.busy--;
|
|
446
|
+
this.touch();
|
|
447
|
+
});
|
|
448
|
+
}, FETCH_BATCH_MS);
|
|
449
|
+
}
|
|
450
|
+
async drainFetches() {
|
|
451
|
+
const batch = [...this.fetchQueue.values()];
|
|
452
|
+
this.fetchQueue.clear();
|
|
453
|
+
if (batch.length === 0 || this.isClosed) return;
|
|
454
|
+
try {
|
|
455
|
+
const bodies = await this.fetchBodies(batch.map((e) => e.rid));
|
|
456
|
+
for (const entry of batch) {
|
|
457
|
+
const key = keyOf(entry.rid);
|
|
458
|
+
const body = bodies.get(key);
|
|
459
|
+
for (const w of entry.waiters) {
|
|
460
|
+
if (!this.subs.has(w.sub.key)) continue;
|
|
461
|
+
if (body) this.upsert(w.sub, key, w.version, body);
|
|
462
|
+
else this.scheduleReread(w.sub);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {
|
|
466
|
+
this.report(error);
|
|
467
|
+
for (const entry of batch) for (const w of entry.waiters) this.scheduleReread(w.sub);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async fetchBodies(rids) {
|
|
471
|
+
const out = /* @__PURE__ */ new Map();
|
|
472
|
+
if (rids.length === 0) return out;
|
|
473
|
+
const [rows] = await this.q(bodySelect(), { ids: rids });
|
|
474
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
475
|
+
const rid = asRecordId(row?.id);
|
|
476
|
+
if (rid) out.set(keyOf(rid), row);
|
|
477
|
+
}
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
async readSnapshot(sub) {
|
|
481
|
+
return snapshotAt(await this.q(singleSnapshotSelect(this.table), { in: sub.rid }), 0);
|
|
482
|
+
}
|
|
483
|
+
/** Make `sub.rows` equal the snapshot: fetch what is new or newer, drop what left. */
|
|
484
|
+
async applySnapshot(sub, snap) {
|
|
485
|
+
const need = [...snap.edges.entries()].filter(([key, e]) => (sub.rows.get(key)?.version ?? -1) < e.version);
|
|
486
|
+
const bodies = await this.fetchBodies(need.map(([, e]) => e.rid));
|
|
487
|
+
if (!this.subs.has(sub.key)) return;
|
|
488
|
+
for (const [key, e] of need) {
|
|
489
|
+
const body = bodies.get(key);
|
|
490
|
+
if (body) this.upsert(sub, key, e.version, body);
|
|
491
|
+
}
|
|
492
|
+
for (const key of [...sub.rows.keys()]) if (!snap.edges.has(key)) this.remove(sub, key);
|
|
493
|
+
}
|
|
494
|
+
scheduleReread(sub) {
|
|
495
|
+
if (sub.rereadTimer || this.isClosed) return;
|
|
496
|
+
this.busy++;
|
|
497
|
+
sub.rereadTimer = setTimeout(() => {
|
|
498
|
+
sub.rereadTimer = null;
|
|
499
|
+
this.reread(sub).finally(() => {
|
|
500
|
+
this.busy--;
|
|
501
|
+
this.touch();
|
|
502
|
+
});
|
|
503
|
+
}, REREAD_MS);
|
|
504
|
+
}
|
|
505
|
+
async reread(sub) {
|
|
506
|
+
if (!this.subs.has(sub.key) || this.isClosed) return;
|
|
507
|
+
try {
|
|
508
|
+
const snap = await this.readSnapshot(sub);
|
|
509
|
+
if (!snap) throw new LiveFeedError("query", "membership re-read did not answer");
|
|
510
|
+
await this.applySnapshot(sub, snap);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
this.report(error);
|
|
513
|
+
for (const l of sub.listeners) l.handlers.onError?.(error);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** Register (or re-register) a view and read its membership back, in one request. */
|
|
517
|
+
async register(sub) {
|
|
518
|
+
let snap = snapshotAt(await this.q(registerSelect(this.table), registerVars(sub.payload)), 1);
|
|
519
|
+
for (let i = 0; snap && snap.state === "materializing" && i < MATERIALIZE_RETRIES; i++) {
|
|
520
|
+
await sleep(MATERIALIZE_RETRY_MS);
|
|
521
|
+
snap = await this.readSnapshot(sub);
|
|
522
|
+
}
|
|
523
|
+
if (!snap) throw new LiveFeedError("register", "registration read-back did not answer");
|
|
524
|
+
sub.registered = true;
|
|
525
|
+
this.ensureHeartbeat();
|
|
526
|
+
return snap;
|
|
527
|
+
}
|
|
528
|
+
async load(sub) {
|
|
529
|
+
this.busy++;
|
|
530
|
+
try {
|
|
531
|
+
await this.connect();
|
|
532
|
+
let snap = await this.register(sub);
|
|
533
|
+
if (await this.ensureLive()) snap = await this.readSnapshot(sub) ?? snap;
|
|
534
|
+
if (!this.subs.has(sub.key)) return;
|
|
535
|
+
const bodies = await this.fetchBodies([...snap.edges.values()].map((e) => e.rid));
|
|
536
|
+
for (const [key, e] of snap.edges) {
|
|
537
|
+
const body = bodies.get(key);
|
|
538
|
+
if (body) sub.rows.set(key, {
|
|
539
|
+
version: e.version,
|
|
540
|
+
row: body
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
sub.loaded = true;
|
|
544
|
+
const rows = sub.list();
|
|
545
|
+
sub.ready.resolve(rows);
|
|
546
|
+
this.touch();
|
|
547
|
+
for (const l of [...sub.listeners]) this.deliverInitial(l, rows);
|
|
548
|
+
if (sub.dirtyDuringLoad) {
|
|
549
|
+
sub.dirtyDuringLoad = false;
|
|
550
|
+
this.scheduleReread(sub);
|
|
551
|
+
}
|
|
552
|
+
} catch (error) {
|
|
553
|
+
sub.failed = true;
|
|
554
|
+
const err = error instanceof LiveFeedError ? error : new LiveFeedError("register", error instanceof Error ? error.message : String(error), error);
|
|
555
|
+
sub.ready.reject(err);
|
|
556
|
+
for (const l of [...sub.listeners]) l.handlers.onError?.(err);
|
|
557
|
+
this.subs.delete(sub.key);
|
|
558
|
+
} finally {
|
|
559
|
+
this.busy--;
|
|
560
|
+
this.touch();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
deliverInitial(listener, rows) {
|
|
564
|
+
try {
|
|
565
|
+
listener.handlers.onSet(rows);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
this.report(error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
ensureHeartbeat() {
|
|
571
|
+
if (this.heartbeatTimer || this.isClosed) return;
|
|
572
|
+
const every = Math.max(5e3, Math.floor(parseDuration(this.ttl) / 2));
|
|
573
|
+
this.heartbeatTimer = setInterval(() => void this.heartbeat(), every);
|
|
574
|
+
this.heartbeatTimer.unref?.();
|
|
575
|
+
}
|
|
576
|
+
async heartbeat() {
|
|
577
|
+
const subs = [...this.subs.values()].filter((s) => s.registered);
|
|
578
|
+
if (subs.length === 0 || this.isClosed) return;
|
|
579
|
+
try {
|
|
580
|
+
const beat = heartbeatBatch(subs.map((s) => s.rid));
|
|
581
|
+
const results = await this.q(beat.sql, beat.vars);
|
|
582
|
+
for (let i = 0; i < subs.length; i++) {
|
|
583
|
+
if (!heartbeatRowGone(results[i])) continue;
|
|
584
|
+
const sub = subs[i];
|
|
585
|
+
try {
|
|
586
|
+
const snap = await this.register(sub);
|
|
587
|
+
await this.applySnapshot(sub, snap);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
this.report(error);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
} catch (error) {
|
|
593
|
+
this.report(error);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
subscribe(query, handlers) {
|
|
597
|
+
const listener = { handlers };
|
|
598
|
+
let key = null;
|
|
599
|
+
let state = null;
|
|
600
|
+
let gone = false;
|
|
601
|
+
this.starting++;
|
|
602
|
+
const attached = (async () => {
|
|
603
|
+
try {
|
|
604
|
+
if (this.isClosed) throw new LiveFeedError("closed", "live feed is closed");
|
|
605
|
+
const input = liveQueryInput(query, this.options.schema);
|
|
606
|
+
key = await sha256Hex(queryHashInput({
|
|
607
|
+
surql: input.surql,
|
|
608
|
+
params: input.params
|
|
609
|
+
}, this.sessionId));
|
|
610
|
+
if (gone) throw new LiveFeedError("closed", "unsubscribed before the view was registered");
|
|
611
|
+
let sub = this.subs.get(key);
|
|
612
|
+
const fresh = !sub;
|
|
613
|
+
if (!sub) {
|
|
614
|
+
sub = new SubState(key, input.surql, input.params, this.ttl);
|
|
615
|
+
this.subs.set(key, sub);
|
|
616
|
+
}
|
|
617
|
+
state = sub;
|
|
618
|
+
sub.listeners.add(listener);
|
|
619
|
+
if (fresh) this.load(sub);
|
|
620
|
+
else if (sub.loaded) this.deliverInitial(listener, sub.list());
|
|
621
|
+
return sub;
|
|
622
|
+
} finally {
|
|
623
|
+
this.starting--;
|
|
624
|
+
}
|
|
625
|
+
})();
|
|
626
|
+
const ready = attached.then((sub) => sub.ready.promise);
|
|
627
|
+
ready.catch((error) => {
|
|
628
|
+
if (!state) handlers.onError?.(error);
|
|
629
|
+
});
|
|
630
|
+
return {
|
|
631
|
+
ready,
|
|
632
|
+
rows: () => state ? state.list() : [],
|
|
633
|
+
get key() {
|
|
634
|
+
return key;
|
|
635
|
+
},
|
|
636
|
+
unsubscribe: async () => {
|
|
637
|
+
if (gone) return;
|
|
638
|
+
gone = true;
|
|
639
|
+
const sub = await attached.catch(() => null);
|
|
640
|
+
if (!sub) return;
|
|
641
|
+
sub.listeners.delete(listener);
|
|
642
|
+
if (sub.listeners.size > 0 || this.subs.get(sub.key) !== sub) return;
|
|
643
|
+
this.subs.delete(sub.key);
|
|
644
|
+
if (sub.flushTimer) clearTimeout(sub.flushTimer);
|
|
645
|
+
if (sub.rereadTimer) {
|
|
646
|
+
clearTimeout(sub.rereadTimer);
|
|
647
|
+
this.busy--;
|
|
648
|
+
}
|
|
649
|
+
if (sub.registered && !this.isClosed) await this.q("fn::query::unsubscribe($id)", { id: sub.rid }).catch((error) => this.report(error));
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
idle(options = {}) {
|
|
654
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
655
|
+
const quietMs = options.quietMs ?? 300;
|
|
656
|
+
const deadline = Date.now() + timeoutMs;
|
|
657
|
+
return new Promise((resolve) => {
|
|
658
|
+
const check = () => {
|
|
659
|
+
if (this.isClosed) return resolve(false);
|
|
660
|
+
const loading = this.starting > 0 || [...this.subs.values()].some((s) => !s.loaded && !s.failed);
|
|
661
|
+
const flushing = [...this.subs.values()].some((s) => s.flushTimer !== null);
|
|
662
|
+
const quietFor = Date.now() - this.lastActivity;
|
|
663
|
+
if (!loading && !flushing && this.busy === 0 && quietFor >= quietMs) return resolve(true);
|
|
664
|
+
const left = deadline - Date.now();
|
|
665
|
+
if (left <= 0) return resolve(false);
|
|
666
|
+
setTimeout(check, Math.max(10, Math.min(50, left, quietMs - quietFor > 0 ? quietMs - quietFor : 50)));
|
|
667
|
+
};
|
|
668
|
+
check();
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
async close() {
|
|
672
|
+
if (this.isClosed) return;
|
|
673
|
+
const db = this.db;
|
|
674
|
+
const views = [...this.subs.values()].filter((s) => s.registered).map((s) => s.rid);
|
|
675
|
+
const live = this.live;
|
|
676
|
+
if (db) {
|
|
677
|
+
if (live) {
|
|
678
|
+
live.off();
|
|
679
|
+
await withTimeout(Promise.resolve(live.stream.kill ? live.stream.kill() : db.query("KILL $u", { u: live.id })), RELEASE_TIMEOUT_MS, "kill timed out").catch(() => void 0);
|
|
680
|
+
}
|
|
681
|
+
if (views.length > 0) {
|
|
682
|
+
const vars = {};
|
|
683
|
+
const sql = views.map((id, i) => {
|
|
684
|
+
vars[`id${i}`] = id;
|
|
685
|
+
return `fn::query::unsubscribe($id${i})`;
|
|
686
|
+
}).join(";\n");
|
|
687
|
+
await withTimeout(Promise.resolve(db.query(sql, vars)), RELEASE_TIMEOUT_MS, "release timed out").catch(() => void 0);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
this.isClosed = true;
|
|
691
|
+
this.live = null;
|
|
692
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
693
|
+
this.heartbeatTimer = null;
|
|
694
|
+
if (this.fetchTimer) clearTimeout(this.fetchTimer);
|
|
695
|
+
this.fetchTimer = null;
|
|
696
|
+
for (const sub of this.subs.values()) {
|
|
697
|
+
if (sub.flushTimer) clearTimeout(sub.flushTimer);
|
|
698
|
+
if (sub.rereadTimer) clearTimeout(sub.rereadTimer);
|
|
699
|
+
if (!sub.loaded) sub.ready.reject(new LiveFeedError("closed", "live feed is closed"));
|
|
700
|
+
}
|
|
701
|
+
this.subs.clear();
|
|
702
|
+
for (const off of this.offs.splice(0)) off();
|
|
703
|
+
if (db) await db.close().catch(() => void 0);
|
|
704
|
+
this.db = null;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
//#endregion
|
|
709
|
+
export { LiveFeedError, createLiveFeed, liveEndpoint, liveQueryInput };
|
package/dist/otel/index.d.ts
CHANGED