data-of-loathing 3.0.3 → 3.1.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.
- package/README.md +14 -3
- package/dist/SqliteWorkerDialect.d.ts +12 -0
- package/dist/SqliteWorkerDialect.js +72 -0
- package/dist/browser.d.ts +9 -2
- package/dist/browser.js +61 -6
- package/dist/node.js +2 -2
- package/dist/vite.d.ts +4 -0
- package/dist/vite.js +23 -0
- package/dist/workers/memory.js +28 -0
- package/dist/workers/opfs.d.ts +1 -0
- package/dist/workers/opfs.js +25 -0
- package/dist/workers/ranged.d.ts +1 -0
- package/dist/workers/ranged.js +203 -0
- package/package.json +7 -9
- package/dist/browser-client.d.ts +0 -17
- package/dist/browser-client.js +0 -58
- package/dist/browser-strategies.d.ts +0 -9
- package/dist/browser-strategies.js +0 -37
- package/dist/client.d.ts +0 -11
- package/dist/client.js +0 -29
- package/dist/http-range-worker.js +0 -25844
- package/dist/http-vfs-dialect.d.ts +0 -9
- package/dist/http-vfs-dialect.js +0 -93
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -5
- package/dist/sqlite3.wasm +0 -0
- package/dist/strategies.d.ts +0 -15
- package/dist/strategies.js +0 -46
- /package/dist/{http-range-worker.d.ts → workers/memory.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -24,8 +24,19 @@ export default defineConfig({
|
|
|
24
24
|
});
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The plugin does
|
|
27
|
+
The plugin does two things by default:
|
|
28
28
|
|
|
29
|
-
- Configures Vite's dependency optimiser
|
|
29
|
+
- Configures Vite's dependency optimiser to prevent pre-bundling of the SQLite WASM
|
|
30
30
|
- Polyfills `Buffer` (required by the underlying SQLite driver)
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
#### OPFS strategy
|
|
33
|
+
|
|
34
|
+
If you use the `"opfs"` strategy, SQLite requires `SharedArrayBuffer` and `Atomics`, which in turn require cross-origin isolation headers. Pass `{ coi: true }` to enable them on the dev server:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
export default defineConfig({
|
|
38
|
+
plugins: [dol({ coi: true })],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This sets `Cross-Origin-Embedder-Policy: credentialless` and `Cross-Origin-Opener-Policy: same-origin` on the dev server. For production you must set these headers at the hosting layer (e.g. Netlify `_headers`, Vercel `headers` config, nginx). The `credentialless` value is used rather than `require-corp` so cross-origin images are not blocked.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type DatabaseIntrospector, type Dialect, type DialectAdapter, type Driver, type Kysely, type QueryCompiler } from "kysely";
|
|
2
|
+
export declare class SqliteWorkerDialect implements Dialect {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(worker: Worker);
|
|
5
|
+
loadMemory(buffer: ArrayBuffer): Promise<void>;
|
|
6
|
+
loadOpfs(filename: string): Promise<void>;
|
|
7
|
+
loadRanged(url: string): Promise<void>;
|
|
8
|
+
createDriver(): Driver;
|
|
9
|
+
createQueryCompiler(): QueryCompiler;
|
|
10
|
+
createAdapter(): DialectAdapter;
|
|
11
|
+
createIntrospector(db: Kysely<any>): DatabaseIntrospector;
|
|
12
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, } from "kysely";
|
|
2
|
+
export class SqliteWorkerDialect {
|
|
3
|
+
#worker;
|
|
4
|
+
#pending = new Map();
|
|
5
|
+
#nextId = 0;
|
|
6
|
+
constructor(worker) {
|
|
7
|
+
this.#worker = worker;
|
|
8
|
+
this.#worker.onmessage = (event) => {
|
|
9
|
+
const pending = this.#pending.get(event.data.id);
|
|
10
|
+
if (!pending)
|
|
11
|
+
return;
|
|
12
|
+
this.#pending.delete(event.data.id);
|
|
13
|
+
if (event.data.type === "error") {
|
|
14
|
+
pending.reject(new Error(event.data.error));
|
|
15
|
+
}
|
|
16
|
+
else if (event.data.type === "exec") {
|
|
17
|
+
pending.resolve(event.data.rows);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
pending.resolve(undefined);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
#send(message, transfer) {
|
|
25
|
+
const id = String(++this.#nextId);
|
|
26
|
+
const promise = new Promise((resolve, reject) => {
|
|
27
|
+
this.#pending.set(id, { resolve: (v) => resolve(v), reject });
|
|
28
|
+
});
|
|
29
|
+
this.#worker.postMessage({ ...message, id }, transfer ?? []);
|
|
30
|
+
return promise;
|
|
31
|
+
}
|
|
32
|
+
loadMemory(buffer) {
|
|
33
|
+
return this.#send({ type: "load", buffer }, [buffer]);
|
|
34
|
+
}
|
|
35
|
+
loadOpfs(filename) {
|
|
36
|
+
return this.#send({ type: "load", filename });
|
|
37
|
+
}
|
|
38
|
+
loadRanged(url) {
|
|
39
|
+
return this.#send({ type: "load", url });
|
|
40
|
+
}
|
|
41
|
+
createDriver() {
|
|
42
|
+
const dialect = this;
|
|
43
|
+
const connection = {
|
|
44
|
+
executeQuery(query) {
|
|
45
|
+
return dialect
|
|
46
|
+
.#send({ type: "exec", sql: query.sql, bind: [...query.parameters] })
|
|
47
|
+
.then((rows) => ({ rows }));
|
|
48
|
+
},
|
|
49
|
+
async *streamQuery() {
|
|
50
|
+
throw new Error("Streaming not supported");
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
async init() { },
|
|
55
|
+
async acquireConnection() { return connection; },
|
|
56
|
+
async beginTransaction() { },
|
|
57
|
+
async commitTransaction() { },
|
|
58
|
+
async rollbackTransaction() { },
|
|
59
|
+
async releaseConnection() { },
|
|
60
|
+
async destroy() { dialect.#worker.terminate(); },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
createQueryCompiler() {
|
|
64
|
+
return new SqliteQueryCompiler();
|
|
65
|
+
}
|
|
66
|
+
createAdapter() {
|
|
67
|
+
return new SqliteAdapter();
|
|
68
|
+
}
|
|
69
|
+
createIntrospector(db) {
|
|
70
|
+
return new SqliteIntrospector(db);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { BaseClient } from "./BaseClient.js";
|
|
2
2
|
export type Strategy = {
|
|
3
|
-
strategy?: "
|
|
3
|
+
strategy?: "memory";
|
|
4
4
|
url?: string;
|
|
5
5
|
force?: boolean;
|
|
6
|
+
} | {
|
|
7
|
+
strategy: "opfs";
|
|
8
|
+
url?: string;
|
|
9
|
+
force?: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
strategy: "ranged";
|
|
12
|
+
url?: string;
|
|
6
13
|
};
|
|
7
14
|
export declare class Client extends BaseClient<Strategy> {
|
|
8
|
-
private
|
|
15
|
+
#private;
|
|
9
16
|
constructor(strategy?: Strategy);
|
|
10
17
|
protected getStoredEtag(_key: string): Promise<string | null>;
|
|
11
18
|
protected storeEtag(_key: string, etag: string): Promise<void>;
|
package/dist/browser.js
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import { SqlMikroORM, SqliteDriver } from "@mikro-orm/sql";
|
|
2
|
-
import { SQLocalKysely } from "sqlocal/kysely";
|
|
3
2
|
import { entities } from "./schema.js";
|
|
3
|
+
import { SqliteWorkerDialect } from "./SqliteWorkerDialect.js";
|
|
4
4
|
import { BaseClient, DEFAULT_URL, ETAG_KEY } from "./BaseClient.js";
|
|
5
|
+
async function getOpfsEtag() {
|
|
6
|
+
try {
|
|
7
|
+
const root = await navigator.storage.getDirectory();
|
|
8
|
+
const handle = await root.getFileHandle(".dol-etag");
|
|
9
|
+
return (await handle.getFile()).text();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function setOpfsEtag(etag) {
|
|
16
|
+
const root = await navigator.storage.getDirectory();
|
|
17
|
+
const handle = await root.getFileHandle(".dol-etag", { create: true });
|
|
18
|
+
const writable = await handle.createWritable();
|
|
19
|
+
await writable.write(etag);
|
|
20
|
+
await writable.close();
|
|
21
|
+
}
|
|
22
|
+
async function writeToOpfs(filename, buffer) {
|
|
23
|
+
const root = await navigator.storage.getDirectory();
|
|
24
|
+
const handle = await root.getFileHandle(filename, { create: true });
|
|
25
|
+
const writable = await handle.createWritable();
|
|
26
|
+
await writable.write(buffer);
|
|
27
|
+
await writable.close();
|
|
28
|
+
}
|
|
5
29
|
export class Client extends BaseClient {
|
|
6
|
-
|
|
30
|
+
#dialect;
|
|
7
31
|
constructor(strategy = {}) {
|
|
8
32
|
super(strategy);
|
|
9
33
|
}
|
|
@@ -15,14 +39,45 @@ export class Client extends BaseClient {
|
|
|
15
39
|
}
|
|
16
40
|
async load() {
|
|
17
41
|
await this._orm?.close();
|
|
42
|
+
this.#dialect?.createDriver().destroy();
|
|
43
|
+
const strategy = this._strategy;
|
|
18
44
|
const url = this._strategy.url ?? DEFAULT_URL;
|
|
19
|
-
|
|
20
|
-
|
|
45
|
+
switch (strategy.strategy) {
|
|
46
|
+
case "ranged": {
|
|
47
|
+
this.#dialect = new SqliteWorkerDialect(new Worker(new URL("./workers/ranged.js", import.meta.url), { type: "module" }));
|
|
48
|
+
await this.#dialect.loadRanged(url);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case "opfs": {
|
|
52
|
+
this.#dialect = new SqliteWorkerDialect(new Worker(new URL("./workers/opfs.js", import.meta.url), { type: "module" }));
|
|
53
|
+
const { force = false } = strategy;
|
|
54
|
+
const remoteEtag = (await fetch(url, { method: "HEAD" })).headers.get("etag");
|
|
55
|
+
const storedEtag = force ? null : await getOpfsEtag();
|
|
56
|
+
if (force || storedEtag !== remoteEtag) {
|
|
57
|
+
const buffer = await this.fetchDb(url);
|
|
58
|
+
await writeToOpfs("dol.sqlite", buffer);
|
|
59
|
+
if (remoteEtag)
|
|
60
|
+
await setOpfsEtag(remoteEtag);
|
|
61
|
+
}
|
|
62
|
+
await this.#dialect.loadOpfs("/dol.sqlite");
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "memory":
|
|
66
|
+
default: {
|
|
67
|
+
this.#dialect = new SqliteWorkerDialect(new Worker(new URL("./workers/memory.js", import.meta.url), { type: "module" }));
|
|
68
|
+
const { force = false } = strategy;
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
cache: force ? "reload" : "default",
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
throw new Error(`Failed to fetch database: ${response.status}`);
|
|
74
|
+
const buffer = await response.arrayBuffer();
|
|
75
|
+
await this.#dialect.loadMemory(buffer);
|
|
76
|
+
}
|
|
21
77
|
}
|
|
22
|
-
await this.syncIfNeeded(url, ETAG_KEY, (data) => this._db.overwriteDatabaseFile(data), this._strategy.force);
|
|
23
78
|
this._orm = await SqlMikroORM.init({
|
|
24
79
|
driver: SqliteDriver,
|
|
25
|
-
driverOptions: this
|
|
80
|
+
driverOptions: this.#dialect,
|
|
26
81
|
dbName: "dol.sqlite",
|
|
27
82
|
entities,
|
|
28
83
|
allowGlobalContext: true,
|
package/dist/node.js
CHANGED
|
@@ -19,10 +19,10 @@ export class Client extends BaseClient {
|
|
|
19
19
|
const strategy = this._strategy;
|
|
20
20
|
switch (strategy.strategy) {
|
|
21
21
|
case "local":
|
|
22
|
-
return
|
|
22
|
+
return strategy.path;
|
|
23
23
|
default:
|
|
24
24
|
case "url": {
|
|
25
|
-
const { url = DEFAULT_URL, force = false } =
|
|
25
|
+
const { url = DEFAULT_URL, force = false } = strategy;
|
|
26
26
|
const cacheDir = join(homedir(), ".cache", "data-of-loathing");
|
|
27
27
|
const dbPath = join(cacheDir, "dol.sqlite");
|
|
28
28
|
const etagPath = join(cacheDir, "etag");
|
package/dist/vite.d.ts
ADDED
package/dist/vite.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
|
2
|
+
export default function dol({ coi = false } = {}) {
|
|
3
|
+
const plugin = {
|
|
4
|
+
name: "data-of-loathing",
|
|
5
|
+
config() {
|
|
6
|
+
return {
|
|
7
|
+
optimizeDeps: {
|
|
8
|
+
exclude: ["@sqlite.org/sqlite-wasm"],
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
if (coi) {
|
|
14
|
+
plugin.configureServer = (server) => {
|
|
15
|
+
server.middlewares.use((_, res, next) => {
|
|
16
|
+
res.setHeader("Cross-Origin-Embedder-Policy", "credentialless");
|
|
17
|
+
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
18
|
+
next();
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return [nodePolyfills({ include: ["buffer"] }), plugin];
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
2
|
+
let db;
|
|
3
|
+
let sqlite3;
|
|
4
|
+
self.onmessage = async (event) => {
|
|
5
|
+
const { type, id } = event.data;
|
|
6
|
+
try {
|
|
7
|
+
sqlite3 ??= await sqlite3InitModule();
|
|
8
|
+
switch (type) {
|
|
9
|
+
case "load": {
|
|
10
|
+
const { buffer } = event.data;
|
|
11
|
+
const p = sqlite3.wasm.allocFromTypedArray(new Uint8Array(buffer));
|
|
12
|
+
db = new sqlite3.oo1.DB();
|
|
13
|
+
sqlite3.capi.sqlite3_deserialize(db.pointer, "main", p, buffer.byteLength, buffer.byteLength, sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE | sqlite3.capi.SQLITE_DESERIALIZE_READONLY);
|
|
14
|
+
self.postMessage({ id, type: "loaded" });
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
case "exec": {
|
|
18
|
+
const rows = [];
|
|
19
|
+
db.exec({ sql: event.data.sql, bind: event.data.bind, rowMode: "object", resultRows: rows });
|
|
20
|
+
self.postMessage({ id, type: "exec", rows });
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
self.postMessage({ id, type: "error", error: String(e) });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
2
|
+
let db;
|
|
3
|
+
let sqlite3;
|
|
4
|
+
self.onmessage = async (event) => {
|
|
5
|
+
const { type, id } = event.data;
|
|
6
|
+
try {
|
|
7
|
+
sqlite3 ??= await sqlite3InitModule();
|
|
8
|
+
switch (type) {
|
|
9
|
+
case "load": {
|
|
10
|
+
db = new sqlite3.oo1.OpfsDb(event.data.filename);
|
|
11
|
+
self.postMessage({ id, type: "loaded" });
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
case "exec": {
|
|
15
|
+
const rows = [];
|
|
16
|
+
db.exec({ sql: event.data.sql, bind: event.data.bind, rowMode: "object", resultRows: rows });
|
|
17
|
+
self.postMessage({ id, type: "exec", rows });
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
self.postMessage({ id, type: "error", error: String(e) });
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
2
|
+
let db;
|
|
3
|
+
let sqlite3;
|
|
4
|
+
const fileUrls = new Map();
|
|
5
|
+
const fileSizes = new Map();
|
|
6
|
+
const readCache = new Map();
|
|
7
|
+
let vfsInstalled = false;
|
|
8
|
+
function syncFetchRange(url, start, end) {
|
|
9
|
+
try {
|
|
10
|
+
const xhr = new XMLHttpRequest();
|
|
11
|
+
xhr.open("GET", url, false);
|
|
12
|
+
xhr.responseType = "arraybuffer";
|
|
13
|
+
xhr.setRequestHeader("Range", `bytes=${start}-${end}`);
|
|
14
|
+
xhr.send();
|
|
15
|
+
if (xhr.status !== 200 && xhr.status !== 206)
|
|
16
|
+
return null;
|
|
17
|
+
const data = new Uint8Array(xhr.response);
|
|
18
|
+
let totalSize = null;
|
|
19
|
+
const contentRange = xhr.getResponseHeader("Content-Range");
|
|
20
|
+
if (contentRange) {
|
|
21
|
+
const match = /\/(\d+)$/.exec(contentRange);
|
|
22
|
+
if (match)
|
|
23
|
+
totalSize = parseInt(match[1], 10);
|
|
24
|
+
}
|
|
25
|
+
if (totalSize === null) {
|
|
26
|
+
const cl = xhr.getResponseHeader("Content-Length");
|
|
27
|
+
if (cl)
|
|
28
|
+
totalSize = parseInt(cl, 10);
|
|
29
|
+
}
|
|
30
|
+
return { data, totalSize };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function structSizeof(capi) {
|
|
37
|
+
const tmp = new capi.sqlite3_file();
|
|
38
|
+
const size = tmp.structInfo.sizeof;
|
|
39
|
+
tmp.dispose();
|
|
40
|
+
return size;
|
|
41
|
+
}
|
|
42
|
+
function installRangedVfs(s3) {
|
|
43
|
+
if (vfsInstalled)
|
|
44
|
+
return;
|
|
45
|
+
vfsInstalled = true;
|
|
46
|
+
const { capi, wasm } = s3;
|
|
47
|
+
const ioMethods = new capi.sqlite3_io_methods();
|
|
48
|
+
ioMethods.iVersion = 1;
|
|
49
|
+
const vfsStruct = new capi.sqlite3_vfs();
|
|
50
|
+
vfsStruct.$iVersion = 1;
|
|
51
|
+
vfsStruct.$szOsFile = structSizeof(capi);
|
|
52
|
+
vfsStruct.$mxPathname = 2048;
|
|
53
|
+
s3.vfs.installVfs({
|
|
54
|
+
io: {
|
|
55
|
+
struct: ioMethods,
|
|
56
|
+
methods: {
|
|
57
|
+
xClose(pFile) {
|
|
58
|
+
fileUrls.delete(pFile);
|
|
59
|
+
fileSizes.delete(pFile);
|
|
60
|
+
readCache.delete(pFile);
|
|
61
|
+
return capi.SQLITE_OK;
|
|
62
|
+
},
|
|
63
|
+
xRead(pFile, pBuf, iAmt, iOfst) {
|
|
64
|
+
const url = fileUrls.get(pFile);
|
|
65
|
+
if (!url)
|
|
66
|
+
return capi.SQLITE_IOERR_READ;
|
|
67
|
+
// iOfst is BigInt at runtime in WASM_BIGINT builds despite the number type
|
|
68
|
+
const offset = Number(iOfst);
|
|
69
|
+
const cacheKey = `${offset}:${iAmt}`;
|
|
70
|
+
const cache = readCache.get(pFile);
|
|
71
|
+
const cached = cache?.get(cacheKey);
|
|
72
|
+
if (cached) {
|
|
73
|
+
wasm.heap8u().set(cached, pBuf);
|
|
74
|
+
return capi.SQLITE_OK;
|
|
75
|
+
}
|
|
76
|
+
const result = syncFetchRange(url, offset, offset + iAmt - 1);
|
|
77
|
+
if (!result)
|
|
78
|
+
return capi.SQLITE_IOERR_READ;
|
|
79
|
+
cache?.set(cacheKey, result.data);
|
|
80
|
+
const heap = wasm.heap8u();
|
|
81
|
+
const toCopy = Math.min(result.data.length, iAmt);
|
|
82
|
+
heap.set(result.data.subarray(0, toCopy), pBuf);
|
|
83
|
+
if (toCopy < iAmt) {
|
|
84
|
+
heap.fill(0, pBuf + toCopy, pBuf + iAmt);
|
|
85
|
+
return capi.SQLITE_IOERR_SHORT_READ;
|
|
86
|
+
}
|
|
87
|
+
return capi.SQLITE_OK;
|
|
88
|
+
},
|
|
89
|
+
xWrite() { return capi.SQLITE_READONLY; },
|
|
90
|
+
xTruncate() { return capi.SQLITE_READONLY; },
|
|
91
|
+
xSync() { return capi.SQLITE_OK; },
|
|
92
|
+
xFileSize(pFile, pSize) {
|
|
93
|
+
const url = fileUrls.get(pFile);
|
|
94
|
+
if (!url)
|
|
95
|
+
return capi.SQLITE_IOERR;
|
|
96
|
+
let size = fileSizes.get(pFile);
|
|
97
|
+
if (size === undefined) {
|
|
98
|
+
const result = syncFetchRange(url, 0, 0);
|
|
99
|
+
size = result?.totalSize ?? 0;
|
|
100
|
+
fileSizes.set(pFile, size);
|
|
101
|
+
}
|
|
102
|
+
// Write i64 as two i32s (little-endian); poke("i64") requires BigInt in WASM_BIGINT mode
|
|
103
|
+
wasm.poke(pSize, size >>> 0, "i32");
|
|
104
|
+
wasm.poke(pSize + 4, Math.floor(size / 0x100000000), "i32");
|
|
105
|
+
return capi.SQLITE_OK;
|
|
106
|
+
},
|
|
107
|
+
xLock() { return capi.SQLITE_OK; },
|
|
108
|
+
xUnlock() { return capi.SQLITE_OK; },
|
|
109
|
+
xCheckReservedLock(_pFile, pResOut) {
|
|
110
|
+
wasm.poke(pResOut, 0, "i32");
|
|
111
|
+
return capi.SQLITE_OK;
|
|
112
|
+
},
|
|
113
|
+
xFileControl() { return capi.SQLITE_NOTFOUND; },
|
|
114
|
+
xSectorSize() { return 512; },
|
|
115
|
+
xDeviceCharacteristics() { return capi.SQLITE_IOCAP_IMMUTABLE; },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
vfs: {
|
|
119
|
+
struct: vfsStruct,
|
|
120
|
+
methods: {
|
|
121
|
+
xOpen(_pVfs, zName, pFile, _flags, pOutFlags) {
|
|
122
|
+
const url = zName ? (wasm.cstrToJs(zName) ?? "") : "";
|
|
123
|
+
const fileObj = new capi.sqlite3_file(pFile);
|
|
124
|
+
fileObj.$pMethods = ioMethods.pointer;
|
|
125
|
+
fileUrls.set(pFile, url);
|
|
126
|
+
const cache = new Map();
|
|
127
|
+
readCache.set(pFile, cache);
|
|
128
|
+
// Pre-fetch first 64KB to warm cache with header + early B-tree pages
|
|
129
|
+
const prefetch = syncFetchRange(url, 0, 65535);
|
|
130
|
+
if (prefetch) {
|
|
131
|
+
if (prefetch.totalSize != null)
|
|
132
|
+
fileSizes.set(pFile, prefetch.totalSize);
|
|
133
|
+
// Detect page size from SQLite header bytes 16-17 (big-endian); value of 1 means 65536
|
|
134
|
+
const rawPgSize = prefetch.data.length >= 18
|
|
135
|
+
? ((prefetch.data[16] << 8) | prefetch.data[17])
|
|
136
|
+
: 0;
|
|
137
|
+
const pageSize = rawPgSize === 1 ? 65536 : (rawPgSize || 4096);
|
|
138
|
+
for (let offset = 0; offset + pageSize <= prefetch.data.length; offset += pageSize) {
|
|
139
|
+
cache.set(`${offset}:${pageSize}`, prefetch.data.slice(offset, offset + pageSize));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (pOutFlags)
|
|
143
|
+
wasm.poke(pOutFlags, capi.SQLITE_OPEN_READONLY, "i32");
|
|
144
|
+
return capi.SQLITE_OK;
|
|
145
|
+
},
|
|
146
|
+
xDelete() { return 1034; /* SQLITE_IOERR_DELETE */ },
|
|
147
|
+
xAccess(_pVfs, _zName, _flags, pResOut) {
|
|
148
|
+
wasm.poke(pResOut, 1, "i32");
|
|
149
|
+
return capi.SQLITE_OK;
|
|
150
|
+
},
|
|
151
|
+
xFullPathname(_pVfs, zName, nOut, zOut) {
|
|
152
|
+
const name = wasm.cstrToJs(zName) ?? "";
|
|
153
|
+
const bytes = new TextEncoder().encode(name + "\0");
|
|
154
|
+
if (bytes.length > nOut)
|
|
155
|
+
return capi.SQLITE_CANTOPEN;
|
|
156
|
+
wasm.heap8u().set(bytes, zOut);
|
|
157
|
+
return capi.SQLITE_OK;
|
|
158
|
+
},
|
|
159
|
+
xRandomness(_pVfs, nByte, zOut) {
|
|
160
|
+
const buf = new Uint8Array(nByte);
|
|
161
|
+
crypto.getRandomValues(buf);
|
|
162
|
+
wasm.heap8u().set(buf, zOut);
|
|
163
|
+
return nByte;
|
|
164
|
+
},
|
|
165
|
+
xSleep() { return capi.SQLITE_OK; },
|
|
166
|
+
xCurrentTime(_pVfs, pTimeOut) {
|
|
167
|
+
wasm.poke(pTimeOut, Date.now() / 86400000 + 2440587.5, "f64");
|
|
168
|
+
return capi.SQLITE_OK;
|
|
169
|
+
},
|
|
170
|
+
xGetLastError(_pVfs, nBuf, zBuf) {
|
|
171
|
+
if (nBuf > 0)
|
|
172
|
+
wasm.poke(zBuf, 0, "i8");
|
|
173
|
+
return 0;
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
name: "ranged",
|
|
177
|
+
asDefault: false,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
self.onmessage = async (event) => {
|
|
182
|
+
const { type, id } = event.data;
|
|
183
|
+
try {
|
|
184
|
+
sqlite3 ??= await sqlite3InitModule();
|
|
185
|
+
switch (type) {
|
|
186
|
+
case "load": {
|
|
187
|
+
installRangedVfs(sqlite3);
|
|
188
|
+
db = new sqlite3.oo1.DB(event.data.url, "r", "ranged");
|
|
189
|
+
self.postMessage({ id, type: "loaded" });
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "exec": {
|
|
193
|
+
const rows = [];
|
|
194
|
+
db.exec({ sql: event.data.sql, bind: event.data.bind, rowMode: "object", resultRows: rows });
|
|
195
|
+
self.postMessage({ id, type: "exec", rows });
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
self.postMessage({ id, type: "error", error: String(e) });
|
|
202
|
+
}
|
|
203
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-of-loathing",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.6.0",
|
|
6
6
|
"repository": {
|
|
7
|
-
"url": "https://github.com/loathers/data-of-loathing"
|
|
7
|
+
"url": "git+https://github.com/loathers/data-of-loathing.git"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
],
|
|
26
26
|
"typesVersions": {
|
|
27
27
|
"*": {
|
|
28
|
-
"vite": [
|
|
28
|
+
"vite": [
|
|
29
|
+
"./dist/vite.d.ts"
|
|
30
|
+
]
|
|
29
31
|
}
|
|
30
32
|
},
|
|
31
33
|
"scripts": {
|
|
@@ -33,24 +35,20 @@
|
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
35
37
|
"@mikro-orm/core": "^7.0.14",
|
|
36
|
-
"@mikro-orm/sql": "^7.0.14"
|
|
38
|
+
"@mikro-orm/sql": "^7.0.14",
|
|
39
|
+
"@sqlite.org/sqlite-wasm": "~3.51.2-build9"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|
|
39
42
|
"@types/node": "^25.0.2",
|
|
40
|
-
"sqlocal": "^0.18.0",
|
|
41
43
|
"typescript": "^5.9.3",
|
|
42
44
|
"vite": "^6.0.0",
|
|
43
45
|
"vite-plugin-node-polyfills": "^0.26.0"
|
|
44
46
|
},
|
|
45
47
|
"peerDependencies": {
|
|
46
|
-
"sqlocal": "^0.18.0",
|
|
47
48
|
"vite": ">=5.0.0",
|
|
48
49
|
"vite-plugin-node-polyfills": "^0.26.0"
|
|
49
50
|
},
|
|
50
51
|
"peerDependenciesMeta": {
|
|
51
|
-
"sqlocal": {
|
|
52
|
-
"optional": true
|
|
53
|
-
},
|
|
54
52
|
"vite": {
|
|
55
53
|
"optional": true
|
|
56
54
|
},
|
package/dist/browser-client.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type EntityManager } from "@mikro-orm/core";
|
|
2
|
-
export type Strategy = {
|
|
3
|
-
strategy?: "cache";
|
|
4
|
-
url?: string;
|
|
5
|
-
} | {
|
|
6
|
-
strategy: "remote";
|
|
7
|
-
url?: string;
|
|
8
|
-
};
|
|
9
|
-
export declare class Client {
|
|
10
|
-
private _orm?;
|
|
11
|
-
private readonly _strategy;
|
|
12
|
-
constructor(strategy?: Strategy);
|
|
13
|
-
get em(): EntityManager;
|
|
14
|
-
load(): Promise<void>;
|
|
15
|
-
private _db?;
|
|
16
|
-
}
|
|
17
|
-
export declare function createClient(strategy?: Strategy): Client;
|
package/dist/browser-client.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { SqlMikroORM, SqliteDriver } from "@mikro-orm/sql";
|
|
2
|
-
import { HttpVFSDialect } from "./http-vfs-dialect.js";
|
|
3
|
-
import { entities } from "data-of-loathing-schema";
|
|
4
|
-
const DEFAULT_URL = "https://data.loathers.net/dol.sqlite";
|
|
5
|
-
const ETAG_KEY = "dol-etag";
|
|
6
|
-
export class Client {
|
|
7
|
-
_orm;
|
|
8
|
-
_strategy;
|
|
9
|
-
constructor(strategy = {}) {
|
|
10
|
-
this._strategy = strategy;
|
|
11
|
-
}
|
|
12
|
-
get em() {
|
|
13
|
-
if (!this._orm)
|
|
14
|
-
throw new Error("Call await client.load() before querying");
|
|
15
|
-
return this._orm.em;
|
|
16
|
-
}
|
|
17
|
-
async load() {
|
|
18
|
-
await this._orm?.close();
|
|
19
|
-
const url = this._strategy.url ?? DEFAULT_URL;
|
|
20
|
-
if (this._strategy.strategy === "remote") {
|
|
21
|
-
this._orm = await SqlMikroORM.init({
|
|
22
|
-
driver: SqliteDriver,
|
|
23
|
-
driverOptions: new HttpVFSDialect(url),
|
|
24
|
-
dbName: url,
|
|
25
|
-
entities,
|
|
26
|
-
allowGlobalContext: true,
|
|
27
|
-
});
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const { SQLocalKysely } = await import("sqlocal/kysely");
|
|
31
|
-
if (!this._db) {
|
|
32
|
-
this._db = new SQLocalKysely("dol.sqlite");
|
|
33
|
-
}
|
|
34
|
-
const storedEtag = localStorage.getItem(ETAG_KEY);
|
|
35
|
-
const head = await fetch(url, { method: "HEAD" });
|
|
36
|
-
const remoteEtag = head.headers.get("etag");
|
|
37
|
-
if (!storedEtag || storedEtag !== remoteEtag) {
|
|
38
|
-
const response = await fetch(url);
|
|
39
|
-
if (!response.ok)
|
|
40
|
-
throw new Error(`Failed to fetch database: ${response.status}`);
|
|
41
|
-
await this._db.overwriteDatabaseFile(await response.arrayBuffer());
|
|
42
|
-
if (remoteEtag)
|
|
43
|
-
localStorage.setItem(ETAG_KEY, remoteEtag);
|
|
44
|
-
}
|
|
45
|
-
this._orm = await SqlMikroORM.init({
|
|
46
|
-
driver: SqliteDriver,
|
|
47
|
-
driverOptions: this._db.dialect,
|
|
48
|
-
dbName: "dol.sqlite",
|
|
49
|
-
entities,
|
|
50
|
-
allowGlobalContext: true,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
// sqlocal instance kept across load() calls to preserve OPFS connection
|
|
54
|
-
_db;
|
|
55
|
-
}
|
|
56
|
-
export function createClient(strategy = {}) {
|
|
57
|
-
return new Client(strategy);
|
|
58
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export declare const DEFAULT_URL = "https://data.loathers.net/data-of-loathing.sqlite";
|
|
2
|
-
export type BrowserStrategy = {
|
|
3
|
-
strategy?: "cache";
|
|
4
|
-
url?: string;
|
|
5
|
-
} | {
|
|
6
|
-
strategy: "remote";
|
|
7
|
-
url?: string;
|
|
8
|
-
};
|
|
9
|
-
export declare function resolveDbPath(opts: BrowserStrategy): Promise<string>;
|