@midsummer-ai/sdk 0.0.0-stage → 0.1.0-next.1305

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +4 -2
  3. package/agents/contract.d.mts +9 -0
  4. package/agents/contract.mjs +221 -0
  5. package/agents/index.d.mts +128 -0
  6. package/agents/index.mjs +203 -0
  7. package/agents/stand-in.d.mts +35 -0
  8. package/agents/stand-in.mjs +149 -0
  9. package/ai/contract.d.mts +10 -0
  10. package/ai/contract.mjs +77 -0
  11. package/ai/index.d.mts +94 -0
  12. package/ai/index.mjs +193 -0
  13. package/ai/stand-in.d.mts +7 -0
  14. package/ai/stand-in.mjs +41 -0
  15. package/build-SQ44jwSa.d.mts +41 -0
  16. package/client.d.mts +45 -0
  17. package/client.mjs +106 -0
  18. package/connection-CxD_Nry5.mjs +1911 -0
  19. package/contract.d.mts +10 -0
  20. package/contract.mjs +469 -0
  21. package/database/build.d.mts +2 -0
  22. package/database/build.mjs +579 -0
  23. package/database/client.d.mts +60 -0
  24. package/database/client.mjs +133 -0
  25. package/database/contract.d.mts +11 -0
  26. package/database/contract.mjs +164 -0
  27. package/database/errors.d.mts +23 -0
  28. package/database/errors.mjs +13 -0
  29. package/database/server.d.mts +3 -0
  30. package/database/server.mjs +320 -0
  31. package/database/stand-in.d.mts +99 -0
  32. package/database/stand-in.mjs +430 -0
  33. package/email/client.d.mts +34 -0
  34. package/email/client.mjs +131 -0
  35. package/email/contract.d.mts +11 -0
  36. package/email/contract.mjs +99 -0
  37. package/email/index.d.mts +74 -0
  38. package/email/index.mjs +59 -0
  39. package/email/smtp.d.mts +22 -0
  40. package/email/smtp.mjs +296 -0
  41. package/email/stand-in.d.mts +13 -0
  42. package/email/stand-in.mjs +51 -0
  43. package/index-DdoK0y7D.d.mts +192 -0
  44. package/index-sgdyoA2P.d.mts +212 -0
  45. package/index.d.mts +2 -0
  46. package/index.mjs +137 -0
  47. package/names-DfCBKupJ.mjs +115 -0
  48. package/package.json +152 -4
  49. package/predict-Drfm_8pO.mjs +179 -0
  50. package/queues/contract.d.mts +13 -0
  51. package/queues/contract.mjs +278 -0
  52. package/queues/index.d.mts +184 -0
  53. package/queues/index.mjs +126 -0
  54. package/queues/stand-in.d.mts +7 -0
  55. package/queues/stand-in.mjs +110 -0
  56. package/queues/wire.d.mts +9 -0
  57. package/queues/wire.mjs +199 -0
  58. package/s3.d.mts +43 -0
  59. package/s3.mjs +1003 -0
  60. package/schema-BF-Y1EVu.mjs +142 -0
  61. package/schema-DS8x1Dn8.mjs +115 -0
  62. package/schema-f7E9uzUn.d.mts +74 -0
  63. package/server-C7O7CICU.d.mts +182 -0
  64. package/socket.d.mts +67 -0
  65. package/socket.mjs +263 -0
  66. package/stand-in.d.mts +7 -0
  67. package/stand-in.mjs +185 -0
  68. package/store-DDuOr-_F.mjs +237 -0
  69. package/tabs-4FLTfg1c.mjs +457 -0
  70. package/vector/contract.d.mts +10 -0
  71. package/vector/contract.mjs +635 -0
  72. package/vector/index.d.mts +2 -0
  73. package/vector/index.mjs +108 -0
  74. package/vector/stand-in.d.mts +11 -0
  75. package/vector/stand-in.mjs +169 -0
  76. package/vector/wire.d.mts +25 -0
  77. package/vector/wire.mjs +175 -0
  78. package/wire.d.mts +54 -0
  79. package/wire.mjs +731 -0
