@lunora/d1 1.0.0-alpha.79 → 1.0.0-alpha.80

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/index.d.mts CHANGED
@@ -109,22 +109,48 @@ interface D1ExecLike {
109
109
  * `errors`; the rest land.
110
110
  */
111
111
  declare const importGlobalRows: (writer: DatabaseWriterLike, schema: SchemaLike, args: ImportGlobalArgs) => Promise<ImportResult>;
112
- /** Thin wrapper over a `D1DatabaseSession` exposing bookmark plumbing. */
112
+ /**
113
+ * Thin wrapper over a `D1DatabaseSession` exposing bookmark plumbing.
114
+ *
115
+ * `all` and `first` retry D1's expected transient failures — but only for a
116
+ * statement that is provably read-only. D1 produces a documented
117
+ * baseline of infrastructural errors even when healthy (Cloudflare's team calls
118
+ * a handful every few hours "not unexpected") and their guidance is to retry;
119
+ * a client that does not has adopted that error rate as the application's own.
120
+ *
121
+ * Nothing else on this class or on {@link D1Client} retries. `run` is the write
122
+ * path, and every transient D1 error is ambiguous about whether the statement
123
+ * applied — re-running a non-idempotent write after a lost response
124
+ * double-applies it. `all` is not a read path either: D1 runs
125
+ * `UPDATE … RETURNING` through it, so the retry is gated on the statement's
126
+ * leading keyword rather than on the method name. Wrap a genuinely idempotent
127
+ * write in {@link withD1Retry} yourself, per call.
128
+ */
113
129
  declare class D1Session {
114
130
  private readonly session;
115
131
  /** See {@link D1Client.stmtCache}. Scoped per session. */
116
132
  private readonly stmtCache;
117
133
  constructor(session: D1SessionLike);
118
134
  prepare(sql: string): D1PreparedStatementLike;
135
+ /**
136
+ * Execute a statement. **Not retried** — `run` is the write path, and a
137
+ * transient D1 error does not say whether the write applied.
138
+ */
119
139
  run<T = unknown>(sql: string, ...binds: unknown[]): Promise<{
120
140
  meta?: Record<string, unknown>;
121
141
  results?: T[];
122
142
  success: boolean;
123
143
  }>;
144
+ /**
145
+ * Read all rows, retrying D1's transient failures when `sql` is a
146
+ * read-only statement. An `UPDATE … RETURNING` runs through here too and is
147
+ * left unretried.
148
+ */
124
149
  all<T = unknown>(sql: string, ...binds: unknown[]): Promise<{
125
150
  results: T[];
126
151
  success: boolean;
127
152
  }>;
153
+ /** Read the first row, retrying under the same read-only rule as {@link all}. */
128
154
  first<T = unknown>(sql: string, ...binds: unknown[]): Promise<T | null>;
129
155
  /**
130
156
  * Returns the most recent bookmark known to the session, or `undefined`
@@ -132,6 +158,16 @@ declare class D1Session {
132
158
  */
133
159
  getBookmark(): string | undefined;
134
160
  }
161
+ /**
162
+ * A D1 handle: prepared-statement caching, Sessions-API sessions, and the
163
+ * drizzle-typed accessors.
164
+ *
165
+ * **Nothing on this class retries.** `prepare`, `drizzle`, `drizzleSession`,
166
+ * `batch` and `raw` all hand back the underlying D1 surface untouched. The
167
+ * automatic retry lives on {@link D1Session.all} / {@link D1Session.first} and
168
+ * on `retryingExec` (the seam `.global()` tables read through); wrap anything
169
+ * else in {@link withD1Retry} yourself, and only when it is safe to run twice.
170
+ */
135
171
  declare class D1Client {
136
172
  private readonly db;
137
173
  /**
@@ -326,10 +362,89 @@ declare class MigrationRunner {
326
362
  private assertUniqueVersions;
327
363
  private assertUniqueSql;
328
364
  }
365
+ /** Tuning for {@link withD1Retry}. */
366
+ interface D1RetryOptions {
367
+ /** Total attempts including the first. Default 3. Must be >= 1. */
368
+ attempts?: number;
369
+ /**
370
+ * Total budget across every attempt and backoff, in ms. An attempt still
371
+ * running when the budget expires is abandoned, and a backoff never sleeps
372
+ * past it.
373
+ *
374
+ * `timeoutMs` bounds one attempt; this bounds the operation.
375
+ */
376
+ deadlineMs?: number;
377
+ /** Sleep implementation. Injected so tests need no timers. */
378
+ sleep?: (ms: number) => Promise<void>;
379
+ /**
380
+ * Abandon a single attempt that has not settled within this many ms, and
381
+ * treat it as a transient failure.
382
+ *
383
+ * There is no default — a legitimate analytical query and a stalled one
384
+ * look identical from outside, so the value has to come from what your
385
+ * workload actually needs. Unset, {@link SLOW_FAILURE_MS} stops a slow
386
+ * failure from being retried rather than guessing a bound for you.
387
+ *
388
+ * Only ever applied to operations that are safe to abandon. It does not
389
+ * cancel the underlying D1 call.
390
+ */
391
+ timeoutMs?: number;
392
+ }
393
+ /**
394
+ * True when `error` looks like one of D1's expected transient failures.
395
+ *
396
+ * Deliberately conservative: an unrecognised error is treated as **permanent**
397
+ * and surfaces immediately. Retrying a genuine bug — a syntax error, a
398
+ * constraint violation — turns one fast failure into three slow ones and
399
+ * hides the cause.
400
+ * @experimental
401
+ */
402
+ declare const isTransientD1Error: (error: unknown) => boolean;
403
+ /**
404
+ * Thrown when an attempt is abandoned for exceeding
405
+ * {@link D1RetryOptions.timeoutMs}.
406
+ *
407
+ * Its own class so a caller can tell "D1 hung" apart from "D1 returned an
408
+ * error" — they have different remedies, and collapsing them hides which one
409
+ * is happening.
410
+ * @experimental
411
+ */
412
+ declare class D1TimeoutError extends Error {
413
+ /** Milliseconds waited before the attempt was abandoned. */
414
+ readonly timeoutMs: number;
415
+ constructor(timeoutMs: number);
416
+ }
417
+ /**
418
+ * Run `operation`, retrying D1's transient failures with exponential backoff
419
+ * and jitter.
420
+ *
421
+ * **Only wrap operations that are safe to run more than once.** Reads always
422
+ * are. A write is only if it is idempotent — an upsert keyed on a primary key,
423
+ * a delete by id, an `INSERT OR IGNORE`. A bare `INSERT` or a relative
424
+ * `UPDATE` is not, and re-running one after a lost response double-applies it.
425
+ * @experimental
426
+ */
427
+ declare const withD1Retry: <T>(operation: () => Promise<T>, options?: D1RetryOptions) => Promise<T>;
428
+ /**
429
+ * Wrap a `.global()` exec so its **read-only** statements retry D1's transient
430
+ * failures.
431
+ *
432
+ * This is where the retry has to live to reach an application: `.global()`
433
+ * tables run every read through the exec codegen builds over the raw D1
434
+ * binding, not through a `D1Client`. `run` and `batch` are passed straight
435
+ * through — they are the write path, and a transient D1 error does not say
436
+ * whether the write applied.
437
+ *
438
+ * `all` is not the read path either. It only retries when
439
+ * {@link isReadOnlyD1Sql} proves the statement is one, because `UPDATE …
440
+ * RETURNING` runs through `all` too.
441
+ * @experimental
442
+ */
443
+ declare const retryingExec: (exec: SqlCtxExec, options?: D1RetryOptions) => SqlCtxExec;
329
444
  /**
330
445
  * The canonical SQLite dialect: column affinities, the shared SQLite value
331
446
  * codec, `RETURNING` support (both D1 and `node:sqlite`), and `sqlite_master`
332
447
  * table probing. The rest of the per-statement shaping is drizzle's.
333
448
  */
334
449
  declare const sqliteDialect: SqlDialect;
335
- export { D1Client, type D1ContextDatabaseOptions as D1CtxDbOptions, D1Session, type ExportGlobalArgs, type FacetGlobalColumnOptions, type ExportRow as GlobalExportRow, type GlobalFacetResult, type GlobalFacetValue, type GlobalFilterClause, type ImportError as GlobalImportError, type ImportResult as GlobalImportResult, type GlobalTableInfo, type GlobalTablePage, type ImportGlobalArgs, type Migration, MigrationRunner, type MigrationRunnerResult, type ReadGlobalTablePageOptions, backfillD1SearchIndexes, createD1ContextDatabase as createD1CtxDb, exportGlobalRows, facetGlobalColumn, importGlobalRows, listGlobalTables, readD1CdcChanges, readGlobalTablePage, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, selectGlobalTables, sqliteDialect, trimD1CdcChanges };
450
+ export { D1Client, type D1ContextDatabaseOptions as D1CtxDbOptions, type D1RetryOptions, D1Session, D1TimeoutError, type ExportGlobalArgs, type FacetGlobalColumnOptions, type ExportRow as GlobalExportRow, type GlobalFacetResult, type GlobalFacetValue, type GlobalFilterClause, type ImportError as GlobalImportError, type ImportResult as GlobalImportResult, type GlobalTableInfo, type GlobalTablePage, type ImportGlobalArgs, type Migration, MigrationRunner, type MigrationRunnerResult, type ReadGlobalTablePageOptions, backfillD1SearchIndexes, createD1ContextDatabase as createD1CtxDb, exportGlobalRows, facetGlobalColumn, importGlobalRows, isTransientD1Error, listGlobalTables, readD1CdcChanges, readGlobalTablePage, retryingExec, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, selectGlobalTables, sqliteDialect, trimD1CdcChanges, withD1Retry };
package/dist/index.d.ts CHANGED
@@ -109,22 +109,48 @@ interface D1ExecLike {
109
109
  * `errors`; the rest land.
110
110
  */
111
111
  declare const importGlobalRows: (writer: DatabaseWriterLike, schema: SchemaLike, args: ImportGlobalArgs) => Promise<ImportResult>;
112
- /** Thin wrapper over a `D1DatabaseSession` exposing bookmark plumbing. */
112
+ /**
113
+ * Thin wrapper over a `D1DatabaseSession` exposing bookmark plumbing.
114
+ *
115
+ * `all` and `first` retry D1's expected transient failures — but only for a
116
+ * statement that is provably read-only. D1 produces a documented
117
+ * baseline of infrastructural errors even when healthy (Cloudflare's team calls
118
+ * a handful every few hours "not unexpected") and their guidance is to retry;
119
+ * a client that does not has adopted that error rate as the application's own.
120
+ *
121
+ * Nothing else on this class or on {@link D1Client} retries. `run` is the write
122
+ * path, and every transient D1 error is ambiguous about whether the statement
123
+ * applied — re-running a non-idempotent write after a lost response
124
+ * double-applies it. `all` is not a read path either: D1 runs
125
+ * `UPDATE … RETURNING` through it, so the retry is gated on the statement's
126
+ * leading keyword rather than on the method name. Wrap a genuinely idempotent
127
+ * write in {@link withD1Retry} yourself, per call.
128
+ */
113
129
  declare class D1Session {
114
130
  private readonly session;
115
131
  /** See {@link D1Client.stmtCache}. Scoped per session. */
116
132
  private readonly stmtCache;
117
133
  constructor(session: D1SessionLike);
118
134
  prepare(sql: string): D1PreparedStatementLike;
135
+ /**
136
+ * Execute a statement. **Not retried** — `run` is the write path, and a
137
+ * transient D1 error does not say whether the write applied.
138
+ */
119
139
  run<T = unknown>(sql: string, ...binds: unknown[]): Promise<{
120
140
  meta?: Record<string, unknown>;
121
141
  results?: T[];
122
142
  success: boolean;
123
143
  }>;
144
+ /**
145
+ * Read all rows, retrying D1's transient failures when `sql` is a
146
+ * read-only statement. An `UPDATE … RETURNING` runs through here too and is
147
+ * left unretried.
148
+ */
124
149
  all<T = unknown>(sql: string, ...binds: unknown[]): Promise<{
125
150
  results: T[];
126
151
  success: boolean;
127
152
  }>;
153
+ /** Read the first row, retrying under the same read-only rule as {@link all}. */
128
154
  first<T = unknown>(sql: string, ...binds: unknown[]): Promise<T | null>;
129
155
  /**
130
156
  * Returns the most recent bookmark known to the session, or `undefined`
@@ -132,6 +158,16 @@ declare class D1Session {
132
158
  */
133
159
  getBookmark(): string | undefined;
134
160
  }
161
+ /**
162
+ * A D1 handle: prepared-statement caching, Sessions-API sessions, and the
163
+ * drizzle-typed accessors.
164
+ *
165
+ * **Nothing on this class retries.** `prepare`, `drizzle`, `drizzleSession`,
166
+ * `batch` and `raw` all hand back the underlying D1 surface untouched. The
167
+ * automatic retry lives on {@link D1Session.all} / {@link D1Session.first} and
168
+ * on `retryingExec` (the seam `.global()` tables read through); wrap anything
169
+ * else in {@link withD1Retry} yourself, and only when it is safe to run twice.
170
+ */
135
171
  declare class D1Client {
136
172
  private readonly db;
137
173
  /**
@@ -326,10 +362,89 @@ declare class MigrationRunner {
326
362
  private assertUniqueVersions;
327
363
  private assertUniqueSql;
328
364
  }
365
+ /** Tuning for {@link withD1Retry}. */
366
+ interface D1RetryOptions {
367
+ /** Total attempts including the first. Default 3. Must be >= 1. */
368
+ attempts?: number;
369
+ /**
370
+ * Total budget across every attempt and backoff, in ms. An attempt still
371
+ * running when the budget expires is abandoned, and a backoff never sleeps
372
+ * past it.
373
+ *
374
+ * `timeoutMs` bounds one attempt; this bounds the operation.
375
+ */
376
+ deadlineMs?: number;
377
+ /** Sleep implementation. Injected so tests need no timers. */
378
+ sleep?: (ms: number) => Promise<void>;
379
+ /**
380
+ * Abandon a single attempt that has not settled within this many ms, and
381
+ * treat it as a transient failure.
382
+ *
383
+ * There is no default — a legitimate analytical query and a stalled one
384
+ * look identical from outside, so the value has to come from what your
385
+ * workload actually needs. Unset, {@link SLOW_FAILURE_MS} stops a slow
386
+ * failure from being retried rather than guessing a bound for you.
387
+ *
388
+ * Only ever applied to operations that are safe to abandon. It does not
389
+ * cancel the underlying D1 call.
390
+ */
391
+ timeoutMs?: number;
392
+ }
393
+ /**
394
+ * True when `error` looks like one of D1's expected transient failures.
395
+ *
396
+ * Deliberately conservative: an unrecognised error is treated as **permanent**
397
+ * and surfaces immediately. Retrying a genuine bug — a syntax error, a
398
+ * constraint violation — turns one fast failure into three slow ones and
399
+ * hides the cause.
400
+ * @experimental
401
+ */
402
+ declare const isTransientD1Error: (error: unknown) => boolean;
403
+ /**
404
+ * Thrown when an attempt is abandoned for exceeding
405
+ * {@link D1RetryOptions.timeoutMs}.
406
+ *
407
+ * Its own class so a caller can tell "D1 hung" apart from "D1 returned an
408
+ * error" — they have different remedies, and collapsing them hides which one
409
+ * is happening.
410
+ * @experimental
411
+ */
412
+ declare class D1TimeoutError extends Error {
413
+ /** Milliseconds waited before the attempt was abandoned. */
414
+ readonly timeoutMs: number;
415
+ constructor(timeoutMs: number);
416
+ }
417
+ /**
418
+ * Run `operation`, retrying D1's transient failures with exponential backoff
419
+ * and jitter.
420
+ *
421
+ * **Only wrap operations that are safe to run more than once.** Reads always
422
+ * are. A write is only if it is idempotent — an upsert keyed on a primary key,
423
+ * a delete by id, an `INSERT OR IGNORE`. A bare `INSERT` or a relative
424
+ * `UPDATE` is not, and re-running one after a lost response double-applies it.
425
+ * @experimental
426
+ */
427
+ declare const withD1Retry: <T>(operation: () => Promise<T>, options?: D1RetryOptions) => Promise<T>;
428
+ /**
429
+ * Wrap a `.global()` exec so its **read-only** statements retry D1's transient
430
+ * failures.
431
+ *
432
+ * This is where the retry has to live to reach an application: `.global()`
433
+ * tables run every read through the exec codegen builds over the raw D1
434
+ * binding, not through a `D1Client`. `run` and `batch` are passed straight
435
+ * through — they are the write path, and a transient D1 error does not say
436
+ * whether the write applied.
437
+ *
438
+ * `all` is not the read path either. It only retries when
439
+ * {@link isReadOnlyD1Sql} proves the statement is one, because `UPDATE …
440
+ * RETURNING` runs through `all` too.
441
+ * @experimental
442
+ */
443
+ declare const retryingExec: (exec: SqlCtxExec, options?: D1RetryOptions) => SqlCtxExec;
329
444
  /**
330
445
  * The canonical SQLite dialect: column affinities, the shared SQLite value
331
446
  * codec, `RETURNING` support (both D1 and `node:sqlite`), and `sqlite_master`
332
447
  * table probing. The rest of the per-statement shaping is drizzle's.
333
448
  */
334
449
  declare const sqliteDialect: SqlDialect;
335
- export { D1Client, type D1ContextDatabaseOptions as D1CtxDbOptions, D1Session, type ExportGlobalArgs, type FacetGlobalColumnOptions, type ExportRow as GlobalExportRow, type GlobalFacetResult, type GlobalFacetValue, type GlobalFilterClause, type ImportError as GlobalImportError, type ImportResult as GlobalImportResult, type GlobalTableInfo, type GlobalTablePage, type ImportGlobalArgs, type Migration, MigrationRunner, type MigrationRunnerResult, type ReadGlobalTablePageOptions, backfillD1SearchIndexes, createD1ContextDatabase as createD1CtxDb, exportGlobalRows, facetGlobalColumn, importGlobalRows, listGlobalTables, readD1CdcChanges, readGlobalTablePage, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, selectGlobalTables, sqliteDialect, trimD1CdcChanges };
450
+ export { D1Client, type D1ContextDatabaseOptions as D1CtxDbOptions, type D1RetryOptions, D1Session, D1TimeoutError, type ExportGlobalArgs, type FacetGlobalColumnOptions, type ExportRow as GlobalExportRow, type GlobalFacetResult, type GlobalFacetValue, type GlobalFilterClause, type ImportError as GlobalImportError, type ImportResult as GlobalImportResult, type GlobalTableInfo, type GlobalTablePage, type ImportGlobalArgs, type Migration, MigrationRunner, type MigrationRunnerResult, type ReadGlobalTablePageOptions, backfillD1SearchIndexes, createD1ContextDatabase as createD1CtxDb, exportGlobalRows, facetGlobalColumn, importGlobalRows, isTransientD1Error, listGlobalTables, readD1CdcChanges, readGlobalTablePage, retryingExec, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, selectGlobalTables, sqliteDialect, trimD1CdcChanges, withD1Retry };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{exportGlobalRows as a,importGlobalRows as o,selectGlobalTables as l}from"./packem_shared/exportGlobalRows-Cc8cXltm.mjs";import{D1Client as i,D1Session as n}from"./packem_shared/D1Client-IzEOlt9I.mjs";import{backfillD1SearchIndexes as b,createD1CtxDb as D,readD1CdcChanges as g,runD1AggregateMigrations as c,runD1CdcMigration as x,runD1GlobalTableMigrations as f,runD1RankMigrations as m,runD1SearchMigrations as p,trimD1CdcChanges as C}from"./packem_shared/backfillD1SearchIndexes-By6bbCLe.mjs";import{facetGlobalColumn as d,listGlobalTables as G,readGlobalTablePage as M}from"./packem_shared/facetGlobalColumn-D1npxpva.mjs";import{MigrationRunner as R}from"./packem_shared/MigrationRunner-IF213ALw.mjs";import{default as T}from"./packem_shared/sqliteDialect-DZoYJOa-.mjs";import{createSqlCtxDb as q}from"@lunora/sql-store";export{i as D1Client,n as D1Session,R as MigrationRunner,b as backfillD1SearchIndexes,D as createD1CtxDb,q as createSqlCtxDb,a as exportGlobalRows,d as facetGlobalColumn,o as importGlobalRows,G as listGlobalTables,g as readD1CdcChanges,M as readGlobalTablePage,c as runD1AggregateMigrations,x as runD1CdcMigration,f as runD1GlobalTableMigrations,m as runD1RankMigrations,p as runD1SearchMigrations,l as selectGlobalTables,T as sqliteDialect,C as trimD1CdcChanges};
1
+ import{exportGlobalRows as o,importGlobalRows as a,selectGlobalTables as t}from"./packem_shared/exportGlobalRows-Cc8cXltm.mjs";import{D1Client as i,D1Session as n}from"./packem_shared/D1Client-CzS0Qfa6.mjs";import{backfillD1SearchIndexes as D,createD1CtxDb as b,readD1CdcChanges as g,runD1AggregateMigrations as x,runD1CdcMigration as c,runD1GlobalTableMigrations as m,runD1RankMigrations as f,runD1SearchMigrations as p,trimD1CdcChanges as u}from"./packem_shared/backfillD1SearchIndexes-By6bbCLe.mjs";import{facetGlobalColumn as d,listGlobalTables as G,readGlobalTablePage as M}from"./packem_shared/facetGlobalColumn-D1npxpva.mjs";import{MigrationRunner as h}from"./packem_shared/MigrationRunner-SadkIbU5.mjs";import{D1TimeoutError as S,isTransientD1Error as w,retryingExec as E,withD1Retry as k}from"./packem_shared/D1TimeoutError-D4YUqZPk.mjs";import{default as y}from"./packem_shared/sqliteDialect-DZoYJOa-.mjs";import{createSqlCtxDb as I}from"@lunora/sql-store";export{i as D1Client,n as D1Session,S as D1TimeoutError,h as MigrationRunner,D as backfillD1SearchIndexes,b as createD1CtxDb,I as createSqlCtxDb,o as exportGlobalRows,d as facetGlobalColumn,a as importGlobalRows,w as isTransientD1Error,G as listGlobalTables,g as readD1CdcChanges,M as readGlobalTablePage,E as retryingExec,x as runD1AggregateMigrations,c as runD1CdcMigration,m as runD1GlobalTableMigrations,f as runD1RankMigrations,p as runD1SearchMigrations,t as selectGlobalTables,y as sqliteDialect,u as trimD1CdcChanges,k as withD1Retry};
@@ -0,0 +1 @@
1
+ import{drizzle as i}from"drizzle-orm/d1";import{isReadOnlyD1Sql as o,withD1Retry as a}from"./D1TimeoutError-D4YUqZPk.mjs";const c=(s,e)=>{if(s.size<e)return;const t=s.keys().next().value;t!==void 0&&s.delete(t)},l=256,h=(s,e,t)=>{const r=s.get(t);if(r)return s.delete(t),s.set(t,r),r;const n=e(t);return c(s,l),s.set(t,n),n},d="first-unconstrained";class u{session;stmtCache=new Map;constructor(e){this.session=e}prepare(e){return h(this.stmtCache,t=>this.session.prepare(t),e)}async run(e,...t){return this.prepare(e).bind(...t).run()}async all(e,...t){const r=async()=>this.prepare(e).bind(...t).all();return o(e)?a(r):r()}async first(e,...t){const r=async()=>this.prepare(e).bind(...t).first();return o(e)?a(r):r()}getBookmark(){return this.session.getBookmark()??void 0}}class b{db;stmtCache=new Map;drizzleHandle;constructor(e){this.db=e}withSession(e){const t=this.db.withSession(e??d);return new u(t)}prepare(e){return h(this.stmtCache,t=>this.db.prepare(t),e)}get drizzle(){return this.drizzleHandle?this.drizzleHandle:(this.drizzleHandle=i(this.db,{logger:!1}),this.drizzleHandle)}drizzleSession(e){const t=this.db.withSession(e??d);return i(t,{logger:!1})}async batch(e){return this.drizzle.batch(e)}get raw(){return this.db}}export{b as D1Client,u as D1Session};
@@ -0,0 +1 @@
1
+ import{LunoraError as h,unreachable as f}from"@lunora/errors";const y=["storage operation exceeded timeout","network connection lost","internal error while starting up d1 db storage","exceeded its memory limit","caused object to be reset"],M=3,S=50,b=1e3,D=2e3,p=/^[\s(]*(?:select|pragma|explain)\b/iu,_=t=>p.test(t),A=t=>t instanceof Error?`${t.name}: ${t.message}`.toLowerCase():typeof t=="string"?t.toLowerCase():"",I=t=>{const e=A(t);return e.length===0?!1:y.some(n=>e.includes(n))},N=t=>{const e=Math.min(b,S*2**(t-1));return Math.round(Math.random()*e)},L=async t=>new Promise(e=>{setTimeout(e,t)});class l extends Error{timeoutMs;constructor(e){super(`@lunora/d1: operation exceeded its ${String(e)}ms timeout and was abandoned (the underlying D1 call may still be running)`),this.name="D1TimeoutError",this.timeoutMs=e}}const R=async(t,e)=>{let n;try{return await Promise.race([t(),new Promise((o,r)=>{n=setTimeout(()=>{r(new l(e))},e)})])}finally{n!==void 0&&clearTimeout(n)}},x=async(t,e={})=>{const n=e.attempts??M;if(!Number.isInteger(n)||n<1)throw new h("BAD_REQUEST","@lunora/d1: `attempts` must be an integer >= 1");const o=e.sleep??L,{deadlineMs:r,timeoutMs:i}=e,d=Date.now(),w=r===void 0&&i===void 0,c=()=>r===void 0?Number.POSITIVE_INFINITY:r-(Date.now()-d);for(let s=1;s<=n;s+=1){const u=Math.min(i??Number.POSITIVE_INFINITY,Math.max(c(),0)),E=Date.now();try{return await(Number.isFinite(u)?R(t,u):t())}catch(a){const g=a instanceof l||I(a),T=w&&Date.now()-E>=D,m=c();if(s===n||!g||T||m<=0)throw a;await o(Math.min(N(s),m))}}return f("@lunora/d1: retry loop exited without returning or throwing")},P=(t,e)=>({...t,all:async(n,o)=>_(n)?x(async()=>t.all(n,o),e):t.all(n,o)});export{l as D1TimeoutError,_ as isReadOnlyD1Sql,I as isTransientD1Error,P as retryingExec,x as withD1Retry};
@@ -1,2 +1,2 @@
1
- import{LunoraError as f}from"@lunora/errors";import{sql as h}from"drizzle-orm";import{D1Client as m}from"./D1Client-IzEOlt9I.mjs";const u="__drizzle_migrations",p=`CREATE TABLE IF NOT EXISTS ${u} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL UNIQUE, created_at NUMERIC)`,d=/\s/u,E=/^[0-9a-f]{64}$/u,S=new RegExp(String.raw`UNIQUE constraint failed:\s*${u}\.hash`,"iu"),w=c=>{const n=c.sql;let e=!1,i=!1,o=!1,a=!1,r;for(let t=0;t<n.length;t+=1){const s=n[t],l=n[t+1];if(o){s===`
1
+ import{LunoraError as f}from"@lunora/errors";import{sql as h}from"drizzle-orm";import{D1Client as m}from"./D1Client-CzS0Qfa6.mjs";const u="__drizzle_migrations",p=`CREATE TABLE IF NOT EXISTS ${u} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL UNIQUE, created_at NUMERIC)`,d=/\s/u,E=/^[0-9a-f]{64}$/u,S=new RegExp(String.raw`UNIQUE constraint failed:\s*${u}\.hash`,"iu"),w=c=>{const n=c.sql;let e=!1,i=!1,o=!1,a=!1,r;for(let t=0;t<n.length;t+=1){const s=n[t],l=n[t+1];if(o){s===`
2
2
  `&&(o=!1);continue}if(a){s==="*"&&l==="/"&&(a=!1,t+=1);continue}if(e){s==="'"&&(l==="'"?t+=1:e=!1);continue}if(i){s==='"'&&(l==='"'?t+=1:i=!1);continue}if(s==="-"&&l==="-"){o=!0,t+=1;continue}if(s==="/"&&l==="*"){a=!0,t+=1;continue}if(r!==void 0){if(s!==void 0&&!d.test(s))throw new f("INTERNAL",`Migration "${c.name}" (v${String(c.version)}) contains more than one SQL statement. Split it into separate migrations — batch() runs them atomically.`);continue}if(s==="'"){e=!0;continue}if(s==='"'){i=!0;continue}s===";"&&(r=t)}return r},g=async c=>{const n=new TextEncoder().encode(c),e=await crypto.subtle.digest("SHA-256",n);return[...new Uint8Array(e)].map(i=>i.toString(16).padStart(2,"0")).join("")};class A{client;migrations;constructor(n,e){this.client=n instanceof m?n:new m(n),this.migrations=[...e].toSorted((i,o)=>i.version-o.version),this.assertUniqueVersions(),this.assertUniqueSql()}async run(){await this.client.drizzle.run(h.raw(p));const n=await this.client.drizzle.all(h.raw(`SELECT hash FROM ${u}`)),e=new Set(n.map(r=>r.hash)),i=[],o=[],a=await Promise.all(this.migrations.map(async r=>g(r.sql)));for(const[r,t]of this.migrations.entries()){const s=a[r];if(e.has(s)){o.push({name:t.name,version:t.version});continue}await this.applyOne(t,s)?i.push({name:t.name,version:t.version}):o.push({name:t.name,version:t.version})}return{applied:i,skipped:o}}async applyOne(n,e){const i=w(n),o=(i===void 0?n.sql:n.sql.slice(0,i)).trim();if(!E.test(e))throw new f("INTERNAL",`migration "${n.name}" produced a non-hex hash; refusing to inline into SQL`);const a=`INSERT INTO ${u} (hash, created_at) VALUES ('${e}', ${String(Date.now())})`,r=[this.client.drizzle.run(h.raw(o)),this.client.drizzle.run(h.raw(a))];try{await this.client.batch(r)}catch(t){if(S.test(t instanceof Error?t.message:String(t)))return!1;throw t}return!0}assertUniqueVersions(){const n=new Set;for(const e of this.migrations){if(n.has(e.version))throw new f("INTERNAL",`Duplicate migration version ${String(e.version)}`);n.add(e.version)}}assertUniqueSql(){const n=new Map;for(const e of this.migrations){const i=n.get(e.sql);if(i!==void 0)throw new f("INTERNAL",`Migrations ${String(i)} and ${String(e.version)} have identical SQL — bump the content, not just the version.`);n.set(e.sql,e.version)}}}export{A as MigrationRunner};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/d1",
3
- "version": "1.0.0-alpha.79",
3
+ "version": "1.0.0-alpha.80",
4
4
  "description": "D1 adapter for Lunora .global() tables, wrapping the Sessions API for read-your-writes",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -51,9 +51,9 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@lunora/errors": "1.0.0-alpha.22",
54
- "@lunora/platform": "1.0.0-alpha.12",
55
- "@lunora/shard-engine": "1.0.0-alpha.29",
56
- "@lunora/sql-store": "1.0.0-alpha.81",
54
+ "@lunora/platform": "1.0.0-alpha.13",
55
+ "@lunora/shard-engine": "1.0.0-alpha.30",
56
+ "@lunora/sql-store": "1.0.0-alpha.82",
57
57
  "drizzle-orm": "^0.45.2"
58
58
  },
59
59
  "engines": {
@@ -1 +0,0 @@
1
- import{drizzle as i}from"drizzle-orm/d1";const a=(s,e)=>{if(s.size<e)return;const t=s.keys().next().value;t!==void 0&&s.delete(t)},h=256,d=(s,e,t)=>{const r=s.get(t);if(r)return s.delete(t),s.set(t,r),r;const n=e(t);return a(s,h),s.set(t,n),n},o="first-unconstrained";class l{session;stmtCache=new Map;constructor(e){this.session=e}prepare(e){return d(this.stmtCache,t=>this.session.prepare(t),e)}async run(e,...t){return this.prepare(e).bind(...t).run()}async all(e,...t){return this.prepare(e).bind(...t).all()}async first(e,...t){return this.prepare(e).bind(...t).first()}getBookmark(){return this.session.getBookmark()??void 0}}class u{db;stmtCache=new Map;drizzleHandle;constructor(e){this.db=e}withSession(e){const t=this.db.withSession(e??o);return new l(t)}prepare(e){return d(this.stmtCache,t=>this.db.prepare(t),e)}get drizzle(){return this.drizzleHandle?this.drizzleHandle:(this.drizzleHandle=i(this.db,{logger:!1}),this.drizzleHandle)}drizzleSession(e){const t=this.db.withSession(e??o);return i(t,{logger:!1})}async batch(e){return this.drizzle.batch(e)}get raw(){return this.db}}export{u as D1Client,l as D1Session};