@midsummer-ai/sdk 0.1.0-next.1305 → 0.1.0-next.1723

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.
@@ -164,12 +164,15 @@ var Aimd = class {
164
164
  return this.v;
165
165
  }
166
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
- }
167
+ //#endregion
168
+ //#region src/database/client/connection.mjs
169
+ /** How long a socket's open may take (its upgrade answered), ms; a reconnect's try still unopened at each of HEDGE_AT
170
+ * (ms from its start) gets another socket beside it, at most three at once. */
171
+ const OPEN_WAIT = 1e4;
172
+ const HEDGE_AT = [100, 200];
173
+ /** A reconnect's waits between tries after the first (which is at once): RETRY_FIRST ms, doubling to RETRY_MAX. */
174
+ const RETRY_FIRST = 50;
175
+ const RETRY_MAX = 1e3;
173
176
  const TAG_TTL = 864e5;
174
177
  const groupOf = (id) => {
175
178
  const t = String(id ?? ""), k = t.indexOf("/");
@@ -403,8 +406,8 @@ var Connection = class Connection {
403
406
  ...options,
404
407
  shape
405
408
  };
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);
409
+ if (o.store === void 0 && typeof indexedDB !== "undefined" && typeof document !== "undefined") o.store = (await import("./store-BA8K-bAR.mjs")).idbStore(new URL(url).host);
410
+ if (o.tabs !== false && typeof document !== "undefined") return (await import("./tabs-DEeMee7M.mjs")).ConnectionTabs.connect(url, o);
408
411
  return Connection.open(url, o);
409
412
  }
410
413
  /** A Connection over one socket, of the shape given (default "browser"), no detection (tabs.mjs opens its own so). */
@@ -412,6 +415,10 @@ var Connection = class Connection {
412
415
  const g = new Connection(url, options);
413
416
  if (g.store) await g.store.attach(g);
414
417
  if (options.functions) g.predictor = new (await (import("./predict-Drfm_8pO.mjs"))).Predictor(g, options.functions);
418
+ if (options.shape === "server" && options.http !== false) {
419
+ g.http = true;
420
+ return g;
421
+ }
415
422
  await g.open();
416
423
  if (!g.dir) await Promise.race([g.hello, new Promise((r) => setTimeout(r, HELLO_WAIT_MS))]);
417
424
  return g;
@@ -589,7 +596,7 @@ var Connection = class Connection {
589
596
  try {
590
597
  ws?.close(1e3);
591
598
  } catch {}
592
- }, 1e4);
599
+ }, OPEN_WAIT);
593
600
  const opened = (ws) => {
594
601
  clearTimeout(timer);
595
602
  if (this.closed) {
@@ -599,6 +606,13 @@ var Connection = class Connection {
599
606
  ready();
600
607
  return;
601
608
  }
609
+ if (this.ws && this.ws !== ws && this.ws.readyState === 1) {
610
+ try {
611
+ ws.close(1e3);
612
+ } catch {}
613
+ ready();
614
+ return;
615
+ }
602
616
  this.ws = ws;
603
617
  this.heard = this.answeredAt = this.openedAt = performance.now();
604
618
  this.probe = 0;
@@ -648,22 +662,39 @@ var Connection = class Connection {
648
662
  }
649
663
  });
