totalum-sdk 0.1.0-dev.6 → 0.1.0-dev.7
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/dist/d1/index.d.ts +12 -1
- package/dist/d1/index.js +17 -3
- package/dist/d1/lazy.js +33 -11
- package/dist/d1/libsql.js +24 -4
- package/dist/d1/session.js +38 -0
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/d1/index.d.ts
CHANGED
|
@@ -84,6 +84,17 @@ declare class TotalumD1Error extends TotalumError {
|
|
|
84
84
|
}
|
|
85
85
|
declare function isTotalumD1Error(e: unknown): e is TotalumD1Error;
|
|
86
86
|
|
|
87
|
+
/** Set on every mutating response; while the browser holds it, its reads go to the primary (read-your-writes). */
|
|
88
|
+
declare const READ_YOUR_WRITES_COOKIE = "__tlm_d1_w";
|
|
89
|
+
/**
|
|
90
|
+
* Runs one request of a Worker with a replicated D1 (A3.23, research 46 §6.3): a `GET`/`HEAD` reads through a
|
|
91
|
+
* `first-unconstrained` session — the nearest replica — unless the browser wrote in the last 10 s; any other method
|
|
92
|
+
* uses the plain binding (writes go to the primary; measured faster than any session) and its response sets the short
|
|
93
|
+
* cookie that sends that browser's next reads to the primary. `run` receives the env to hand to the app, whose
|
|
94
|
+
* `totalumD1()` reads `TOTALUM_DB` from it on every statement. No D1 binding (Turso, a laptop): `run(env)` unchanged.
|
|
95
|
+
*/
|
|
96
|
+
declare function withTotalumSession<E extends object>(request: Request, env: E, run: (env: E) => Promise<Response>): Promise<Response>;
|
|
97
|
+
|
|
87
98
|
/**
|
|
88
99
|
* A `D1Database` for the project's own database wherever the code runs (A3.4, plan 05 §3): the bound
|
|
89
100
|
* `TOTALUM_DB` inside a Worker, the libsql adapter when the environment moved to Turso, and an HTTPS client to
|
|
@@ -91,5 +102,5 @@ declare function isTotalumD1Error(e: unknown): e is TotalumD1Error;
|
|
|
91
102
|
*/
|
|
92
103
|
declare function totalumD1(options?: TotalumD1Options): D1Database;
|
|
93
104
|
|
|
94
|
-
export { TotalumD1Error, TotalumError, TotalumErrorCode, isTotalumD1Error, totalumD1 };
|
|
105
|
+
export { READ_YOUR_WRITES_COOKIE, TotalumD1Error, TotalumError, TotalumErrorCode, isTotalumD1Error, totalumD1, withTotalumSession };
|
|
95
106
|
export type { D1Database, D1DatabaseSession, D1ExecResult, D1PreparedStatement, D1Result, TotalumD1ClientCode, TotalumD1ErrorCode, TotalumD1Options };
|
package/dist/d1/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { lazyD1 } from './lazy.js';
|
|
|
6
6
|
import { libsqlExecutor } from './libsql.js';
|
|
7
7
|
export { TotalumError, isTotalumError } from '../errors.js';
|
|
8
8
|
export { TotalumD1Error, isTotalumD1Error } from './errors.js';
|
|
9
|
+
export { READ_YOUR_WRITES_COOKIE, withTotalumSession } from './session.js';
|
|
9
10
|
/**
|
|
10
11
|
* A `D1Database` for the project's own database wherever the code runs (A3.4, plan 05 §3): the bound
|
|
11
12
|
* `TOTALUM_DB` inside a Worker, the libsql adapter when the environment moved to Turso, and an HTTPS client to
|
|
@@ -36,15 +37,28 @@ async function resolveTarget(options) {
|
|
|
36
37
|
destructive: options.destructive === true,
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* OpenNext exposes the request's bindings; absent (a laptop) or outside a request, there is no binding. The executor
|
|
42
|
+
* reads `TOTALUM_DB` again on every statement: it is the current request's database — the plain binding, or the
|
|
43
|
+
* read-replica session `withTotalumSession` put there — never the first request's for the isolate's life.
|
|
44
|
+
*/
|
|
40
45
|
async function openNextBinding() {
|
|
46
|
+
let openNext;
|
|
41
47
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
openNext = (await import('@opennextjs/cloudflare'));
|
|
49
|
+
if (!openNext.getCloudflareContext().env['TOTALUM_DB'])
|
|
50
|
+
return undefined;
|
|
44
51
|
}
|
|
45
52
|
catch {
|
|
46
53
|
return undefined;
|
|
47
54
|
}
|
|
55
|
+
const current = () => openNext.getCloudflareContext().env['TOTALUM_DB'];
|
|
56
|
+
return {
|
|
57
|
+
prepare: (sql) => current().prepare(sql),
|
|
58
|
+
batch: (statements) => current().batch(statements),
|
|
59
|
+
exec: (sql) => current().exec(sql),
|
|
60
|
+
withSession: (c) => current().withSession(c),
|
|
61
|
+
};
|
|
48
62
|
}
|
|
49
63
|
async function tursoTarget() {
|
|
50
64
|
const url = readEnv('TURSO_DATABASE_URL');
|
package/dist/d1/lazy.js
CHANGED
|
@@ -2,8 +2,10 @@ import { TotalumD1Error } from './errors.js';
|
|
|
2
2
|
import { splitStatements } from './sql.js';
|
|
3
3
|
/**
|
|
4
4
|
* The object `totalumD1()` hands out: no I/O and no environment detection until the first statement runs;
|
|
5
|
-
* the target is resolved once and cached (plan 05 §3.1)
|
|
6
|
-
*
|
|
5
|
+
* the target is resolved once and cached (plan 05 §3.1) — in a Worker that target reads the request's own binding on
|
|
6
|
+
* every statement (`index.ts`), so one module-scope instance serves every request with that request's database.
|
|
7
|
+
* Statements remember `sql` + params and are materialised on the target when executed, so `drizzle(totalumD1())` at
|
|
8
|
+
* module scope is legal.
|
|
7
9
|
*/
|
|
8
10
|
export function lazyD1(resolve, migrate) {
|
|
9
11
|
let target;
|
|
@@ -15,15 +17,17 @@ export function lazyD1(resolve, migrate) {
|
|
|
15
17
|
class LazyStatement {
|
|
16
18
|
sql;
|
|
17
19
|
params;
|
|
18
|
-
|
|
20
|
+
on;
|
|
21
|
+
constructor(sql, params = [], on = resolved) {
|
|
19
22
|
this.sql = sql;
|
|
20
23
|
this.params = params;
|
|
24
|
+
this.on = on;
|
|
21
25
|
}
|
|
22
26
|
bind(...values) {
|
|
23
|
-
return new LazyStatement(this.sql, values);
|
|
27
|
+
return new LazyStatement(this.sql, values, this.on);
|
|
24
28
|
}
|
|
25
29
|
async real() {
|
|
26
|
-
return (await
|
|
30
|
+
return (await this.on()).prepare(this.sql).bind(...this.params);
|
|
27
31
|
}
|
|
28
32
|
async first(colName) {
|
|
29
33
|
return (await this.real()).first(colName);
|
|
@@ -39,8 +43,8 @@ export function lazyD1(resolve, migrate) {
|
|
|
39
43
|
return options?.columnNames === true ? s.raw({ columnNames: true }) : s.raw();
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
|
-
const
|
|
43
|
-
const t = await
|
|
46
|
+
const batchOn = (on) => async (statements) => {
|
|
47
|
+
const t = await on();
|
|
44
48
|
const real = statements.map((s) => {
|
|
45
49
|
if (!(s instanceof LazyStatement)) {
|
|
46
50
|
throw TotalumD1Error.client('BATCH_FOREIGN_STATEMENT', 'batch() only accepts statements prepared by this totalumD1() instance');
|
|
@@ -49,6 +53,26 @@ export function lazyD1(resolve, migrate) {
|
|
|
49
53
|
});
|
|
50
54
|
return t.batch(real);
|
|
51
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* A D1 session (read replication, A3.23): on the native binding the binding's own `withSession()`, created on the
|
|
58
|
+
* first statement, so bookmarks work in hand-written code; on HTTPS and Turso there are no replicas and the session
|
|
59
|
+
* is the database itself, `getBookmark()` → `null`.
|
|
60
|
+
*/
|
|
61
|
+
const withSession = (constraintOrBookmark) => {
|
|
62
|
+
let session;
|
|
63
|
+
let real;
|
|
64
|
+
const on = () => (session ??= resolved().then((t) => {
|
|
65
|
+
if (!t.withSession)
|
|
66
|
+
return t;
|
|
67
|
+
real = t.withSession(constraintOrBookmark);
|
|
68
|
+
return real;
|
|
69
|
+
}));
|
|
70
|
+
return {
|
|
71
|
+
prepare: (sql) => new LazyStatement(sql, [], on),
|
|
72
|
+
batch: batchOn(on),
|
|
73
|
+
getBookmark: () => real?.getBookmark() ?? null,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
52
76
|
const exec = async (script) => {
|
|
53
77
|
if (!migrate) {
|
|
54
78
|
throw TotalumD1Error.client('EXEC_REQUIRES_MIGRATE_MODE', 'exec() runs DDL scripts: use totalumD1({ mode: "migrate" }) from scripts/migrate.ts, or db.batch([...]) for data');
|
|
@@ -62,11 +86,9 @@ export function lazyD1(resolve, migrate) {
|
|
|
62
86
|
};
|
|
63
87
|
return {
|
|
64
88
|
prepare: (sql) => new LazyStatement(sql),
|
|
65
|
-
batch,
|
|
89
|
+
batch: batchOn(resolved),
|
|
66
90
|
exec,
|
|
67
91
|
dump: () => Promise.reject(TotalumD1Error.client('NOT_SUPPORTED', 'dump() is not available; use the platform backups')),
|
|
68
|
-
withSession
|
|
69
|
-
throw TotalumD1Error.client('NOT_SUPPORTED', 'withSession() is not available in this SDK version');
|
|
70
|
-
},
|
|
92
|
+
withSession,
|
|
71
93
|
};
|
|
72
94
|
}
|
package/dist/d1/libsql.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { TotalumD1Error } from './errors.js';
|
|
2
2
|
import { assertBindable, pickFirst } from './sql.js';
|
|
3
|
+
/**
|
|
4
|
+
* D1 enforces foreign keys; a Turso database on the new engine (A3.24) starts every connection with them off, and each
|
|
5
|
+
* Hrana request is a new connection. So every request carries the pragma, inside the one transaction libsql opens for
|
|
6
|
+
* it (the new engine applies it there; libSQL has them on already and ignores it inside a transaction).
|
|
7
|
+
*/
|
|
8
|
+
const FOREIGN_KEYS_ON = { sql: 'PRAGMA foreign_keys = ON', args: [] };
|
|
3
9
|
/**
|
|
4
10
|
* The Turso path (A3.8, plan 05 §3.10): a `D1Database`-compatible executor over a libsql client created with
|
|
5
11
|
* `intMode: 'bigint'` (integers beyond 2^53 then degrade to the same double D1 returns instead of throwing).
|
|
@@ -42,8 +48,9 @@ export function libsqlExecutor(client) {
|
|
|
42
48
|
assertBindable(values);
|
|
43
49
|
return new LibsqlD1Statement(this.sql, values);
|
|
44
50
|
}
|
|
45
|
-
execute() {
|
|
46
|
-
|
|
51
|
+
async execute() {
|
|
52
|
+
const [, rs] = await run(() => client.batch([FOREIGN_KEYS_ON, { sql: this.sql, args: this.args }], 'deferred'));
|
|
53
|
+
return rs;
|
|
47
54
|
}
|
|
48
55
|
async first(colName) {
|
|
49
56
|
return pickFirst(toResult(await this.execute()).results, colName);
|
|
@@ -68,7 +75,9 @@ export function libsqlExecutor(client) {
|
|
|
68
75
|
sql: s.sql,
|
|
69
76
|
args: s.args,
|
|
70
77
|
}));
|
|
71
|
-
return (await run(() => client.batch(stmts, 'write')))
|
|
78
|
+
return (await run(() => client.batch([FOREIGN_KEYS_ON, ...stmts], 'write')))
|
|
79
|
+
.slice(1)
|
|
80
|
+
.map((rs) => toResult(rs));
|
|
72
81
|
},
|
|
73
82
|
};
|
|
74
83
|
}
|
|
@@ -79,6 +88,11 @@ function fromLibsqlValue(v) {
|
|
|
79
88
|
return Array.from(new Uint8Array(v));
|
|
80
89
|
return v;
|
|
81
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The new engine (A3.24, recorded 2026-09-24) words the same SQLite failures as `Tursodb error: Runtime error: <msg> (19)`
|
|
93
|
+
* or `Tursodb error: Parse error: <msg>`, and a composite key as `t.(a, b)` where SQLite says `t.a, t.b`.
|
|
94
|
+
*/
|
|
95
|
+
const TURSODB_PREFIX = /^Tursodb error: [A-Za-z ]+ error: /;
|
|
82
96
|
/**
|
|
83
97
|
* `LibsqlError { code, message: '<CODE>: <sqlite message>' }` → the `D1_ERROR: <sqlite message>: <CODE>` shape. A
|
|
84
98
|
* remote Turso database (recorded 2026-09-23) also prefixes `SQLite error: `, answers `SQLITE_UNKNOWN` where SQLite
|
|
@@ -96,7 +110,13 @@ function mapLibsqlError(e) {
|
|
|
96
110
|
const text = err.message
|
|
97
111
|
.replace(/^((SQLITE_\w+|SQL_PARSE_ERROR|SQL_INPUT_ERROR): )+/, '')
|
|
98
112
|
.replace(/^SQLite (input )?error: /, '')
|
|
99
|
-
.replace(/ \(at offset (\d+)\)$/, ' at offset $1')
|
|
113
|
+
.replace(/ \(at offset (\d+)\)$/, ' at offset $1')
|
|
114
|
+
.replace(TURSODB_PREFIX, '')
|
|
115
|
+
.replace(/ \(\d+\)$/, '')
|
|
116
|
+
.replace(/(\w+)\.\(([^)]+)\)/, (_, t, cols) => cols
|
|
117
|
+
.split(', ')
|
|
118
|
+
.map((c) => `${t}.${c}`)
|
|
119
|
+
.join(', '));
|
|
100
120
|
const sqliteMessage = `${text}: ${code}`;
|
|
101
121
|
return new TotalumD1Error('SQL_ERROR', `D1_ERROR: ${sqliteMessage}`, {
|
|
102
122
|
status: 400,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Set on every mutating response; while the browser holds it, its reads go to the primary (read-your-writes). */
|
|
2
|
+
export const READ_YOUR_WRITES_COOKIE = '__tlm_d1_w';
|
|
3
|
+
/** Replicas lag the primary by 30–75 ms (Cloudflare); 10 s leaves a wide margin (research 46 §6.3). */
|
|
4
|
+
const WINDOW_SECONDS = 10;
|
|
5
|
+
const READS = new Set(['GET', 'HEAD']);
|
|
6
|
+
/**
|
|
7
|
+
* Runs one request of a Worker with a replicated D1 (A3.23, research 46 §6.3): a `GET`/`HEAD` reads through a
|
|
8
|
+
* `first-unconstrained` session — the nearest replica — unless the browser wrote in the last 10 s; any other method
|
|
9
|
+
* uses the plain binding (writes go to the primary; measured faster than any session) and its response sets the short
|
|
10
|
+
* cookie that sends that browser's next reads to the primary. `run` receives the env to hand to the app, whose
|
|
11
|
+
* `totalumD1()` reads `TOTALUM_DB` from it on every statement. No D1 binding (Turso, a laptop): `run(env)` unchanged.
|
|
12
|
+
*/
|
|
13
|
+
export async function withTotalumSession(request, env, run) {
|
|
14
|
+
const db = env.TOTALUM_DB;
|
|
15
|
+
if (typeof db?.withSession !== 'function')
|
|
16
|
+
return run(env);
|
|
17
|
+
const binding = db;
|
|
18
|
+
if (!READS.has(request.method)) {
|
|
19
|
+
const res = await run(env);
|
|
20
|
+
if (res.status === 101)
|
|
21
|
+
return res; // a WebSocket upgrade cannot be re-wrapped
|
|
22
|
+
const out = new Response(res.body, res);
|
|
23
|
+
out.headers.append('set-cookie', `${READ_YOUR_WRITES_COOKIE}=1; Max-Age=${String(WINDOW_SECONDS)}; Path=/; HttpOnly; Secure; SameSite=None; Partitioned`);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
const cookies = request.headers.get('cookie') ?? '';
|
|
27
|
+
if (new RegExp(`(?:^|;\\s*)${READ_YOUR_WRITES_COOKIE}=`).test(cookies))
|
|
28
|
+
return run(env);
|
|
29
|
+
const session = binding.withSession('first-unconstrained');
|
|
30
|
+
const replicaDb = {
|
|
31
|
+
prepare: (sql) => session.prepare(sql),
|
|
32
|
+
batch: (statements) => session.batch(statements),
|
|
33
|
+
exec: (sql) => binding.exec(sql),
|
|
34
|
+
dump: () => binding.dump(),
|
|
35
|
+
withSession: (c) => binding.withSession(c),
|
|
36
|
+
};
|
|
37
|
+
return run({ ...env, TOTALUM_DB: replicaDb });
|
|
38
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { totalumWeb } from './web/index.js';
|
|
|
28
28
|
export { TotalumWeb } from './web/index.js';
|
|
29
29
|
import { totalumWebhooks } from './webhooks/index.js';
|
|
30
30
|
export { TotalumWebhookError, TotalumWebhooks, verifyWebhook } from './webhooks/index.js';
|
|
31
|
-
export { D1Database, D1DatabaseSession, D1ExecResult, D1PreparedStatement, D1Result, TotalumD1ClientCode, TotalumD1Error, TotalumD1ErrorCode, TotalumD1Options, isTotalumD1Error, totalumD1 } from './d1/index.js';
|
|
31
|
+
export { D1Database, D1DatabaseSession, D1ExecResult, D1PreparedStatement, D1Result, READ_YOUR_WRITES_COOKIE, TotalumD1ClientCode, TotalumD1Error, TotalumD1ErrorCode, TotalumD1Options, isTotalumD1Error, totalumD1, withTotalumSession } from './d1/index.js';
|
|
32
32
|
export { T as TotalumClientOptions, a as TotalumError, b as TotalumErrorCode, i as isTotalumError } from './_types/errors.d-yrue1e9O.js';
|
|
33
33
|
export { C as CheckoutInput, a as CheckoutOutput, b as CronJob, c as CronJobInput, d as CronJobPatch, e as CronRun, f as CronRunsPage, g as CustomerPortalLinkInput, h as CustomerPortalLinkOutput, E as EmailSendInput, i as EmailSendOutput, j as EmailView, F as FileDescriptor, k as FileUploadUrlInput, l as FileUploadUrlOutput, m as FilesListQuery, n as FilesPage, J as JobCreated, O as OnboardingLinkInput, o as OnboardingLinkOutput, P as PaymentsStatus, p as PdfFromHtmlInput, q as PdfFromUrlInput, r as PdfOutput, S as ScanDocumentInput, s as ScanOcrInput, t as ScanOcrOutput, u as ScreenshotInput, v as ScreenshotOutput, w as SeoIndexNowKey, x as SeoNotifyOutput, y as SpeakInput, z as StoredMedia, T as TranscribeInput, A as TranscribeOutput, W as WebJob, B as WebScrapeOutput, D as WebSearchOutput } from './_types/integrations.d-BKPWost0.js';
|
|
34
34
|
export { L as LogsPage, S as SiteAnalyticsOutput, a as SiteDimension, b as SiteMetric } from './_types/ops.d-BOMT9yln.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totalum-sdk",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.7",
|
|
4
4
|
"description": "The SDK generated Totalum apps import: every integration namespace over SDK-API (totalum-sdk, or one subpath each: /files, /ai, /web, …) and totalum-sdk/d1, the D1 driver",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|