@catalyst-cloud/sdk 0.8.1 → 0.8.2
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.
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import { type OpenedReplica } from "./ports.js";
|
|
2
2
|
export type { OpenedReplica } from "./ports.js";
|
|
3
|
+
/**
|
|
4
|
+
* Is this throw "the bytes on disk are not a usable database"? (CTC-337)
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ THE ALLOWLIST IS THE SAFETY PROPERTY, not the recovery it guards. A `true` here authorizes
|
|
7
|
+
* DESTROYING the pool, so it must match ONLY genuine corruption. In particular it must never match
|
|
8
|
+
* `NoModificationAllowedError` — that is a second tab (or a not-yet-released worker) holding the
|
|
9
|
+
* SyncAccessHandles, and wiping on it would delete a database another live tab is actively using,
|
|
10
|
+
* turning a self-healing recovery into data loss for someone else. Contention is transient and the
|
|
11
|
+
* caller retries; corruption is permanent and only a wipe clears it. Anything unrecognised RETHROWS.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isCorruptDatabaseError(err: unknown): boolean;
|
|
14
|
+
/** The slice of SAHPoolUtil the recovery needs — narrowed so a test can supply a fake pool. */
|
|
15
|
+
export interface RecoverablePool {
|
|
16
|
+
OpfsSAHPoolDb: new (dbPath: string) => unknown;
|
|
17
|
+
wipeFiles: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Open the database, and if the FILE ITSELF is corrupt, destroy it and open a fresh one — ONCE.
|
|
21
|
+
*
|
|
22
|
+
* Safe in a way it would not be for most databases: this replica is 100% DERIVED. Every row came from
|
|
23
|
+
* `/snapshot` plus the change feed, so the local copy holds nothing that does not exist upstream, and
|
|
24
|
+
* re-seeding is the ordinary cold-start path rather than a recovery hack.
|
|
25
|
+
*
|
|
26
|
+
* Without this, one corrupt page bricked the replica PERMANENTLY. The corruption survives reloads, so
|
|
27
|
+
* `start()` threw on every subsequent boot and the reader sat behind a red pill forever unless they
|
|
28
|
+
* knew to clear origin storage by hand — a disposable cache that could not dispose of itself. That is
|
|
29
|
+
* exactly how CTC-337 presented: `SQLITE_CORRUPT: database disk image is malformed`, every load.
|
|
30
|
+
*
|
|
31
|
+
* Retries exactly once. A second failure after a wipe is not corruption we can clear — it is a broken
|
|
32
|
+
* environment (no quota, VFS refusing to register) — and looping would turn it into a hang.
|
|
33
|
+
*/
|
|
34
|
+
export declare function openWithCorruptionRecovery(pool: RecoverablePool, dbPath: string, build: (db: unknown) => OpenedReplica, log?: (message: string) => void): Promise<OpenedReplica>;
|
|
3
35
|
/**
|
|
4
36
|
* Open the OPFS SAHPool replica at `dbPath` (absolute, leading slash REQUIRED) and bring its schema up
|
|
5
37
|
* to date via the shared migration bundle. Async — wasm init + SAHPool acquire are async, unlike the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-db.d.ts","sourceRoot":"","sources":["../../../src/replica/browser/sqlite-db.ts"],"names":[],"mappings":"AAaA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"sqlite-db.d.ts","sourceRoot":"","sources":["../../../src/replica/browser/sqlite-db.ts"],"names":[],"mappings":"AAaA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAc5D;AAED,+FAA+F;AAC/F,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,KAAK,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAC/C,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,aAAa,EACrC,GAAG,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAA6B,GACtD,OAAO,CAAC,aAAa,CAAC,CAaxB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAU3F"}
|
|
@@ -10,6 +10,59 @@
|
|
|
10
10
|
// not the primary one.
|
|
11
11
|
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
12
12
|
import { buildOpenedReplica } from "./ports.js";
|
|
13
|
+
/**
|
|
14
|
+
* Is this throw "the bytes on disk are not a usable database"? (CTC-337)
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ THE ALLOWLIST IS THE SAFETY PROPERTY, not the recovery it guards. A `true` here authorizes
|
|
17
|
+
* DESTROYING the pool, so it must match ONLY genuine corruption. In particular it must never match
|
|
18
|
+
* `NoModificationAllowedError` — that is a second tab (or a not-yet-released worker) holding the
|
|
19
|
+
* SyncAccessHandles, and wiping on it would delete a database another live tab is actively using,
|
|
20
|
+
* turning a self-healing recovery into data loss for someone else. Contention is transient and the
|
|
21
|
+
* caller retries; corruption is permanent and only a wipe clears it. Anything unrecognised RETHROWS.
|
|
22
|
+
*/
|
|
23
|
+
export function isCorruptDatabaseError(err) {
|
|
24
|
+
const text = err instanceof Error ? `${err.name}: ${err.message}` : typeof err === "string" ? err : "";
|
|
25
|
+
if (text === "")
|
|
26
|
+
return false;
|
|
27
|
+
// Explicit rather than relying on the absence of a corruption marker: this is the case that must
|
|
28
|
+
// not wipe, so it should be impossible to reach the allowlist below by accident.
|
|
29
|
+
if (/NoModificationAllowedError|InvalidStateError/i.test(text))
|
|
30
|
+
return false;
|
|
31
|
+
return (/SQLITE_CORRUPT/i.test(text) ||
|
|
32
|
+
/SQLITE_NOTADB/i.test(text) ||
|
|
33
|
+
/database disk image is malformed/i.test(text) ||
|
|
34
|
+
/file is not a database/i.test(text) ||
|
|
35
|
+
/malformed database schema/i.test(text));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Open the database, and if the FILE ITSELF is corrupt, destroy it and open a fresh one — ONCE.
|
|
39
|
+
*
|
|
40
|
+
* Safe in a way it would not be for most databases: this replica is 100% DERIVED. Every row came from
|
|
41
|
+
* `/snapshot` plus the change feed, so the local copy holds nothing that does not exist upstream, and
|
|
42
|
+
* re-seeding is the ordinary cold-start path rather than a recovery hack.
|
|
43
|
+
*
|
|
44
|
+
* Without this, one corrupt page bricked the replica PERMANENTLY. The corruption survives reloads, so
|
|
45
|
+
* `start()` threw on every subsequent boot and the reader sat behind a red pill forever unless they
|
|
46
|
+
* knew to clear origin storage by hand — a disposable cache that could not dispose of itself. That is
|
|
47
|
+
* exactly how CTC-337 presented: `SQLITE_CORRUPT: database disk image is malformed`, every load.
|
|
48
|
+
*
|
|
49
|
+
* Retries exactly once. A second failure after a wipe is not corruption we can clear — it is a broken
|
|
50
|
+
* environment (no quota, VFS refusing to register) — and looping would turn it into a hang.
|
|
51
|
+
*/
|
|
52
|
+
export async function openWithCorruptionRecovery(pool, dbPath, build, log = (m) => console.warn(m)) {
|
|
53
|
+
try {
|
|
54
|
+
return build(new pool.OpfsSAHPoolDb(dbPath));
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (!isCorruptDatabaseError(err))
|
|
58
|
+
throw err;
|
|
59
|
+
// Loud on purpose. Healing silently would hide that corruption HAPPENED, and the RATE of it is
|
|
60
|
+
// the signal worth having — a replica that quietly re-seeds every session is a different bug.
|
|
61
|
+
log(`[replica] OPFS database ${dbPath} is corrupt (${String(err)}); wiping and re-seeding — the replica is derived, so nothing is lost`);
|
|
62
|
+
await pool.wipeFiles();
|
|
63
|
+
return build(new pool.OpfsSAHPoolDb(dbPath));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
13
66
|
/**
|
|
14
67
|
* Open the OPFS SAHPool replica at `dbPath` (absolute, leading slash REQUIRED) and bring its schema up
|
|
15
68
|
* to date via the shared migration bundle. Async — wasm init + SAHPool acquire are async, unlike the
|
|
@@ -22,7 +75,6 @@ export async function openReplica(dbPath, directory) {
|
|
|
22
75
|
initialCapacity: 6, // ≥ 2× the db count (one db + journals).
|
|
23
76
|
directory, // one OPFS dir per app, so engines don't collide.
|
|
24
77
|
});
|
|
25
|
-
|
|
26
|
-
return buildOpenedReplica(db);
|
|
78
|
+
return openWithCorruptionRecovery(poolUtil, dbPath, (db) => buildOpenedReplica(db));
|
|
27
79
|
}
|
|
28
80
|
//# sourceMappingURL=sqlite-db.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-db.js","sourceRoot":"","sources":["../../../src/replica/browser/sqlite-db.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,mGAAmG;AACnG,sGAAsG;AACtG,0BAA0B;AAC1B,EAAE;AACF,qGAAqG;AACrG,kGAAkG;AAClG,kGAAkG;AAClG,qGAAqG;AACrG,uBAAuB;AAEvB,OAAO,iBAAiB,MAAM,yBAAyB,CAAC;AAExD,OAAO,EAAE,kBAAkB,EAAsB,MAAM,YAAY,CAAC;AAIpE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc,EAAE,SAAiB;IACjE,MAAM,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAC1C,MAAM,QAAQ,GAAgB,MAAM,OAAO,CAAC,qBAAqB,CAAC;QAChE,WAAW,EAAE,KAAK,EAAE,8DAA8D;QAClF,eAAe,EAAE,CAAC,EAAE,yCAAyC;QAC7D,SAAS,EAAE,kDAAkD;KAC9D,CAAC,CAAC;IACH,
|
|
1
|
+
{"version":3,"file":"sqlite-db.js","sourceRoot":"","sources":["../../../src/replica/browser/sqlite-db.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,mGAAmG;AACnG,sGAAsG;AACtG,0BAA0B;AAC1B,EAAE;AACF,qGAAqG;AACrG,kGAAkG;AAClG,kGAAkG;AAClG,qGAAqG;AACrG,uBAAuB;AAEvB,OAAO,iBAAiB,MAAM,yBAAyB,CAAC;AAExD,OAAO,EAAE,kBAAkB,EAAsB,MAAM,YAAY,CAAC;AAIpE;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAY;IACjD,MAAM,IAAI,GACR,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5F,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC9B,iGAAiG;IACjG,iFAAiF;IACjF,IAAI,+CAA+C,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7E,OAAO,CACL,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5B,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC;QAC3B,mCAAmC,CAAC,IAAI,CAAC,IAAI,CAAC;QAC9C,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC;QACpC,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,CACxC,CAAC;AACJ,CAAC;AAQD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,IAAqB,EACrB,MAAc,EACd,KAAqC,EACrC,MAAiC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAEvD,IAAI,CAAC;QACH,OAAO,KAAK,CAAC,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC;YAAE,MAAM,GAAG,CAAC;QAC5C,+FAA+F;QAC/F,8FAA8F;QAC9F,GAAG,CACD,2BAA2B,MAAM,gBAAgB,MAAM,CAAC,GAAG,CAAC,uEAAuE,CACpI,CAAC;QACF,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc,EAAE,SAAiB;IACjE,MAAM,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAC1C,MAAM,QAAQ,GAAgB,MAAM,OAAO,CAAC,qBAAqB,CAAC;QAChE,WAAW,EAAE,KAAK,EAAE,8DAA8D;QAClF,eAAe,EAAE,CAAC,EAAE,yCAAyC;QAC7D,SAAS,EAAE,kDAAkD;KAC9D,CAAC,CAAC;IACH,OAAO,0BAA0B,CAAC,QAAsC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CACvF,kBAAkB,CAAC,EAA8C,CAAC,CACnE,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Keep a live local copy of your Linear and GitHub project data \u2014 pushed in real time, without polling rate limits or webhook tunnels.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Coalesce Labs",
|