mvcc-api 1.2.2 → 1.2.4

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/README.md CHANGED
@@ -38,8 +38,23 @@ It easily and powerfully solves complex concurrency problems that are difficult
38
38
 
39
39
  ## Installation
40
40
 
41
+ ### Node.js
42
+
41
43
  ```bash
42
44
  npm install mvcc-api
45
+ # or
46
+ npx jsr add @izure/mvcc-api
47
+ ```
48
+
49
+ ### Browser
50
+
51
+ ```javascript
52
+ import {
53
+ SyncMVCCStrategy,
54
+ SyncMVCCTransaction,
55
+ AsyncMVCCStrategy,
56
+ AsyncMVCCTransaction
57
+ } from 'https://cdn.jsdelivr.net/npm/mvcc-api@7/+esm'
43
58
  ```
44
59
 
45
60
  ## Usage
@@ -187,10 +202,12 @@ const bResult = b.commit()
187
202
 
188
203
  ```typescript
189
204
  type TransactionEntry<K, T> = { key: K, data: T }
205
+ type TransactionConflict<K, T> = { key: K, parent: T, child: T }
190
206
 
191
207
  {
192
208
  success: boolean // Success status
193
209
  error?: string // Error message on failure (e.g. conflict)
210
+ conflict?: TransactionConflict<K, T> // Conflict information on failure
194
211
  created: TransactionEntry[] // Keys and values created via create()
195
212
  updated: TransactionEntry[] // Keys and values updated via write()
196
213
  deleted: TransactionEntry[] // Keys deleted via delete() and their previous values
@@ -84,6 +84,19 @@ var MVCCTransaction = class {
84
84
  isRoot() {
85
85
  return !this.parent;
86
86
  }
87
+ /**
88
+ * Checks if any ancestor transaction has already been committed.
89
+ * A nested transaction cannot commit if its parent or any higher ancestor is committed.
90
+ * @returns True if at least one ancestor is committed, false otherwise.
91
+ */
92
+ hasCommittedAncestor() {
93
+ let current = this.parent;
94
+ while (current) {
95
+ if (current.committed) return true;
96
+ current = current.parent;
97
+ }
98
+ return false;
99
+ }
87
100
  // --- Internal buffer manipulation helpers ---
88
101
  _bufferCreate(key, value) {
89
102
  this.localVersion++;
@@ -106,12 +119,7 @@ var MVCCTransaction = class {
106
119
  this.createdKeys.delete(key);
107
120
  this.keyVersions.set(key, this.localVersion);
108
121
  }
109
- /**
110
- * Rolls back the transaction.
111
- * Clears all buffers and marks the transaction as finished.
112
- * @returns The result object with success, created, updated, and deleted keys.
113
- */
114
- rollback() {
122
+ _getResultEntries() {
115
123
  const created = [];
116
124
  const updated = [];
117
125
  for (const [key, data] of this.writeBuffer.entries()) {
@@ -129,6 +137,15 @@ var MVCCTransaction = class {
129
137
  deleted.push({ key, data });
130
138
  }
131
139
  }
140
+ return { created, updated, deleted };
141
+ }
142
+ /**
143
+ * Rolls back the transaction.
144
+ * Clears all buffers and marks the transaction as finished.
145
+ * @returns The result object with success, created, updated, and deleted keys.
146
+ */
147
+ rollback() {
148
+ const { created, updated, deleted } = this._getResultEntries();
132
149
  this.writeBuffer.clear();
133
150
  this.deleteBuffer.clear();
134
151
  this.createdKeys.clear();
@@ -222,38 +239,57 @@ var SyncMVCCTransaction = class _SyncMVCCTransaction extends MVCCTransaction {
222
239
  return this._diskRead(key, snapshotVersion);
223
240
  }
224
241
  }
225
- commit() {
242
+ commit(label) {
243
+ const { created, updated, deleted } = this._getResultEntries();
226
244
  if (this.committed) {
227
- return { success: false, error: "Transaction already committed", created: [], updated: [], deleted: [] };
228
- }
229
- const created = [];
230
- const updated = [];
231
- for (const [key, data] of this.writeBuffer.entries()) {
232
- if (this.createdKeys.has(key)) {
233
- created.push({ key, data });
234
- } else {
235
- updated.push({ key, data });
236
- }
245
+ return {
246
+ label,
247
+ success: false,
248
+ error: "Transaction already committed",
249
+ conflict: void 0,
250
+ created,
251
+ updated,
252
+ deleted
253
+ };
237
254
  }
238
- const deleted = [];
239
- for (const key of this.deleteBuffer) {
240
- if (!this.originallyExisted.has(key)) continue;
241
- const data = this.deletedValues.get(key);
242
- if (data !== void 0) {
243
- deleted.push({ key, data });
244
- }
255
+ if (this.hasCommittedAncestor()) {
256
+ return {
257
+ label,
258
+ success: false,
259
+ error: "Ancestor transaction already committed",
260
+ conflict: void 0,
261
+ created,
262
+ updated,
263
+ deleted
264
+ };
245
265
  }
246
266
  if (this.parent) {
247
- const error = this.parent._merge(this);
248
- if (error) {
249
- return { success: false, error, created: [], updated: [], deleted: [] };
267
+ const failure = this.parent._merge(this);
268
+ if (failure) {
269
+ return {
270
+ label,
271
+ success: false,
272
+ error: failure.error,
273
+ conflict: failure.conflict,
274
+ created,
275
+ updated,
276
+ deleted
277
+ };
250
278
  }
251
279
  this.committed = true;
252
280
  } else {
253
281
  if (this.writeBuffer.size > 0 || this.deleteBuffer.size > 0) {
254
- const error = this._merge(this);
255
- if (error) {
256
- return { success: false, error, created: [], updated: [], deleted: [] };
282
+ const failure = this._merge(this);
283
+ if (failure) {
284
+ return {
285
+ label,
286
+ success: false,
287
+ error: failure.error,
288
+ conflict: failure.conflict,
289
+ created: [],
290
+ updated: [],
291
+ deleted: []
292
+ };
257
293
  }
258
294
  this.writeBuffer.clear();
259
295
  this.deleteBuffer.clear();
@@ -264,20 +300,40 @@ var SyncMVCCTransaction = class _SyncMVCCTransaction extends MVCCTransaction {
264
300
  this.localVersion = 0;
265
301
  }
266
302
  }
267
- return { success: true, created, updated, deleted };
303
+ return {
304
+ label,
305
+ success: true,
306
+ created,
307
+ updated,
308
+ deleted
309
+ };
268
310
  }
269
311
  _merge(child) {
270
312
  if (this.parent) {
271
313
  for (const key of child.writeBuffer.keys()) {
272
314
  const lastModLocalVer = this.keyVersions.get(key);
273
315
  if (lastModLocalVer !== void 0 && lastModLocalVer > child.snapshotLocalVersion) {
274
- return `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`;
316
+ return {
317
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`,
318
+ conflict: {
319
+ key,
320
+ parent: this.read(key),
321
+ child: child.read(key)
322
+ }
323
+ };
275
324
  }
