@matter/general 0.16.0-alpha.0-20251030-e9ca79f93 → 0.16.0-alpha.0-20251031-0f308af69

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 (62) hide show
  1. package/dist/cjs/MatterError.d.ts +17 -0
  2. package/dist/cjs/MatterError.d.ts.map +1 -1
  3. package/dist/cjs/MatterError.js +24 -0
  4. package/dist/cjs/MatterError.js.map +1 -1
  5. package/dist/cjs/util/Abort.d.ts +83 -7
  6. package/dist/cjs/util/Abort.d.ts.map +1 -1
  7. package/dist/cjs/util/Abort.js +138 -24
  8. package/dist/cjs/util/Abort.js.map +2 -2
  9. package/dist/cjs/util/Error.d.ts.map +1 -1
  10. package/dist/cjs/util/Error.js +7 -0
  11. package/dist/cjs/util/Error.js.map +1 -1
  12. package/dist/cjs/util/Function.d.ts +18 -0
  13. package/dist/cjs/util/Function.d.ts.map +1 -0
  14. package/dist/cjs/util/Function.js +38 -0
  15. package/dist/cjs/util/Function.js.map +6 -0
  16. package/dist/cjs/util/Observable.d.ts +16 -4
  17. package/dist/cjs/util/Observable.d.ts.map +1 -1
  18. package/dist/cjs/util/Observable.js +8 -3
  19. package/dist/cjs/util/Observable.js.map +1 -1
  20. package/dist/cjs/util/Set.d.ts +1 -1
  21. package/dist/cjs/util/Set.d.ts.map +1 -1
  22. package/dist/cjs/util/Set.js +4 -1
  23. package/dist/cjs/util/Set.js.map +1 -1
  24. package/dist/cjs/util/index.d.ts +1 -0
  25. package/dist/cjs/util/index.d.ts.map +1 -1
  26. package/dist/cjs/util/index.js +1 -0
  27. package/dist/cjs/util/index.js.map +1 -1
  28. package/dist/esm/MatterError.d.ts +17 -0
  29. package/dist/esm/MatterError.d.ts.map +1 -1
  30. package/dist/esm/MatterError.js +24 -0
  31. package/dist/esm/MatterError.js.map +1 -1
  32. package/dist/esm/util/Abort.d.ts +83 -7
  33. package/dist/esm/util/Abort.d.ts.map +1 -1
  34. package/dist/esm/util/Abort.js +139 -25
  35. package/dist/esm/util/Abort.js.map +2 -2
  36. package/dist/esm/util/Error.d.ts.map +1 -1
  37. package/dist/esm/util/Error.js +7 -0
  38. package/dist/esm/util/Error.js.map +1 -1
  39. package/dist/esm/util/Function.d.ts +18 -0
  40. package/dist/esm/util/Function.d.ts.map +1 -0
  41. package/dist/esm/util/Function.js +18 -0
  42. package/dist/esm/util/Function.js.map +6 -0
  43. package/dist/esm/util/Observable.d.ts +16 -4
  44. package/dist/esm/util/Observable.d.ts.map +1 -1
  45. package/dist/esm/util/Observable.js +8 -3
  46. package/dist/esm/util/Observable.js.map +1 -1
  47. package/dist/esm/util/Set.d.ts +1 -1
  48. package/dist/esm/util/Set.d.ts.map +1 -1
  49. package/dist/esm/util/Set.js +4 -1
  50. package/dist/esm/util/Set.js.map +1 -1
  51. package/dist/esm/util/index.d.ts +1 -0
  52. package/dist/esm/util/index.d.ts.map +1 -1
  53. package/dist/esm/util/index.js +1 -0
  54. package/dist/esm/util/index.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/MatterError.ts +30 -0
  57. package/src/util/Abort.ts +235 -34
  58. package/src/util/Error.ts +9 -0
  59. package/src/util/Function.ts +23 -0
  60. package/src/util/Observable.ts +31 -12
  61. package/src/util/Set.ts +5 -1
  62. package/src/util/index.ts +1 -0
package/src/util/Abort.ts CHANGED
@@ -4,26 +4,216 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { TimeoutError } from "#MatterError.js";
7
+ import { AbortedError, TimeoutError } from "#MatterError.js";
8
8
  import { Duration } from "#time/Duration.js";
9
9
  import { Time, Timer } from "#time/Time.js";
10
+ import { asError } from "./Error.js";
11
+ import { Callable } from "./Function.js";
10
12
  import { SafePromise } from "./Promises.js";
11
13
 
