@matter/general 0.13.1-alpha.0-20250520-d699cd56d → 0.14.0-alpha.0-20250524-51a7e1721

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.
Files changed (42) hide show
  1. package/dist/cjs/polyfills/index.d.ts +1 -0
  2. package/dist/cjs/polyfills/index.d.ts.map +1 -1
  3. package/dist/cjs/polyfills/index.js +1 -0
  4. package/dist/cjs/polyfills/index.js.map +1 -1
  5. package/dist/cjs/polyfills/suppressed-error.d.ts +7 -0
  6. package/dist/cjs/polyfills/suppressed-error.d.ts.map +1 -0
  7. package/dist/cjs/polyfills/suppressed-error.js +21 -0
  8. package/dist/cjs/polyfills/suppressed-error.js.map +6 -0
  9. package/dist/cjs/transaction/Transaction.d.ts +49 -22
  10. package/dist/cjs/transaction/Transaction.d.ts.map +1 -1
  11. package/dist/cjs/transaction/Transaction.js +22 -3
  12. package/dist/cjs/transaction/Transaction.js.map +1 -1
  13. package/dist/cjs/transaction/Tx.d.ts +9 -4
  14. package/dist/cjs/transaction/Tx.d.ts.map +1 -1
  15. package/dist/cjs/transaction/Tx.js +152 -122
  16. package/dist/cjs/transaction/Tx.js.map +2 -2
  17. package/dist/cjs/util/Observable.js +1 -1
  18. package/dist/cjs/util/Observable.js.map +1 -1
  19. package/dist/esm/polyfills/index.d.ts +1 -0
  20. package/dist/esm/polyfills/index.d.ts.map +1 -1
  21. package/dist/esm/polyfills/index.js +1 -0
  22. package/dist/esm/polyfills/index.js.map +1 -1
  23. package/dist/esm/polyfills/suppressed-error.d.ts +7 -0
  24. package/dist/esm/polyfills/suppressed-error.d.ts.map +1 -0
  25. package/dist/esm/polyfills/suppressed-error.js +20 -0
  26. package/dist/esm/polyfills/suppressed-error.js.map +6 -0
  27. package/dist/esm/transaction/Transaction.d.ts +49 -22
  28. package/dist/esm/transaction/Transaction.d.ts.map +1 -1
  29. package/dist/esm/transaction/Transaction.js +23 -4
  30. package/dist/esm/transaction/Transaction.js.map +1 -1
  31. package/dist/esm/transaction/Tx.d.ts +9 -4
  32. package/dist/esm/transaction/Tx.d.ts.map +1 -1
  33. package/dist/esm/transaction/Tx.js +152 -122
  34. package/dist/esm/transaction/Tx.js.map +2 -2
  35. package/dist/esm/util/Observable.js +1 -1
  36. package/dist/esm/util/Observable.js.map +1 -1
  37. package/package.json +2 -2
  38. package/src/polyfills/index.ts +1 -0
  39. package/src/polyfills/suppressed-error.ts +27 -0
  40. package/src/transaction/Transaction.ts +42 -4
  41. package/src/transaction/Tx.ts +202 -165
  42. package/src/util/Observable.ts +1 -1
@@ -8,6 +8,7 @@ import { Diagnostic } from "#log/Diagnostic.js";
8
8
  import { Logger } from "#log/Logger.js";
9
9
  import { ImplementationError, ReadOnlyError } from "#MatterError.js";
10
10
  import { Time, Timer } from "#time/Time.js";
11
+ import { asError } from "#util/Error.js";
11
12
  import { Observable } from "#util/Observable.js";
12
13
  import { MaybePromise } from "#util/Promises.js";
13
14
  import { describeList } from "#util/String.js";
@@ -29,106 +30,14 @@ const MAX_CHAINED_COMMITS = 5;
29
30
  /**
30
31
  * This is the only public interface to this file.
31
32
  */