276
325
  }
277
326
  for (const key of child.deleteBuffer) {
278
327
  const lastModLocalVer = this.keyVersions.get(key);
279
328
  if (lastModLocalVer !== void 0 && lastModLocalVer > child.snapshotLocalVersion) {
280
- return `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`;
329
+ return {
330
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`,
331
+ conflict: {
332
+ key,
333
+ parent: this.read(key),
334
+ child: child.read(key)
335
+ }
336
+ };
281
337
  }
282
338
  }
283
339
  const newLocalVersion = this.localVersion + 1;
@@ -305,18 +361,45 @@ var SyncMVCCTransaction = class _SyncMVCCTransaction extends MVCCTransaction {
305
361
  this.localVersion = newLocalVersion;
306
362
  this.root.activeTransactions.delete(child);
307
363
  } else {
308
- this.root.activeTransactions.delete(child);
309
364
  const newVersion = this.version + 1;
310
- const modifiedKeys = /* @__PURE__ */ new Set([...child.writeBuffer.keys(), ...child.deleteBuffer]);
311
- for (const key of modifiedKeys) {
312
- const versions = this.versionIndex.get(key);
313
- if (versions && versions.length > 0) {
314
- const lastVer = versions[versions.length - 1].version;
315
- if (lastVer > child.snapshotVersion) {
316
- return `Commit conflict: Key '${key}' was modified by a newer transaction (v${lastVer})`;
365
+ if (child !== this) {
366
+ const modifiedKeys = /* @__PURE__ */ new Set([...child.writeBuffer.keys(), ...child.deleteBuffer]);
367
+ for (const key of modifiedKeys) {
368
+ const versions = this.versionIndex.get(key);
369
+ if (versions && versions.length > 0) {
370
+ const lastVer = versions[versions.length - 1].version;
371
+ if (lastVer > child.snapshotVersion) {
372
+ return {
373
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (v${lastVer})`,
374
+ conflict: {
375
+ key,
376
+ parent: this.read(key),
377
+ child: child.read(key)
378
+ }
379
+ };
380
+ }
317
381
  }
318
382
  }
319
383
  }
384
+ for (const [key, value] of child.writeBuffer) {
385
+ this.writeBuffer.set(key, value);
386
+ this.deleteBuffer.delete(key);
387
+ if (child.createdKeys.has(key)) {
388
+ this.createdKeys.add(key);
389
+ }
390
+ }
391
+ for (const key of child.deleteBuffer) {
392
+ this.deleteBuffer.add(key);
393
+ this.writeBuffer.delete(key);
394
+ this.createdKeys.delete(key);
395
+ const deletedValue = child.deletedValues.get(key);
396
+ if (deletedValue !== void 0) {
397
+ this.deletedValues.set(key, deletedValue);
398
+ }
399
+ if (child.originallyExisted.has(key)) {
400
+ this.originallyExisted.add(key);
401
+ }
402
+ }
320
403
  for (const [key, value] of child.writeBuffer) {
321
404
  this._diskWrite(key, value, newVersion);
322
405
  }
@@ -324,6 +407,7 @@ var SyncMVCCTransaction = class _SyncMVCCTransaction extends MVCCTransaction {
324
407
  this._diskDelete(key, newVersion);
325
408
  }
326
409
  this.version = newVersion;
410
+ this.root.activeTransactions.delete(child);
327
411
  this._cleanupDeletedCache();
328
412
  }
329
413
  return null;
@@ -774,51 +858,74 @@ var AsyncMVCCTransaction = class _AsyncMVCCTransaction extends MVCCTransaction {
774
858
  return this._diskRead(key, snapshotVersion);
775
859
  }
