stratus-db 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -8
  2. package/package.json +1 -1
  3. package/stratus-db.js +24 -52
package/README.md CHANGED
@@ -51,27 +51,27 @@ of the intended type.
51
51
  ## API
52
52
 
53
53
  All methods return promises; failures reject with `StratusError` (`status`, `error` - e.g.
54
- `"invalid-query"`, `"conflict"` -, `message`). 429s are retried with backoff (`retries`).
54
+ `"invalid-query"`, `"conflict"` -, `message`). 429s are retried with backoff (`retries`), and so is API
55
+ Gateway's 403 Forbidden for a brand-new key, which works within a minute or two (`newKeyWaitMs`, 120000).
55
56
 
56
57
  - Catalog: `listDbs({deleted})`, `dbByName(name)`, `dbRecord(id)`, `createDb({name, primaryKey})`,
57
58
  `renameDb(id, name)`, `deleteDb(id)`.
58
59
  - A database: `status(id)`, `transact(id, {tx, retract, delete, txId, match})`, `query(id, q)`,
59
60
  `queryAll(id, q)` (async iterator), `pull(id, pks, {asOf})` (and `asOf: t` in a query: the database as it stood after t), `requestIndex(id, att)`,
60
61
  `awaitIndex(id, att)`, `dropIndex(id, att)`, `history(id, {from, limit})`, `historyAll`,
