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.
@@ -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
- /** OpenNext exposes the request's bindings; absent (a laptop) or outside a request, there is no binding. */
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
- const openNext = (await import('@opennextjs/cloudflare'));
43
- return openNext.getCloudflareContext().env['TOTALUM_DB'];
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). Statements remember `sql` + params and are
6
- * materialised on the resolved target when executed, so `drizzle(totalumD1())` at module scope is legal.
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
- constructor(sql, params = []) {
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 resolved()).prepare(this.sql).bind(...this.params);
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 batch = async (statements) => {
43
- const t = await resolved();
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
- return run(() => client.execute({ sql: this.sql, args: this.args }));
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'))).map((rs) => toResult(rs));
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.6",
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",