650
664
  }
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) {
665
+ /** Opens a socket again until one opens (one loop at a time): the first try at once, then after 50 ms doubling to
666
+ * 1 s, each wait jittered (clients a dead server dropped together do not come back together). A try still unopened
667
+ * at 100 ms gets a second socket beside it and at 200 ms a third, and the first to open serves (the others are
668
+ * closed): a socket the network sent to a replica that is gone costs at most 100 ms, while a slow open (a wake, a
669
+ * long replay, a takeover waiting for a dead replica's lease) is never given up. */
670
+ async reconnect(delay = 0) {
654
671
  if (this.reopening || this.closed) return;
655
672
  this.reopening = true;
656
673
  this.stats.reconnects += 1;
674
+ const opens = () => this.open().then(() => true, () => false);
675
+ const firstOf = (tries) => new Promise((done) => {
676
+ let n = 0;
677
+ for (const p of tries) p.then((ok) => {
678
+ if (ok) done(true);
679
+ else if (++n === tries.length) done(false);
680
+ });
681
+ });
657
682
  try {
658
- for (;;) {
659
- await new Promise((r) => setTimeout(r, delay));
683
+ for (let step = RETRY_FIRST;;) {
684
+ if (delay > 0) await new Promise((r) => setTimeout(r, delay));
660
685
  if (this.closed || this.ws?.readyState === 1) return;
661
- try {
662
- await this.open();
663
- return;
664
- } catch {
665
- delay = jitterBackoff(delay, 50, 5e3);
686
+ const tries = [opens()];
687
+ let opened = null;
688
+ for (let k = 0, at = 0; k <= HEDGE_AT.length; k += 1) {
689
+ const wait = k < HEDGE_AT.length ? new Promise((r) => setTimeout(() => r(null), HEDGE_AT[k] - at)) : new Promise(() => {});
690
+ opened = await Promise.race([firstOf(tries), wait]);
691
+ if (opened !== null) break;
692
+ at = HEDGE_AT[k];
693
+ tries.push(opens());
666
694
  }
695
+ if (opened) return;
696
+ delay = step * (.5 + Math.random() / 2);
697
+ step = Math.min(RETRY_MAX, step * 2);
667
698
  }
668
699
  } finally {
669
700
  this.reopening = false;
@@ -779,6 +810,7 @@ var Connection = class Connection {
779
810
  * same tag, and its answer is still read here); "b" or "e": never waits for durable (server shapes), and its round
780
811
  * trip tunes the conflation window. */
781
812
  send(unit, op, keep = false, now = null, route = void 0, relax = null) {
813
+ if (this.http && !this.ws && !this.reopening && !this.closed) this.reconnect(0);
782
814
  const i = op.i ?? this.next++;
783
815
  const o = {
784
816
  i,
@@ -1379,12 +1411,15 @@ var Connection = class Connection {
1379
1411
  this.notifyRow(key);
1380
1412
  }
1381
1413
  const guess = optimistic || as != null ? null : this.predictor?.begin(name, args, route);
1382
- const a = await this.send(this.unitOf(route), {
1414
+ const op = {
1383
1415
  k: "m",
1384
1416
  f: name,
1385
1417
  a: args,
1386
1418
  t: tag ?? this.tag(),
1387
- ...asOf(as),
1419
+ ...asOf(as)
1420
+ };
1421
+ const a = this.http && this.ws?.readyState !== 1 ? await this.overHttp(op) : await this.send(this.unitOf(route), {
1422
+ ...op,
1388
1423
  ...this.urgent()
1389
1424
  }, true, null, route);
1390
1425
  guess?.end(a);
@@ -1395,6 +1430,68 @@ var Connection = class Connection {
1395
1430
  if (refused(a)) throw coded(a);
1396
1431
  return a;
1397
1432
  }
1433
+ /** One query or mutation over HTTP (`POST <db>/call`, the token in its header), with no socket: its answer as a
1434
+ * socket's. A mutation's tag rides with it, so one sent again (after a failure the network gave no answer to)
1435
+ * applies once; a query, or a tagged mutation, is sent again on a 5xx or no answer, twice at most; a 401 renews the
1436
+ * token once. */
1437
+ async overHttp(op) {
1438
+ const body = JSON.stringify([{
1439
+ i: 1,
1440
+ ...op
1441
+ }]), again = op.k === "q" || !!op.t;
1442
+ for (let tries = 0;; tries += 1) {
1443
+ let r;
1444
+ try {
1445
+ r = await fetchAt(`${this.base}/call`, {
1446
+ method: "POST",
1447
+ headers: {
1448
+ authorization: `Bearer ${this.token}`,
1449
+ "content-type": "application/json"
1450
+ },
1451
+ body
1452
+ });
1453
+ } catch (e) {
1454
+ if (again && tries < 2) continue;
1455
+ return {
1456
+ i: 1,
1457
+ ok: false,
1458
+ why: "unavailable",
1459
+ error: String(e?.message ?? e)
1460
+ };
1461
+ }
1462
+ if (r.status === 401 && this.refresh && tries === 0) {
1463
+ await this.renew(false);
1464
+ continue;
1465
+ }
1466
+ const text = await r.text();
1467
+ if (!r.ok) {
1468
+ if (r.status >= 500 && again && tries < 2) continue;
1469
+ let j = {};
1470
+ try {
1471
+ j = JSON.parse(text);
1472
+ } catch {}
1473
+ return {
1474
+ i: 1,
1475
+ ok: false,
1476
+ why: r.status === 401 ? "denied" : j.error ?? "refused",
1477
+ error: j.why ?? text.slice(0, 200)
1478
+ };
1479
+ }
1480
+ for (const line of text.split("\n")) try {
1481
+ const v = JSON.parse(line);
1482
+ if (Array.isArray(v)) {
1483
+ const a = v.find((x) => x?.i === 1);
1484
+ if (a) return a;
1485
+ }
1486
+ } catch {}
1487
+ return {
1488
+ i: 1,
1489
+ ok: false,
1490
+ why: "unavailable",
1491
+ error: "no answer in the call's reply"
1492
+ };
1493
+ }
1494
+ }
1398
1495
  /** An ACTION of a the database app (it may do I/O; not a transaction): called as a mutation is, by reference or
1399
1496
  * name, tagged (a resend after a reconnect is answered from its tag). Resolves its answer. */
1400
1497
  async action(name, args, options = {}) {
@@ -1403,12 +1500,13 @@ var Connection = class Connection {
1403
1500
  /** A query function's value, once, on unit `unit` (default "0"). */
1404
1501
  async query(name, args, { unit = "0", as } = {}) {
1405
1502
  name = fnName(name);
1406
- const a = await this.send(String(unit), {
1503
+ const op = {
1407
1504
  k: "q",
1408
1505
  f: name,
1409
1506
  a: args,
1410
1507
  ...asOf(as)
1411
- });
1508
+ };
1509
+ const a = this.http && this.ws?.readyState !== 1 ? await this.overHttp(op) : await this.send(String(unit), op);
1412
1510
  if (!a.ok) throw coded(a);
1413
1511
  return a.value;
1414
1512
  }
@@ -1,5 +1,5 @@
1
1
  import { DatabaseError } from "./errors.mjs";
2
- import { t as Connection } from "../connection-CxD_Nry5.mjs";
2
+ import { t as Connection } from "../connection-BHjDaLYA.mjs";
3
3
  //#region src/database/client.ts
4
4
  /**
5
5
  * `@midsummer-ai/sdk/database`: a Project's Database, from its app or its server.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midsummer-ai/sdk",
3
- "version": "0.1.0-next.1305",
3
+ "version": "0.1.0-next.1723",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "Midsummer's handles for an app's code: its Database, Storage, Queues, Vector DB, AI, Email and Agents, with stand-ins for tests.",
@@ -1,4 +1,4 @@
1
- import "./connection-CxD_Nry5.mjs";
1
+ import "./connection-BHjDaLYA.mjs";
2
2
  //#region src/database/client/store.mjs
3
3
  const STORES = [
4
4
  "ops",
@@ -1,4 +1,4 @@
1
- import { t as Connection } from "./connection-CxD_Nry5.mjs";
1
+ import { t as Connection } from "./connection-BHjDaLYA.mjs";
2
2
  //#region src/database/client/tabs.mjs
3
3
  /** Argument index of the options that take a `tag` (a tab tags its own writes: the same tag after a handover). */
4
4
  const TAGGED = {