@syncular/client 0.30.6 → 0.30.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/client.js CHANGED
@@ -2327,7 +2327,8 @@ export class SyncClient {
2327
2327
  };
2328
2328
  this.#emitDiagnostics();
2329
2329
  return summary;
2330
- }, (error) => {
2330
+ }, (caught) => {
2331
+ const error = classifySqliteFailure(caught).error;
2331
2332
  const completedAtMs = this.#now();
2332
2333
  const code = error.code;
2333
2334
  this.#progress.update({
@@ -63,4 +63,7 @@ export declare function assertImageAlias(alias: string): void;
63
63
  */
64
64
  export declare function runTransaction<T>(depthHolder: {
65
65
  depth: number;
66
+ aborted?: {
67
+ error: unknown;
68
+ };
66
69
  }, run: (sql: string) => void, fn: () => T): T;
package/dist/database.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { classifySqliteFailure } from './errors.js';
1
2
  /** Attach-schema aliases are code-chosen, never user input — enforce it. */
2
3
  export function assertImageAlias(alias) {
3
4
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) {
@@ -10,6 +11,10 @@ export function assertImageAlias(alias) {
10
11
  */
11
12
  export function runTransaction(depthHolder, run, fn) {
12
13
  const depth = depthHolder.depth;
14
+ if (depth === 0)
15
+ delete depthHolder.aborted;
16
+ else if (depthHolder.aborted)
17
+ throw depthHolder.aborted.error;
13
18
  const savepoint = `syncular_sp_${depth}`;
14
19
  if (depth === 0)
15
20
  run('BEGIN');
@@ -18,6 +23,9 @@ export function runTransaction(depthHolder, run, fn) {
18
23
  depthHolder.depth = depth + 1;
19
24
  try {
20
25
  const result = fn();
26
+ const aborted = depthHolder.aborted;
27
+ if (aborted)
28
+ throw aborted.error;
21
29
  depthHolder.depth = depth;
22
30
  if (depth === 0)
23
31
  run('COMMIT');
@@ -25,15 +33,26 @@ export function runTransaction(depthHolder, run, fn) {
25
33
  run(`RELEASE ${savepoint}`);
26
34
  return result;
27
35
  }
28
- catch (error) {
36
+ catch (caught) {
37
+ const aborted = depthHolder.aborted;
38
+ const error = aborted?.error ?? caught;
29
39
  depthHolder.depth = depth;
30
- if (depth === 0) {
31
- run('ROLLBACK');
40
+ try {
41
+ if (depth === 0)
42
+ run('ROLLBACK');
43
+ else {
44
+ run(`ROLLBACK TO ${savepoint}`);
45
+ run(`RELEASE ${savepoint}`);
46
+ }
32
47
  }
33
- else {
34
- run(`ROLLBACK TO ${savepoint}`);
35
- run(`RELEASE ${savepoint}`);
48
+ catch (rollbackError) {
49
+ // SQLite can end the whole transaction after SQLITE_FULL or SQLITE_IOERR.
50
+ // Poison remaining nested scopes until the outer owner unwinds.
51
+ depthHolder.aborted = { error };
52
+ if (error instanceof Error && !('rollbackError' in error)) {
53
+ Object.defineProperty(error, 'rollbackError', { value: rollbackError });
54
+ }
36
55
  }
37
- throw error;
56
+ throw classifySqliteFailure(error).error;
38
57
  }
39
58
  }
@@ -113,7 +113,7 @@ export interface DiagnosticQueryFailure {
113
113
  readonly id: string;
114
114
  /** Distinct generated table names the query depends on, ascending. */
115
115
  readonly tables: readonly string[];
116
- readonly code: 'client.storage_corrupt' | 'client.storage_io' | 'client.query_failed';
116
+ readonly code: 'client.storage_corrupt' | 'client.storage_full' | 'client.storage_io' | 'client.query_failed';
117
117
  /** SQLite extended result code, when the driver exposed one. */
118
118
  readonly sqliteCode?: number;
119
119
  /** First failure since this id's last successful read. */
package/dist/errors.d.ts CHANGED
@@ -5,6 +5,8 @@ export declare const STORAGE_BUSY_CODE = "client.storage_busy";
5
5
  export declare const STORAGE_UNAVAILABLE_CODE = "client.storage_unavailable";
6
6
  /** A local read hit SQLITE_CORRUPT or SQLITE_NOTADB (SPEC §7.5). */
7
7
  export declare const STORAGE_CORRUPT_CODE = "client.storage_corrupt";
8
+ /** Local SQLite storage has no space for the write. */
9
+ export declare const STORAGE_FULL_CODE = "client.storage_full";
8
10
  /** A local read hit SQLITE_IOERR (SPEC §7.5). */
9
11
  export declare const STORAGE_IO_CODE = "client.storage_io";
10
12
  /** SPEC §8.8: a round under `realtimePolicy: 'required'` found no socket. */
@@ -61,6 +63,6 @@ export declare class RealtimeUnavailableError extends ClientSyncError {
61
63
  */
62
64
  export declare function classifySqliteFailure(error: unknown): {
63
65
  readonly sqliteCode: number | undefined;
64
- readonly code: typeof STORAGE_CORRUPT_CODE | typeof STORAGE_IO_CODE | undefined;
66
+ readonly code: typeof STORAGE_CORRUPT_CODE | typeof STORAGE_FULL_CODE | typeof STORAGE_IO_CODE | undefined;
65
67
  readonly error: unknown;
66
68
  };
package/dist/errors.js CHANGED
@@ -4,6 +4,8 @@ export const STORAGE_BUSY_CODE = 'client.storage_busy';
4
4
  export const STORAGE_UNAVAILABLE_CODE = 'client.storage_unavailable';
5
5
  /** A local read hit SQLITE_CORRUPT or SQLITE_NOTADB (SPEC §7.5). */
6
6
  export const STORAGE_CORRUPT_CODE = 'client.storage_corrupt';
7
+ /** Local SQLite storage has no space for the write. */
8
+ export const STORAGE_FULL_CODE = 'client.storage_full';
7
9
  /** A local read hit SQLITE_IOERR (SPEC §7.5). */
8
10
  export const STORAGE_IO_CODE = 'client.storage_io';
9
11
  /** SPEC §8.8: a round under `realtimePolicy: 'required'` found no socket. */
@@ -73,6 +75,31 @@ export class RealtimeUnavailableError extends ClientSyncError {
73
75
  * original value.
74
76
  */
75
77
  export function classifySqliteFailure(error) {
78
+ if (error instanceof ClientSyncError) {
79
+ const sqliteCode = error.details?.sqliteCode;
80
+ let retained = error;
81
+ if ('rollbackError' in error &&
82
+ error.details?.rollbackFailure === undefined) {
83
+ const rollback = error.rollbackError;
84
+ retained = new ClientSyncError(error.code, error.message, error.retryable, {
85
+ ...error.details,
86
+ rollbackFailure: {
87
+ message: rollback instanceof Error ? rollback.message : String(rollback),
88
+ sqliteCode: classifySqliteFailure(rollback).sqliteCode,
89
+ },
90
+ });
91
+ Object.defineProperty(retained, 'cause', { value: error });
92
+ }
93
+ return {
94
+ sqliteCode: typeof sqliteCode === 'number' ? sqliteCode : undefined,
95
+ code: error.code === STORAGE_FULL_CODE ||
96
+ error.code === STORAGE_IO_CODE ||
97
+ error.code === STORAGE_CORRUPT_CODE
98
+ ? error.code
99
+ : undefined,
100
+ error: retained,
101
+ };
102
+ }
76
103
  const sqliteCode = error instanceof Error
77
104
  ? error.name === 'SQLiteError' &&
78
105
  'errno' in error &&
@@ -87,19 +114,34 @@ export function classifySqliteFailure(error) {
87
114
  : undefined
88
115
  : undefined;
89
116
  const primary = sqliteCode === undefined ? undefined : sqliteCode & 0xff;
90
- if (primary === 11 || primary === 26) {
91
- return {
92
- sqliteCode,
93
- code: STORAGE_CORRUPT_CODE,
94
- error: new ClientSyncError(STORAGE_CORRUPT_CODE, 'local SQLite storage is corrupt'),
95
- };
96
- }
97
- if (primary === 10) {
98
- return {
99
- sqliteCode,
100
- code: STORAGE_IO_CODE,
101
- error: new ClientSyncError(STORAGE_IO_CODE, 'local SQLite storage I/O failed'),
102
- };
103
- }
104
- return { sqliteCode, code: undefined, error };
117
+ const code = primary === 13
118
+ ? STORAGE_FULL_CODE
119
+ : primary === 10
120
+ ? STORAGE_IO_CODE
121
+ : primary === 11 || primary === 26
122
+ ? STORAGE_CORRUPT_CODE
123
+ : undefined;
124
+ if (code === undefined)
125
+ return { sqliteCode, code, error };
126
+ const rollback = error instanceof Error && 'rollbackError' in error
127
+ ? error.rollbackError
128
+ : undefined;
129
+ const failure = new ClientSyncError(code, primary === 13
130
+ ? 'local SQLite storage is full'
131
+ : primary === 10
132
+ ? 'local SQLite storage I/O failed'
133
+ : 'local SQLite storage is corrupt', false, {
134
+ sqliteCode,
135
+ sqliteMessage: error instanceof Error ? error.message : String(error),
136
+ ...(rollback !== undefined
137
+ ? {
138
+ rollbackFailure: {
139
+ message: rollback instanceof Error ? rollback.message : String(rollback),
140
+ sqliteCode: classifySqliteFailure(rollback).sqliteCode,
141
+ },
142
+ }
143
+ : {}),
144
+ });
145
+ Object.defineProperty(failure, 'cause', { value: error });
146
+ return { sqliteCode, code, error: failure };
105
147
  }
@@ -16,11 +16,12 @@
16
16
  import { SyncClient } from './client.js';
17
17
  import { withClientDiagnosticsHost, } from './diagnostics.js';
18
18
  import { encryptionConfigFromKeyring } from './encryption.js';
19
- import { ClientSyncError, STORAGE_BUSY_CODE } from './errors.js';
19
+ import { classifySqliteFailure, ClientSyncError, STORAGE_BUSY_CODE, } from './errors.js';
20
20
  import { httpBlobTransport, httpSegmentDownloader, httpSyncTransport, webSocketRealtimeConnector, } from './http.js';
21
21
  import { openPersistentWasmDatabase } from './wasm-database.js';
22
22
  import { WORKER_FAILED_CODE, } from './worker-protocol.js';
23
- function toErrorShape(error) {
23
+ function toErrorShape(caught) {
24
+ const error = classifySqliteFailure(caught).error;
24
25
  if (error instanceof ClientSyncError) {
25
26
  return {
26
27
  code: error.code,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client",
3
- "version": "0.30.6",
3
+ "version": "0.30.7",
4
4
  "description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -89,10 +89,10 @@
89
89
  },
90
90
  "dependencies": {
91
91
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
92
- "@syncular/core": "0.30.6"
92
+ "@syncular/core": "0.30.7"
93
93
  },
94
94
  "devDependencies": {
95
- "@syncular/server": "0.30.6",
95
+ "@syncular/server": "0.30.7",
96
96
  "playwright": "1.63.0"
97
97
  }
98
98
  }
package/src/client.ts CHANGED
@@ -3391,7 +3391,8 @@ export class SyncClient {
3391
3391
  this.#emitDiagnostics();
3392
3392
  return summary;
3393
3393
  },
3394
- (error: unknown) => {
3394
+ (caught: unknown) => {
3395
+ const error = classifySqliteFailure(caught).error;
3395
3396
  const completedAtMs = this.#now();
3396
3397
  const code = (error as { code?: unknown }).code;
3397
3398
  this.#progress.update({
package/src/database.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { classifySqliteFailure } from './errors';
2
+
1
3
  /**
2
4
  * Storage abstraction: the client core runs on any SQLite that
3
5
  * implements this minimal synchronous surface. Tests use bun:sqlite
@@ -75,29 +77,43 @@ export function assertImageAlias(alias: string): void {
75
77
  * `run` executes a parameterless statement.
76
78
  */
77
79
  export function runTransaction<T>(
78
- depthHolder: { depth: number },
80
+ depthHolder: { depth: number; aborted?: { error: unknown } },
79
81
  run: (sql: string) => void,
80
82
  fn: () => T,
81
83
  ): T {
82
84
  const depth = depthHolder.depth;
85
+ if (depth === 0) delete depthHolder.aborted;
86
+ else if (depthHolder.aborted) throw depthHolder.aborted.error;
83
87
  const savepoint = `syncular_sp_${depth}`;
84
88
  if (depth === 0) run('BEGIN');
85
89
  else run(`SAVEPOINT ${savepoint}`);
86
90
  depthHolder.depth = depth + 1;
87
91
  try {
88
92
  const result = fn();
93
+ const aborted = depthHolder.aborted as { error: unknown } | undefined;
94
+ if (aborted) throw aborted.error;
89
95
  depthHolder.depth = depth;
90
96
  if (depth === 0) run('COMMIT');
91
97
  else run(`RELEASE ${savepoint}`);
92
98
  return result;
93
- } catch (error) {
99
+ } catch (caught) {
100
+ const aborted = depthHolder.aborted as { error: unknown } | undefined;
101
+ const error = aborted?.error ?? caught;
94
102
  depthHolder.depth = depth;
95
- if (depth === 0) {
96
- run('ROLLBACK');
97
- } else {
98
- run(`ROLLBACK TO ${savepoint}`);
99
- run(`RELEASE ${savepoint}`);
103
+ try {
104
+ if (depth === 0) run('ROLLBACK');
105
+ else {
106
+ run(`ROLLBACK TO ${savepoint}`);
107
+ run(`RELEASE ${savepoint}`);
108
+ }
109
+ } catch (rollbackError) {
110
+ // SQLite can end the whole transaction after SQLITE_FULL or SQLITE_IOERR.
111
+ // Poison remaining nested scopes until the outer owner unwinds.
112
+ depthHolder.aborted = { error };
113
+ if (error instanceof Error && !('rollbackError' in error)) {
114
+ Object.defineProperty(error, 'rollbackError', { value: rollbackError });
115
+ }
100
116
  }
101
- throw error;
117
+ throw classifySqliteFailure(error).error;
102
118
  }
103
119
  }
@@ -151,6 +151,7 @@ export interface DiagnosticQueryFailure {
151
151
  readonly tables: readonly string[];
152
152
  readonly code:
153
153
  | 'client.storage_corrupt'
154
+ | 'client.storage_full'
154
155
  | 'client.storage_io'
155
156
  | 'client.query_failed';
156
157
  /** SQLite extended result code, when the driver exposed one. */
package/src/errors.ts CHANGED
@@ -9,6 +9,9 @@ export const STORAGE_UNAVAILABLE_CODE = 'client.storage_unavailable';
9
9
  /** A local read hit SQLITE_CORRUPT or SQLITE_NOTADB (SPEC §7.5). */
10
10
  export const STORAGE_CORRUPT_CODE = 'client.storage_corrupt';
11
11
 
12
+ /** Local SQLite storage has no space for the write. */
13
+ export const STORAGE_FULL_CODE = 'client.storage_full';
14
+
12
15
  /** A local read hit SQLITE_IOERR (SPEC §7.5). */
13
16
  export const STORAGE_IO_CODE = 'client.storage_io';
14
17
 
@@ -107,10 +110,45 @@ export function classifySqliteFailure(error: unknown): {
107
110
  readonly sqliteCode: number | undefined;
108
111
  readonly code:
109
112
  | typeof STORAGE_CORRUPT_CODE
113
+ | typeof STORAGE_FULL_CODE
110
114
  | typeof STORAGE_IO_CODE
111
115
  | undefined;
112
116
  readonly error: unknown;
113
117
  } {
118
+ if (error instanceof ClientSyncError) {
119
+ const sqliteCode = error.details?.sqliteCode;
120
+ let retained = error;
121
+ if (
122
+ 'rollbackError' in error &&
123
+ error.details?.rollbackFailure === undefined
124
+ ) {
125
+ const rollback = error.rollbackError;
126
+ retained = new ClientSyncError(
127
+ error.code,
128
+ error.message,
129
+ error.retryable,
130
+ {
131
+ ...error.details,
132
+ rollbackFailure: {
133
+ message:
134
+ rollback instanceof Error ? rollback.message : String(rollback),
135
+ sqliteCode: classifySqliteFailure(rollback).sqliteCode,
136
+ },
137
+ },
138
+ );
139
+ Object.defineProperty(retained, 'cause', { value: error });
140
+ }
141
+ return {
142
+ sqliteCode: typeof sqliteCode === 'number' ? sqliteCode : undefined,
143
+ code:
144
+ error.code === STORAGE_FULL_CODE ||
145
+ error.code === STORAGE_IO_CODE ||
146
+ error.code === STORAGE_CORRUPT_CODE
147
+ ? error.code
148
+ : undefined,
149
+ error: retained,
150
+ };
151
+ }
114
152
  const sqliteCode =
115
153
  error instanceof Error
116
154
  ? error.name === 'SQLiteError' &&
@@ -126,25 +164,41 @@ export function classifySqliteFailure(error: unknown): {
126
164
  : undefined
127
165
  : undefined;
128
166
  const primary = sqliteCode === undefined ? undefined : sqliteCode & 0xff;
129
- if (primary === 11 || primary === 26) {
130
- return {
131
- sqliteCode,
132
- code: STORAGE_CORRUPT_CODE,
133
- error: new ClientSyncError(
134
- STORAGE_CORRUPT_CODE,
135
- 'local SQLite storage is corrupt',
136
- ),
137
- };
138
- }
139
- if (primary === 10) {
140
- return {
167
+ const code =
168
+ primary === 13
169
+ ? STORAGE_FULL_CODE
170
+ : primary === 10
171
+ ? STORAGE_IO_CODE
172
+ : primary === 11 || primary === 26
173
+ ? STORAGE_CORRUPT_CODE
174
+ : undefined;
175
+ if (code === undefined) return { sqliteCode, code, error };
176
+ const rollback =
177
+ error instanceof Error && 'rollbackError' in error
178
+ ? error.rollbackError
179
+ : undefined;
180
+ const failure = new ClientSyncError(
181
+ code,
182
+ primary === 13
183
+ ? 'local SQLite storage is full'
184
+ : primary === 10
185
+ ? 'local SQLite storage I/O failed'
186
+ : 'local SQLite storage is corrupt',
187
+ false,
188
+ {
141
189
  sqliteCode,
142
- code: STORAGE_IO_CODE,
143
- error: new ClientSyncError(
144
- STORAGE_IO_CODE,
145
- 'local SQLite storage I/O failed',
146
- ),
147
- };
148
- }
149
- return { sqliteCode, code: undefined, error };
190
+ sqliteMessage: error instanceof Error ? error.message : String(error),
191
+ ...(rollback !== undefined
192
+ ? {
193
+ rollbackFailure: {
194
+ message:
195
+ rollback instanceof Error ? rollback.message : String(rollback),
196
+ sqliteCode: classifySqliteFailure(rollback).sqliteCode,
197
+ },
198
+ }
199
+ : {}),
200
+ },
201
+ );
202
+ Object.defineProperty(failure, 'cause', { value: error });
203
+ return { sqliteCode, code, error: failure };
150
204
  }
@@ -20,7 +20,11 @@ import {
20
20
  withClientDiagnosticsHost,
21
21
  } from './diagnostics';
22
22
  import { encryptionConfigFromKeyring } from './encryption';
23
- import { ClientSyncError, STORAGE_BUSY_CODE } from './errors';
23
+ import {
24
+ classifySqliteFailure,
25
+ ClientSyncError,
26
+ STORAGE_BUSY_CODE,
27
+ } from './errors';
24
28
  import {
25
29
  httpBlobTransport,
26
30
  httpSegmentDownloader,
@@ -77,7 +81,8 @@ interface WorkerScope {
77
81
  ): void;
78
82
  }
79
83
 
80
- function toErrorShape(error: unknown): WorkerErrorShape {
84
+ function toErrorShape(caught: unknown): WorkerErrorShape {
85
+ const error = classifySqliteFailure(caught).error;
81
86
  if (error instanceof ClientSyncError) {
82
87
  return {
83
88
  code: error.code,