14
+ /**
15
+ * Convenience abort implementation.
16
+ *
17
+ * Acts as both an {@link AbortController} and {@link AbortSignal}.
18
+ *
19
+ * May be awaited like a promise, although it returns the {@link reason} rather than throwing.
20
+ *
21
+ * May be invoked as a function to perform abort.
22
+ *
23
+ * Optionally will register for abort with an outer {@link AbortController} and/or add a timeout. You must abort or
24
+ * invoke {@link close} if you use either of these options.
25
+ */
26
+ export class Abort extends Callable<[reason?: Error]> implements AbortController, AbortSignal, PromiseLike<Error> {
27
+ // The native controller implementation
28
+ #controller: AbortController;
29
+
30
+ // Optional abort chaining
31
+ #dependents?: AbortSignal[];
32
+ #listener?: (reason: any) => void;
33
+
34
+ // Optional PromiseLike behavior
35
+ #aborted?: Promise<Error>;
36
+ #resolve?: (reason: Error) => void;
37
+
38
+ // Optional timeout
39
+ #timeout?: Timer;
40
+
41
+ constructor({ abort, timeout, handler }: Abort.Options = {}) {
42
+ super(() => this.abort());
43
+
44
+ this.#controller = new AbortController();
45
+
46
+ const self = (reason?: any) => {
47
+ this.abort(reason);
48
+ };
49
+ Object.setPrototypeOf(self, Object.getPrototypeOf(this));
50
+
51
+ if (abort && !Array.isArray(abort)) {
52
+ abort = [abort];
53
+ }
54
+
55
+ if (abort?.length) {
56
+ const dependents = abort.map(abort => ("signal" in abort ? abort.signal : abort));
57
+ this.#dependents = dependents;
58
+
59
+ this.#listener = (reason: any) => this.abort(reason);
60
+ for (const dependent of dependents) {
61
+ dependent.addEventListener("abort", this.#listener);
62
+ }
63
+ }
64
+
65
+ if (timeout) {
66
+ this.#timeout = Time.getPeriodicTimer("subtask timeout", timeout, () => {
67
+ if (this.aborted) {
68
+ return;
69
+ }
70
+
71
+ this.abort(new TimeoutError());
72
+ });
73
+
74
+ this.#timeout.start();
75
+ }
76
+
77
+ if (handler) {
78
+ this.addEventListener("abort", () => handler(this.reason));
79
+ }
80
+ }
81
+
82
+ abort(reason?: any) {
83
+ this.#controller.abort(reason ?? new AbortedError());
84
+ }
85
+
86
+ get signal() {
87
+ return this.#controller.signal;
88
+ }
89
+
90
+ /**
91
+ * Race one or more promises with my abort signal.
92
+ *
93
+ * If aborted returns undefined.
94
+ */
95
+ async race<T>(...promises: Array<T | PromiseLike<T>>): Promise<Awaited<T> | void> {
96
+ return Abort.race(this, ...promises);
97
+ }
98
+
99
+ /**
100
+ * Free resources.
101
+ *
102
+ * You must abort or invoke {@link close} when finished if you construct with {@link Abort.Options#abort} or
103
+ * {@link Abort.Options#timeout}.
104
+ */
105
+ close() {
106
+ this.#timeout?.stop();
107
+ if (this.#listener && this.#dependents) {
108
+ for (const dependent of this.#dependents) {
109
+ dependent.removeEventListener("abort", this.#listener);
110
+ }
111
+ }
112
+ }
113
+
114
+ [Symbol.dispose]() {
115
+ this.close();
116
+ }
117
+
118
+ get aborted() {
119
+ return this.signal.aborted;
120
+ }
121
+
122
+ set onabort(onabort: ((this: AbortSignal, ev: Event) => any) | null) {
123
+ this.signal.onabort = onabort;
124
+ }
125
+
126
+ get onabort() {
127
+ return this.signal.onabort;
128
+ }
129
+
130
+ get reason() {
131
+ return asError(this.signal.reason);
132
+ }
133
+
134
+ throwIfAborted() {
135
+ this.signal.throwIfAborted();
136
+ }
137
+
138
+ then<TResult1 = void, TResult2 = never>(
139
+ onfulfilled?: ((value: Error) => TResult1 | PromiseLike<TResult1>) | null,
140
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
141
+ ): PromiseLike<TResult1 | TResult2> {
142
+ if (!this.#aborted) {
143
+ this.#aborted = new Promise(resolve => (this.#resolve = resolve));
144
+ this.addEventListener("abort", () => this.#resolve!(asError(this.reason)));
145
+ }
146
+ return this.#aborted.then(onfulfilled, onrejected);
147
+ }
148
+
149
+ addEventListener<K extends keyof AbortSignalEventMap>(
150
+ type: K,
151
+ listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any,
152
+ options?: boolean | AddEventListenerOptions,
153
+ ): void;
154
+ addEventListener(
155
+ type: string,
156
+ listener: EventListenerOrEventListenerObject,
157
+ options?: boolean | AddEventListenerOptions,
158
+ ): void;
159
+ addEventListener(type: any, listener: any, options?: any) {
160
+ this.signal.addEventListener(type, listener, options);
161
+ }
162
+
163
+ removeEventListener<K extends keyof AbortSignalEventMap>(
164
+ type: K,
165
+ listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any,
166
+ options?: boolean | EventListenerOptions,
167
+ ): void;
168
+ removeEventListener(
169
+ type: string,
170
+ listener: EventListenerOrEventListenerObject,
171
+ options?: boolean | EventListenerOptions,
172
+ ): void;
173
+ removeEventListener(type: any, listener: any, options?: any) {
174
+ this.signal.addEventListener(type, listener, options);
175
+ }
176
+
177
+ dispatchEvent(event: Event) {
178
+ return this.signal.dispatchEvent(event);
179
+ }
180
+ }
181
+
12
182
  /**
13
183
  * Utilities for implementing abort logic.
14
184
  */
15
185
  export namespace Abort {
16
186
  /**
17
- * An entity that may be used to signal abort of an operation.
187
+ * Optional configuration for {@link Abort}.
18
188
  */
19
- export type Signal = AbortController | AbortSignal;
189
+ export interface Options {
190
+ /**
191
+ * One or more parent abort signals.
192
+ *
193
+ * If a parent aborts, this {@link Abort} will abort as well. However the inverse is not true, so this task is
194
+ * independently abortable.
195
+ *
196
+ * This functions similarly to {@link AbortSignal.any} but has additional protection against memory leaks.
197
+ */
198
+ abort?: Signal | Signal[];
199
+
200
+ /**
201
+ * An abort timeout.
202
+ *
203
+ * If you specify a timeout, you must either abort or close the {@link Abort}.
204
+ */
205
+ timeout?: Duration;
206
+
207
+ /**
208
+ * Adds a default abort handler.
209
+ */
210
+ handler?: (reason?: Error) => void;
211
+ }
20
212
 
21
213
  /**
22
- * An abort controller that can be closed.
214
+ * An entity that may be used to signal abort of an operation.
23
215
  */
24
- export interface DisposableController extends AbortController {
25
- [Symbol.dispose](): void;
26
- }
216
+ export type Signal = AbortController | AbortSignal;
27
217
 
28
218
  /**
29
219
  * Determine whether a {@link Signal} is aborted.
@@ -72,6 +262,17 @@ export namespace Abort {
72
262
  return SafePromise.race(promises);
73
263
  }
74
264
 
265
+ /**
266
+ * Perform abortable sleep.
267
+ */
268
+ export function sleep(description: string, abort: Signal | undefined, duration: Duration) {
269
+ let timer!: Timer;
270
+ const rested = new Promise<void>(resolve => {
271
+ timer = Time.getTimer(description, duration, resolve);
272
+ });
273
+ return race(abort, rested).finally(timer.stop.bind(timer));
274
+ }
275
+
75
276
  /**
76
277
  * Create independently abortable subtask with a new {@link AbortController} that is aborted if another controller
77
278
  * aborts.
@@ -80,39 +281,39 @@ export namespace Abort {
80
281
  *
81
282
  * {@link timeout} is a convenience for adding a timeout.
82
283
  */
83
- export function subtask(signal: Signal | undefined, timeout?: Duration): DisposableController {
84
- let timer: Timer | undefined;
85
-
86
- if (signal && "signal" in signal) {
87
- signal = signal.signal;
88
- }
284
+ export function subtask(signal: Signal | undefined, timeout?: Duration): Abort {
285
+ return new Abort({ abort: signal, timeout });
286
+ }
89
287
 
90
- const controller = new AbortController() as DisposableController;
288
+ /**
289
+ * Like {@link AbortSignal.any} but does not leak memory so long as the returned {@link Abort} is aborted or closed.
290
+ */
291
+ export function any(...signals: (Signal | undefined)[]) {
292
+ return new Abort({ abort: [...(signals.filter(signal => signal) as Signal[])] });
293
+ }
91
294
 
92
- if (timeout) {
93
- timer = Time.getPeriodicTimer("subtask timeout", timeout, () => {
94
- if (controller.signal.aborted) {
95
- return;
96
- }
295
+ /**
296
+ * Generate a function that will throw if aborted.
297
+ */
298
+ export function checkerFor(signal?: Signal | { abort?: Signal }) {
299
+ if (!signal) {
300
+ return () => {};
301
+ }
97
302
 
98
- controller.abort(new TimeoutError());
99
- });
100
- timer.start();
303
+ if ("abort" in signal && typeof signal.abort === "object") {
304
+ signal = signal.abort;
305
+ }
306
+ if (!signal) {
307
+ return () => {};
101
308
  }
102
309
 
103
- if (signal) {
104
- const outerHandler = () => controller.abort(signal.reason);
105
- signal.addEventListener("abort", outerHandler);
106
- controller[Symbol.dispose] = () => {
107
- signal.removeEventListener("abort", outerHandler);
108
- timer?.stop();
109
- };
110
- } else {
111
- controller[Symbol.dispose] = () => {
112
- timer?.stop();
113
- };
310
+ if ("signal" in signal) {
311
+ signal = signal.signal;
312
+ }
313
+ if (!signal) {
314
+ return () => {};
114
315
  }
115
316
 
116
- return controller;
317
+ return (signal as AbortSignal).throwIfAborted.bind(signal);
117
318
  }
118
319
  }
package/src/util/Error.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import { AbortedError } from "#MatterError.js";
7
8
  import type { Constructable } from "./Construction.js";
8
9
  import { ClassExtends } from "./Type.js";
9
10
 
@@ -14,6 +15,14 @@ function considerAsError(error: unknown): error is Error {
14
15
 
15
16
  export function asError(e: any): Error {
16
17
  if (considerAsError(e)) {
18
+ // AbortController defaults to a DOMException which isn't ideal; translate here
19
+ if (e instanceof DOMException && e.name === "AbortError") {
20
+ const aborted = new AbortedError(e.message);
21
+ aborted.stack = e.stack;
22
+ aborted.cause = e.cause;
23
+ return aborted;
24
+ }
25
+
17
26
  return e;
18
27
  }
19
28
  return new Error(e?.toString() ?? "Unknown error");
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * A base for classes that are also functions.
9
+ */
10
+ export interface Callable<A extends unknown[], R = void> {
11
+ (...args: A): R;
12
+ }
13
+
14
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
15
+ export class Callable<A extends unknown[], R> {
16
+ /**
17
+ * Create a new invocable
18
+ */
19
+ constructor(invoke: Callable<A, R>) {
20
+ Object.setPrototypeOf(invoke, new.target.prototype);
21
+ return invoke;
22
+ }
23
+ }
@@ -138,7 +138,9 @@ export interface Observable<T extends any[] = any[], R = void> extends AsyncIter
138
138
  * Also unlike a normal {@link Observable}, an {@link ObservableValue} may be placed into an error state which will
139
139
  * result in rejection if awaited.
140
140
  */
141
- export interface ObservableValue<T extends [any, ...any[]] = [boolean]> extends Observable<T, void>, Promise<T[0]> {
141
+ export interface ObservableValue<T extends [any, ...any[]] = [boolean], R extends MaybePromise<void> = void>
142
+ extends Observable<T, R>,
143
+ Promise<T[0]> {
142
144
  value: T[0] | undefined;
143
145
  error?: Error;
144
146
 
@@ -170,6 +172,11 @@ export const observant = Symbol("consider-observed");
170
172
  */
171
173
  export interface AsyncObservable<T extends any[] = any[], R = void> extends Observable<T, MaybePromise<R>> {}
172
174
 
175
+ /**
176
+ * An {@link ObservableValue} that explicitly supports asynchronous observers.
177
+ */
178
+ export interface AsyncObservableValue<T extends [any, ...any[]] = [boolean]> extends ObservableValue<T, MaybePromise> {}
179
+
173
180
  function defaultErrorHandler(error: Error) {
174
181
  throw error;
175
182
  }
@@ -425,10 +432,6 @@ export class BasicObservable<T extends any[] = any[], R = void> implements Obser
425
432
 
426
433
  type Next<T> = undefined | { value: T; promise: Promise<Next<T>> };
427
434
 
428
- function constructObservable(handleError?: ObserverErrorHandler) {
429
- return new BasicObservable(handleError);
430
- }
431
-
432
435
  /**
433
436
  * Create an {@link Observable}.
434
437
  */
@@ -437,8 +440,8 @@ export const Observable = constructObservable as unknown as {
437
440
  <T extends any[], R = void>(errorHandler?: ObserverErrorHandler): Observable<T, R>;
438
441
  };
439
442
 
440
- function constructAsyncObservable(handleError?: ObserverErrorHandler) {
441
- return new BasicObservable(handleError, true);
443
+ function constructObservable(handleError?: ObserverErrorHandler) {
444
+ return new BasicObservable(handleError);
442
445
  }
443
446
 
444
447
  /**
@@ -449,6 +452,10 @@ export const AsyncObservable = constructAsyncObservable as unknown as {
449
452
  <T extends any[], R = void>(handleError?: ObserverErrorHandler): AsyncObservable<T, R>;
450
453
  };
451
454
 
455
+ function constructAsyncObservable(handleError?: ObserverErrorHandler) {
456
+ return new BasicObservable(handleError, true);
457
+ }
458
+
452
459
  function event<E, N extends string>(emitter: E, name: N) {
453
460
  const observer = (emitter as any)[name];
454
461
  if (typeof !observer?.on !== "function") {
@@ -471,7 +478,7 @@ export class BasicObservableValue<T extends [any, ...any[]] = [boolean]>
471
478
  reject?: ((reason: any) => void) | null;
472
479
  }[];
473
480
 
474
- constructor(value?: T, handleError?: ObserverErrorHandler, asyncConfig?: ObserverPromiseHandler | boolean) {
481
+ constructor(value?: T[0], handleError?: ObserverErrorHandler, asyncConfig?: ObserverPromiseHandler | boolean) {
475
482
  super(handleError, asyncConfig);
476
483
  this.#value = value;
477
484
  this.on(this.#maybeResolve.bind(this) as unknown as Observer<T, void>);
@@ -558,12 +565,24 @@ export class BasicObservableValue<T extends [any, ...any[]] = [boolean]>
558
565
  * Create an {@link ObservableValue}.
559
566
  */
560
567
  export const ObservableValue = constructObservableValue as unknown as {
561
- new <T extends [any, ...any[]]>(value?: T, errorHandler?: ObserverErrorHandler): ObservableValue<T>;
562
- <T extends [any, ...any[]]>(value?: T, errorHandler?: ObserverErrorHandler): ObservableValue<T>;
568
+ new <T extends [any, ...any[]]>(value?: T[0], errorHandler?: ObserverErrorHandler): ObservableValue<T>;
569
+ <T extends [any, ...any[]]>(value?: T[0], errorHandler?: ObserverErrorHandler): ObservableValue<T>;
570
+ };
571
+
572
+ function constructObservableValue(value?: unknown, handleError?: ObserverErrorHandler) {
573
+ return new BasicObservableValue<any>(value, handleError);
574
+ }
575
+
576
+ /**
577
+ * Create an {@link AsyncObservableValue}.
578
+ */
579
+ export const AsyncObservableValue = constructAsyncObservableValue as unknown as {
580
+ new <T extends [any, ...any[]]>(value?: T[0], errorHandler?: ObserverErrorHandler): AsyncObservableValue<T>;
581
+ <T extends [any, ...any[]]>(value?: T[0], errorHandler?: ObserverErrorHandler): AsyncObservableValue<T>;
563
582
  };
564
583
 
565
- function constructObservableValue(value?: [unknown, ...unknown[]], handleError?: ObserverErrorHandler) {
566
- return new ObservableValue(value, handleError);
584
+ function constructAsyncObservableValue(value?: unknown, handleError?: ObserverErrorHandler) {
585
+ return new BasicObservableValue<any>(value, handleError, true);
567
586
  }
568
587
 
569
588
  /**
package/src/util/Set.ts CHANGED
@@ -138,6 +138,10 @@ export class BasicSet<T, AddT = T> implements ImmutableSet<T>, MutableSet<T, Add
138
138
  }
139
139
 
140
140
  this.#added?.emit(created);
141
+
142
+ if (this.#empty && this.#empty.value) {
143
+ this.#empty.emit(false);
144
+ }
141
145
  }
142
146
 
143
147
  get<F extends keyof T>(field: F, value: T[F]) {
@@ -242,7 +246,7 @@ export class BasicSet<T, AddT = T> implements ImmutableSet<T>, MutableSet<T, Add
242
246
 
243
247
  get empty() {
244
248
  if (this.#empty === undefined) {
245
- this.#empty = ObservableValue();
249
+ this.#empty = ObservableValue(!this.#entries.size);
246
250
  }
247
251
  return this.#empty;
248
252
  }
package/src/util/index.ts CHANGED
@@ -20,6 +20,7 @@ export * from "./DeepEqual.js";
20
20
  export * from "./Entropy.js";
21
21
  export * from "./Error.js";
22
22
  export * from "./FormattedText.js";
23
+ export * from "./Function.js";
23
24
  export * from "./GeneratedClass.js";
24
25
  export * from "./Ip.js";
25
26
  export * from "./Lifecycle.js";