openvibers/openvibe.sdk Commits
openvibe-sdk 0.82.0: the outbox's pending() and rejected() keep their count (#114)
Changes
@@@ -1,5 +1,13 @@# Changelog++## 0.82.0 (2026-11-02)++++- **`openvibe-sdk/events` outbox: `pending()` and `rejected()` keep their count** (`countsCacheMs`, 30 s when++ `NODE_ENV` is production, off elsewhere), and callers at the same moment share one count. The metric gauges and++ readiness ask on every scrape and probe, and `rejected()` has no index, so a large outbox was read whole each time:++ 115 ms on Deals, 56 ms on Trade, 49 ms on Sources, 45 ms on Search and 24 ms on Reviews (pg_stat_statements,++ 2026-11-02). Pass `countsCacheMs: 0` to count on every call.++## 0.81.0 (2026-11-02)- **`openvibe-sdk/vision`: familiar faces (`face.familiar`), F1 of the L24 plan.** The camera owner's own Vision@@@ -2,16 +2,16 @@> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.81.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.++**Status:** alpha, v0.82.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.--After v0.81.0 is tagged:++After v0.82.0 is tagged:```bash--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0```It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from Contracts v0.86.0, a devDependency the tests check them against).@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.81.0",++ "version": "0.82.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag",@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.81.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.82.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces// (auth, registry, modules, realtime, media, community, bot, search, bench, meme, vision, themes, redact, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */@@@ -47,7 +47,7 @@ const { paginate, offsetPager } = __load(7);const semver = __load(6);/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.81.0';++const SDK_VERSION = '0.82.0';module.exports = {createClient,@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.81.0",++ "version": "0.82.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.81.0",++ "version": "0.82.0","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.81.0",++ "version": "0.82.0","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');const semver = require('./semver');/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.81.0';++const SDK_VERSION = '0.82.0';module.exports = {createClient,@@@ -96,9 +96,29 @@ function inboxSchema(table = 'idempotency_receipts') {* relay one table without double-sending; a relay that dies mid-batch leaves its rows to the next after the* lease. Delivery stays at least once (Events answers a repeated event_id as a duplicate).*/++/**++ * A count kept for keepMs, shared by callers at the same moment. pending() and rejected() count the outbox table++ * (rejected rows have no index, so a large outbox is read whole: 115 ms on Deals, 45 ms on Search, 2026-11-02),++ * and the metric gauges and readiness ask on every scrape and probe.++ */++function keptCount(read, keepMs, now) {++ let kept = null;++ return () => {++ if (!(keepMs > 0)) return read();++ const t = now();++ if (kept && (kept.pending || t - kept.at < keepMs)) return kept.pending || Promise.resolve(kept.value);++ const pending = Promise.resolve().then(read);++ kept = { at: t, pending };++ return pending.then((value) => { if (kept && kept.pending === pending) kept = { at: t, value }; return value; },++ (err) => { if (kept && kept.pending === pending) kept = null; throw err; });++ };++}++function createPgOutbox(db, {events, table = 'event_outbox', batchSize = 50, intervalMs = 1000, leaseMs = 60000,backoffMs = [1000, 5000, 30000, 120000, 600000], now = () => Date.now(), onError = null,++ // How long pending() and rejected() keep their count: 30 s in production (NODE_ENV), every call counts elsewhere.++ countsCacheMs = process.env.NODE_ENV === 'production' ? 30000 : 0,} = {}) {if (!db || typeof db.query !== 'function' || typeof db.tx !== 'function') throw new TypeError('an openvibe-sdk/db handle is required');if (!events || typeof events.publish !== 'function' || typeof events.prepare !== 'function') {@@@ -201,8 +221,8 @@ function createPgOutbox(db, {start() { if (!running) { running = true; schedule(0); } },stop() { running = false; clearTimeout(timer); timer = null; return flush.idle(); },kick() { schedule(0); },-- pending: () => db.value(`SELECT count(*) FROM ${table} WHERE sent_at IS NULL AND rejected_at IS NULL`),-- rejected: () => db.value(`SELECT count(*) FROM ${table} WHERE rejected_at IS NOT NULL`),++ pending: keptCount(() => db.value(`SELECT count(*) FROM ${table} WHERE sent_at IS NULL AND rejected_at IS NULL`), countsCacheMs, now),++ rejected: keptCount(() => db.value(`SELECT count(*) FROM ${table} WHERE rejected_at IS NOT NULL`), countsCacheMs, now),prune: (olderThanMs = 7 * 24 * 60 * 60 * 1000) => db.exec(`DELETE FROM ${table} WHERE sent_at IS NOT NULL AND sent_at < $1`, [now() - olderThanMs]),};}@@@ -26,6 +26,36 @@ function eventsClient() {function cases(label, open) {return [++ [`${label}: pending() and rejected() keep their count for countsCacheMs, shared by callers at the same moment`, async () => {++ const { db, done } = await open();++ try {++ const { events } = eventsClient();++ let counts = 0;++ const watched = new Proxy(db, {++ get(target, key) {++ if (key === 'value') return (...args) => { counts += 1; return target.value(...args); };++ const v = target[key];++ return typeof v === 'function' ? v.bind(target) : v;++ },++ });++ let t = 1000;++ const kept = createPgOutbox(watched, { events, now: () => t, countsCacheMs: 30000 });++ const [a, b] = await Promise.all([kept.pending(), kept.pending()]);++ assert.equal(counts, 1, 'two callers at once: one count');++ assert.equal(a, b);++ await db.tx((tx) => kept.enqueue(tx, { event_type: 'live.stream.started', actor, subject }));++ assert.equal(await kept.pending(), 0, 'within the window: the kept count');++ assert.equal(counts, 1);++ t += 30000;++ assert.equal(await kept.pending(), 1, 'past it: counted again');++ assert.equal(await kept.rejected(), 0);++ assert.equal(counts, 3, 'rejected() keeps its own count');++ const every = createPgOutbox(watched, { events });++ await every.pending();++ await every.pending();++ assert.equal(counts, 5, 'without countsCacheMs (and outside production) every call counts');++ } finally { await done(); }++ }],[`${label}: a kick() from inside a transaction never hands the relay that transaction`, async () => {const { db, done } = await open();try {
@@ -1,5 +1,13 @@
@@ -1,5 +1,13 @@
# Changelog
# Changelog
+## 0.82.0 (2026-11-02)
+
+- **`openvibe-sdk/events` outbox: `pending()` and `rejected()` keep their count** (`countsCacheMs`, 30 s when
+ `NODE_ENV` is production, off elsewhere), and callers at the same moment share one count. The metric gauges and
+ readiness ask on every scrape and probe, and `rejected()` has no index, so a large outbox was read whole each time:
+ 115 ms on Deals, 56 ms on Trade, 49 ms on Sources, 45 ms on Search and 24 ms on Reviews (pg_stat_statements,
+ 2026-11-02). Pass `countsCacheMs: 0` to count on every call.
+
## 0.81.0 (2026-11-02)
## 0.81.0 (2026-11-02)
- **`openvibe-sdk/vision`: familiar faces (`face.familiar`), F1 of the L24 plan.** The camera owner's own Vision
- **`openvibe-sdk/vision`: familiar faces (`face.familiar`), F1 of the L24 plan.** The camera owner's own Vision
@@ -2,16 +2,16 @@
@@ -2,16 +2,16 @@
> Supported browser and server clients for the OpenVibe platform.
> Supported browser and server clients for the OpenVibe platform.
-**Status:** alpha, v0.81.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
+**Status:** alpha, v0.82.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.
**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.
-After v0.81.0 is tagged:
+After v0.82.0 is tagged:
```bash
```bash
-npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0
```
```
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from Contracts v0.86.0, a devDependency the tests check them against).
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from Contracts v0.86.0, a devDependency the tests check them against).
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.81.0",
+ "version": "0.82.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.81.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.82.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, registry, modules, realtime, media, community, bot, search, bench, meme, vision, themes, redact, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
// (auth, registry, modules, realtime, media, community, bot, search, bench, meme, vision, themes, redact, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
@@ -47,7 +47,7 @@ const { paginate, offsetPager } = __load(7);
@@ -47,7 +47,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.81.0';
+const SDK_VERSION = '0.82.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.81.0",
+ "version": "0.82.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.81.0",
+ "version": "0.82.0",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.81.0",
+ "version": "0.82.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.81.0';
+const SDK_VERSION = '0.82.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -96,9 +96,29 @@ function inboxSchema(table = 'idempotency_receipts') {
@@ -96,9 +96,29 @@ function inboxSchema(table = 'idempotency_receipts') {
* relay one table without double-sending; a relay that dies mid-batch leaves its rows to the next after the
* relay one table without double-sending; a relay that dies mid-batch leaves its rows to the next after the
* lease. Delivery stays at least once (Events answers a repeated event_id as a duplicate).
* lease. Delivery stays at least once (Events answers a repeated event_id as a duplicate).
*/
*/
+/**
+ * A count kept for keepMs, shared by callers at the same moment. pending() and rejected() count the outbox table
+ * (rejected rows have no index, so a large outbox is read whole: 115 ms on Deals, 45 ms on Search, 2026-11-02),
+ * and the metric gauges and readiness ask on every scrape and probe.
+ */
+function keptCount(read, keepMs, now) {
+ let kept = null;
+ return () => {
+ if (!(keepMs > 0)) return read();
+ const t = now();
+ if (kept && (kept.pending || t - kept.at < keepMs)) return kept.pending || Promise.resolve(kept.value);
+ const pending = Promise.resolve().then(read);
+ kept = { at: t, pending };
+ return pending.then((value) => { if (kept && kept.pending === pending) kept = { at: t, value }; return value; },
+ (err) => { if (kept && kept.pending === pending) kept = null; throw err; });
+ };
+}
+
function createPgOutbox(db, {
function createPgOutbox(db, {
events, table = 'event_outbox', batchSize = 50, intervalMs = 1000, leaseMs = 60000,
events, table = 'event_outbox', batchSize = 50, intervalMs = 1000, leaseMs = 60000,
backoffMs = [1000, 5000, 30000, 120000, 600000], now = () => Date.now(), onError = null,
backoffMs = [1000, 5000, 30000, 120000, 600000], now = () => Date.now(), onError = null,
+ // How long pending() and rejected() keep their count: 30 s in production (NODE_ENV), every call counts elsewhere.
+ countsCacheMs = process.env.NODE_ENV === 'production' ? 30000 : 0,
} = {}) {
} = {}) {
if (!db || typeof db.query !== 'function' || typeof db.tx !== 'function') throw new TypeError('an openvibe-sdk/db handle is required');
if (!db || typeof db.query !== 'function' || typeof db.tx !== 'function') throw new TypeError('an openvibe-sdk/db handle is required');
if (!events || typeof events.publish !== 'function' || typeof events.prepare !== 'function') {
if (!events || typeof events.publish !== 'function' || typeof events.prepare !== 'function') {
@@ -201,8 +221,8 @@ function createPgOutbox(db, {
@@ -201,8 +221,8 @@ function createPgOutbox(db, {
start() { if (!running) { running = true; schedule(0); } },
start() { if (!running) { running = true; schedule(0); } },
stop() { running = false; clearTimeout(timer); timer = null; return flush.idle(); },
stop() { running = false; clearTimeout(timer); timer = null; return flush.idle(); },
kick() { schedule(0); },
kick() { schedule(0); },
- pending: () => db.value(`SELECT count(*) FROM ${table} WHERE sent_at IS NULL AND rejected_at IS NULL`),
- rejected: () => db.value(`SELECT count(*) FROM ${table} WHERE rejected_at IS NOT NULL`),
+ pending: keptCount(() => db.value(`SELECT count(*) FROM ${table} WHERE sent_at IS NULL AND rejected_at IS NULL`), countsCacheMs, now),
+ rejected: keptCount(() => db.value(`SELECT count(*) FROM ${table} WHERE rejected_at IS NOT NULL`), countsCacheMs, now),
prune: (olderThanMs = 7 * 24 * 60 * 60 * 1000) => db.exec(`DELETE FROM ${table} WHERE sent_at IS NOT NULL AND sent_at < $1`, [now() - olderThanMs]),
prune: (olderThanMs = 7 * 24 * 60 * 60 * 1000) => db.exec(`DELETE FROM ${table} WHERE sent_at IS NOT NULL AND sent_at < $1`, [now() - olderThanMs]),
};
};
}
}
@@ -26,6 +26,36 @@ function eventsClient() {
@@ -26,6 +26,36 @@ function eventsClient() {
function cases(label, open) {
function cases(label, open) {
return [
return [
+ [`${label}: pending() and rejected() keep their count for countsCacheMs, shared by callers at the same moment`, async () => {
+ const { db, done } = await open();
+ try {
+ const { events } = eventsClient();
+ let counts = 0;
+ const watched = new Proxy(db, {
+ get(target, key) {
+ if (key === 'value') return (...args) => { counts += 1; return target.value(...args); };
+ const v = target[key];
+ return typeof v === 'function' ? v.bind(target) : v;
+ },
+ });
+ let t = 1000;
+ const kept = createPgOutbox(watched, { events, now: () => t, countsCacheMs: 30000 });
+ const [a, b] = await Promise.all([kept.pending(), kept.pending()]);
+ assert.equal(counts, 1, 'two callers at once: one count');
+ assert.equal(a, b);
+ await db.tx((tx) => kept.enqueue(tx, { event_type: 'live.stream.started', actor, subject }));
+ assert.equal(await kept.pending(), 0, 'within the window: the kept count');
+ assert.equal(counts, 1);
+ t += 30000;
+ assert.equal(await kept.pending(), 1, 'past it: counted again');
+ assert.equal(await kept.rejected(), 0);
+ assert.equal(counts, 3, 'rejected() keeps its own count');
+ const every = createPgOutbox(watched, { events });
+ await every.pending();
+ await every.pending();
+ assert.equal(counts, 5, 'without countsCacheMs (and outside production) every call counts');
+ } finally { await done(); }
+ }],
[`${label}: a kick() from inside a transaction never hands the relay that transaction`, async () => {
[`${label}: a kick() from inside a transaction never hands the relay that transaction`, async () => {
const { db, done } = await open();
const { db, done } = await open();
try {
try {