61
- `fork(id, {t, name})` (hosted: a new database, this one as of t), `excise(id, {rows,
62
+ `fork(id, {t, name})` (a new database, this one as of t), `excise(id, {rows,
62
63
  attributes})` (erasure: rows and {pk, att} out of the state, indexes, history and forks),
63
64
  `setHistoryRetention(id, days)` (history kept 30 to 36500 days, or `null` for forever).
64
- - Keys (hosted, with a key reaching every database): `listKeys()`, `createKey({name, scope})`,
65
+ - Keys (with a key reaching every database): `listKeys()`, `createKey({name, scope})`,
65
66
  `revokeKey(keyId)`.
66
67
  - Codec: `encode`, `decode` (typed-JSON text), `toJSONValue`, `fromJSONValue`.
67
68
 
68
- Queries: docs/query.md ("JSON"). Addressing: `"hosted"` (default: `<url>/v1/dbs/<id>/...`,
69
- `Authorization: Bearer <apiKey>`) or `"function-urls"` (the internal service's Function URLs,
70
- resolved through its catalog; `caller` for a local service).
69
+ Queries: docs/query.md ("JSON"). Requests go to `<url>/v1/dbs/<id>/...` with `Authorization:
70
+ Bearer <apiKey>`.
71
71
 
72
72
  ## Tests
73
73
 
74
74
  `node --test test/ejson.test.js` (the codec: velojity's own output in
75
75
  `clients/fixtures/velojity-ejson.json`, which stratus's client-fixtures-test keeps current), and
76
- `scripts/test-js-client` from the repo root: the codec, then the client against the local
77
- service.
76
+ `scripts/test-js-client` from the repo root: the codec, then the client against the hosted
77
+ product run locally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stratus-db",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The stratus-db service from JavaScript: typed JSON over fetch, no dependencies.",
5
5
  "homepage": "https://stratus-db.com",
6
6
  "license": "Apache-2.0",
package/stratus-db.js CHANGED
@@ -11,14 +11,11 @@
11
11
  // await s.transact(id, { tx: [{ "order/id": squuid(), "order/status": kw("status/new") }] });
12
12
  // const { rows } = await s.query(id, { where: ["=", "order/status", kw("status/new")] });
13
13
  //
14
- // Addressing: "hosted" (the default) - the catalog at <url>/v1/dbs, a database at
15
- // <url>/v1/dbs/<id>/..., the API key as "Authorization: Bearer <key>". "function-urls" - the
16
- // internal service: the catalog at url, each database at the Function URL its catalog record
17
- // names (looked up, cached for resolveMs, and again when one answers 403 or 404); for testing
18
- // against a local service, opts.caller names the caller (x-stratus-caller).
14
+ // Addressing: the catalog at <url>/v1/dbs, a database at <url>/v1/dbs/<id>/..., the API key as
15
+ // "Authorization: Bearer <key>".
19
16
 
20
17
  /** The client's version: package.json's, which release-js.yml checks against it. */
21
- export const VERSION = "0.2.0";
18
+ export const VERSION = "0.3.0";
22
19
 
23
20
  const USER_AGENT = `stratus-db-js/${VERSION}`;
24
21
  const IN_NODE = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
@@ -537,28 +534,25 @@ export function connect(opts) {
537
534
  return new Client(opts);
538
535
  }
539
536
 
537
+ const GATEWAY_FORBIDDEN = /^\s*\{\s*"message"\s*:\s*"Forbidden"\s*\}\s*$/;
538
+
540
539
  export class Client {
541
540
  /**
542
541
  * @param {object} opts
543
- * @param {string} opts.url the service's base URL (hosted), or the catalog's URL (function-urls)
542
+ * @param {string} opts.url the service's base URL
544
543
  * @param {string} [opts.apiKey] sent as "Authorization: Bearer <key>"
545
- * @param {"hosted"|"function-urls"} [opts.addressing]
546
- * @param {string} [opts.caller] x-stratus-caller, for a local service
547
544
  * @param {number} [opts.retries] attempts after a 429 (default 10)
548
- * @param {number} [opts.resolveMs] function-urls: how long a database's URL is cached (5 minutes)
545
+ * @param {number} [opts.newKeyWaitMs] how long to keep retrying API Gateway's own 403 Forbidden, which a
546
+ * brand-new key gets for its first minute or so (default 120000; 0: never)
549
547
  * @param {typeof fetch} [opts.fetch] default globalThis.fetch
550
548
  */
551
549
  constructor(opts) {
552
550
  if (!opts?.url) throw new TypeError("connect needs { url }");
553
551
  this.url = trimSlash(opts.url);
554
- this.addressing = opts.addressing ?? "hosted";
555
- if (!["hosted", "function-urls"].includes(this.addressing)) throw new TypeError(`addressing: ${this.addressing}`);
556
552
  this.apiKey = opts.apiKey;
557
- this.caller = opts.caller;
558
553
  this.retries = opts.retries ?? 10;
559
- this.resolveMs = opts.resolveMs ?? 300000;
554
+ this.newKeyWaitMs = opts.newKeyWaitMs ?? 120000;
560
555
  this.fetch = opts.fetch ?? globalThis.fetch;
561
- this.urls = { at: 0, byId: new Map() };
562
556
  }
563
557
 
564
558
  // -- requests
@@ -569,8 +563,8 @@ export class Client {
569
563
  if (IN_NODE) headers["user-agent"] = USER_AGENT;
570
564
  if (body !== undefined) headers["content-type"] = "application/json";
571
565
  if (this.apiKey) headers.authorization = `Bearer ${this.apiKey}`;
572
- if (this.caller) headers["x-stratus-caller"] = this.caller;
573
- for (let attempt = 0; ; attempt++) {
566
+ const started = Date.now();
567
+ for (let attempt = 0, forbidden = 0; ; attempt++) {
574
568
  const r = await this.fetch(url, { method, headers, body: body === undefined ? undefined : encode(body) });
575
569
  const textBody = await r.text();
576
570
  if (r.status === 429 && attempt < this.retries) {
@@ -578,6 +572,12 @@ export class Client {
578
572
  await sleep(Math.random() * Math.min(5000, 50 * 2 ** attempt));
579
573
  continue;
580
574
  }
575
+ // API Gateway's own refusal of a key it hasn't learned yet (a new key, its first minute or so):
576
+ // {"message":"Forbidden"} - the service's own errors always carry "error", and aren't retried
577
+ if (r.status === 403 && GATEWAY_FORBIDDEN.test(textBody) && Date.now() - started < this.newKeyWaitMs) {
578
+ await sleep(Math.min(15000, 1000 * 2 ** forbidden++));
579
+ continue;
580
+ }
581
581
  const ct = r.headers.get("content-type") ?? "";
582
582
  let value = textBody;
583
583
  if (ct.startsWith("application/json") && textBody !== "") {
@@ -594,42 +594,14 @@ export class Client {
594
594
  throw new StratusError(r.status, isObj ? b.error : null, isObj ? b.message : String(b ?? ""), b);
595
595
  }
596
596
 
597
- catalogUrl(path) {
598
- return this.addressing === "hosted" ? `${this.url}/v1${path}` : `${this.url}${path}`;
599
- }
600
-
601
597
  /** method path body to the catalog: { status, body }. */
602
598
  callCatalog(method, path, body) {
603
- return this.send(this.catalogUrl(path), method, body);
604
- }
605
-
606
- async resolve() {
607
- const recs = Client.ok(await this.callCatalog("GET", "/dbs"));
608
- const byId = new Map();
609
- for (const r of recs) if (r.url) byId.set(String(r.id), r.url);
610
- this.urls = { at: Date.now(), byId };
611
- return byId;
612
- }
613
-
614
- async dbBase(id) {
615
- const key = String(id);
616
- if (this.addressing === "hosted") return `${this.url}/v1/dbs/${encodeURIComponent(key)}`;
617
- const { at, byId } = this.urls;
618
- if (byId.has(key) && Date.now() - at < this.resolveMs) return trimSlash(byId.get(key));
619
- const u = (await this.resolve()).get(key);
620
- return u === undefined ? null : trimSlash(u);
599
+ return this.send(`${this.url}/v1${path}`, method, body);
621
600
  }
622
601
 
623
602
  /** method path body to database id: { status, body }. */
624
- async callDb(id, method, path, body) {
625
- const base = await this.dbBase(id);
626
- if (base === null) return { status: 404, body: { error: "no-database", message: `No database ${id}` } };
627
- const r = await this.send(base + path, method, body);
628
- if (this.addressing === "function-urls" && (r.status === 403 || r.status === 404)) {
629
- const again = (await this.resolve()).get(String(id));
630
- if (again !== undefined && trimSlash(again) !== base) return this.send(trimSlash(again) + path, method, body);
631
- }
632
- return r;
603
+ callDb(id, method, path, body) {
604
+ return this.send(`${this.url}/v1/dbs/${encodeURIComponent(String(id))}${path}`, method, body);
633
605
  }
634
606
 
635
607
  // -- the catalog
@@ -664,7 +636,7 @@ export class Client {
664
636
  return Client.ok(await this.callCatalog("DELETE", `/dbs/${encodeURIComponent(String(id))}`));
665
637
  }
666
638
 
667
- // -- keys (hosted: an account's keys, managed with a key whose scope is every database)
639
+ // -- keys (an account's keys, managed with a key whose scope is every database)
668
640
 
669
641
  /** The account's keys' records (no secrets). */
670
642
  async listKeys() {
@@ -688,7 +660,7 @@ export class Client {
688
660
 
689
661
  /** The database's state: { id, "primary-key", t, "checkpoint-t", indexes, "index-errors", status }. */
690
662
  async status(id) {
691
- return Client.ok(await this.callDb(id, "GET", this.addressing === "hosted" ? "/status" : "/"));
663
+ return Client.ok(await this.callDb(id, "GET", "/status"));
692
664
  }
693
665
 
694
666
  /**
@@ -755,7 +727,7 @@ export class Client {
755
727
  }
756
728
 
757
729
  /**
758
- * Forks database id (hosted): a new database, this one as of t (default its latest), recorded
730
+ * Forks database id: a new database, this one as of t (default its latest), recorded
759
731
  * in both logs. The fork's record ({ id, "fork-of": { db, t }, ... }).
760
732
  */
761
733
  async fork(id, { t, name } = {}) {
@@ -767,7 +739,7 @@ export class Client {
767
739
 
768
740
  /**
769
741
  * Excises rows (primary keys) and attributes ({ pk, att }) - erasure: gone from the current
770
- * state, its indexes and history (and, hosted, the database's forks): { t, rows, attributes }.
742
+ * state, its indexes and history (and the database's forks): { t, rows, attributes }.
771
743
  */
772
744
  async excise(id, { rows, attributes } = {}) {
773
745
  const body = {};