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