@syncular/server 0.15.23 → 0.15.25

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.
@@ -69,11 +69,17 @@ class SqliteTransaction implements StorageTransaction {
69
69
  #storage: SqliteServerStorage;
70
70
  #partition: string;
71
71
  #open = true;
72
- #commitValidationSavepoint = false;
72
+ #pushApplySavepoint = false;
73
+ readonly #release: () => void;
73
74
 
74
- constructor(storage: SqliteServerStorage, partition: string) {
75
+ constructor(
76
+ storage: SqliteServerStorage,
77
+ partition: string,
78
+ release: () => void,
79
+ ) {
75
80
  this.#storage = storage;
76
81
  this.#partition = partition;
82
+ this.#release = release;
77
83
  storage.db.exec('BEGIN IMMEDIATE');
78
84
  }
79
85
 
@@ -96,11 +102,11 @@ class SqliteTransaction implements StorageTransaction {
96
102
  return this.#storage.scanRowsByIndex(this.#partition, query);
97
103
  }
98
104
 
99
- async lockPartitionForCommitValidation(): Promise<void> {
105
+ async lockPartitionForPush(): Promise<void> {
100
106
  this.#assertOpen();
101
107
  // BEGIN IMMEDIATE in the constructor already owns SQLite's writer lock.
102
- this.#storage.db.exec('SAVEPOINT syncular_commit_validation_candidate');
103
- this.#commitValidationSavepoint = true;
108
+ this.#storage.db.exec('SAVEPOINT syncular_push_candidate');
109
+ this.#pushApplySavepoint = true;
104
110
  }
105
111
 
106
112
  async commitRejectedPushResult(
@@ -109,18 +115,12 @@ class SqliteTransaction implements StorageTransaction {
109
115
  result: StoredPushResult,
110
116
  ): Promise<void> {
111
117
  this.#assertOpen();
112
- if (!this.#commitValidationSavepoint) {
113
- throw new Error(
114
- 'whole-commit rejection requires its validation savepoint',
115
- );
118
+ if (!this.#pushApplySavepoint) {
119
+ throw new Error('push rejection requires its apply savepoint');
116
120
  }
117
- this.#storage.db.exec(
118
- 'ROLLBACK TO SAVEPOINT syncular_commit_validation_candidate',
119
- );
120
- this.#storage.db.exec(
121
- 'RELEASE SAVEPOINT syncular_commit_validation_candidate',
122
- );
123
- this.#commitValidationSavepoint = false;
121
+ this.#storage.db.exec('ROLLBACK TO SAVEPOINT syncular_push_candidate');
122
+ this.#storage.db.exec('RELEASE SAVEPOINT syncular_push_candidate');
123
+ this.#pushApplySavepoint = false;
124
124
  await this.putPushResult(clientId, clientCommitId, result);
125
125
  await this.commit();
126
126
  }
@@ -245,18 +245,28 @@ class SqliteTransaction implements StorageTransaction {
245
245
  async commit(): Promise<void> {
246
246
  this.#assertOpen();
247
247
  this.#open = false;
248
- this.#storage.db.exec('COMMIT');
248
+ try {
249
+ this.#storage.db.exec('COMMIT');
250
+ } finally {
251
+ this.#release();
252
+ }
249
253
  }
250
254
 
251
255
  async rollback(): Promise<void> {
252
256
  if (!this.#open) return;
253
257
  this.#open = false;
254
- this.#storage.db.exec('ROLLBACK');
258
+ try {
259
+ this.#storage.db.exec('ROLLBACK');
260
+ } finally {
261
+ this.#release();
262
+ }
255
263
  }
256
264
  }
257
265
 
258
266
  export class SqliteServerStorage implements ServerStorage {
259
267
  readonly db: Database;
268
+ /** One bun:sqlite connection can own only one transaction at a time. */
269
+ #transactionTail: Promise<void> = Promise.resolve();
260
270
  /** Set by `ensureSchema`: app-table lookup for the relational row store. */
261
271
  #tables: ReadonlyMap<string, CompiledTable> | undefined;
262
272
  #schemaVersion: number | undefined;
@@ -405,7 +415,18 @@ export class SqliteServerStorage implements ServerStorage {
405
415
  }
406
416
 
407
417
  async begin(partition: string): Promise<StorageTransaction> {
408
- return new SqliteTransaction(this, partition);
418
+ const previous = this.#transactionTail;
419
+ let release!: () => void;
420
+ this.#transactionTail = new Promise<void>((resolve) => {
421
+ release = resolve;
422
+ });
423
+ await previous;
424
+ try {
425
+ return new SqliteTransaction(this, partition, release);
426
+ } catch (error) {
427
+ release();
428
+ throw error;
429
+ }
409
430
  }
410
431
 
411
432
  /** Internal: write a row + refresh its scope-index entries. */
package/src/storage.ts CHANGED
@@ -203,15 +203,23 @@ export interface StorageTransaction {
203
203
  */
204
204
  scanRowsByIndex?(query: IndexRowScanQuery): Promise<StoredRow[]>;
205
205
  /**
206
- * Serialize candidate-state validation for this partition before any row
207
- * read/write. Required at runtime when `commitValidator` is configured.
206
+ * Serialize every push apply for this partition before any operation read,
207
+ * validation, merge, or write. The push layer re-checks idempotency only
208
+ * after this resolves and retains the lock through terminal-result commit.
209
+ * Missing support fails closed before an app-row mutation.
210
+ */
211
+ lockPartitionForPush?(): Promise<void>;
212
+ /**
213
+ * @deprecated Implement `lockPartitionForPush`. Kept as a compatibility
214
+ * bridge for custom adapters whose existing implementation already locks
215
+ * the complete partition from before candidate reads through commit.
208
216
  */
209
217
  lockPartitionForCommitValidation?(): Promise<void>;
210
218
  /**
211
- * §6.8 rejection finalization while the validation serialization lock is
212
- * still held: discard every candidate write, persist the rejected
213
- * idempotency result, and finish the transaction atomically. Required when
214
- * `commitValidator` is configured so a concurrent duplicate cannot rerun it.
219
+ * Rejection finalization while the push-apply serialization lock is still
220
+ * held: discard every candidate write, persist the rejected idempotency
221
+ * result, and finish the transaction atomically. Required for every push so
222
+ * a concurrent duplicate cannot rerun operations, validators, or merges.
215
223
  */
216
224
  commitRejectedPushResult?(
217
225
  clientId: string,