@@ -0,0 +1,1911 @@
1
+ //#region src/database/client/policy.mjs
2
+ /** The platform's prices in USD, per unit named: an object request, a WebSocket message (billed 20:1 as requests), a SQL row
3
+ * written, an object awake second (128 MB at $12.50 per million GB-s), a server CPU ms, a server request. And two
4
+ * MEASURED costs in CPU ms, not USD: `relayCpuMs` a relayed op (in and its
5
+ * answer), `pushCpuMs` a push. */
6
+ const PRICES = {
7
+ objectRequest: 15e-8,
8
+ wsMsg: 15e-8 / 20,
9
+ row: 1e-6,
10
+ awakeSec: 15625e-10,
11
+ serverCpuMs: 2e-8,
12
+ serverRequest: 3e-7,
13
+ relayCpuMs: .217,
14
+ pushCpuMs: .156
15
+ };
16
+ const LN2 = Math.log(2);
17
+ const fin = (x, fallback) => Number.isFinite(x) ? x : fallback;
18
+ /** An event rate in events per second, an EWMA with a half-life: a steady stream of r events/s reads r; after the
19
+ * stream stops the rate halves every `halfLifeMs`. `add(n, now)` records n events; `perSec(now)` reads the rate. */
20
+ var Rate = class {
21
+ constructor(halfLifeMs = 5e3) {
22
+ this.halfLifeMs = Math.max(1, fin(halfLifeMs, 5e3));
23
+ this.lambda = LN2 / (this.halfLifeMs / 1e3);
24
+ this.v = 0;
25
+ this.at = null;
26
+ }
27
+ /** The decayed value at `now` (a clock that steps back counts as no time passed). */
28
+ #at(now) {
29
+ return this.at === null ? 0 : this.v * Math.exp(-this.lambda * Math.max(0, now - this.at) / 1e3);
30
+ }
31
+ add(n = 1, now = Date.now()) {
32
+ this.v = this.#at(now) + fin(n, 0) * this.lambda;
33
+ this.at = this.at === null ? now : Math.max(this.at, now);
34
+ return this;
35
+ }
36
+ perSec(now = Date.now()) {
37
+ return this.#at(now);
38
+ }
39
+ };
40
+ /** A2: the ski-rental break-even, in ms to keep a thing held after its last use. Holding costs `holdUsdPerSec`;
41
+ * letting go costs `eachUsd` the next time it is needed (a wake, a reconnect, a request billed instead of a message).
42
+ * The deterministic break-even is eachUsd / holdUsdPerSec (never more than twice the cost of the best choice made in
43
+ * hindsight). With an observed `ratePerSec`, hold only while holding costs less than paying per event at that rate
44
+ * (holdUsdPerSec < eachUsd * ratePerSec): then keep the break-even, else let go at once (0). Holding for free:
45
+ * Infinity; an event for free: 0. */
46
+ function breakEvenMs({ holdUsdPerSec, eachUsd, ratePerSec } = {}) {
47
+ const hold = fin(holdUsdPerSec, 0), each = fin(eachUsd, 0);
48
+ if (each <= 0) return 0;
49
+ if (hold <= 0) return Infinity;
50
+ const ms = each / hold * 1e3;
51
+ if (ratePerSec === void 0 || ratePerSec === null) return ms;
52
+ return hold < each * fin(ratePerSec, 0) ? ms : 0;
53
+ }
54
+ /** A1: the hybrid histogram keep-alive policy. `gap(ms)` records an idle gap between two uses, in `bins` bins of
55
+ * `binMs` (gaps past the last bin count as out of range). Once `minSamples` gaps are in and the histogram is
56
+ * PATTERNED (the bin counts' coefficient of variation is at least `cv`, and at most half the gaps are out of range),
57
+ * `keepMs()` is the gap's 99th percentile (the bin's upper edge, plus `margin`) and `prewarmMs()` its 5th percentile
58
+ * (the bin's lower edge, less `margin`; null when that is 0: no point letting go). Otherwise `keepMs()` is
59
+ * `fallbackMs` (pass the break-even, A2) and `prewarmMs()` null. Every `halfAt` gaps the counts are halved, so the
60
+ * histogram follows a changing pattern. A pre-wake needs a timer: the database sets none while idle, so a caller uses
61
+ * `prewarmMs` only where it already holds one (or an alarm). */
62
+ var KeepAlive = class {
63
+ constructor({ binMs = 1e3, bins = 240, minSamples = 20, fallbackMs = 1e4, cv = 2, margin = .1, halfAt = 1e4 } = {}) {
64
+ this.binMs = Math.max(1, fin(binMs, 1e3));
65
+ this.bins = Math.max(1, Math.floor(fin(bins, 240)));
66
+ this.minSamples = Math.max(1, fin(minSamples, 20));
67
+ this.fallbackMs = fin(fallbackMs, 1e4);
68
+ this.cv = fin(cv, 2);
69
+ this.margin = Math.min(.9, Math.max(0, fin(margin, .1)));
70
+ this.halfAt = Math.max(2, fin(halfAt, 1e4));
71
+ this.counts = new Float64Array(this.bins);
72
+ this.inRange = 0;
73
+ this.oob = 0;
74
+ this.memo = null;
75
+ }
76
+ /** Records one idle gap (ms between the end of a use and the next use). */
77
+ gap(ms) {
78
+ if (!Number.isFinite(ms) || ms < 0) return this;
79
+ const b = Math.floor(ms / this.binMs);
80
+ if (b < this.bins) {
81
+ this.counts[b] += 1;
82
+ this.inRange += 1;
83
+ } else this.oob += 1;
84
+ if (this.inRange + this.oob >= this.halfAt) {
85
+ for (let k = 0; k < this.bins; k += 1) this.counts[k] /= 2;
86
+ this.inRange /= 2;
87
+ this.oob /= 2;
88
+ }
89
+ this.memo = null;
90
+ return this;
91
+ }
92
+ /** The gaps seen (after halving), and whether the histogram is patterned; p5 / p99 bins when it is. */
93
+ shape() {
94
+ if (this.memo) return this.memo;
95
+ const total = this.inRange + this.oob;
96
+ let memo = {
97
+ samples: total,
98
+ patterned: false,
99
+ cv: 0,
100
+ p5: null,
101
+ p99: null
102
+ };
103
+ if (total >= this.minSamples && this.inRange > 0 && this.oob <= total / 2) {
104
+ const mean = this.inRange / this.bins;
105
+ let sq = 0;
106
+ for (let k = 0; k < this.bins; k += 1) sq += (this.counts[k] - mean) ** 2;
107
+ const cv = Math.sqrt(sq / this.bins) / mean;
108
+ const at = (p) => {
109
+ const want = p * total;
110
+ let acc = 0;
111
+ for (let k = 0; k < this.bins; k += 1) {
112
+ acc += this.counts[k];
113
+ if (acc >= want) return k;
114
+ }
115
+ return null;
116
+ };
117
+ const p5 = at(.05), p99 = at(.99);
118
+ memo = {
119
+ samples: total,
120
+ patterned: cv >= this.cv && p99 !== null,
121
+ cv,
122
+ p5,
123
+ p99
124
+ };
125
+ }
126
+ this.memo = memo;
127
+ return memo;
128
+ }
129
+ /** How long to stay warm or held after the last use. */
130
+ keepMs() {
131
+ const s = this.shape();
132
+ return s.patterned ? Math.ceil((s.p99 + 1) * this.binMs * (1 + this.margin)) : this.fallbackMs;
133
+ }
134
+ /** When to pre-wake after the last use, or null (not patterned, or the next use usually comes at once). */
135
+ prewarmMs() {
136
+ const s = this.shape();
137
+ if (!s.patterned) return null;
138
+ const ms = Math.floor(s.p5 * this.binMs * (1 - this.margin));
139
+ return ms > 0 ? ms : null;
140
+ }
141
+ };
142
+ /** A3: additive increase, multiplicative decrease (Clipper's batch sizing): `observe(latencyMs)` raises the value by
143
+ * `add` while the latency is at or under `targetMs` and multiplies it by `mult` above it, clamped to [min, max].
144
+ * The value is a number (round it where a count is needed). */
145
+ var Aimd = class {
146
+ constructor({ targetMs, min = 0, max = 1e3, add = 1, mult = .5, start = min } = {}) {
147
+ if (!Number.isFinite(targetMs)) throw new RangeError("Aimd: targetMs");
148
+ if (!(min <= max)) throw new RangeError("Aimd: min > max");
149
+ Object.assign(this, {
150
+ targetMs,
151
+ min,
152
+ max,
153
+ add,
154
+ mult
155
+ });
156
+ this.v = Math.min(max, Math.max(min, fin(start, min)));
157
+ }
158
+ observe(latencyMs) {
159
+ if (!Number.isFinite(latencyMs)) return this.v;
160
+ this.v = latencyMs <= this.targetMs ? Math.min(this.max, this.v + this.add) : Math.max(this.min, this.v * this.mult);
161
+ return this.v;
162
+ }
163
+ value() {
164
+ return this.v;
165
+ }
166
+ };
167
+ /** Decorrelated jitter (AWS, "Exponential Backoff And Jitter"): the next wait after `prev` ms is uniform between
168
+ * `base` and 3x `prev` (at least `base`), at most `cap`. `rand`: a [0, 1) source (tests pass their own). */
169
+ function jitterBackoff(prev, base, cap, rand = Math.random) {
170
+ const lo = Math.max(0, fin(base, 0)), hi = Math.max(lo, fin(prev, 0) * 3);
171
+ return Math.min(fin(cap, Infinity), lo + rand() * (hi - lo));
172
+ }
173
+ const TAG_TTL = 864e5;
174
+ const groupOf = (id) => {
175
+ const t = String(id ?? ""), k = t.indexOf("/");
176
+ return k < 0 ? t : t.slice(0, k);
177
+ };
178
+ const unitIn = (dir, id) => {
179
+ const h = fnv(groupOf(id));
180
+ for (const u of dir.units) if (h % 2 ** u.d === u.p) return u.name;
181
+ return dir.units[0]?.name ?? "0";
182
+ };
183
+ const fnv = (id) => {
184
+ let h = 2166136261;
185
+ for (let i = 0; i < id.length; i += 1) h = Math.imul(h ^ id.charCodeAt(i), 16777619) >>> 0;
186
+ return h;
187
+ };
188
+ /** A row id the map puts on unit `name` (`~0`, `~1`, ...: a lane's `home`), or null. */
189
+ const rowOn = (dir, name) => {
190
+ for (let k = 0; k < 65536; k += 1) if (unitIn(dir, `~${k}`) === name) return `~${k}`;
191
+ return null;
192
+ };
193
+ /** A token's payload (not verified here: the server does that): a JWT's second part (the platform's, `header.payload.sig`),
194
+ * or the first of our own (`payload.sig`). Null when it is neither. */
195
+ const payloadOf = (token) => {
196
+ try {
197
+ const parts = String(token).split(".");
198
+ return JSON.parse(atob(parts[parts.length === 3 ? 1 : 0].replace(/-/gu, "+").replace(/_/gu, "/")));
199
+ } catch {
200
+ return null;
201
+ }
202
+ };
203
+ /** A token's `exp` (seconds). */
204
+ const expOf = (token) => payloadOf(token)?.exp ?? null;
205
+ /** An answer the caller must see as a failure: an Error whose `code` is `denied`, `short`, ... (`answer`: the answer). */
206
+ const coded = (a) => Object.assign(new Error(a.err ?? a.why ?? "failed"), {
207
+ code: a.err ?? a.why ?? "failed",
208
+ answer: a
209
+ });
210
+ const refused = (a) => ["denied", "short"].includes(a.err ?? a.why);
211
+ /** (dd) Ops that only read (a get, a range, a query function, the map): sent again after a reconnect or a lost link,
212
+ * never answered `closed` (a deploy resets every edge: a read in flight must not fail for it; reading twice is harmless). */
213
+ const READ_OPS = /* @__PURE__ */ new Set([
214
+ "g",
215
+ "r",
216
+ "f",
217
+ "q",
218
+ "dir"
219
+ ]);
220
+ /** Who tagged a write: the tag's part before its last `:` (a Connection's client id, or a tab's id). */
221
+ const originOf = (tag) => {
222
+ const t = String(tag), k = t.lastIndexOf(":");
223
+ return k < 0 ? t : t.slice(0, k);
224
+ };
225
+ /** Ms an unsubscribe waits for a message to its unit to ride before it goes alone; ms of nothing sent or heard
226
+ * before a keepalive ping; ms ops wait for the hello's map when none is known. */
227
+ const LEAVE_MS = 1e3;
228
+ const IDLE_PING_MS = 5e4;
229
+ const HELLO_WAIT_MS = 2e3;
230
+ /** A push's cost and a read's (the server CPU of each; a read is also a message the client sends). */
231
+ const PUSH_USD = PRICES.pushCpuMs * PRICES.serverCpuMs;
232
+ const READ_USD = PRICES.wsMsg + PRICES.relayCpuMs * PRICES.serverCpuMs;
233
+ /** The window in which `subscribeAfter` reads of a row subscribe it: the break-even of holding a quiet row (a
234
+ * push a minute) against reading it again (~227 s), the same rule that lets it go (`quiet`). Was 1 s: a client
235
+ * reading a row every few seconds paid a message a read for ever (s19: 0 of 28k reads answered here). */
236
+ const SUBSCRIBE_WINDOW_MS = breakEvenMs({
237
+ holdUsdPerSec: PUSH_USD / 60,
238
+ eachUsd: READ_USD
239
+ });
240
+ /** Whether a token says server code: ours with `ro: "server"`, or the platform's for a Database Key (`iss: "midsummer"`,
241
+ * `sub: "key:<id>"`; a person's, `user:<handle>`, is not). */
242
+ const serverToken = (token) => {
243
+ const p = payloadOf(token);
244
+ return p?.ro === "server" || p?.iss === "midsummer" && String(p?.sub ?? "").startsWith("key:");
245
+ };
246
+ /** The shape `Connection.connect` takes: "binding" (a server object binding in `opts.env.DATABASE_EDGE` or
247
+ * `opts.binding`), "server" (a runtime with no `window` / `document`, and a server token), else "browser". `g`: the
248
+ * globals to look at (tests pass their own). */
249
+ function shapeOf(opts = {}, g = globalThis) {
250
+ if (opts.shape) return opts.shape;
251
+ if (opts.binding || opts.env?.DATABASE_EDGE) return "binding";
252
+ const ua = String(g.navigator?.userAgent ?? "");
253
+ return !(typeof g.window !== "undefined" || typeof g.document !== "undefined" || /^Mozilla\//u.test(ua) && !g.process?.versions?.node && !g.Deno && !g.Bun) && serverToken(opts.token) ? "server" : "browser";
254
+ }
255
+ /** A row's key in the row copies: `tb/id`, and the acting user's when a server acts as one (rules differ). */
256
+ const rowKey = (tb, id, as) => as === void 0 ? `${tb}/${id}` : `${tb}/${id}@${as}`;
257
+ const asOf = (as) => as === void 0 || as === null ? {} : { as: String(as) };
258
+ /** A server function named by its reference (`api.pay`: `{ name, kind }`, the seal's functions/_generated/api.mjs) or its name. */
259
+ const fnName = (f) => f !== null && typeof f === "object" && typeof f.name === "string" ? f.name : f;
260
+ /** The process's server sockets: url|token -> the Connection (one socket per process, however often connected). */
261
+ const SHARED = /* @__PURE__ */ new Map();
262
+ /** (A11) W-TinyLFU: which keys a bounded cache keeps. New keys enter a small window LRU (1%); a key leaving the
263
+ * window is admitted to the main LRU only if a count-min sketch (4 rows of 4-bit-ish counters, halved every 10x the
264
+ * size in accesses) says it is used more often than the main LRU's victim. `touch` every access (hit or miss), `add`
265
+ * a missed key: it returns the keys that left the cache (possibly the one leaving the window). */
266
+ /**
267
+ * Where a request to `url` goes, and the headers that name it. A `*.localhost` name is loopback (RFC 6761)
268
+ * whatever the system's resolver says, and a Debian container's says nothing: outside a browser, which
269
+ * resolves it itself and never lets a page set Host, it goes to 127.0.0.1 with the name as its Host
270
+ * (and, over TLS, as the name the certificate is checked for). Anything else goes as it is.
271
+ * const { url, init } = loopback("http://db-a.localhost:8792/ws"); fetch(url, init)
272
+ */
273
+ function loopback(url, g = globalThis) {
274
+ const at = new URL(url);
275
+ if (g.document !== void 0 || !at.hostname.endsWith(".localhost")) return {
276
+ url: String(url),
277
+ init: {}
278
+ };
279
+ const host = at.host, name = at.hostname;
280
+ at.hostname = "127.0.0.1";
281
+ return {
282
+ url: at.href,
283
+ init: {
284
+ headers: { Host: host },
285
+ .../^(https|wss):$/u.test(at.protocol) ? { tls: { serverName: name } } : {}
286
+ }
287
+ };
288
+ }
289
+ /** `fetch` of `url` through `loopback`, `init`'s headers kept beside its Host. */
290
+ const fetchAt = (url, init = {}) => {
291
+ const to = loopback(url);
292
+ return to.init.headers ? fetch(to.url, {
293
+ ...init,
294
+ ...to.init,
295
+ headers: {
296
+ ...Object.fromEntries(new Headers(init.headers ?? {})),
297
+ ...to.init.headers
298
+ }
299
+ }) : fetch(to.url, init);
300
+ };
301
+ /** A WebSocket to `url` through `loopback`. */
302
+ const socketAt = (url) => {
303
+ const to = loopback(url);
304
+ return to.init.headers ? new WebSocket(to.url, to.init) : new WebSocket(to.url);
305
+ };
306
+ var TinyLfu = class {
307
+ constructor(size, { windowShare = .01 } = {}) {
308
+ this.size = Math.max(2, Math.floor(size));
309
+ this.wcap = Math.max(1, Math.round(this.size * windowShare));
310
+ this.mcap = this.size - this.wcap;
311
+ this.window = /* @__PURE__ */ new Map();
312
+ this.main = /* @__PURE__ */ new Map();
313
+ const width = 2 ** Math.ceil(Math.log2(Math.max(16, this.size * 4)));
314
+ this.width = width;
315
+ this.mask = width - 1;
316
+ this.sketch = new Uint8Array(width * 4);
317
+ this.samples = 0;
318
+ this.resetAt = 10 * this.size;
319
+ }
320
+ slots(key) {
321
+ const h1 = fnv(key), h2 = Math.imul(h1 ^ h1 >>> 16, 2246822507) >>> 0 | 1;
322
+ return [
323
+ 0,
324
+ 1,
325
+ 2,
326
+ 3
327
+ ].map((r) => r * this.width + (h1 + Math.imul(r, h2) >>> 0 & this.mask));
328
+ }
329
+ freq(key) {
330
+ let f = 15;
331
+ for (const s of this.slots(key)) f = Math.min(f, this.sketch[s]);
332
+ return f;
333
+ }
334
+ touch(key) {
335
+ for (const s of this.slots(key)) if (this.sketch[s] < 15) this.sketch[s] += 1;
336
+ if ((this.samples += 1) >= this.resetAt) {
337
+ this.samples /= 2;
338
+ for (let k = 0; k < this.sketch.length; k += 1) this.sketch[k] >>= 1;
339
+ }
340
+ if (this.window.has(key)) {
341
+ this.window.delete(key);
342
+ this.window.set(key, 1);
343
+ } else if (this.main.has(key)) {
344
+ this.main.delete(key);
345
+ this.main.set(key, 1);
346
+ }
347
+ }
348
+ has(key) {
349
+ return this.window.has(key) || this.main.has(key);
350
+ }
351
+ get count() {
352
+ return this.window.size + this.main.size;
353
+ }
354
+ add(key) {
355
+ if (this.has(key)) return [];
356
+ this.window.set(key, 1);
357
+ if (this.window.size <= this.wcap) return [];
358
+ const cand = this.window.keys().next().value;
359
+ this.window.delete(cand);
360
+ if (this.main.size < this.mcap) {
361
+ this.main.set(cand, 1);
362
+ return [];
363
+ }
364
+ const victim = this.main.keys().next().value;
365
+ if (this.freq(cand) > this.freq(victim)) {
366
+ this.main.delete(victim);
367
+ this.main.set(cand, 1);
368
+ return [victim];
369
+ }
370
+ return [cand];
371
+ }
372
+ delete(key) {
373
+ this.window.delete(key);
374
+ this.main.delete(key);
375
+ }
376
+ };
377
+ var Connection = class Connection {
378
+ /** Opens the database in the shape of where it runs (see the top of this file). */
379
+ static async connect(url, options = {}) {
380
+ const shape = shapeOf(options);
381
+ if (shape === "binding") throw Object.assign(/* @__PURE__ */ new Error("connect over the network with a server token: a binding is not part of this client"), { code: "binding" });
382
+ if (shape === "server") {
383
+ const key = `${url}|${options.token ?? ""}`;
384
+ let p = SHARED.get(key);
385
+ if (!p) {
386
+ p = Connection.open(url, {
387
+ ...options,
388
+ shape
389
+ }).then((g) => {
390
+ g.sharedKey = key;
391
+ return g;
392
+ }, (e) => {
393
+ SHARED.delete(key);
394
+ throw e;
395
+ });
396
+ SHARED.set(key, p);
397
+ }
398
+ const g = await p;
399
+ g.refs += 1;
400
+ return g;
401
+ }
402
+ const o = {
403
+ ...options,
404
+ shape
405
+ };
406
+ if (o.store === void 0 && typeof indexedDB !== "undefined" && typeof document !== "undefined") o.store = (await import("./store-DDuOr-_F.mjs")).idbStore(new URL(url).host);
407
+ if (o.tabs !== false && typeof document !== "undefined") return (await import("./tabs-4FLTfg1c.mjs")).ConnectionTabs.connect(url, o);
408
+ return Connection.open(url, o);
409
+ }
410
+ /** A Connection over one socket, of the shape given (default "browser"), no detection (tabs.mjs opens its own so). */
411
+ static async open(url, options) {
412
+ const g = new Connection(url, options);
413
+ if (g.store) await g.store.attach(g);
414
+ if (options.functions) g.predictor = new (await (import("./predict-Drfm_8pO.mjs"))).Predictor(g, options.functions);
415
+ await g.open();
416
+ if (!g.dir) await Promise.race([g.hello, new Promise((r) => setTimeout(r, HELLO_WAIT_MS))]);
417
+ return g;
418
+ }
419
+ constructor(url, { token = "", refresh = null, home = "", store = null, silent = 3e3, window = null, budgetMs = 5, cacheRows = 256, subscribeAfter = 3, subscribeWindowMs = SUBSCRIBE_WINDOW_MS, shape = "browser", dial = null, lanes = false }) {
420
+ this.silent = silent;
421
+ this.shape = shape;
422
+ this.dial = dial;
423
+ this.durable = shape !== "browser";
424
+ this.refs = 0;
425
+ this.window = window ?? null;
426
+ this.lastFlush = -Infinity;
427
+ this.gather = null;
428
+ this.aimd = new Aimd({
429
+ targetMs: budgetMs / 2,
430
+ min: 0,
431
+ max: budgetMs,
432
+ add: budgetMs / 10,
433
+ mult: .5
434
+ });
435
+ this.dur = null;
436
+ this.conflating = /* @__PURE__ */ new Map();
437
+ this.conflateTimer = null;
438
+ this.conflateAimd = new Aimd({
439
+ targetMs: 100,
440
+ min: 50,
441
+ max: 100,
442
+ add: 5,
443
+ mult: .5,
444
+ start: 50
445
+ });
446
+ this.leaving = /* @__PURE__ */ new Map();
447
+ this.leaveTimer = null;
448
+ this.lastSent = -Infinity;
449
+ this.helloSeen = false;
450
+ this.subscribeAfter = subscribeAfter;
451
+ this.subscribeWindowMs = subscribeWindowMs;
452
+ this.lfu = new TinyLfu(cacheRows);
453
+ this.auto = /* @__PURE__ */ new Map();
454
+ this.heat = /* @__PURE__ */ new Map();
455
+ this.never = /* @__PURE__ */ new Set();
456
+ this.keep = new KeepAlive({
457
+ binMs: 100,
458
+ bins: 600,
459
+ minSamples: 20
460
+ });
461
+ this.store = store;
462
+ this.predictor = null;
463
+ this.base = url.replace(/^ws/u, "http");
464
+ this.token = token;
465
+ this.refresh = refresh;
466
+ this.qs = home ? `&home=${encodeURIComponent(home)}` : "";
467
+ this.lanes = lanes && !dial ? /* @__PURE__ */ new Map() : null;
468
+ this.esc = null;
469
+ this.hello = new Promise((r) => {
470
+ this.helloed = r;
471
+ });
472
+ this.unconfirmed = /* @__PURE__ */ new Map();
473
+ this.sentMsgs = /* @__PURE__ */ new Map();
474
+ this.durTimes = [];
475
+ this.units = 1;
476
+ this.client = Math.random().toString(36).slice(2, 10);
477
+ this.next = 0;
478
+ this.tags = 0;
479
+ this.waiting = /* @__PURE__ */ new Map();
480
+ this.outbox = /* @__PURE__ */ new Map();
481
+ this.held = [];
482
+ this.reads = /* @__PURE__ */ new Map();
483
+ this.rows = /* @__PURE__ */ new Map();
484
+ this.mine = /* @__PURE__ */ new Map();
485
+ this.overlay = /* @__PURE__ */ new Map();
486
+ this.watches = /* @__PURE__ */ new Map();
487
+ this.byI = /* @__PURE__ */ new Map();
488
+ this.closed = false;
489
+ this.dir = null;
490
+ this.dirListeners = /* @__PURE__ */ new Set();
491
+ this.stats = {
492
+ messages: 0,
493
+ ops: 0,
494
+ localReads: 0,
495
+ sharedReads: 0,
496
+ sharedWatches: 0,
497
+ pushes: 0,
498
+ outOfOrder: 0,
499
+ resent: 0,
500
+ reconnects: 0,
501
+ moved: 0,
502
+ maps: 0,
503
+ conflated: 0,
504
+ autoSubs: 0,
505
+ autoDrops: 0,
506
+ pings: 0,
507
+ unsubs: 0,
508
+ unsubsRode: 0,
509
+ rewatched: 0
510
+ };
511
+ }
512
+ unitOf(id = "") {
513
+ return this.dir ? unitIn(this.dir, id) : String(fnv(groupOf(id)) % this.units);
514
+ }
515
+ unitNames() {
516
+ return this.dir ? this.dir.units.map((u) => u.name) : Array.from({ length: this.units }, (_, k) => String(k));
517
+ }
518
+ setDir(dir) {
519
+ if (dir && (!this.dir || dir.v > this.dir.v)) {
520
+ this.dir = dir;
521
+ this.stats.maps += 1;
522
+ for (const f of [...this.dirListeners]) f();
523
+ }
524
+ }
525
+ /** Asks the edge for the director's map (one question in flight). */
526
+ refreshDir() {
527
+ this.dirAsk ??= this.send("*", { k: "dir" }).then((a) => {
528
+ this.dirAsk = null;
529
+ if (a.ok) this.setDir(a.dir);
530
+ });
531
+ return this.dirAsk;
532
+ }
533
+ /** The socket's URL with the current token. */
534
+ get url() {
535
+ return `${this.base.replace(/^http/u, "ws")}/ws?token=${encodeURIComponent(this.token)}${this.qs}`;
536
+ }
537
+ /** The token as query parameters, for the HTTP routes. */
538
+ auth() {
539
+ return `token=${encodeURIComponent(this.token)}`;
540
+ }
541
+ /** Opens the socket; a refused open (401: the token expired or is bad) gets a new token from `refresh` and tries once
542
+ * more. Every token is renewed 60 s before its `exp`. */
543
+ async open() {
544
+ this.renewAt();
545
+ try {
546
+ await this.openOnce();
547
+ } catch (e) {
548
+ if (!await this.denied()) throw e;
549
+ await this.renew(false);
550
+ await this.openOnce();
551
+ }
552
+ }
553
+ /** Whether a failed open was a 401: an expiring token is, else the same URL over HTTP is asked (the server checks the
554
+ * token before anything; a browser's WebSocket never shows the status). */
555
+ async denied() {
556
+ if (!this.refresh) return false;
557
+ const exp = expOf(this.token);
558
+ if (!this.token || exp && exp * 1e3 - Date.now() < 6e4) return true;
559
+ try {
560
+ return (await fetchAt(this.url.replace(/^ws/u, "http"), { signal: AbortSignal.timeout(1e4) })).status === 401;
561
+ } catch {
562
+ return false;
563
+ }
564
+ }
565
+ /** A new token from `refresh`; `reopen`: the socket closes, and the reconnect (resend, resubscribe) uses it. */
566
+ async renew(reopen = true) {
567
+ this.renewing ??= Promise.resolve(this.refresh()).then((t) => {
568
+ this.token = t;
569
+ this.renewAt();
570
+ }).finally(() => {
571
+ this.renewing = null;
572
+ });
573
+ await this.renewing;
574
+ if (reopen) this.drop("renew");
575
+ }
576
+ renewAt() {
577
+ clearTimeout(this.renewer);
578
+ const exp = this.refresh && expOf(this.token);
579
+ if (!exp || this.closed) return;
580
+ this.renewer = setTimeout(() => this.renew().catch(() => this.renewAt()), Math.max(1e3, exp * 1e3 - Date.now() - 6e4));
581
+ this.renewer.unref?.();
582
+ }
583
+ openOnce() {
584
+ return new Promise((ready, fail) => {
585
+ const dialed = this.dial ? Promise.resolve(this.dial(this)) : null;
586
+ const ws = dialed ? null : socketAt(this.url);
587
+ const timer = setTimeout(() => {
588
+ fail(/* @__PURE__ */ new Error("open timeout"));
589
+ try {
590
+ ws?.close(1e3);
591
+ } catch {}
592
+ }, 1e4);
593
+ const opened = (ws) => {
594
+ clearTimeout(timer);
595
+ if (this.closed) {
596
+ try {
597
+ ws.close(1e3);
598
+ } catch {}
599
+ ready();
600
+ return;
601
+ }
602
+ this.ws = ws;
603
+ this.heard = this.answeredAt = this.openedAt = performance.now();
604
+ this.probe = 0;
605
+ this.stuckSince = 0;
606
+ this.resend();
607
+ ready();
608
+ if (!this.dog) {
609
+ this.dog = setInterval(() => this.watchdog(), 1e3);
610
+ this.dog.unref?.();
611
+ }
612
+ };
613
+ if (dialed) {
614
+ dialed.then((s) => {
615
+ this.listen(s);
616
+ opened(s);
617
+ }, (e) => {
618
+ clearTimeout(timer);
619
+ fail(e);
620
+ });
621
+ return;
622
+ }
623
+ ws.addEventListener("open", () => opened(ws), { once: true });
624
+ ws.addEventListener("error", () => {
625
+ if (ws.readyState !== 1) {
626
+ clearTimeout(timer);
627
+ fail(/* @__PURE__ */ new Error("socket error"));
628
+ }
629
+ });
630
+ this.listen(ws);
631
+ });
632
+ }
633
+ /** A socket's messages and close (a socket left by `drop` is not heard any more). */
634
+ listen(ws) {
635
+ ws.addEventListener("message", (event) => {
636
+ if (this.ws !== ws) return;
637
+ this.heard = performance.now();
638
+ this.probe = 0;
639
+ if (event.data !== "pong") this.answeredAt = this.heard;
640
+ this.stats.bytesIn = (this.stats.bytesIn ?? 0) + (event.data.length ?? event.data.byteLength ?? 0);
641
+ this.received(event.data);
642
+ if (this.held.length) this.schedule();
643
+ });
644
+ ws.addEventListener("close", () => {
645
+ if (this.ws === ws) {
646
+ this.ws = null;
647
+ if (!this.closed) this.reconnect();
648
+ }
649
+ });
650
+ }
651
+ /** Opens a socket again until one opens (one loop at a time), backing off from `delay` to 5 s. Decorrelated
652
+ * jitter (policy `jitterBackoff`): clients a dead edge dropped together do not come back together. */
653
+ async reconnect(delay = 100) {
654
+ if (this.reopening || this.closed) return;
655
+ this.reopening = true;
656
+ this.stats.reconnects += 1;
657
+ try {
658
+ for (;;) {
659
+ await new Promise((r) => setTimeout(r, delay));
660
+ if (this.closed || this.ws?.readyState === 1) return;
661
+ try {
662
+ await this.open();
663
+ return;
664
+ } catch {
665
+ delay = jitterBackoff(delay, 50, 5e3);
666
+ }
667
+ }
668
+ } finally {
669
+ this.reopening = false;
670
+ }
671
+ }
672
+ /** Leaves the socket NOW and opens another (resend, resubscribe), without waiting for its close handshake: a socket
673
+ * whose close is never answered stays CLOSING for ever, its ops never sent (every user an edge `{move}`d
674
+ * stalled, the edge never answering a close without a code). */
675
+ drop(why) {
676
+ const ws = this.ws;
677
+ if (!ws) return;
678
+ this.ws = null;
679
+ this.stats.drops = (this.stats.drops ?? 0) + 1;
680
+ try {
681
+ ws.close(4e3, why);
682
+ } catch {}
683
+ if (!this.closed) this.reconnect(0);
684
+ }
685
+ /** WATCHDOG, each second: a socket not open for 2 s, or none and no reconnect going while ops are owed, or an open
686
+ * socket SILENT while an op waits (nothing heard `silent` ms after it was sent: a ping goes, and nothing heard for
687
+ * `silent` ms more; or pongs but no answer for 5x `silent`) is left and another opened. Everything unanswered goes
688
+ * again, same tags: exactly once (a unit answers a repeated tag from its tags). */
689
+ watchdog() {
690
+ if (this.closed) return;
691
+ const now = performance.now(), ws = this.ws, silent = this.silent;
692
+ const fire = (why) => {
693
+ this.stats.watchdog = (this.stats.watchdog ?? 0) + 1;
694
+ if (ws) this.drop(why);
695
+ else this.reconnect(0);
696
+ };
697
+ if (this.lanes) this.laneDog(now);
698
+ if (!ws) {
699
+ if (!this.reopening && (this.waiting.size || this.unconfirmed.size || this.outbox.size)) fire("none");
700
+ return;
701
+ }
702
+ if (ws.readyState !== 1) {
703
+ this.stuckSince ||= now;
704
+ if (now - this.stuckSince > 2e3) fire("stuck");
705
+ return;
706
+ }
707
+ this.stuckSince = 0;
708
+ if (!this.dir && !this.helloSeen && now - this.openedAt > HELLO_WAIT_MS) {
709
+ this.helloSeen = true;
710
+ if (this.outbox.size) this.schedule();
711
+ }
712
+ if (now - Math.max(this.heard, this.lastSent) > IDLE_PING_MS) {
713
+ this.ping(ws, now);
714
+ return;
715
+ }
716
+ let oldest = Infinity;
717
+ for (const w of this.waiting.values()) if (w.sentAt < oldest) oldest = w.sentAt;
718
+ if (!(now - oldest > silent)) {
719
+ this.probe = 0;
720
+ return;
721
+ }
722
+ if (this.probe ? now - this.probe > silent : now - Math.max(this.answeredAt, oldest) > 5 * silent) {
723
+ fire("silent");
724
+ return;
725
+ }
726
+ if (!this.probe && now - Math.max(this.heard, oldest) > silent) {
727
+ this.probe = now;
728
+ this.ping(ws, now);
729
+ }
730
+ }
731
+ /** A ping (a message the client sends: counted, and it resets the idle clock). */
732
+ ping(ws, now) {
733
+ this.lastSent = now;
734
+ this.stats.pings += 1;
735
+ try {
736
+ ws.send("ping");
737
+ } catch {}
738
+ }
739
+ /** After (re)opening: everything unanswered that must land goes again, with its tag; every subscription starts
740
+ * again from a fresh snapshot (the new edge knows nothing of the old socket's). */
741
+ resend() {
742
+ this.outbox.clear();
743
+ this.held = [];
744
+ for (const w of this.leaving.values()) this.gone(w);
745
+ this.leaving.clear();
746
+ clearTimeout(this.leaveTimer);
747
+ this.leaveTimer = null;
748
+ const again = /* @__PURE__ */ new Set();
749
+ for (const w of this.waiting.values()) {
750
+ if (w.sent && !w.keep && !READ_OPS.has(w.op.k)) {
751
+ this.waiting.delete(w.op.i);
752
+ w.resolve({
753
+ i: w.op.i,
754
+ ok: false,
755
+ why: "closed"
756
+ });
757
+ continue;
758
+ }
759
+ if (w.sent) this.stats.resent += 1;
760
+ w.sentAt = void 0;
761
+ again.add(w.op.i);
762
+ this.queue(w.unit, w.op);
763
+ }
764
+ for (const [i, u] of this.unconfirmed) if (!again.has(i)) {
765
+ this.stats.resent += 1;
766
+ this.waiting.set(i, {
767
+ unit: u.unit,
768
+ op: u.op,
769
+ resolve: () => {},
770
+ keep: true,
771
+ sent: false
772
+ });
773
+ this.queue(u.unit, u.op);
774
+ }
775
+ for (const w of this.watches.values()) if (!again.has(w.i)) this.subscribe(w);
776
+ }
777
+ /** Sends an op to a unit with the others of this turn; resolves with its answer. */
778
+ /** `relax`: a write to an "e" table resolves when first sent (`{ ok: true, sent: true }`; it still lands once,
779
+ * same tag, and its answer is still read here); "b" or "e": never waits for durable (server shapes), and its round
780
+ * trip tunes the conflation window. */
781
+ send(unit, op, keep = false, now = null, route = void 0, relax = null) {
782
+ const i = op.i ?? this.next++;
783
+ const o = {
784
+ i,
785
+ ...op
786
+ };
787
+ return new Promise((resolve) => {
788
+ let timer = null, me = null;
789
+ const wait = () => {
790
+ timer = setTimeout(() => {
791
+ if (this.waiting.get(i) !== me) return;
792
+ if (!keep) {
793
+ this.waiting.delete(i);
794
+ resolve({
795
+ i,
796
+ ok: false,
797
+ why: "timeout"
798
+ });
799
+ return;
800
+ }
801
+ if (me.sent && !me.queued && !me.later) {
802
+ this.stats.resent += 1;
803
+ this.queue(me.unit, me.op);
804
+ }
805
+ wait();
806
+ }, keep ? 5e3 : 1e4);
807
+ };
808
+ wait();
809
+ me = {
810
+ unit,
811
+ op: o,
812
+ resolve: (a) => {
813
+ clearTimeout(timer);
814
+ if (o.t) this.store?.answered(o, a);
815
+ resolve(a);
816
+ },
817
+ keep,
818
+ now,
819
+ route,
820
+ sent: false,
821
+ at: performance.now(),
822
+ durable: keep && !!o.t && this.durable && !relax,
823
+ relax,
824
+ early: relax === "e" ? resolve : null
825
+ };
826
+ this.waiting.set(i, me);
827
+ if (keep && o.t) this.store?.issued(o, route);
828
+ this.queue(unit, o);
829
+ });
830
+ }
831
+ queue(unit, op) {
832
+ const w = this.waiting.get(op.i);
833
+ if (w && w.op === op) {
834
+ w.queued = true;
835
+ w.gen = (w.gen ?? 0) + 1;
836
+ }
837
+ this.schedule();
838
+ const now = performance.now(), g = this.gather ??= {
839
+ n: 0,
840
+ sum: 0
841
+ };
842
+ g.n += 1;
843
+ g.sum += now;
844
+ if (!this.outbox.has(unit)) this.outbox.set(unit, []);
845
+ this.outbox.get(unit).push(op);
846
+ }
847
+ /** The send window now: `window` when given, else the AIMD's (0 .. budgetMs). */
848
+ sendWindow() {
849
+ return this.window ?? this.aimd.value();
850
+ }
851
+ schedule() {
852
+ if (this.flushing) return;
853
+ this.flushing = true;
854
+ const wait = this.sendWindow() - (performance.now() - this.lastFlush);
855
+ if (wait > 0) this.flushTimer = setTimeout(() => {
856
+ this.flushing = false;
857
+ this.flush();
858
+ }, wait);
859
+ else queueMicrotask(() => {
860
+ this.flushing = false;
861
+ this.flush();
862
+ });
863
+ }
864
+ /** (A3) After a send: the mean wait its ops were held for; the window rises a tenth of the budget while that is
865
+ * at most half the budget and halves above it (an op issued right after an answer, while the window is open, waits
866
+ * most of it). The window never passes the budget, so no op waits longer; a sparse client (ops further apart than the
867
+ * budget) never waits at all. */
868
+ tune(now) {
869
+ const g = this.gather;
870
+ this.gather = null;
871
+ if (this.window === null && g?.n) this.aimd.observe(now - g.sum / g.n);
872
+ }
873
+ /** Tagged writes sent and not yet answered or durable: a unit remembers a client's last TAG_KEEP answers, so
874
+ * at most TAG_KEEP may be in doubt at once (a resend of one older than that could run again). Counted per
875
+ * acting user (`as`): the edge keeps each user's tags apart, so each has its own TAG_KEEP. */
876
+ doubts() {
877
+ const d = /* @__PURE__ */ new Map(), inc = (op) => d.set(op.as ?? "", (d.get(op.as ?? "") ?? 0) + 1);
878
+ for (const u of this.unconfirmed.values()) inc(u.op);
879
+ for (const w of this.waiting.values()) if (w.sent && w.op.t && !this.unconfirmed.has(w.op.i)) inc(w.op);
880
+ return d;
881
+ }
882
+ ambiguous() {
883
+ let n = 0;
884
+ for (const v of this.doubts().values()) n += v;
885
+ return n;
886
+ }
887
+ /** ACK: the lowest number of this client's own tags still in doubt (waiting for its answer, held, or answered
888
+ * and not durable yet). Every number below it is settled and never sent again, so each tagged op carries it (`ta`)
889
+ * and the unit drops that client's answers below it: a unit keeps the answers in flight, not the last TAG_KEEP. */
890
+ low() {
891
+ let low = this.tags + 1;
892
+ const see = (op) => {
893
+ if (!op?.t) return;
894
+ const t = String(op.t), k = t.lastIndexOf(":");
895
+ if (k < 0 || t.slice(0, k) !== this.client) return;
896
+ const n = Number(t.slice(k + 1));
897
+ if (n < low) low = n;
898
+ };
899
+ for (const w of this.waiting.values()) see(w.op);
900
+ for (const u of this.unconfirmed.values()) see(u.op);
901
+ for (const [, op] of this.held) see(op);
902
+ return low;
903
+ }
904
+ flush() {
905
+ if (!this.ws || this.ws.readyState !== 1) return;
906
+ if (!this.dir && !this.helloSeen) return;
907
+ const now = performance.now();
908
+ const doubt = this.doubts(), room = (ns) => (doubt.get(ns) ?? 0) < (this.tagKeep ?? 64), count = (ns) => doubt.set(ns, (doubt.get(ns) ?? 0) + 1);
909
+ const blocked = /* @__PURE__ */ new Set(), still = [];
910
+ const go = /* @__PURE__ */ new Map(), add = (unit, op) => {
911
+ if (!go.has(unit)) go.set(unit, []);
912
+ go.get(unit).push(op);
913
+ };
914
+ const seen = /* @__PURE__ */ new Set(), owed = (op) => !seen.has(op.i) && (op.k === "unsub" || this.waiting.get(op.i)?.op === op) && !!seen.add(op.i);
915
+ for (const [unit, op] of this.held) {
916
+ if (!owed(op)) continue;
917
+ const ns = op.as ?? "";
918
+ if (!blocked.has(ns) && room(ns)) {
919
+ add(unit, op);
920
+ count(ns);
921
+ } else {
922
+ blocked.add(ns);
923
+ still.push([unit, op]);
924
+ }
925
+ }
926
+ this.held = still;
927
+ for (const [unit, all] of this.outbox) for (const op of all) {
928
+ if (!owed(op)) continue;
929
+ const w = this.waiting.get(op.i), ns = op.as ?? "";
930
+ if (op.t && w && !w.sent) {
931
+ if (blocked.has(ns) || !room(ns)) {
932
+ blocked.add(ns);
933
+ this.held.push([unit, op]);
934
+ this.stats.held = (this.stats.held ?? 0) + 1;
935
+ continue;
936
+ }
937
+ count(ns);
938
+ }
939
+ add(unit, op);
940
+ }
941
+ this.outbox.clear();
942
+ for (const w of [...this.leaving.values()]) if (go.has(w.unit)) {
943
+ go.get(w.unit).push(this.unsubOp(w));
944
+ this.stats.unsubsRode += 1;
945
+ }
946
+ if (!this.leaving.size) {
947
+ clearTimeout(this.leaveTimer);
948
+ this.leaveTimer = null;
949
+ }
950
+ if (go.size) {
951
+ this.tune(now);
952
+ this.lastFlush = this.lastSent = now;
953
+ }
954
+ const low = go.size ? this.low() : 0;
955
+ const sends = [];
956
+ for (const [unit, all] of go) {
957
+ if (!this.far) {
958
+ sends.push([
959
+ unit,
960
+ all,
961
+ false
962
+ ]);
963
+ continue;
964
+ }
965
+ const atEdge = (op) => op.k === "g" || op.k === "sub" || op.k === "unsub", here = all.filter(atEdge), there = all.filter((op) => !atEdge(op));
966
+ if (here.length) sends.push([
967
+ unit,
968
+ here,
969
+ true
970
+ ]);
971
+ if (there.length) sends.push([
972
+ unit,
973
+ there,
974
+ false
975
+ ]);
976
+ }
977
+ for (const [unit, all, edgeOnly] of sends) for (let s = 0; s < all.length; s += 1e3) {
978
+ const ops = all.slice(s, s + 1e3);
979
+ this.stats.messages += 1;
980
+ this.stats.ops += ops.length;
981
+ for (const op of ops) {
982
+ if (op.t && originOf(op.t) === this.client) op.ta = low;
983
+ const w = this.waiting.get(op.i);
984
+ if (!w) continue;
985
+ if (!w.sent && op.t) this.store?.sent(op);
986
+ w.sent = true;
987
+ w.queued = false;
988
+ w.sentAt ??= now;
989
+ if (w.early) {
990
+ w.early({
991
+ i: op.i,
992
+ ok: true,
993
+ sent: true
994
+ });
995
+ w.early = null;
996
+ }
997
+ }
998
+ this.sentMsgs.delete(ops[0].i);
999
+ this.sentMsgs.set(ops[0].i, {
1000
+ unit,
1001
+ ops
1002
+ });
1003
+ if (this.sentMsgs.size > 4096) this.sentMsgs.delete(this.sentMsgs.keys().next().value);
1004
+ const text = `${unit}|${JSON.stringify(ops)}`;
1005
+ this.stats.bytesOut = (this.stats.bytesOut ?? 0) + text.length;
1006
+ const lane = edgeOnly ? null : this.laneFor(unit);
1007
+ if (lane) {
1008
+ this.stats.laneMsgs = (this.stats.laneMsgs ?? 0) + 1;
1009
+ for (const op of ops) {
1010
+ const w = this.waiting.get(op.i);
1011
+ if (w) w.lane = lane;
1012
+ if (op.k === "sub") {
1013
+ const sub = this.byI.get(op.i);
1014
+ if (sub) sub.lane = lane;
1015
+ }
1016
+ }
1017
+ } else for (const op of ops) {
1018
+ const w = this.waiting.get(op.i);
1019
+ if (w) w.lane = null;
1020
+ }
1021
+ (lane?.ws ?? this.ws).send(text);
1022
+ }
1023
+ }
1024
+ received(data, lane = null) {
1025
+ if (data === "pong") return;
1026
+ const body = JSON.parse(data);
1027
+ if (body.hello) {
1028
+ this.setDir(body.hello.dir);
1029
+ this.esc = body.hello.esc ?? this.esc;
1030
+ this.helloed();
1031
+ if (body.hello.far) {
1032
+ this.far = true;
1033
+ this.lanes ??= /* @__PURE__ */ new Map();
1034
+ }
1035
+ this.dur = body.hello.dur ?? {};
1036
+ this.helloSeen = true;
1037
+ if (body.hello.tk > 64) this.tagKeep = body.hello.tk;
1038
+ if (this.outbox.size) this.schedule();
1039
+ return;
1040
+ }
1041
+ if (body.dir && !Array.isArray(body)) {
1042
+ this.setDir(body.dir);
1043
+ return;
1044
+ }
1045
+ if (body.move) {
1046
+ if (lane) {
1047
+ this.laneLost(lane, "move");
1048
+ return;
1049
+ }
1050
+ this.stats.edgeMoves = (this.stats.edgeMoves ?? 0) + 1;
1051
+ this.drop("move");
1052
+ return;
1053
+ }
1054
+ if (!Array.isArray(body) && body.a) {
1055
+ this.received(JSON.stringify(body.a), lane);
1056
+ delete body.a;
1057
+ }
1058
+ if (!Array.isArray(body)) {
1059
+ if (body.busy !== void 0) {
1060
+ this.refusedMsg(body);
1061
+ return;
1062
+ }
1063
+ if (body.resub !== void 0) {
1064
+ for (const w of this.watches.values()) if (w.unit === body.resub) this.subscribe(w);
1065
+ for (const w of [...this.leaving.values()]) if (w.unit === body.resub) {
1066
+ this.leaving.delete(w.key);
1067
+ this.gone(w);
1068
+ }
1069
+ }
1070
+ if (body.edgeReset) {
1071
+ this.stats.edgeResets = (this.stats.edgeResets ?? 0) + 1;
1072
+ if (lane) this.laneLost(lane, "reset");
1073
+ else this.resend();
1074
+ }
1075
+ if (body.unitLost !== void 0) {
1076
+ for (const [i, u] of this.unconfirmed) if (u.unit === body.unitLost && !this.waiting.has(i)) {
1077
+ this.stats.resent += 1;
1078
+ this.waiting.set(i, {
1079
+ unit: u.unit,
1080
+ op: u.op,
1081
+ resolve: () => {},
1082
+ keep: true,
1083
+ sent: false,
1084
+ at: u.at
1085
+ });
1086
+ this.queue(u.unit, u.op);
1087
+ }
1088
+ }
1089
+ if (body.push) for (const item of body.push) this.pushed(item);
1090
+ if (body.dur) for (const i of body.dur) {
1091
+ const u = this.unconfirmed.get(i);
1092
+ if (u) {
1093
+ this.unconfirmed.delete(i);
1094
+ this.store?.durable(u.op);
1095
+ this.durTimes.push(performance.now() - u.at);
1096
+ if (this.durTimes.length > 1e5) this.durTimes.shift();
1097
+ u.done?.();
1098
+ }
1099
+ }
1100
+ return;
1101
+ }
1102
+ const link = [], busy = [], moved = [];
1103
+ let linkMs = 0, busyMs = 0, movedMs = 0;
1104
+ for (const a of body) {
1105
+ const w = this.waiting.get(a.i);
1106
+ if (!w) continue;
1107
+ if (/^link/u.test(a.why ?? "") && (w.keep || READ_OPS.has(w.op.k)) && (w.retries = (w.retries ?? 0) + 1) <= (w.keep ? 40 : 10)) {
1108
+ this.stats.resent += 1;
1109
+ link.push(w);
1110
+ linkMs = Math.max(linkMs, Math.min(2e3, 50 * w.retries));
1111
+ continue;
1112
+ }
1113
+ if ((a.err ?? a.why) === "busy" && (w.busy = (w.busy ?? 0) + 1) <= 50) {
1114
+ this.stats.busy = (this.stats.busy ?? 0) + 1;
1115
+ busy.push(w);
1116
+ busyMs = Math.max(busyMs, a.retry ?? 100);
1117
+ continue;
1118
+ }
1119
+ if (a.why === "moved" && w.route !== void 0 && (w.moves = (w.moves ?? 0) + 1) <= 50) {
1120
+ this.stats.moved += 1;
1121
+ moved.push(w);
1122
+ movedMs = Math.max(movedMs, w.moves > 2 ? 10 * w.moves : 0);
1123
+ continue;
1124
+ }
1125
+ if (w.relax && w.sentAt !== void 0) this.conflateAimd.observe(performance.now() - w.sentAt);
1126
+ this.waiting.delete(a.i);
1127
+ for (const r of a.wrote ?? []) this.wrote(`${r.t}/${r.id}`, r.v);
1128
+ const prev = this.unconfirmed.get(a.i), last = w.durable && a.pend ? () => w.resolve({
1129
+ ...a,
1130
+ pend: void 0,
1131
+ durable: true
1132
+ }) : null;
1133
+ if (a.pend) this.unconfirmed.set(a.i, {
1134
+ unit: w.unit,
1135
+ op: w.op,
1136
+ at: w.at ?? prev?.at ?? performance.now(),
1137
+ done: prev?.done ?? last,
1138
+ lane: w.lane ?? null
1139
+ });
1140
+ else if (prev) {
1141
+ this.unconfirmed.delete(a.i);
1142
+ prev.done?.();
1143
+ }
1144
+ w.now?.(a);
1145
+ if (!last) w.resolve(a);
1146
+ }
1147
+ if (link.length) this.retry(link, linkMs);
1148
+ if (busy.length) this.retry(busy, busyMs + Math.random() * busyMs / 2);
1149
+ if (moved.length) this.retry(moved, this.refreshDir().then(() => movedMs), (w) => {
1150
+ w.unit = this.unitOf(w.route);
1151
+ });
1152
+ }
1153
+ /** Ops answered "again later" go again together after `wait` ms (a number or a Promise of one), each only if
1154
+ * it still waits and nothing has queued it since (a reconnect's resend, the keep timer): never queued twice.
1155
+ * `then(w)` runs just before (a moved op takes its new unit). */
1156
+ retry(ws, wait, then) {
1157
+ const gens = ws.map((w) => {
1158
+ w.later = (w.later ?? 0) + 1;
1159
+ return w.gen;
1160
+ });
1161
+ Promise.resolve(wait).then((ms) => setTimeout(() => ws.forEach((w, k) => {
1162
+ w.later -= 1;
1163
+ if (this.waiting.get(w.op.i) !== w || w.gen !== gens[k] || w.queued) return;
1164
+ then?.(w);
1165
+ this.queue(w.unit, w.op);
1166
+ }), ms));
1167
+ }
1168
+ /** A whole message refused `{busy: ms, m}`: its ops still unanswered go again after `ms` and up to half that
1169
+ * again (one timer for the message), same tags; an op refused 50 times is answered busy. */
1170
+ refusedMsg({ busy: ms, m }) {
1171
+ const sent = this.sentMsgs.get(m);
1172
+ if (!sent) return;
1173
+ this.sentMsgs.delete(m);
1174
+ this.stats.busyMsgs = (this.stats.busyMsgs ?? 0) + 1;
1175
+ const again = [], unsubs = [];
1176
+ for (const op of sent.ops) {
1177
+ if (op.k === "unsub") {
1178
+ unsubs.push(op);
1179
+ continue;
1180
+ }
1181
+ const w = this.waiting.get(op.i);
1182
+ if (!w || w.op !== op) continue;
1183
+ this.stats.busy = (this.stats.busy ?? 0) + 1;
1184
+ if ((w.busy = (w.busy ?? 0) + 1) > 50) {
1185
+ const a = {
1186
+ i: op.i,
1187
+ ok: false,
1188
+ err: "busy",
1189
+ why: "busy",
1190
+ retry: ms
1191
+ };
1192
+ this.waiting.delete(op.i);
1193
+ w.now?.(a);
1194
+ w.resolve(a);
1195
+ continue;
1196
+ }
1197
+ again.push(w);
1198
+ }
1199
+ const wait = ms + Math.random() * ms / 2;
1200
+ if (again.length) this.retry(again, wait);
1201
+ if (unsubs.length) {
1202
+ const ws = this.ws;
1203
+ setTimeout(() => {
1204
+ if (this.ws !== ws) return;
1205
+ for (const op of unsubs) this.queueRaw(sent.unit, op);
1206
+ this.schedule();
1207
+ }, wait);
1208
+ }
1209
+ }
1210
+ /** An op into the outbox with no answer waited for (an unsub). */
1211
+ queueRaw(unit, op) {
1212
+ if (!this.outbox.has(unit)) this.outbox.set(unit, []);
1213
+ this.outbox.get(unit).push(op);
1214
+ }
1215
+ wrote(key, v) {
1216
+ if (v > (this.mine.get(key) ?? -1)) this.mine.set(key, v);
1217
+ }
1218
+ tag() {
1219
+ return `${this.client}:${this.tags += 1}`;
1220
+ }
1221
+ /** A row's value (null when unborn): the optimistic value, else a fresh subscribed copy, else one read shared by
1222
+ * every caller asking at once. `as`: a server reads as that user (that user's rules). */
1223
+ async get(tb, id, { as } = {}) {
1224
+ const key = `${tb}/${id}`, rk = rowKey(tb, id, as ?? void 0);
1225
+ if (as == null && this.overlay.has(key)) {
1226
+ this.stats.localReads += 1;
1227
+ return this.overlay.get(key);
1228
+ }
1229
+ let mineNow;
1230
+ for (const u of this.unconfirmed.values()) if (u.op.k === "s" && u.op.as === (as ?? void 0) && `${u.op.tb}/${u.op.id}` === key) mineNow = u;
1231
+ if (mineNow) {
1232
+ this.stats.localReads += 1;
1233
+ return mineNow.op.value;
1234
+ }
1235
+ const pending = this.conflating.get(`${as ?? ""}|${key}`);
1236
+ if (pending) {
1237
+ this.stats.localReads += 1;
1238
+ return pending.value;
1239
+ }
1240
+ const held = this.rows.get(rk);
1241
+ if (held && held.v >= (this.mine.get(key) ?? 0)) {
1242
+ this.stats.localReads += 1;
1243
+ if (this.auto.has(rk)) this.readAuto(tb, id, as ?? void 0, rk);
1244
+ return held.value;
1245
+ }
1246
+ this.readAuto(tb, id, as ?? void 0, rk);
1247
+ if (this.reads.has(rk)) {
1248
+ this.stats.sharedReads += 1;
1249
+ return this.reads.get(rk);
1250
+ }
1251
+ const ask = (fresh) => this.send(this.unitOf(id), {
1252
+ k: "g",
1253
+ tb,
1254
+ id,
1255
+ ...asOf(as),
1256
+ ...fresh ? { fresh: 1 } : {}
1257
+ }, true, null, id);
1258
+ const read = ask(false).then((a) => a.edge && (a.v ?? 0) < (this.mine.get(key) ?? 0) ? ask(true) : a).then((a) => {
1259
+ this.reads.delete(rk);
1260
+ if (!a.ok && a.why !== "unborn") throw coded(a);
1261
+ return a.ok ? a.value : null;
1262
+ });
1263
+ this.reads.set(rk, read);
1264
+ return read;
1265
+ }
1266
+ /** Sets a row (expecting version `expect` when given: refused `changed` otherwise). A set of an "e" or "b"
1267
+ * table (the hello's `dur`) without `expect` is conflated (see the top of this file); an "s" table's never. */
1268
+ async set(tb, id, value, { expect, tag, as } = {}) {
1269
+ if (expect === void 0 && this.durOf(tb) !== "s") return this.conflate(tb, id, value, {
1270
+ tag,
1271
+ as
1272
+ });
1273
+ return this.setNow(tb, id, value, {
1274
+ expect,
1275
+ tag,
1276
+ as
1277
+ });
1278
+ }
1279
+ async setNow(tb, id, value, { expect, tag, as } = {}) {
1280
+ const d = this.durOf(tb), relax = d === "s" ? null : expect === void 0 ? d : "b";
1281
+ const a = await this.send(this.unitOf(id), {
1282
+ k: "s",
1283
+ tb,
1284
+ id,
1285
+ e: expect,
1286
+ value,
1287
+ t: tag ?? this.tag(),
1288
+ ...asOf(as),
1289
+ ...relax ? {} : this.urgent()
1290
+ }, true, null, id, relax);
1291
+ if (refused(a)) throw coded(a);
1292
+ if (a.ok) this.wrote(`${tb}/${id}`, a.v);
1293
+ return a;
1294
+ }
1295
+ /** Server shapes ask for an urgent frame and an immediate receipt (`w: 1`: their writes resolve when durable). */
1296
+ urgent() {
1297
+ return this.durable ? { w: 1 } : {};
1298
+ }
1299
+ /** A table's durability from the hello: "e" | "b" | "s" (absent, or before the hello: "s"). */
1300
+ durOf(tb) {
1301
+ const d = this.dur?.[tb];
1302
+ return d === "e" || d === "b" ? d : "s";
1303
+ }
1304
+ /** The conflation window now, ms (AIMD on the round trips of "e"/"b" writes, 50..100). */
1305
+ conflateWindow() {
1306
+ return this.conflateAimd.value();
1307
+ }
1308
+ /** (A10) The first conflated set opens ONE window for every row (`conflateWindow()` ms); a later set of
1309
+ * a row waiting in it only replaces the value (and the tag, when it brings one); at its end every row goes with its
1310
+ * last value, together (one message per unit). Every caller gets its row's answer. A set tagged by another tab (or
1311
+ * untagged after a tab's, or the reverse) is never merged: the waiting one goes now, the new one waits (else the
1312
+ * older value, resent by its tab with its own tag after a handover, could land last). */
1313
+ conflate(tb, id, value, { tag, as }) {
1314
+ const key = `${as ?? ""}|${tb}/${id}`, from = tag === void 0 || tag === null ? null : originOf(tag);
1315
+ const c = this.conflating.get(key);
1316
+ if (c && c.from === from) {
1317
+ c.value = value;
1318
+ c.tag = tag ?? c.tag;
1319
+ this.stats.conflated += 1;
1320
+ return c.answer;
1321
+ }
1322
+ if (c) {
1323
+ this.conflating.delete(key);
1324
+ c.go();
1325
+ }
1326
+ const n = {
1327
+ value,
1328
+ tag,
1329
+ from
1330
+ };
1331
+ n.answer = new Promise((r) => {
1332
+ n.go = () => r(this.setNow(tb, id, n.value, {
1333
+ tag: n.tag,
1334
+ as
1335
+ }));
1336
+ });
1337
+ this.conflating.set(key, n);
1338
+ if (!this.conflateTimer) {
1339
+ this.conflateTimer = setTimeout(() => this.conflateFlush(), this.conflateWindow());
1340
+ this.conflateTimer.unref?.();
1341
+ }
1342
+ return n.answer;
1343
+ }
1344
+ /** The conflation window's end: every row waiting goes now, in one turn. */
1345
+ conflateFlush() {
1346
+ this.conflateTimer = null;
1347
+ const all = [...this.conflating.values()];
1348
+ this.conflating.clear();
1349
+ for (const n of all) n.go();
1350
+ }
1351
+ /** Takes `n` from `row[f]` if at least `min` is left (escrow: hot stock). A table the hello lists in `esc` keeps the
1352
+ * amount in slices `id~0..slices-1`: this client always takes from its own slice (the unit borrows from the others).
1353
+ * Resolves `{ v, left }`; throws code `short` (`have` on the answer) when there is not enough. */
1354
+ async take(tb, id, f, n, { min = 0, tag, as } = {}) {
1355
+ await Promise.race([this.hello, new Promise((r) => setTimeout(r, 2e3))]);
1356
+ const e = this.esc?.[tb], row = e ? `${id}~${fnv(this.client) % e.slices}` : id;
1357
+ const a = await this.send(this.unitOf(row), {
1358
+ k: "take",
1359
+ tb,
1360
+ id: row,
1361
+ f,
1362
+ n,
1363
+ min,
1364
+ t: tag ?? this.tag(),
1365
+ ...asOf(as),
1366
+ ...this.urgent()
1367
+ }, true, null, row);
1368
+ if (a.err || a.ok === false) throw coded(a);
1369
+ for (const r of a.wrote ?? []) this.wrote(`${r.t}/${r.id}`, r.v);
1370
+ return a;
1371
+ }
1372
+ /** Runs a server mutation in the unit of `route` (a row id it writes). `optimistic`: { "tb/id": value } shown at
1373
+ * once to `get` and to row watchers until the answer. */
1374
+ async mutation(name, args, { route = "", optimistic, tag, as } = {}) {
1375
+ name = fnName(name);
1376
+ const keys = Object.keys(optimistic ?? {});
1377
+ for (const key of keys) {
1378
+ this.overlay.set(key, optimistic[key]);
1379
+ this.notifyRow(key);
1380
+ }
1381
+ const guess = optimistic || as != null ? null : this.predictor?.begin(name, args, route);
1382
+ const a = await this.send(this.unitOf(route), {
1383
+ k: "m",
1384
+ f: name,
1385
+ a: args,
1386
+ t: tag ?? this.tag(),
1387
+ ...asOf(as),
1388
+ ...this.urgent()
1389
+ }, true, null, route);
1390
+ guess?.end(a);
1391
+ for (const key of keys) {
1392
+ this.overlay.delete(key);
1393
+ this.notifyRow(key);
1394
+ }
1395
+ if (refused(a)) throw coded(a);
1396
+ return a;
1397
+ }
1398
+ /** An ACTION of a the database app (it may do I/O; not a transaction): called as a mutation is, by reference or
1399
+ * name, tagged (a resend after a reconnect is answered from its tag). Resolves its answer. */
1400
+ async action(name, args, options = {}) {
1401
+ return this.mutation(name, args, options);
1402
+ }
1403
+ /** A query function's value, once, on unit `unit` (default "0"). */
1404
+ async query(name, args, { unit = "0", as } = {}) {
1405
+ name = fnName(name);
1406
+ const a = await this.send(String(unit), {
1407
+ k: "q",
1408
+ f: name,
1409
+ a: args,
1410
+ ...asOf(as)
1411
+ });
1412
+ if (!a.ok) throw coded(a);
1413
+ return a.value;
1414
+ }
1415
+ /** A range over an index, once: every unit's part (or `unit`'s), rows concatenated in unit order. */
1416
+ async range(tb, index, o = {}, { unit, as } = {}) {
1417
+ if (unit === void 0 && !this.dir) await Promise.race([this.hello, new Promise((r) => setTimeout(r, HELLO_WAIT_MS))]);
1418
+ const units = unit !== void 0 ? [String(unit)] : this.unitNames();
1419
+ const parts = await Promise.all(units.map((u) => this.send(u, {
1420
+ k: "r",
1421
+ tb,
1422
+ index,
1423
+ ...o,
1424
+ ...asOf(as)
1425
+ })));
1426
+ const bad = parts.find((a) => !a.ok);
1427
+ if (bad) throw coded(bad);
1428
+ return parts.flatMap((a) => a.rows ?? []);
1429
+ }
1430
+ /** A read of a row not held here (or held because it was read often): counted; the `subscribeAfter`th within
1431
+ * `subscribeWindowMs` subscribes it, if the cache admits it. */
1432
+ readAuto(tb, id, as, rk) {
1433
+ if (!this.subscribeAfter) return;
1434
+ const now = performance.now();
1435
+ this.lfu.touch(rk);
1436
+ const a = this.auto.get(rk);
1437
+ if (a) {
1438
+ this.keep.gap(now - a.last);
1439
+ a.last = now;
1440
+ a.reads.add(1, now);
1441
+ this.auto.delete(rk);
1442
+ this.auto.set(rk, a);
1443
+ this.sweepAuto(now);
1444
+ return;
1445
+ }
1446
+ this.sweepAuto(now);
1447
+ if (this.never.has(rk) || this.watches.has(`${this.unitOf(id)}|${JSON.stringify({
1448
+ k: "g",
1449
+ tb,
1450
+ id
1451
+ })}${as === void 0 ? "" : `|${as}`}`)) return;
1452
+ let h = this.heat.get(rk);
1453
+ if (!h || now - h.at > this.subscribeWindowMs) {
1454
+ this.heat.delete(rk);
1455
+ h = {
1456
+ at: now,
1457
+ n: 0
1458
+ };
1459
+ this.heat.set(rk, h);
1460
+ if (this.heat.size > 4 * this.lfu.size + 1e3) this.heat.delete(this.heat.keys().next().value);
1461
+ }
1462
+ if ((h.n += 1) < this.subscribeAfter) return;
1463
+ this.heat.delete(rk);
1464
+ for (const k of this.lfu.add(rk)) if (k !== rk) this.dropAuto(k);
1465
+ if (!this.lfu.has(rk)) return;
1466
+ const entry = {
1467
+ tb,
1468
+ id,
1469
+ as,
1470
+ last: now,
1471
+ reads: new Rate(1e4).add(h.n, now),
1472
+ pushes: new Rate(1e4),
1473
+ stop: null
1474
+ };
1475
+ this.auto.set(rk, entry);
1476
+ this.stats.autoSubs += 1;
1477
+ entry.stop = this.watchRow(tb, id, (view, w, item) => {
1478
+ if (view === void 0 && w?.error) {
1479
+ if (/denied/u.test(w.error.err ?? w.error.why ?? "")) {
1480
+ if (this.never.size > 1e4) this.never.clear();
1481
+ this.never.add(rk);
1482
+ }
1483
+ this.dropAuto(rk);
1484
+ return;
1485
+ }
1486
+ if (item) {
1487
+ entry.pushes.add(1);
1488
+ this.sweepAuto(performance.now(), rk);
1489
+ }
1490
+ }, as === void 0 ? void 0 : { as });
1491
+ }
1492
+ /** Whether a subscribed row has gone quiet: idle past the p99 of the read gaps seen (when they form a pattern) or
1493
+ * past the break-even of holding it against reading it again. Pushes are not billed messages: holding costs
1494
+ * only their server CPU (`pushCpuMs`, at least one push a minute); a read costs a message the client sends plus its
1495
+ * relay CPU (`relayCpuMs`), at its read rate. */
1496
+ quiet(a, now) {
1497
+ const be = breakEvenMs({
1498
+ holdUsdPerSec: Math.max(a.pushes.perSec(now), 1 / 60) * PUSH_USD,
1499
+ eachUsd: READ_USD,
1500
+ ratePerSec: a.reads.perSec(now)
1501
+ });
1502
+ const keepMs = this.keep.shape().patterned ? Math.min(this.keep.keepMs(), be) : be;
1503
+ return now - a.last > keepMs;
1504
+ }
1505
+ /** Drops quiet subscriptions: `rk` (a row just pushed), and the least recently read few. No timer: run as reads and
1506
+ * pushes come. */
1507
+ sweepAuto(now, rk) {
1508
+ if (rk !== void 0) {
1509
+ const a = this.auto.get(rk);
1510
+ if (a && this.quiet(a, now)) this.dropAuto(rk);
1511
+ }
1512
+ let n = 0;
1513
+ for (const [k, a] of this.auto) {
1514
+ if (n++ >= 4 || !this.quiet(a, now)) break;
1515
+ this.dropAuto(k);
1516
+ }
1517
+ }
1518
+ dropAuto(rk) {
1519
+ const a = this.auto.get(rk);
1520
+ this.lfu.delete(rk);
1521
+ if (!a) return;
1522
+ this.auto.delete(rk);
1523
+ this.stats.autoDrops += 1;
1524
+ a.stop?.();
1525
+ }
1526
+ /** Stores a file (R2) and its `_files` row (owner: this user); resolves its id. */
1527
+ async upload(blob, name) {
1528
+ const put = () => fetchAt(`${this.base}/files?${this.auth()}&name=${encodeURIComponent(name)}`, {
1529
+ method: "PUT",
1530
+ body: blob
1531
+ });
1532
+ let r = await put();
1533
+ if (r.status === 401 && this.refresh) {
1534
+ await this.renew();
1535
+ r = await put();
1536
+ }
1537
+ if (!r.ok) throw coded({ err: r.status === 401 || r.status === 403 ? "denied" : `http ${r.status}` });
1538
+ return (await r.json()).id;
1539
+ }
1540
+ /** A URL that streams a stored file (for <img src>, links); it carries the current token. */
1541
+ fileUrl(id) {
1542
+ return `${this.base}/files/${encodeURIComponent(id)}?${this.auth()}`;
1543
+ }
1544
+ /** Watches a query on one unit; identical watches share one subscription. Returns the unsubscribe function.
1545
+ * `as`: a server watches as that user (its own subscription: that user's rules). */
1546
+ watch(q, unit, listener, { as } = {}) {
1547
+ as = as ?? void 0;
1548
+ const key = `${unit}|${JSON.stringify(q)}${as === void 0 ? "" : `|${as}`}`;
1549
+ let w = this.watches.get(key);
1550
+ const back = !w && this.leaving.get(key);
1551
+ if (back) {
1552
+ w = back;
1553
+ this.leaving.delete(key);
1554
+ this.watches.set(key, w);
1555
+ this.stats.rewatched += 1;
1556
+ }
1557
+ if (w) {
1558
+ if (!back) this.stats.sharedWatches += 1;
1559
+ w.listeners.add(listener);
1560
+ if (w.state) listener(this.view(w), w);
1561
+ } else {
1562
+ w = {
1563
+ key,
1564
+ q,
1565
+ unit,
1566
+ as,
1567
+ state: null,
1568
+ seq: -1,
1569
+ listeners: /* @__PURE__ */ new Set([listener])
1570
+ };
1571
+ this.watches.set(key, w);
1572
+ this.subscribe(w);
1573
+ }
1574
+ if (!w.state) {
1575
+ const old = this.store?.cached(w);
1576
+ if (old !== void 0) listener(old, { stale: true });
1577
+ }
1578
+ return () => {
1579
+ w.listeners.delete(listener);
1580
+ if (w.listeners.size || this.watches.get(key) !== w) return;
1581
+ this.watches.delete(key);
1582
+ this.leave(w);
1583
+ };
1584
+ }
1585
+ /** The last listener left: the subscription stays (pushes keep its copy current, reads use it) until its unsub
1586
+ * rides a message to its unit (flush), or LEAVE_MS passes and it goes alone. A refused one has nothing to unsub; one
1587
+ * whose sub was never sent is simply not sent. */
1588
+ leave(w) {
1589
+ if (!w.state && w.error) {
1590
+ this.gone(w);
1591
+ return;
1592
+ }
1593
+ const sub = this.waiting.get(w.i);
1594
+ if (sub && !sub.sent) {
1595
+ this.waiting.delete(w.i);
1596
+ sub.resolve({
1597
+ i: w.i,
1598
+ ok: false,
1599
+ why: "left"
1600
+ });
1601
+ this.gone(w);
1602
+ return;
1603
+ }
1604
+ this.leaving.set(w.key, w);
1605
+ if (!this.leaveTimer) {
1606
+ this.leaveTimer = setTimeout(() => this.leaveAll(), LEAVE_MS);
1607
+ this.leaveTimer.unref?.();
1608
+ }
1609
+ }
1610
+ /** The unsubs still waiting go now (one message per unit). */
1611
+ leaveAll() {
1612
+ this.leaveTimer = null;
1613
+ for (const w of [...this.leaving.values()]) this.queueRaw(w.unit, this.unsubOp(w));
1614
+ this.schedule();
1615
+ }
1616
+ /** A left watch's unsub op; it is gone here from now on. */
1617
+ unsubOp(w) {
1618
+ this.leaving.delete(w.key);
1619
+ this.gone(w);
1620
+ this.stats.unsubs += 1;
1621
+ return {
1622
+ i: this.next++,
1623
+ k: "unsub",
1624
+ s: w.i
1625
+ };
1626
+ }
1627
+ /** A watch no longer subscribed: its pushes are not heard, its row copy not used. */
1628
+ gone(w) {
1629
+ if (this.byI.get(w.i) === w) this.byI.delete(w.i);
1630
+ if (w.q.k === "g") this.rows.delete(rowKey(w.q.tb ?? "rows", w.q.id, w.as));
1631
+ }
1632
+ /** (Re)subscribes a watch under a new op i: its next answer resets its state. */
1633
+ subscribe(w) {
1634
+ if (w.i !== void 0) this.byI.delete(w.i);
1635
+ w.i = this.next++;
1636
+ w.state = null;
1637
+ this.byI.set(w.i, w);
1638
+ if (w.q.k === "g") w.unit = this.unitOf(w.q.id);
1639
+ w.ready = this.send(w.unit, {
1640
+ k: "sub",
1641
+ q: w.q,
1642
+ i: w.i,
1643
+ ...asOf(w.as)
1644
+ }, false, (a) => this.started(w, a), w.q.k === "g" ? w.q.id : void 0);
1645
+ }
1646
+ started(w, a) {
1647
+ if (!a.ok) {
1648
+ w.error = a;
1649
+ for (const l of w.listeners) l(void 0, w);
1650
+ return a;
1651
+ }
1652
+ w.seq = a.seq ?? 0;
1653
+ w.state = w.q.k === "r" ? {
1654
+ ids: a.rows.map((r) => r._id),
1655
+ rows: new Map(a.rows.map((r) => [r._id, r]))
1656
+ } : w.q.k === "f" ? { value: a.value } : a.gone ? {
1657
+ v: 0,
1658
+ gone: true
1659
+ } : {
1660
+ v: a.v,
1661
+ value: a.value
1662
+ };
1663
+ this.settled(w);
1664
+ return a;
1665
+ }
1666
+ pushed(item) {
1667
+ const w = this.byI.get(item.s);
1668
+ if (!w || !w.state) return;
1669
+ if (item.lost) {
1670
+ if (this.leaving.get(w.key) === w) {
1671
+ this.leaving.delete(w.key);
1672
+ this.gone(w);
1673
+ } else this.subscribe(w);
1674
+ return;
1675
+ }
1676
+ this.stats.pushes += 1;
1677
+ if (!(item.seq > w.seq)) this.stats.outOfOrder += 1;
1678
+ w.seq = item.seq;
1679
+ if (w.q.k === "r") {
1680
+ const s = w.state, ids = item.ids ?? s.ids, keep = new Set(ids);
1681
+ for (const r of item.rows ?? []) s.rows.set(r._id, r);
1682
+ s.ids = ids;
1683
+ for (const id of [...s.rows.keys()]) if (!keep.has(id)) s.rows.delete(id);
1684
+ } else if (w.q.k === "f") w.state = { value: item.value };
1685
+ else w.state = item.gone ? {
1686
+ v: 0,
1687
+ gone: true
1688
+ } : {
1689
+ v: item.v,
1690
+ value: item.value
1691
+ };
1692
+ this.settled(w, item);
1693
+ }
1694
+ settled(w, item) {
1695
+ if (w.q.k === "g") {
1696
+ const key = rowKey(w.q.tb ?? "rows", w.q.id, w.as);
1697
+ this.rows.set(key, w.state.gone ? {
1698
+ v: 0,
1699
+ value: null
1700
+ } : {
1701
+ v: w.state.v,
1702
+ value: w.state.value
1703
+ });
1704
+ }
1705
+ for (const l of w.listeners) l(this.view(w), w, item);
1706
+ this.store?.saw(w);
1707
+ this.predictor?.saw(w);
1708
+ }
1709
+ view(w) {
1710
+ if (w.q.k === "r") return w.state.ids.map((id) => w.state.rows.get(id));
1711
+ if (w.q.k === "f") return w.state.value;
1712
+ const key = `${w.q.tb ?? "rows"}/${w.q.id}`;
1713
+ return w.as === void 0 && this.overlay.has(key) ? {
1714
+ v: w.state.v,
1715
+ value: this.overlay.get(key),
1716
+ optimistic: true
1717
+ } : w.state;
1718
+ }
1719
+ notifyRow(key) {
1720
+ const cut = key.indexOf("/");
1721
+ const w = this.watches.get(`${this.unitOf(key.slice(cut + 1))}|${JSON.stringify({
1722
+ k: "g",
1723
+ tb: key.slice(0, cut),
1724
+ id: key.slice(cut + 1)
1725
+ })}`);
1726
+ if (w?.state) for (const l of w.listeners) l(this.view(w), w);
1727
+ }
1728
+ watchRow(tb, id, listener, { as } = {}) {
1729
+ return this.watch({
1730
+ k: "g",
1731
+ tb,
1732
+ id
1733
+ }, this.unitOf(id), listener, { as });
1734
+ }
1735
+ /** A query on every unit of the map, kept in step with it (a split adds a unit): `onParts(Map name -> value)` once
1736
+ * every unit has answered. Returns the unsubscribe function. */
1737
+ everyUnit(q, onParts, { as } = {}) {
1738
+ const parts = /* @__PURE__ */ new Map(), stops = /* @__PURE__ */ new Map(), stale = /* @__PURE__ */ new Set();
1739
+ const emit = () => {
1740
+ const names = this.unitNames();
1741
+ if (names.every((n) => parts.get(n) !== void 0)) onParts(new Map(names.map((n) => [n, parts.get(n)])), { stale: names.some((n) => stale.has(n)) });
1742
+ };
1743
+ const sync = () => {
1744
+ const names = new Set(this.unitNames());
1745
+ for (const name of names) if (!stops.has(name)) stops.set(name, this.watch(q, name, (v, w) => {
1746
+ parts.set(name, v);
1747
+ if (w?.stale) stale.add(name);
1748
+ else stale.delete(name);
1749
+ emit();
1750
+ }, { as }));
1751
+ for (const [name, stop] of [...stops]) if (!names.has(name)) {
1752
+ stop();
1753
+ stops.delete(name);
1754
+ parts.delete(name);
1755
+ }
1756
+ emit();
1757
+ };
1758
+ this.dirListeners.add(sync);
1759
+ sync();
1760
+ return () => {
1761
+ this.dirListeners.delete(sync);
1762
+ for (const stop of stops.values()) stop();
1763
+ };
1764
+ }
1765
+ /** A range over an index on every unit, merged: by `by` (the index's field) then id. */
1766
+ watchRange(tb, index, o, listener, { by, as } = {}) {
1767
+ return this.everyUnit({
1768
+ k: "r",
1769
+ tb,
1770
+ index,
1771
+ ...o
1772
+ }, (parts, meta) => {
1773
+ const rows = /* @__PURE__ */ new Map();
1774
+ for (const part of parts.values()) for (const r of part) rows.set(r._id, r);
1775
+ listener([...rows.values()].sort((x, y) => by && x[by] !== y[by] ? x[by] < y[by] ? -1 : 1 : x._id < y._id ? -1 : x._id > y._id ? 1 : 0), meta);
1776
+ }, { as });
1777
+ }
1778
+ /** A query function on one unit (by name). */
1779
+ watchQuery(name, args, { unit = "0", as } = {}, listener) {
1780
+ return this.watch({
1781
+ k: "f",
1782
+ f: fnName(name),
1783
+ a: args
1784
+ }, String(unit), listener, { as });
1785
+ }
1786
+ /** A query function on every unit: `listener(Map unit -> value)`. */
1787
+ watchQueryAll(name, args, listener, { as } = {}) {
1788
+ return this.everyUnit({
1789
+ k: "f",
1790
+ f: name,
1791
+ a: args
1792
+ }, listener, { as });
1793
+ }
1794
+ /** Closes the socket. A server shape's socket is the process's: it closes when every `connect` closed it. */
1795
+ close() {
1796
+ if (this.sharedKey && (this.refs -= 1) > 0) return;
1797
+ if (this.sharedKey && SHARED.get(this.sharedKey)) SHARED.delete(this.sharedKey);
1798
+ this.closed = true;
1799
+ clearInterval(this.dog);
1800
+ clearTimeout(this.renewer);
1801
+ clearTimeout(this.flushTimer);
1802
+ clearTimeout(this.leaveTimer);
1803
+ try {
1804
+ this.ws?.close(1e3);
1805
+ } catch {}
1806
+ for (const lane of this.lanes?.values() ?? []) try {
1807
+ lane.ws.close(1e3);
1808
+ } catch {}
1809
+ }
1810
+ /** Unit `unit`'s lane when it is open; else null (its ops take the socket), and one is opened when the map names a
1811
+ * row there. */
1812
+ laneFor(unit) {
1813
+ if (!this.lanes || !this.dir || unit === "*" || this.closed) return null;
1814
+ let lane = this.lanes.get(unit);
1815
+ if (!lane) {
1816
+ const hk = `${this.dir.v}|${unit}`;
1817
+ if (!this.homes?.has(hk)) {
1818
+ if ((this.homes ??= /* @__PURE__ */ new Map()).size > 256) this.homes.clear();
1819
+ this.homes.set(hk, rowOn(this.dir, unit));
1820
+ }
1821
+ const home = this.homes.get(hk);
1822
+ if (!home) return null;
1823
+ const ws = socketAt(`${this.base.replace(/^http/u, "ws")}/ws?token=${encodeURIComponent(this.token)}&home=${encodeURIComponent(home)}`);
1824
+ lane = {
1825
+ unit,
1826
+ ws,
1827
+ open: false,
1828
+ at: performance.now(),
1829
+ heard: performance.now()
1830
+ };
1831
+ this.lanes.set(unit, lane);
1832
+ this.stats.lanesOpened = (this.stats.lanesOpened ?? 0) + 1;
1833
+ ws.addEventListener("open", () => {
1834
+ if (this.lanes.get(unit) === lane) {
1835
+ lane.open = true;
1836
+ lane.heard = performance.now();
1837
+ }
1838
+ }, { once: true });
1839
+ ws.addEventListener("message", (event) => {
1840
+ if (this.lanes.get(unit) !== lane) return;
1841
+ lane.heard = performance.now();
1842
+ this.received(event.data, lane);
1843
+ if (this.held.length) this.schedule();
1844
+ });
1845
+ ws.addEventListener("close", () => this.laneLost(lane, "close"));
1846
+ }
1847
+ return lane.open && lane.ws.readyState === 1 ? lane : null;
1848
+ }
1849
+ /** A lane gone (closed, `{move}`, silent, never opened): what it carries goes again on the next send (the unit's next
1850
+ * lane, or the socket), same tags, as a reconnect sends it; its subscriptions start again. */
1851
+ laneLost(lane, why) {
1852
+ if (this.lanes?.get(lane.unit) !== lane) return;
1853
+ this.lanes.delete(lane.unit);
1854
+ this.stats.lanesLost = (this.stats.lanesLost ?? 0) + 1;
1855
+ try {
1856
+ lane.ws.close(4e3, why);
1857
+ } catch {}
1858
+ if (this.closed) return;
1859
+ const again = /* @__PURE__ */ new Set();
1860
+ for (const w of [...this.waiting.values()]) {
1861
+ if (w.lane !== lane) continue;
1862
+ w.lane = null;
1863
+ if (w.op.k === "sub") continue;
1864
+ if (w.sent && !w.keep && !READ_OPS.has(w.op.k)) {
1865
+ this.waiting.delete(w.op.i);
1866
+ w.resolve({
1867
+ i: w.op.i,
1868
+ ok: false,
1869
+ why: "closed"
1870
+ });
1871
+ continue;
1872
+ }
1873
+ this.stats.resent += 1;
1874
+ w.sentAt = void 0;
1875
+ again.add(w.op.i);
1876
+ this.queue(w.unit, w.op);
1877
+ }
1878
+ for (const [i, u] of this.unconfirmed) if (u.lane === lane && !again.has(i) && !this.waiting.has(i)) {
1879
+ u.lane = null;
1880
+ this.stats.resent += 1;
1881
+ this.waiting.set(i, {
1882
+ unit: u.unit,
1883
+ op: u.op,
1884
+ resolve: () => {},
1885
+ keep: true,
1886
+ sent: false,
1887
+ at: u.at
1888
+ });
1889
+ this.queue(u.unit, u.op);
1890
+ }
1891
+ for (const w of this.watches.values()) if (w.lane === lane) {
1892
+ w.lane = null;
1893
+ this.subscribe(w);
1894
+ }
1895
+ }
1896
+ /** The watchdog's look at the lanes: one not open in 2 s, or silent while its ops wait (nothing heard `silent` ms, the
1897
+ * oldest op waiting 3x that), is lost. */
1898
+ laneDog(now) {
1899
+ for (const lane of [...this.lanes.values()]) {
1900
+ if (!lane.open) {
1901
+ if (now - lane.at > 2e3) this.laneLost(lane, "stuck");
1902
+ continue;
1903
+ }
1904
+ let oldest = Infinity;
1905
+ for (const w of this.waiting.values()) if (w.lane === lane && w.sentAt < oldest) oldest = w.sentAt;
1906
+ if (now - oldest > 3 * this.silent && now - lane.heard > this.silent) this.laneLost(lane, "silent");
1907
+ }
1908
+ }
1909
+ };
1910
+ //#endregion
1911
+ export { TAG_TTL as n, Connection as t };