776
860
  }
777
- async commit() {
778
- return this.writeLock(async () => {
779
- if (this.committed) {
780
- return { success: false, error: "Transaction already committed", created: [], updated: [], deleted: [] };
781
- }
782
- const created = [];
783
- const updated = [];
784
- for (const [key, data] of this.writeBuffer.entries()) {
785
- if (this.createdKeys.has(key)) {
786
- created.push({ key, data });
787
- } else {
788
- updated.push({ key, data });
789
- }
790
- }
791
- const deleted = [];
792
- for (const key of this.deleteBuffer) {
793
- if (!this.originallyExisted.has(key)) continue;
794
- const data = this.deletedValues.get(key);
795
- if (data !== void 0) {
796
- deleted.push({ key, data });
797
- }
861
+ async commit(label) {
862
+ const { created, updated, deleted } = this._getResultEntries();
863
+ if (this.committed) {
864
+ return {
865
+ label,
866
+ success: false,
867
+ error: "Transaction already committed",
868
+ conflict: void 0,
869
+ created,
870
+ updated,
871
+ deleted
872
+ };
873
+ }
874
+ if (this.hasCommittedAncestor()) {
875
+ return {
876
+ label,
877
+ success: false,
878
+ error: "Ancestor transaction already committed",
879
+ conflict: void 0,
880
+ created,
881
+ updated,
882
+ deleted
883
+ };
884
+ }
885
+ if (this.parent) {
886
+ const failure = await this.parent._merge(this);
887
+ if (failure) {
888
+ return {
889
+ label,
890
+ success: false,
891
+ error: failure.error,
892
+ conflict: failure.conflict,
893
+ created,
894
+ updated,
895
+ deleted
896
+ };
798
897
  }
799
- if (this.parent) {
800
- const error = await this.parent._merge(this);
801
- if (error) {
802
- return { success: false, error, created: [], updated: [], deleted: [] };
803
- }
804
- this.committed = true;
805
- } else {
806
- if (this.writeBuffer.size > 0 || this.deleteBuffer.size > 0) {
807
- const error = await this._merge(this);
808
- if (error) {
809
- return { success: false, error, created: [], updated: [], deleted: [] };
810
- }
811
- this.writeBuffer.clear();
812
- this.deleteBuffer.clear();
813
- this.createdKeys.clear();
814
- this.deletedValues.clear();
815
- this.originallyExisted.clear();
816
- this.keyVersions.clear();
817
- this.localVersion = 0;
898
+ this.committed = true;
899
+ } else {
900
+ if (this.writeBuffer.size > 0 || this.deleteBuffer.size > 0) {
901
+ const failure = await this._merge(this);
902
+ if (failure) {
903
+ return {
904
+ label,
905
+ success: false,
906
+ error: failure.error,
907
+ conflict: failure.conflict,
908
+ created: [],
909
+ updated: [],
910
+ deleted: []
911
+ };
818
912
  }
913
+ this.writeBuffer.clear();
914
+ this.deleteBuffer.clear();
915
+ this.createdKeys.clear();
916
+ this.deletedValues.clear();
917
+ this.originallyExisted.clear();
918
+ this.keyVersions.clear();
919
+ this.localVersion = 0;
819
920
  }
820
- return { success: true, created, updated, deleted };
821
- });
921
+ }
922
+ return {
923
+ label,
924
+ success: true,
925
+ created,
926
+ updated,
927
+ deleted
928
+ };
822
929
  }
823
930
  async _merge(child) {
824
931
  return this.writeLock(async () => {
@@ -826,13 +933,27 @@ var AsyncMVCCTransaction = class _AsyncMVCCTransaction extends MVCCTransaction {
826
933
  for (const key of child.writeBuffer.keys()) {
827
934
  const lastModLocalVer = this.keyVersions.get(key);
828
935
  if (lastModLocalVer !== void 0 && lastModLocalVer > child.snapshotLocalVersion) {
829
- return `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`;
936
+ return {
937
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`,
938
+ conflict: {
939
+ key,
940
+ parent: await this.read(key),
941
+ child: await child.read(key)
942
+ }
943
+ };
830
944
  }
831
945
  }
832
946
  for (const key of child.deleteBuffer) {
833
947
  const lastModLocalVer = this.keyVersions.get(key);
834
948
  if (lastModLocalVer !== void 0 && lastModLocalVer > child.snapshotLocalVersion) {
835
- return `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`;
949
+ return {
950
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`,
951
+ conflict: {
952
+ key,
953
+ parent: await this.read(key),
954
+ child: await child.read(key)
955
+ }
956
+ };
836
957
  }
837
958
  }
838
959
  const newLocalVersion = this.localVersion + 1;
@@ -859,19 +980,47 @@ var AsyncMVCCTransaction = class _AsyncMVCCTransaction extends MVCCTransaction {
859
980
  }
860
981
  this.localVersion = newLocalVersion;
861
982
  this.root.activeTransactions.delete(child);
983
+ return null;
862
984
  } else {
863
- this.root.activeTransactions.delete(child);
864
985
  const newVersion = this.version + 1;
865
- const modifiedKeys = /* @__PURE__ */ new Set([...child.writeBuffer.keys(), ...child.deleteBuffer]);
866
- for (const key of modifiedKeys) {
867
- const versions = this.versionIndex.get(key);
868
- if (versions && versions.length > 0) {
869
- const lastVer = versions[versions.length - 1].version;
870
- if (lastVer > child.snapshotVersion) {
871
- return `Commit conflict: Key '${key}' was modified by a newer transaction (v${lastVer})`;
986
+ if (child !== this) {
987
+ const modifiedKeys = /* @__PURE__ */ new Set([...child.writeBuffer.keys(), ...child.deleteBuffer]);
988
+ for (const key of modifiedKeys) {
989
+ const versions = this.versionIndex.get(key);
990
+ if (versions && versions.length > 0) {
991
+ const lastVer = versions[versions.length - 1].version;
992
+ if (lastVer > child.snapshotVersion) {
993
+ return {
994
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (v${lastVer})`,
995
+ conflict: {
996
+ key,
997
+ parent: await this.read(key),
998
+ child: await child.read(key)
999
+ }
1000
+ };
1001
+ }
872
1002
  }
873
1003
  }
874
1004
  }
1005
+ for (const [key, value] of child.writeBuffer) {
1006
+ this.writeBuffer.set(key, value);
1007
+ this.deleteBuffer.delete(key);
1008
+ if (child.createdKeys.has(key)) {
1009
+ this.createdKeys.add(key);
1010
+ }
1011
+ }
1012
+ for (const key of child.deleteBuffer) {
1013
+ this.deleteBuffer.add(key);
1014
+ this.writeBuffer.delete(key);
1015
+ this.createdKeys.delete(key);
1016
+ const deletedValue = child.deletedValues.get(key);
1017
+ if (deletedValue !== void 0) {
1018
+ this.deletedValues.set(key, deletedValue);
1019
+ }
1020
+ if (child.originallyExisted.has(key)) {
1021
+ this.originallyExisted.add(key);
1022
+ }
1023
+ }
875
1024
  for (const [key, value] of child.writeBuffer) {
876
1025
  await this._diskWrite(key, value, newVersion);
877
1026
  }
@@ -879,9 +1028,10 @@ var AsyncMVCCTransaction = class _AsyncMVCCTransaction extends MVCCTransaction {
879
1028
  await this._diskDelete(key, newVersion);
880
1029
  }
881
1030
  this.version = newVersion;
1031
+ this.root.activeTransactions.delete(child);
882
1032
  this._cleanupDeletedCache();
1033
+ return null;
883
1034
  }
884
- return null;
885
1035
  });
886
1036
  }
887
1037
  // --- Internal IO Helpers (Root Only) ---
@@ -55,6 +55,19 @@ var MVCCTransaction = class {
55
55
  isRoot() {
56
56
  return !this.parent;
57
57
  }
58
+ /**
59
+ * Checks if any ancestor transaction has already been committed.
60
+ * A nested transaction cannot commit if its parent or any higher ancestor is committed.
61
+ * @returns True if at least one ancestor is committed, false otherwise.
62
+ */
63
+ hasCommittedAncestor() {
64
+ let current = this.parent;
65
+ while (current) {
66
+ if (current.committed) return true;
67
+ current = current.parent;
68
+ }
69
+ return false;
70
+ }
58
71
  // --- Internal buffer manipulation helpers ---
59
72
  _bufferCreate(key, value) {
60
73
  this.localVersion++;
@@ -77,12 +90,7 @@ var MVCCTransaction = class {
77
90
  this.createdKeys.delete(key);
78
91
  this.keyVersions.set(key, this.localVersion);
79
92
  }
80
- /**
81
- * Rolls back the transaction.
82
- * Clears all buffers and marks the transaction as finished.
83
- * @returns The result object with success, created, updated, and deleted keys.
84
- */
85
- rollback() {
93
+ _getResultEntries() {
86
94
  const created = [];
87
95
  const updated = [];
88
96
  for (const [key, data] of this.writeBuffer.entries()) {
@@ -100,6 +108,15 @@ var MVCCTransaction = class {
100
108
  deleted.push({ key, data });
101
109
  }
102
110
  }
111
+ return { created, updated, deleted };
112
+ }
113
+ /**
114
+ * Rolls back the transaction.
115
+ * Clears all buffers and marks the transaction as finished.
116
+ * @returns The result object with success, created, updated, and deleted keys.
117
+ */
118
+ rollback() {
119
+ const { created, updated, deleted } = this._getResultEntries();
103
120
  this.writeBuffer.clear();
104
121
  this.deleteBuffer.clear();
105
122
  this.createdKeys.clear();
@@ -193,38 +210,57 @@ var SyncMVCCTransaction = class _SyncMVCCTransaction extends MVCCTransaction {
193
210
  return this._diskRead(key, snapshotVersion);
194
211
  }
195
212
  }
196
- commit() {
213
+ commit(label) {
214
+ const { created, updated, deleted } = this._getResultEntries();
197
215
  if (this.committed) {
198
- return { success: false, error: "Transaction already committed", created: [], updated: [], deleted: [] };
199
- }
200
- const created = [];
201
- const updated = [];
202
- for (const [key, data] of this.writeBuffer.entries()) {
203
- if (this.createdKeys.has(key)) {
204
- created.push({ key, data });
205
- } else {
206
- updated.push({ key, data });
207
- }
216
+ return {
217
+ label,
218
+ success: false,
219
+ error: "Transaction already committed",
220
+ conflict: void 0,
221
+ created,
222
+ updated,
223
+ deleted
224
+ };
208
225
  }
209
- const deleted = [];
210
- for (const key of this.deleteBuffer) {
211
- if (!this.originallyExisted.has(key)) continue;
212
- const data = this.deletedValues.get(key);
213
- if (data !== void 0) {
214
- deleted.push({ key, data });
215
- }
226
+ if (this.hasCommittedAncestor()) {
227
+ return {
228
+ label,
229
+ success: false,
230
+ error: "Ancestor transaction already committed",
231
+ conflict: void 0,
232
+ created,
233
+ updated,
234
+ deleted
235
+ };
216
236
  }
217
237
  if (this.parent) {
218
- const error = this.parent._merge(this);
219
- if (error) {
220
- return { success: false, error, created: [], updated: [], deleted: [] };
238
+ const failure = this.parent._merge(this);
239
+ if (failure) {
240
+ return {
241
+ label,
242
+ success: false,
243
+ error: failure.error,
244
+ conflict: failure.conflict,
245
+ created,
246
+ updated,
247
+ deleted
248
+ };
221
249
  }
222
250
  this.committed = true;
223
251
  } else {
224
252
  if (this.writeBuffer.size > 0 || this.deleteBuffer.size > 0) {
225
- const error = this._merge(this);
226
- if (error) {
227
- return { success: false, error, created: [], updated: [], deleted: [] };
253
+ const failure = this._merge(this);
254
+ if (failure) {
255
+ return {
256
+ label,
257
+ success: false,
258
+ error: failure.error,
259
+ conflict: failure.conflict,
260
+ created: [],
261
+ updated: [],
262
+ deleted: []
263
+ };
228
264
  }
229
265
  this.writeBuffer.clear();
230
266
  this.deleteBuffer.clear();
@@ -235,20 +271,40 @@ var SyncMVCCTransaction = class _SyncMVCCTransaction extends MVCCTransaction {
235
271
  this.localVersion = 0;
236
272
  }
237
273
  }
238
- return { success: true, created, updated, deleted };
274
+ return {
275
+ label,
276
+ success: true,
277
+ created,
278
+ updated,
279
+ deleted
280
+ };
239
281
  }
240
282
  _merge(child) {
241
283
  if (this.parent) {
242
284
  for (const key of child.writeBuffer.keys()) {
243
285
  const lastModLocalVer = this.keyVersions.get(key);
244
286
  if (lastModLocalVer !== void 0 && lastModLocalVer > child.snapshotLocalVersion) {
245
- return `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`;
287
+ return {
288
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`,
289
+ conflict: {
290
+ key,
291
+ parent: this.read(key),
292
+ child: child.read(key)
293
+ }
294
+ };
246
295
  }
247
296
  }
248
297
  for (const key of child.deleteBuffer) {
249
298
  const lastModLocalVer = this.keyVersions.get(key);
250
299
  if (lastModLocalVer !== void 0 && lastModLocalVer > child.snapshotLocalVersion) {
251
- return `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`;
300
+ return {
301
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`,
302
+ conflict: {
303
+ key,
304
+ parent: this.read(key),
305
+ child: child.read(key)
306
+ }
307
+ };
252
308
  }
253
309
  }
254
310
  const newLocalVersion = this.localVersion + 1;
@@ -276,18 +332,45 @@ var SyncMVCCTransaction = class _SyncMVCCTransaction extends MVCCTransaction {
276
332
  this.localVersion = newLocalVersion;
277
333
  this.root.activeTransactions.delete(child);
278
334
  } else {
279
- this.root.activeTransactions.delete(child);
280
335
  const newVersion = this.version + 1;
281
- const modifiedKeys = /* @__PURE__ */ new Set([...child.writeBuffer.keys(), ...child.deleteBuffer]);
282
- for (const key of modifiedKeys) {
283
- const versions = this.versionIndex.get(key);
284
- if (versions && versions.length > 0) {
285
- const lastVer = versions[versions.length - 1].version;
286
- if (lastVer > child.snapshotVersion) {
287
- return `Commit conflict: Key '${key}' was modified by a newer transaction (v${lastVer})`;
336
+ if (child !== this) {
337
+ const modifiedKeys = /* @__PURE__ */ new Set([...child.writeBuffer.keys(), ...child.deleteBuffer]);
338
+ for (const key of modifiedKeys) {
339
+ const versions = this.versionIndex.get(key);
340
+ if (versions && versions.length > 0) {
341
+ const lastVer = versions[versions.length - 1].version;
342
+ if (lastVer > child.snapshotVersion) {
343
+ return {
344
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (v${lastVer})`,
345
+ conflict: {
346
+ key,
347
+ parent: this.read(key),
348
+ child: child.read(key)
349
+ }
350
+ };
351
+ }
288
352
  }
289
353
  }
290
354
  }
355
+ for (const [key, value] of child.writeBuffer) {
356
+ this.writeBuffer.set(key, value);
357
+ this.deleteBuffer.delete(key);
358
+ if (child.createdKeys.has(key)) {
359
+ this.createdKeys.add(key);
360
+ }
361
+ }
362
+ for (const key of child.deleteBuffer) {
363
+ this.deleteBuffer.add(key);
364
+ this.writeBuffer.delete(key);
365
+ this.createdKeys.delete(key);
366
+ const deletedValue = child.deletedValues.get(key);
367
+ if (deletedValue !== void 0) {
368
+ this.deletedValues.set(key, deletedValue);
369
+ }
370
+ if (child.originallyExisted.has(key)) {
371
+ this.originallyExisted.add(key);
372
+ }
373
+ }
291
374
  for (const [key, value] of child.writeBuffer) {
292
375
  this._diskWrite(key, value, newVersion);
293
376
  }
@@ -295,6 +378,7 @@ var SyncMVCCTransaction = class _SyncMVCCTransaction extends MVCCTransaction {
295
378
  this._diskDelete(key, newVersion);
296
379
  }
297
380
  this.version = newVersion;
381
+ this.root.activeTransactions.delete(child);
298
382
  this._cleanupDeletedCache();
299
383
  }
300
384
  return null;
@@ -745,51 +829,74 @@ var AsyncMVCCTransaction = class _AsyncMVCCTransaction extends MVCCTransaction {
745
829
  return this._diskRead(key, snapshotVersion);
746
830
  }
747
831
  }
748
- async commit() {
749
- return this.writeLock(async () => {
750
- if (this.committed) {
751
- return { success: false, error: "Transaction already committed", created: [], updated: [], deleted: [] };
752
- }
753
- const created = [];
754
- const updated = [];
755
- for (const [key, data] of this.writeBuffer.entries()) {
756
- if (this.createdKeys.has(key)) {
757
- created.push({ key, data });
758
- } else {
759
- updated.push({ key, data });
760
- }
761
- }
762
- const deleted = [];
763
- for (const key of this.deleteBuffer) {
764
- if (!this.originallyExisted.has(key)) continue;
765
- const data = this.deletedValues.get(key);
766
- if (data !== void 0) {
767
- deleted.push({ key, data });
768
- }
832
+ async commit(label) {
833
+ const { created, updated, deleted } = this._getResultEntries();
834
+ if (this.committed) {
835
+ return {
836
+ label,
837
+ success: false,
838
+ error: "Transaction already committed",
839
+ conflict: void 0,
840
+ created,
841
+ updated,
842
+ deleted
843
+ };
844
+ }
845
+ if (this.hasCommittedAncestor()) {
846
+ return {
847
+ label,
848
+ success: false,
849
+ error: "Ancestor transaction already committed",
850
+ conflict: void 0,
851
+ created,
852
+ updated,
853
+ deleted
854
+ };
855
+ }
856
+ if (this.parent) {
857
+ const failure = await this.parent._merge(this);
858
+ if (failure) {
859
+ return {
860
+ label,
861
+ success: false,
862
+ error: failure.error,
863
+ conflict: failure.conflict,
864
+ created,
865
+ updated,
866
+ deleted
867
+ };
769
868
  }
770
- if (this.parent) {
771
- const error = await this.parent._merge(this);
772
- if (error) {
773
- return { success: false, error, created: [], updated: [], deleted: [] };
774
- }
775
- this.committed = true;
776
- } else {
777
- if (this.writeBuffer.size > 0 || this.deleteBuffer.size > 0) {
778
- const error = await this._merge(this);
779
- if (error) {
780
- return { success: false, error, created: [], updated: [], deleted: [] };
781
- }
782
- this.writeBuffer.clear();
783
- this.deleteBuffer.clear();
784
- this.createdKeys.clear();
785
- this.deletedValues.clear();
786
- this.originallyExisted.clear();
787
- this.keyVersions.clear();
788
- this.localVersion = 0;
869
+ this.committed = true;
870
+ } else {
871
+ if (this.writeBuffer.size > 0 || this.deleteBuffer.size > 0) {
872
+ const failure = await this._merge(this);
873
+ if (failure) {
874
+ return {
875
+ label,
876
+ success: false,
877
+ error: failure.error,
878
+ conflict: failure.conflict,
879
+ created: [],
880
+ updated: [],
881
+ deleted: []
882
+ };
789
883
  }
884
+ this.writeBuffer.clear();
885
+ this.deleteBuffer.clear();
886
+ this.createdKeys.clear();
887
+ this.deletedValues.clear();
888
+ this.originallyExisted.clear();
889
+ this.keyVersions.clear();
890
+ this.localVersion = 0;
790
891
  }
791
- return { success: true, created, updated, deleted };
792
- });
892
+ }
893
+ return {
894
+ label,
895
+ success: true,
896
+ created,
897
+ updated,
898
+ deleted
899
+ };
793
900
  }
794
901
  async _merge(child) {
795
902
  return this.writeLock(async () => {
@@ -797,13 +904,27 @@ var AsyncMVCCTransaction = class _AsyncMVCCTransaction extends MVCCTransaction {
797
904
  for (const key of child.writeBuffer.keys()) {
798
905
  const lastModLocalVer = this.keyVersions.get(key);
799
906
  if (lastModLocalVer !== void 0 && lastModLocalVer > child.snapshotLocalVersion) {
800
- return `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`;
907
+ return {
908
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`,
909
+ conflict: {
910
+ key,
911
+ parent: await this.read(key),
912
+ child: await child.read(key)
913
+ }
914
+ };
801
915
  }
802
916
  }
803
917
  for (const key of child.deleteBuffer) {
804
918
  const lastModLocalVer = this.keyVersions.get(key);
805
919
  if (lastModLocalVer !== void 0 && lastModLocalVer > child.snapshotLocalVersion) {
806
- return `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`;
920
+ return {
921
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (Local v${lastModLocalVer})`,
922
+ conflict: {
923
+ key,
924
+ parent: await this.read(key),
925
+ child: await child.read(key)
926
+ }
927
+ };
807
928
  }
808
929
  }
809
930
  const newLocalVersion = this.localVersion + 1;
@@ -830,19 +951,47 @@ var AsyncMVCCTransaction = class _AsyncMVCCTransaction extends MVCCTransaction {
830
951
  }
831
952
  this.localVersion = newLocalVersion;
832
953
  this.root.activeTransactions.delete(child);
954
+ return null;
833
955
  } else {
834
- this.root.activeTransactions.delete(child);
835
956
  const newVersion = this.version + 1;
836
- const modifiedKeys = /* @__PURE__ */ new Set([...child.writeBuffer.keys(), ...child.deleteBuffer]);
837
- for (const key of modifiedKeys) {
838
- const versions = this.versionIndex.get(key);
839
- if (versions && versions.length > 0) {
840
- const lastVer = versions[versions.length - 1].version;
841
- if (lastVer > child.snapshotVersion) {
842
- return `Commit conflict: Key '${key}' was modified by a newer transaction (v${lastVer})`;
957
+ if (child !== this) {
958
+ const modifiedKeys = /* @__PURE__ */ new Set([...child.writeBuffer.keys(), ...child.deleteBuffer]);
959
+ for (const key of modifiedKeys) {
960
+ const versions = this.versionIndex.get(key);
961
+ if (versions && versions.length > 0) {
962
+ const lastVer = versions[versions.length - 1].version;
963
+ if (lastVer > child.snapshotVersion) {
964
+ return {
965
+ error: `Commit conflict: Key '${key}' was modified by a newer transaction (v${lastVer})`,
966
+ conflict: {
967
+ key,
968
+ parent: await this.read(key),
969
+ child: await child.read(key)
970
+ }
971
+ };
972
+ }
843
973
  }
844
974
  }
845
975
  }
976
+ for (const [key, value] of child.writeBuffer) {
977
+ this.writeBuffer.set(key, value);
978
+ this.deleteBuffer.delete(key);
979
+ if (child.createdKeys.has(key)) {
980
+ this.createdKeys.add(key);
981
+ }
982
+ }
983
+ for (const key of child.deleteBuffer) {
984
+ this.deleteBuffer.add(key);
985
+ this.writeBuffer.delete(key);
986
+ this.createdKeys.delete(key);
987
+ const deletedValue = child.deletedValues.get(key);
988
+ if (deletedValue !== void 0) {
989
+ this.deletedValues.set(key, deletedValue);
990
+ }
991
+ if (child.originallyExisted.has(key)) {
992
+ this.originallyExisted.add(key);
993
+ }
994
+ }
846
995
  for (const [key, value] of child.writeBuffer) {
847
996
  await this._diskWrite(key, value, newVersion);
848
997
  }
@@ -850,9 +999,10 @@ var AsyncMVCCTransaction = class _AsyncMVCCTransaction extends MVCCTransaction {
850
999
  await this._diskDelete(key, newVersion);
851
1000
  }
852
1001
  this.version = newVersion;
1002
+ this.root.activeTransactions.delete(child);
853
1003
  this._cleanupDeletedCache();
1004
+ return null;
854
1005
  }
855
- return null;
856
1006
  });
857
1007
  }
858
1008
  // --- Internal IO Helpers (Root Only) ---
@@ -1,5 +1,5 @@
1
1
  import type { AsyncMVCCStrategy } from './Strategy';
2
- import type { TransactionResult } from '../../types';
2
+ import type { TransactionMergeFailure, TransactionResult } from '../../types';
3
3
  import { MVCCTransaction } from '../base';
4
4
  export declare class AsyncMVCCTransaction<S extends AsyncMVCCStrategy<K, T>, K, T> extends MVCCTransaction<S, K, T> {
5
5
  private lock;
@@ -11,8 +11,8 @@ export declare class AsyncMVCCTransaction<S extends AsyncMVCCStrategy<K, T>, K,
11
11
  read(key: K): Promise<T | null>;
12
12
  exists(key: K): Promise<boolean>;
13
13
  _readSnapshot(key: K, snapshotVersion: number, snapshotLocalVersion?: number): Promise<T | null>;
14
- commit(): Promise<TransactionResult<K, T>>;
15
- _merge(child: MVCCTransaction<S, K, T>): Promise<string | null>;
14
+ commit(label?: string): Promise<TransactionResult<K, T>>;
15
+ _merge(child: MVCCTransaction<S, K, T>): Promise<TransactionMergeFailure<K, T> | null>;
16
16
  _diskWrite(key: K, value: T, version: number): Promise<void>;
17
17
  _diskRead(key: K, snapshotVersion: number): Promise<T | null>;
18
18
  _diskExists(key: K, snapshotVersion: number): Promise<boolean>;
@@ -1,4 +1,4 @@
1
- import type { Deferred, TransactionResult } from '../../types';
1
+ import type { Deferred, TransactionResult, TransactionEntry, TransactionMergeFailure } from '../../types';
2
2
  import type { MVCCStrategy } from './Strategy';
3
3
  /**
4
4
  * MVCC Transaction abstract class.
@@ -34,6 +34,12 @@ export declare abstract class MVCCTransaction<S extends MVCCStrategy<K, T>, K, T
34
34
  protected activeTransactions: Set<MVCCTransaction<S, K, T>>;
35
35
  constructor(strategy?: S, parent?: MVCCTransaction<S, K, T>, snapshotVersion?: number);
36
36
  isRoot(): boolean;
37
+ /**
38
+ * Checks if any ancestor transaction has already been committed.
39
+ * A nested transaction cannot commit if its parent or any higher ancestor is committed.
40
+ * @returns True if at least one ancestor is committed, false otherwise.
41
+ */
42
+ hasCommittedAncestor(): boolean;
37
43
  /**
38
44
  * Schedules a creation (insert) of a key-value pair.
39
45
  * Throws if the key already exists.
@@ -60,6 +66,11 @@ export declare abstract class MVCCTransaction<S extends MVCCStrategy<K, T>, K, T
60
66
  protected _bufferCreate(key: K, value: T): void;
61
67
  protected _bufferWrite(key: K, value: T): void;
62
68
  protected _bufferDelete(key: K): void;
69
+ protected _getResultEntries(): {
70
+ created: TransactionEntry<K, T>[];
71
+ updated: TransactionEntry<K, T>[];
72
+ deleted: TransactionEntry<K, T>[];
73
+ };
63
74
  /**
64
75
  * Rolls back the transaction.
65
76
  * Clears all buffers and marks the transaction as finished.
@@ -82,9 +93,10 @@ export declare abstract class MVCCTransaction<S extends MVCCStrategy<K, T>, K, T
82
93
  * Commits the transaction.
83
94
  * If root, persists to storage.
84
95
  * If nested, merges to parent.
96
+ * @param label The label for the commit.
85
97
  * @returns The result object with success, created, and obsolete keys.
86
98
  */
87
- abstract commit(): Deferred<TransactionResult<K, T>>;
99
+ abstract commit(label?: string): Deferred<TransactionResult<K, T>>;
88
100
  /**
89
101
  * Creates a nested transaction (child) from this transaction.
90
102
  * @returns A new nested transaction instance.
@@ -95,7 +107,7 @@ export declare abstract class MVCCTransaction<S extends MVCCStrategy<K, T>, K, T
95
107
  * @param child The committed child transaction.
96
108
  * @returns Error message if conflict, null if success.
97
109
  */
98
- abstract _merge(child: MVCCTransaction<S, K, T>): Deferred<string | null>;
110
+ abstract _merge(child: MVCCTransaction<S, K, T>): Deferred<TransactionMergeFailure<K, T> | null>;
99
111
  /**
100
112
  * Reads a value at a specific snapshot version.
101
113
  * Used by child transactions to read from parent respecting the child's snapshot.
@@ -1,5 +1,5 @@
1
1
  import type { SyncMVCCStrategy } from './Strategy';
2
- import type { TransactionResult } from '../../types';
2
+ import type { TransactionResult, TransactionMergeFailure } from '../../types';
3
3
  import { MVCCTransaction } from '../base';
4
4
  export declare class SyncMVCCTransaction<S extends SyncMVCCStrategy<K, T>, K, T> extends MVCCTransaction<S, K, T> {
5
5
  create(key: K, value: T): this;
@@ -9,8 +9,8 @@ export declare class SyncMVCCTransaction<S extends SyncMVCCStrategy<K, T>, K, T>
9
9
  read(key: K): T | null;
10
10
  exists(key: K): boolean;
11
11
  _readSnapshot(key: K, snapshotVersion: number, snapshotLocalVersion?: number): T | null;
12
- commit(): TransactionResult<K, T>;
13
- _merge(child: MVCCTransaction<S, K, T>): string | null;
12
+ commit(label?: string): TransactionResult<K, T>;
13
+ _merge(child: SyncMVCCTransaction<S, K, T>): TransactionMergeFailure<K, T> | null;
14
14
  _diskWrite(key: K, value: T, version: number): void;
15
15
  _diskRead(key: K, snapshotVersion: number): T | null;
16
16
  _diskExists(key: K, snapshotVersion: number): boolean;
@@ -7,9 +7,26 @@ export type TransactionEntry<K, T> = {
7
7
  key: K;
8
8
  data: T;
9
9
  };
10
+ export type TransactionConflict<K, T> = {
11
+ key: K;
12
+ parent: T;
13
+ child: T;
14
+ };
15
+ export type TransactionMergeFailure<K, T> = {
16
+ error: string;
17
+ conflict: TransactionConflict<K, T>;
18
+ };
10
19
  export type TransactionResult<K, T> = {
11
- success: boolean;
12
- error?: string;
20
+ label?: string;
21
+ success: true;
22
+ created: TransactionEntry<K, T>[];
23
+ updated: TransactionEntry<K, T>[];
24
+ deleted: TransactionEntry<K, T>[];
25
+ } | {
26
+ label?: string;
27
+ success: false;
28
+ error: string;
29
+ conflict?: TransactionConflict<K, T>;
13
30
  created: TransactionEntry<K, T>[];
14
31
  updated: TransactionEntry<K, T>[];
15
32
  deleted: TransactionEntry<K, T>[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mvcc-api",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Multiversion Concurrency Control (MVCC) API for TypeScript",
5
5
  "license": "MIT",
6
6
  "author": "izure <admin@izure.org>",