totalum-sdk 0.1.0-dev.11 → 0.1.0-dev.12

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.
@@ -90,7 +90,8 @@ declare const READ_YOUR_WRITES_COOKIE = "__tlm_d1_w";
90
90
  * Runs one request of a Worker with a replicated D1 (A3.23, research 46 §6.3): a `GET`/`HEAD` reads through a
91
91
  * `first-unconstrained` session — the nearest replica — unless the browser wrote in the last 10 s; any other method
92
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
93
+ * cookie that sends that browser's next reads to the primary. A replica read that takes over `REPLICA_READ_TIMEOUT_MS`
94
+ * is answered by the primary instead, and the rest of that request reads the primary. `run` receives the env to hand to the app, whose
94
95
  * `totalumD1()` reads `TOTALUM_DB` from it on every statement. No D1 binding (Turso, a laptop): `run(env)` unchanged.
95
96
  */
96
97
  declare function withTotalumSession<E extends object>(request: Request, env: E, run: (env: E) => Promise<Response>): Promise<Response>;
@@ -3,11 +3,21 @@ export const READ_YOUR_WRITES_COOKIE = '__tlm_d1_w';
3
3
  /** Replicas lag the primary by 30–75 ms (Cloudflare); 10 s leaves a wide margin (research 46 §6.3). */
4
4
  const WINDOW_SECONDS = 10;
5
5
  const READS = new Set(['GET', 'HEAD']);
6
+ /**
7
+ * A replica read slower than this is re-run on the primary, and the rest of the request reads the primary. Replicas
8
+ * answer in tens of ms; measured stalls last 10 s or more while the primary stays fast (ui-evidence/preview-hang).
9
+ * D1's Sessions API documents no timeout or fallback of its own.
10
+ */
11
+ export const REPLICA_READ_TIMEOUT_MS = 2000;
12
+ /** Only a `SELECT` is re-run: a write that stalled may still land, and running it twice would duplicate it. */
13
+ const PURE_READ = /^\s*select\b/i;
14
+ const LATE = Symbol('late');
6
15
  /**
7
16
  * Runs one request of a Worker with a replicated D1 (A3.23, research 46 §6.3): a `GET`/`HEAD` reads through a
8
17
  * `first-unconstrained` session — the nearest replica — unless the browser wrote in the last 10 s; any other method
9
18
  * 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
19
+ * cookie that sends that browser's next reads to the primary. A replica read that takes over `REPLICA_READ_TIMEOUT_MS`
20
+ * is answered by the primary instead, and the rest of that request reads the primary. `run` receives the env to hand to the app, whose
11
21
  * `totalumD1()` reads `TOTALUM_DB` from it on every statement. No D1 binding (Turso, a laptop): `run(env)` unchanged.
12
22
  */
13
23
  export async function withTotalumSession(request, env, run) {
@@ -26,13 +36,57 @@ export async function withTotalumSession(request, env, run) {
26
36
  const cookies = request.headers.get('cookie') ?? '';
27
37
  if (new RegExp(`(?:^|;\\s*)${READ_YOUR_WRITES_COOKIE}=`).test(cookies))
28
38
  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),
39
+ return run({ ...env, TOTALUM_DB: boundedReplica(binding, binding.withSession('first-unconstrained')) });
40
+ }
41
+ /**
42
+ * The request's database on the nearest replica, each pure read bounded by `REPLICA_READ_TIMEOUT_MS`: past it, the read
43
+ * runs on the primary (the stalled replica call is abandoned) and so does every later statement of this request.
44
+ */
45
+ function boundedReplica(binding, session) {
46
+ let primary = false;
47
+ const on = () => (primary ? binding : session);
48
+ async function read(sqls, call) {
49
+ if (primary || !sqls.every((sql) => PURE_READ.test(sql)))
50
+ return call(on());
51
+ let timer;
52
+ const late = new Promise((resolve) => {
53
+ timer = setTimeout(() => {
54
+ resolve(LATE);
55
+ }, REPLICA_READ_TIMEOUT_MS);
56
+ });
57
+ try {
58
+ const out = await Promise.race([call(session), late]);
59
+ if (out !== LATE)
60
+ return out;
61
+ }
62
+ finally {
63
+ clearTimeout(timer);
64
+ }
65
+ primary = true;
66
+ console.warn(JSON.stringify({ event: 'd1_replica_read_timeout', ms: REPLICA_READ_TIMEOUT_MS }));
67
+ return call(binding);
68
+ }
69
+ const statement = (sql, params = []) => {
70
+ const real = (db) => db.prepare(sql).bind(...params);
71
+ const bounded = (method) => (...args) => read([sql], (db) => real(db)[method](...args));
72
+ return {
73
+ sql,
74
+ real,
75
+ bind: (...values) => statement(sql, values),
76
+ first: bounded('first'),
77
+ run: bounded('run'),
78
+ all: bounded('all'),
79
+ raw: bounded('raw'),
80
+ };
81
+ };
82
+ return {
83
+ prepare: (sql) => statement(sql),
84
+ batch: (statements) => {
85
+ const mine = statements;
86
+ return read(mine.map((s) => s.sql), (db) => db.batch(mine.map((s) => s.real(db))));
87
+ },
33
88
  exec: (sql) => binding.exec(sql),
34
89
  dump: () => binding.dump(),
35
90
  withSession: (c) => binding.withSession(c),
36
91
  };
37
- return run({ ...env, TOTALUM_DB: replicaDb });
38
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totalum-sdk",
3
- "version": "0.1.0-dev.11",
3
+ "version": "0.1.0-dev.12",
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",