32
- export function act<T>(via: string, actor: (transaction: Transaction) => T): T {
33
- const tx = new Tx(via);
34
- let commits = 0;
35
-
36
- // Post-commit logic may result in the transaction requiring commit again so commit iteratively up to
37
- // MAX_CHAINED_COMMITS times
38
- function commitTransaction(finalResult: T): MaybePromise<T> {
39
- commits++;
40
-
41
- if (commits > MAX_CHAINED_COMMITS) {
42
- throw new TransactionFlowError(
43
- `Transaction commits have cascaded ${MAX_CHAINED_COMMITS} times which likely indicates an infinite loop`,
44
- );
45
- }
46
-
47
- // Avoid MaybePromise.then to shorten stack traces
48
- const result = tx.commit();
49
- if (MaybePromise.is(result)) {
50
- return result.then(() => {
51
- if (tx.status === Status.Exclusive) {
52
- return commitTransaction(finalResult);
53
- }
54
- return finalResult;
55
- });
56
- } else if (tx.status === Status.Exclusive) {
57
- return commitTransaction(finalResult);
58
- }
59
-
60
- return finalResult;
61
- }
62
-
63
- const handleTransactionError = ((error: any) => {
64
- // If we've committed, error happened during commit and we've already logged and cleaned up
65
- if (commits) {
66
- throw error;
67
- }
68
-
69
- logger.error("Rolling back", tx.via, "due to error:", Diagnostic.weak(error?.message || `${error}`));
70
-
71
- try {
72
- const result = tx.rollback();
73
- if (MaybePromise.is(result)) {
74
- return Promise.resolve(result).catch(error2 => {
75
- if (error2 !== error) {
76
- logger.error("Secondary error in", tx.via, "rollback:", error2);
77
- }
78
- throw error;
79
- });
80
- }
81
- } catch (error2) {
82
- if (error2 !== error) {
83
- logger.error("Secondary error in", tx.via, "rollback:", error2);
84
- }
85
- }
86
-
87
- throw error;
88
- }) as (error: any) => MaybePromise<T>; // Cast because otherwise type is MaybePromise<void>
89
-
90
- const closeTransaction = tx.close.bind(tx);
91
-
92
- let isAsync = false;
93
- try {
94
- // Execute the actor
95
- const actorResult = actor(tx);
96
-
97
- // If actor is async, chain commit and close asynchronously
98
- if (MaybePromise.is(actorResult)) {
99
- // If the actor is async mark the transaction as async; this will enable reporting on lock changes
100
- isAsync = tx.isAsync = true;
101
- return Promise.resolve(actorResult)
102
- .then(commitTransaction, handleTransactionError)
103
- .finally(closeTransaction) as T;
104
- }
105
-
106
- // Actor is not async but if commit is, chain closeTransaction
107
- const commitResult = commitTransaction(actorResult);
108
- if (MaybePromise.is(commitResult)) {
109
- isAsync = true;
110
- return Promise.resolve(commitResult).catch(handleTransactionError).finally(closeTransaction) as T;
111
- }
112
-
113
- // Fully synchronous action
114
- return commitResult;
115
- } catch (e) {
116
- const result = handleTransactionError(e);
117
-
118
- // Above throws if synchronous so this is async code path
119
- isAsync = true;
120
- return Promise.resolve(result).finally(closeTransaction) as T;
121
- } finally {
122
- if (!isAsync) {
123
- tx.close();
124
- }
125
- }
33
+ export function open(via: string): Transaction & Transaction.Finalization {
34
+ return new Tx(via);
126
35
  }
127
36
 
128
37
  /**
129
38
  * The concrete implementation of the Transaction interface.
130
39
  */
131
- class Tx implements Transaction {
40
+ class Tx implements Transaction, Transaction.Finalization {
132
41
  #participants = new Set<Participant>();
133
42
  #roles = new Map<{}, Participant>();
134
43
  #resources = new Set<Resource>();
@@ -149,12 +58,9 @@ class Tx implements Transaction {
149
58
  }
150
59
  }
151
60
 
152
- close() {
153
- Monitor.delete(this);
61
+ [Symbol.dispose]() {
62
+ this.#reset("dropped");
154
63
  this.#status = Status.Destroyed;
155
- this.#resources.clear();
156
- this.#roles.clear();
157
- this.#participants.clear();
158
64
  this.#closed?.emit();
159
65
  }
160
66
 
@@ -182,6 +88,9 @@ class Tx implements Transaction {
182
88
  return this.#isAsync;
183
89
  }
184
90
 
91
+ /**
92
+ * We set this during async processing. This enables the lock reporting when too much time ellapses.
93
+ */
185
94
  set isAsync(isAsync: true) {
186
95
  // When the transaction is noted as async we start reporting locks. A further optimization would be to not even
187
96
  // acquire locks for synchronous transactions
@@ -318,28 +227,51 @@ class Tx implements Transaction {
318
227
  }
319
228
 
320
229
  commit() {
230
+ if (this.status === Status.Shared) {
231
+ return this.rollback();
232
+ }
233
+
321
234
  this.#assertAvailable();
322
235
 
323
- if (this.#status === Status.Shared) {
324
- // Use rollback() to reset state
325
- return this.rollback();
236
+ const result = this.#executeCommitCycle(0);
237
+ if (result) {
238
+ this.isAsync = true;
326
239
  }
327
240
 
328
- // Perform the actual commit once preCommit completes
329
- const performCommit = () => {
330
- const participants = [...this.#participants];
331
- const result = this.#finalize(Status.CommittingPhaseOne, "committed", this.#executeCommit.bind(this));
332
- if (MaybePromise.is(result)) {
333
- return result.then(() => this.#executePostCommit(participants));
334
- }
335
- return this.#executePostCommit(participants);
336
- };
241
+ return result;
242
+ }
337
243
 
338
- const result = this.#executePreCommit();
244
+ resolve<T>(result: T): MaybePromise<Awaited<T>> {
245
+ // If result is a promise, we wait for resolution and then commit (success) or roll back (error)
339
246
  if (MaybePromise.is(result)) {
340
- return result.then(performCommit);
247
+ this.isAsync = true;
248
+ return result.then(this.resolve.bind(this), this.reject.bind(this));
249
+ }
250
+
251
+ // Result is not a promise. Commit immediately
252
+ let promise;
253
+ try {
254
+ promise = this.commit();
255
+ } catch (e) {
256
+ // Commit failed synchronously
257
+ return this.reject(e);
341
258
  }
342
- return performCommit();
259
+
260
+ // If commit is async then wait for commit before destruction
261
+ if (MaybePromise.is(promise)) {
262
+ this.isAsync = true;
263
+
264
+ return Promise.resolve(promise)
265
+ .then(() => {
266
+ this[Symbol.dispose]();
267
+ return result as Awaited<T>;
268
+ }, this.reject.bind(this))
269
+ .finally(this[Symbol.dispose].bind(this));
270
+ }
271
+
272
+ // Result and commit succeeded synchronously
273
+ this[Symbol.dispose]();
274
+ return result as Awaited<T>;
343
275
  }
344
276
 
345
277
  rollback() {
@@ -348,6 +280,79 @@ class Tx implements Transaction {
348
280
  return this.#finalize(Status.RollingBack, "rolled back", () => this.#executeRollback());
349
281
  }
350
282
 
283
+ reject(cause: unknown): MaybePromise<never> {
284
+ if (this.#status === Status.Shared) {
285
+ this.#reset("released");
286
+ throw cause;
287
+ }
288
+
289
+ logger.error("Rolling back", this.via, "due to error:", Diagnostic.weak(asError(cause).message));
290
+
291
+ try {
292
+ const result = this.rollback();
293
+ if (MaybePromise.is(result)) {
294
+ return Promise.resolve(result)
295
+ .catch(cause2 => {
296
+ if (cause2 === cause) {
297
+ return;
298
+ }
299
+
300
+ // TODO - once SuppressedError support is confirmed solid, consider using it here
301
+ logger.error("Secondary error in", this.via, "rollback:", cause2);
302
+ })
303
+ .finally(() => {
304
+ this[Symbol.dispose]();
305
+ throw cause;
306
+ }) as Promise<never>;
307
+ }
308
+ } catch (cause2) {
309
+ if (cause2 !== cause) {
310
+ logger.error("Secondary error in", this.via, "rollback:", cause2);
311
+ }
312
+ }
313
+
314
+ this[Symbol.dispose]();
315
+
316
+ throw cause;
317
+ }
318
+
319
+ /**
320
+ * Execute commit logic for a single commit cycle.
321
+ *
322
+ * A "cycle" performs all commit logic and normally brings us back to shared state. But we allow post-commit
323
+ * handlers to re-enter exclusive state. If that happens, we trigger another commit cycle.
324
+ */
325
+ #executeCommitCycle(count: number): MaybePromise<void> {
326
+ count++;
327
+
328
+ if (count > MAX_CHAINED_COMMITS) {
329
+ throw new TransactionFlowError(
330
+ `Transaction commits have cascaded ${count} times which likely indicates an infinite loop`,
331
+ );
332
+ }
333
+
334
+ // Precommit first
335
+ let result = this.#createPreCommitExecutor()();
336
+
337
+ // Then rest of normal commit
338
+ if (MaybePromise.is(result)) {
339
+ result = result.then(this.#executeCommit.bind(this));
340
+ } else {
341
+ result = this.#executeCommit();
342
+ }
343
+
344
+ // Then, if transaction is once again exclusive, recurse
345
+ if (MaybePromise.is(result)) {
346
+ return result.then(() => {
347
+ if (this.#status === Status.Exclusive) {
348
+ return this.#executeCommitCycle(count);
349
+ }
350
+ });
351
+ } else if (this.#status === Status.Exclusive) {
352
+ return this.#executeCommitCycle(count);
353
+ }
354
+ }
355
+
351
356
  waitFor(others: Set<Transaction>) {
352
357
  this.#assertAvailable();
353
358
 
@@ -401,27 +406,6 @@ class Tx implements Transaction {
401
406
  );
402
407
  }
403
408
 
404
- // Post-finalization state reset
405
- const cleanup = () => {
406
- // Release locks
407
- const set = new ResourceSet(this, this.#resources);
408
- const unlocked = set.releaseLocks();
409
- this.#locksChanged(unlocked, `${why} and unlocked`);
410
-
411
- // Reset "slow" transaction state
412
- Monitor.delete(this);
413
- this.#reportingLocks = false;
414
-
415
- // Release participants
416
- this.#participants.clear();
417
-
418
- // Revert to shared
419
- this.#status = Status.Shared;
420
-
421
- // Notify listeners
422
- this.#shared?.emit();
423
- };
424
-
425
409
  // Perform the commit or rollback
426
410
  let isAsync = false;
427
411
  try {
@@ -429,19 +413,47 @@ class Tx implements Transaction {
429
413
  const result = finalizer();
430
414
  if (MaybePromise.is(result)) {
431
415
  isAsync = true;
432
- return Promise.resolve(result).finally(cleanup);
416
+ return Promise.resolve(result).finally(() => this.#reset(why));
433
417
  }
434
418
  } finally {
435
419
  if (!isAsync) {
436
- cleanup();
420
+ this.#reset(why);
437
421
  }
438
422
  }
439
423
  }
440
424
 
425
+ /**
426
+ * Reset state to shared with no resources or participants.
427
+ */
428
+ #reset(why: string) {
429
+ // Post-finalization state reset
430
+ // Release locks
431
+ const set = new ResourceSet(this, this.#resources);
432
+ const unlocked = set.releaseLocks();
433
+ this.#locksChanged(unlocked, `${why} and unlocked`);
434
+
435
+ // Remove resources
436
+ this.#resources.clear();
437
+
438
+ // Reset "slow" transaction state
439
+ Monitor.delete(this);
440
+ this.#reportingLocks = false;
441
+
442
+ // Release participants
443
+ this.#participants.clear();
444
+ this.#roles.clear();
445
+
446
+ // Revert to shared
447
+ this.#status = Status.Shared;
448
+
449
+ // Notify listeners
450
+ this.#shared?.emit();
451
+ }
452
+
441
453
  /**
442
454
  * Iteratively execute pre-commit until all participants "settle" and report no possible mutation.
443
455
  */
444
- #executePreCommit(): MaybePromise<void> {
456
+ #createPreCommitExecutor(): () => MaybePromise<void> {
445
457
  let mayHaveMutated = false;
446
458
  let abortedDueToError = false;
447
459
  let iterator = this.participants[Symbol.iterator]();
@@ -482,7 +494,7 @@ class Tx implements Transaction {
482
494
  iterator = this.participants[Symbol.iterator]();
483
495
  };
484
496
 
485
- const nextPreCommit = (previousResult?: boolean): MaybePromise<void> => {
497
+ const executePreCommit = (previousResult?: boolean): MaybePromise<void> => {
486
498
  // If an error occurred
487
499
  if (abortedDueToError) {
488
500
  return;
@@ -526,7 +538,7 @@ class Tx implements Transaction {
526
538
  try {
527
539
  const result = participant.preCommit?.();
528
540
  if (MaybePromise.is(result)) {
529
- return Promise.resolve(result).catch(handleError).then(nextPreCommit);
541
+ return Promise.resolve(result).catch(handleError).then(executePreCommit);
530
542
  }
531
543
  if (result) {
532
544
  mayHaveMutated = true;
@@ -537,19 +549,33 @@ class Tx implements Transaction {
537
549
  }
538
550
  };
539
551
 
540
- return nextPreCommit();
552
+ return executePreCommit;
541
553
  }
542
554
 
543
555
  /**
544
- * Commit logic passed to #finalize.
556
+ * Handle actual commit and post-commit.
545
557
  */
546
- #executeCommit(): MaybePromise {
547
- //this.#log("commit");
548
- const result = this.#executeCommit1();
558
+ #executeCommit() {
559
+ // Ensure participants are immutable
560
+ const participants = [...this.#participants];
561
+
562
+ // Commit phases 1 & 2
563
+ const executeCommit = (): MaybePromise => {
564
+ const result = this.#executeCommit1();
565
+
566
+ if (MaybePromise.is(result)) {
567
+ return Promise.resolve(result).then(this.#executeCommit2.bind(this));
568
+ }
569
+ return this.#executeCommit2();
570
+ };
571
+ const result = this.#finalize(Status.CommittingPhaseOne, "committed", executeCommit);
572
+
573
+ // Post commit
574
+ const executePostCommit = this.#createPostCommitExecutor(participants);
549
575
  if (MaybePromise.is(result)) {
550
- return Promise.resolve(result).then(this.#executeCommit2.bind(this));
576
+ return result.then(executePostCommit);
551
577
  }
552
- return this.#executeCommit2();
578
+ return executePostCommit();
553
579
  }
554
580
 
555
581
  #executeCommit1(): MaybePromise {
@@ -636,28 +662,39 @@ class Tx implements Transaction {
636
662
  }
637
663
  }
638
664
 
639
- #executePostCommit(participants: Participant[]) {
640
- const participantIterator = participants[Symbol.iterator]();
665
+ /**
666
+ * Execute post-commit phase.
667
+ *
668
+ * We notify each participant sequentially. If a participant throws, we log the error and move on to the next
669
+ * participant.
670
+ */
671
+ #createPostCommitExecutor(participants: Participant[]): () => MaybePromise {
672
+ let i = 0;
641
673
 
642
- const postCommitNextParticipant = (): MaybePromise => {
643
- const next = participantIterator.next();
674
+ const executePostCommit = (): MaybePromise => {
675
+ for (; i < participants.length; i++) {
676
+ const participant = participants[i];
644
677
 
645
- if (next.done) {
646
- return;
647
- }
678
+ try {
679
+ const promise = participant.postCommit?.();
648
680
 
649
- const participant = next.value;
681
+ if (MaybePromise.is(promise)) {
682
+ return Promise.resolve(promise).then(executePostCommit, e => {
683
+ reportParticipantError(e);
684
+ executePostCommit();
685
+ });
686
+ }
687
+ } catch (e) {
688
+ reportParticipantError(e);
689
+ }
650
690
 
651
- return MaybePromise.then(
652
- () => participant.postCommit?.(),
653
- () => postCommitNextParticipant(),
654
- error => {
655
- logger.error(`Error post-commit of ${participant}:`, error);
656
- },
657
- );
691
+ function reportParticipantError(e: unknown) {
692
+ logger.error(`Error post-commit of ${participant}:`, e);
693
+ }
694
+ }
658
695
  };
659
696
 
660
- return postCommitNextParticipant();
697
+ return executePostCommit;
661
698
  }
662
699
 
663
700
  /**
@@ -684,7 +721,7 @@ class Tx implements Transaction {
684
721
  },
685
722
  );
686
723
 
687
- // If commit is asynchronous, collect the promise
724
+ // If rollback is asynchronous, collect the promise
688
725
  if (MaybePromise.is(promise)) {
689
726
  if (ongoing) {
690
727
  ongoing.push(promise as Promise<void>);
@@ -755,7 +755,7 @@ export class QuietObservable<T extends any[] = any[]> extends BasicObservable<T>
755
755
  if (this.#source && this.#sourceObserver) {
756
756
  this.#source.off(this.#sourceObserver);
757
757
  } else if (this.#sourceObserver === undefined) {
758
- this.#sourceObserver = (...payload) => this.emit(...payload);
758
+ this.#sourceObserver = this.emit.bind(this);
759
759
  }
760
760
  this.#source = source;
761
761
  if (source) {