uneventful 0.0.11 → 0.0.12

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/ext.d.ts CHANGED
@@ -16,7 +16,7 @@ import { P as PlainFunction } from './types-N2ua11te.js';
16
16
  * @module uneventful/ext
17
17
  *
18
18
  * @experimental
19
- *
19
+ * @disableGroups
20
20
  * @summary Tools for extending objects with extra state and behavior, without
21
21
  * directly modifying them.
22
22
  */
@@ -107,16 +107,12 @@ declare namespace Ext {
107
107
  /**
108
108
  * Get the type of extension that will be returned by the static API.
109
109
  *
110
- * Defaults to the subclass instance type, but can be overridden by `declare
111
- * readonly __type__: OtherType` in a subclass, so long as the
112
- * {@link Ext.__new__ `__new__()`} method is also overridden to return that
113
- * type.
110
+ * Defaults to the subclass instance type, but can be changed by overriding
111
+ * {@link Ext.__new__ `__new__()`} to return a different type.
114
112
  */
115
113
  type Type<T extends Ext.Class> = InstanceType<T> extends {
116
- __type__: infer R;
114
+ __new__(ob: any): infer R;
117
115
  } ? (unknown extends R ? InstanceType<T> : R) : InstanceType<T>;
118
- /** The type of weakmap passed to {@link Ext.__new__ `__new__()`} */
119
- type Map<Class extends Ext.Class> = WeakMap<Ext.Target<Class>, any>;
120
116
  /**
121
117
  * The type constraint for static generics in the API; you probably won't use this directly.
122
118
  */
@@ -160,28 +156,13 @@ declare abstract class Ext<Target extends WeakKey = WeakKey> {
160
156
  */
161
157
  readonly of: Target;
162
158
  /**
163
- * A "virtual" property you can override in subclasses to change the static
164
- * interface's return type. For example, if a subclass does `declare readonly __type__:
165
- * Promise<this>`, and overrides `__new__`() to return a promise, then the
166
- * static APIs for the subclass (like `.for()`) will return promises instead
167
- * of instances. (See the {@link __new__ `__new__`} method for more
168
- * details.)
169
- *
170
- * Note: this property is not actually set by any code, so you can't do anything
171
- * other than declare it. It's just a hack to work around TypeScript's limited
172
- * type parameterization for static generics.
173
- *
174
- * @category Lifecycle Hooks
175
- */
176
- readonly __type__: unknown;
177
- /**
178
- * @deprecated Use .for() instead!
159
+ * @deprecated Use `.for()` or `prototype.__inst__()` instead!
179
160
  *
180
- * Never directly call the constructor of an Ext subclass except from the
181
- * {@link __new__ `__new__()`} method - otherwise you run the risk of having
182
- * multiple instances for the same target. (And it may accept invalid
183
- * parameter values if you've redefined the type of the {@link Ext.of `of`}
184
- * property in a sub-subclass.)
161
+ * Never directly call the constructor of an Ext subclass. If you're
162
+ * creating an instance in {@link __new__ `__new__()`}, use the
163
+ * {@link __inst__ `__inst__()`} method instead. Otherwise,
164
+ * you should use the .for or .get methods, as they are properly typed
165
+ * and won't create multiple instances for the same target.
185
166
  */
186
167
  constructor(of: Target);
187
168
  /**
@@ -215,21 +196,15 @@ declare abstract class Ext<Target extends WeakKey = WeakKey> {
215
196
  * behavior, e.g. to execute the constructor within a job, or create a
216
197
  * promise for an extension instance to be asynchronously initiaized, etc.
217
198
  *
218
- * If you will be creating something other than an instance of the subclass,
219
- * you must also redeclare the type of the {@link __type__ `__type__`}
220
- * property. For example:
199
+ * For example:
221
200
  *
222
201
  * ```ts
223
- * class AsyncExt extends Ext {
224
- * declare readonly __type__: Job<this>
225
- *
202
+ * class AsyncExt extends Ext<SomeType> {
226
203
  * // simulate slow initialization
227
204
  * *setup() { yield *sleep(100); return this; }
228
205
  *
229
- * static __new__<Class extends typeof AsyncExt>(
230
- * tgt: Ext.Target<Class>, map: Ext.Map<Class>
231
- * ) {
232
- * const ext = new this(tgt);
206
+ * __new__(tgt: SomeType): Job<this> {
207
+ * const ext = this.__inst__(tgt);
233
208
  * map.set(tgt, root.start(ext.setup()) as Ext.Type<Class>);
234
209
  * }
235
210
  * }
@@ -239,13 +214,25 @@ declare abstract class Ext<Target extends WeakKey = WeakKey> {
239
214
  * extension whose `setup()` has finished. (And subclasses of `AsyncExt`
240
215
  * will share the same behavior, while being subtyped appropriately.)
241
216
  *
242
- * (Note: your `__new__` method *must* store what it created in the supplied
243
- * map for the given target, and the value it sets must conform to the
244
- * declared `__type__`, which is *not* checked for you by TypeScript!)
217
+ * @remarks Note that while this is technically an instance method, it's
218
+ * actually called with the class *prototype*, so you should not use any
219
+ * properties or methods of `this` other than `__inst__`. Think of it
220
+ * as a function that's just on the class as a convenient way of configuring
221
+ * it.
222
+ *
223
+ * @category Lifecycle Hooks
224
+ */
225
+ __new__<C extends Ext>(this: C, tgt: C["of"]): unknown;
226
+ /**
227
+ * Given a target, create an extension instance.
228
+ *
229
+ * You do not need to override this, nor should you: it's just a type-safe
230
+ * way to construct an extension instance, since an Ext subclass's
231
+ * constructor may accept a wider type than the class actually requires.
245
232
  *
246
233
  * @category Lifecycle Hooks
247
234
  */
248
- static __new__<Class extends Ext.Class>(this: Class, tgt: Ext.Target<Class>, map: Ext.Map<Class>): void;
235
+ __inst__<T extends Ext>(this: T, tgt: T["of"]): T;
249
236
  /**
250
237
  * This method is called by {@link delete}() if it finds an existing
251
238
  * extension for the target. You can override it in a subclass to do any
package/dist/ext.mjs CHANGED
@@ -9,13 +9,13 @@ function method(factory, map = /* @__PURE__ */ new WeakMap()) {
9
9
  const classMap = /* @__PURE__ */ ext((cls) => /* @__PURE__ */ new WeakMap());
10
10
  class Ext {
11
11
  /**
12
- * @deprecated Use .for() instead!
12
+ * @deprecated Use `.for()` or `prototype.__inst__()` instead!
13
13
  *
14
- * Never directly call the constructor of an Ext subclass except from the
15
- * {@link __new__ `__new__()`} method - otherwise you run the risk of having
16
- * multiple instances for the same target. (And it may accept invalid
17
- * parameter values if you've redefined the type of the {@link Ext.of `of`}
18
- * property in a sub-subclass.)
14
+ * Never directly call the constructor of an Ext subclass. If you're
15
+ * creating an instance in {@link __new__ `__new__()`}, use the
16
+ * {@link __inst__ `__inst__()`} method instead. Otherwise,
17
+ * you should use the .for or .get methods, as they are properly typed
18
+ * and won't create multiple instances for the same target.
19
19
  */
20
20
  constructor(of) {
21
21
  this.of = of;
@@ -26,7 +26,7 @@ class Ext {
26
26
  */
27
27
  static for(tgt) {
28
28
  const map = classMap(this);
29
- return map.get(tgt) ?? (this.__new__(tgt, map), map.get(tgt));
29
+ return map.get(tgt) ?? setMap(map, tgt, this.prototype.__new__(tgt));
30
30
  }
31
31
  /**
32
32
  * Get the current extension instance for the given target, or `undefined`
@@ -64,21 +64,15 @@ class Ext {
64
64
  * behavior, e.g. to execute the constructor within a job, or create a
65
65
  * promise for an extension instance to be asynchronously initiaized, etc.
66
66
  *
67
- * If you will be creating something other than an instance of the subclass,
68
- * you must also redeclare the type of the {@link __type__ `__type__`}
69
- * property. For example:
67
+ * For example:
70
68
  *
71
69
  * ```ts
72
- * class AsyncExt extends Ext {
73
- * declare readonly __type__: Job<this>
74
- *
70
+ * class AsyncExt extends Ext<SomeType> {
75
71
  * // simulate slow initialization
76
72
  * *setup() { yield *sleep(100); return this; }
77
73
  *
78
- * static __new__<Class extends typeof AsyncExt>(
79
- * tgt: Ext.Target<Class>, map: Ext.Map<Class>
80
- * ) {
81
- * const ext = new this(tgt);
74
+ * __new__(tgt: SomeType): Job<this> {
75
+ * const ext = this.__inst__(tgt);
82
76
  * map.set(tgt, root.start(ext.setup()) as Ext.Type<Class>);
83
77
  * }
84
78
  * }
@@ -88,14 +82,28 @@ class Ext {
88
82
  * extension whose `setup()` has finished. (And subclasses of `AsyncExt`
89
83
  * will share the same behavior, while being subtyped appropriately.)
90
84
  *
91
- * (Note: your `__new__` method *must* store what it created in the supplied
92
- * map for the given target, and the value it sets must conform to the
93
- * declared `__type__`, which is *not* checked for you by TypeScript!)
85
+ * @remarks Note that while this is technically an instance method, it's
86
+ * actually called with the class *prototype*, so you should not use any
87
+ * properties or methods of `this` other than `__inst__`. Think of it
88
+ * as a function that's just on the class as a convenient way of configuring
89
+ * it.
90
+ *
91
+ * @category Lifecycle Hooks
92
+ */
93
+ __new__(tgt) {
94
+ return this.__inst__(tgt);
95
+ }
96
+ /**
97
+ * Given a target, create an extension instance.
98
+ *
99
+ * You do not need to override this, nor should you: it's just a type-safe
100
+ * way to construct an extension instance, since an Ext subclass's
101
+ * constructor may accept a wider type than the class actually requires.
94
102
  *
95
103
  * @category Lifecycle Hooks
96
104
  */
97
- static __new__(tgt, map) {
98
- map.set(tgt, new this(tgt, map));
105
+ __inst__(tgt) {
106
+ return new this.constructor(tgt);
99
107
  }
100
108
  /**
101
109
  * This method is called by {@link delete}() if it finds an existing
package/dist/shared.d.ts CHANGED
@@ -5,6 +5,7 @@ import { Y as Yielding } from './types-N2ua11te.js';
5
5
  * boundaries.
6
6
  *
7
7
  * @module uneventful/shared
8
+ * @disableGroups
8
9
  */
9
10
 
10
11
  /**
package/dist/signals.d.ts CHANGED
@@ -194,7 +194,7 @@ interface RuleFactory {
194
194
  *
195
195
  * @returns A function that can be called to terminate the rule.
196
196
  *
197
- * @category Reactive Behaviors
197
+ * @category none
198
198
  * @function
199
199
  */
200
200
  declare const rule: ((fn: () => OptionalCleanup) => DisposeFn) & RuleFactory;
@@ -214,7 +214,7 @@ declare const rule: ((fn: () => OptionalCleanup) => DisposeFn) & RuleFactory;
214
214
  * factory you wish to run pending rules for. If not given, the default
215
215
  * {@link rule}() factory is targeted.
216
216
  *
217
- * @category Reactive Behaviors
217
+ * @category Scheduling
218
218
  */
219
219
  declare function runRules(scheduleFn?: SchedulerFn): void;
220
220
  /**
@@ -246,34 +246,6 @@ declare class WriteConflict extends Error {
246
246
  */
247
247
  declare class CircularDependency extends Error {
248
248
  }
249
- /**
250
- * Keep an expression's old value unless there's a semantic change
251
- *
252
- * By default, reactive values (i.e. {@link cached}(), or {@link value}() with a
253
- * {@link Configurable.setf setf}()) are considered to have "changed" (and thus
254
- * trigger recalculation of their dependents) when they are different according
255
- * to `===` comparison.
256
- *
257
- * This works well for primitive values, but for arrays and objects it's not
258
- * always ideal, because two arrays can have the exact same elements and still
259
- * be different according to `===`. So this function lets you substitute a
260
- * different comparison function (like a deep-equal or shallow-equal) instead.
261
- * (The default is {@link arrayEq}() if no compare function is supplied.)
262
- *
263
- * Specifically, if your reactive expression returns `unchangedIf(newVal,
264
- * compare)`, then the expression's previous value will be kept if the compare
265
- * function returns true when called with the old and new values. Otherwise, the
266
- * new value will be used.
267
- *
268
- * @remarks
269
- * - If the reactive expression's last "value" was an error, the new value is
270
- * returned
271
- * - An error will be thrown if this function is called outside a reactive
272
- * expression or from within a {@link peek}() call or {@link action} wrapper.
273
- *
274
- * @category Reactive Values
275
- */
276
- declare function unchangedIf<T>(newVal: T, equals?: (v1: T, v2: T) => boolean): T;
277
249
 
278
250
  /**
279
251
  * The Signals API for uneventful.
@@ -377,7 +349,7 @@ interface Configurable<T> extends Writable<T> {
377
349
  /**
378
350
  * Create a {@link Configurable} signal with the given inital value
379
351
  *
380
- * @category Reactive Values
352
+ * @category none
381
353
  */
382
354
  declare function value<T>(val?: T): Configurable<T>;
383
355
  /**
@@ -389,7 +361,7 @@ declare function value<T>(val?: T): Configurable<T>;
389
361
  * calling signature below will apply, even if TypeScript doesn't see it that
390
362
  * way!)
391
363
  *
392
- * @category Reactive Values
364
+ * @category none
393
365
  */
394
366
  declare function cached<T>(compute: () => T): Signal<T>;
395
367
  /**
@@ -430,7 +402,7 @@ declare function cached<T extends Signal<any>>(signal: T): T;
430
402
  *
431
403
  * @returns The result of calling `fn(..args)`
432
404
  *
433
- * @category Reactive Values
405
+ * @category Dependency Tracking
434
406
  */
435
407
  declare function peek<F extends PlainFunction>(fn: F, ...args: Parameters<F>): ReturnType<F>;
436
408
  /**
@@ -472,7 +444,7 @@ declare function peek<F extends PlainFunction>(fn: F, ...args: Parameters<F>): R
472
444
  * to the original function, while running with dependency tracking suppressed
473
445
  * (as with {@link peek}()).
474
446
  *
475
- * @category Reactive Behaviors
447
+ * @category Dependency Tracking
476
448
  */
477
449
  declare function action<F extends AnyFunction>(fn: F): F;
478
450
  /** @hidden TC39 Decorator protocol */
@@ -483,6 +455,78 @@ declare function action<F extends AnyFunction>(fn: F, ctx: {
483
455
  declare function action<F extends AnyFunction, D extends {
484
456
  value?: F;
485
457
  }>(clsOrProto: any, name: string | symbol, desc: D): D;
458
+ /**
459
+ * Keep an expression's old value unless there's a semantic change
460
+ *
461
+ * By default, reactive values (i.e. {@link cached}(), or {@link value}() with a
462
+ * {@link Configurable.setf setf}()) are considered to have "changed" (and thus
463
+ * trigger recalculation of their dependents) when they are different according
464
+ * to `===` comparison.
465
+ *
466
+ * This works well for primitive values, but for arrays and objects it's not
467
+ * always ideal, because two arrays can have the exact same elements and still
468
+ * be different according to `===`. So this function lets you substitute a
469
+ * different comparison function (like a deep-equal or shallow-equal) instead.
470
+ *
471
+ * Specifically, if your reactive expression returns `stable(compare, newVal)`,
472
+ * then the expression's previous value will be kept if the `compare` function
473
+ * returns true when called with the old and new values. Otherwise, the new
474
+ * value will be used.
475
+ *
476
+ * @remarks
477
+ * - If the reactive expression's last "value" was an error, the new value is
478
+ * returned.
479
+ * - An error will be thrown if this function is called outside a reactive
480
+ * expression or from within a {@link peek}() call or {@link action} wrapper.
481
+ * - You can use the {@link stabilizer}() function to create custom functions
482
+ * similar to {@link stableJSON}. or {@link stableArray}().
483
+ *
484
+ * @category Dependency Tracking
485
+ */
486
+ declare function stable<T>(equals: (v1: T, v2: T) => boolean, newVal: T): T;
487
+ /**
488
+ * @deprecated This is {@link stable}() but with swapped arguments. Please switch
489
+ * to using stable(), stableArray(), or a stabilizer(), as this function will be removed
490
+ * in a future release.
491
+ *
492
+ * @hidden
493
+ */
494
+ declare function unchangedIf<T>(newVal: T, equals?: (v1: T, v2: T) => boolean): T;
495
+ /**
496
+ * Create a shorthand version of {@link stable}(), pre-bound to a specific comparison
497
+ * function.
498
+ *
499
+ * @param equals The comparison function to use
500
+ * @template T The type of value the comparison function works on
501
+ *
502
+ * @category Dependency Tracking
503
+ */
504
+ declare function stabilizer<T>(equals: <T>(v1: T, v2: T) => boolean): <R extends T>(newVal: R) => R;
505
+ /**
506
+ * Keep an expression's old value if the new value is an array with the same
507
+ * contents.
508
+ *
509
+ * (Shorthand for {@link stable}({@link arrayEq}, ...).)
510
+ *
511
+ * @function
512
+ * @category Dependency Tracking
513
+ */
514
+ declare const stableArray: <R extends unknown[]>(newVal: R) => R;
515
+ /**
516
+ * Keep an expression's old value if its JSON string is the same as the new one.
517
+ *
518
+ * (Shorthand for {@link stable}() with a JSON-string comparison function.)
519
+ *
520
+ * Note that JSON string comparison is slow and memory intensive for more
521
+ * complex data structures, is order-sensitive for object keys, and doesn't
522
+ * support maps or sets. If you need any of those things, you should probably
523
+ * use a {@link stabilizer}() based on
524
+ * [fast-equals](https://github.com/planttheidea/fast-equals) instead.
525
+ *
526
+ * @function
527
+ * @category Dependency Tracking
528
+ */
529
+ declare const stableJSON: <R extends any>(newVal: R) => R;
486
530
  /**
487
531
  * Wait for and return the next truthy value (or error) from a data source (when
488
532
  * processed with `yield *` within a {@link Job}).
@@ -514,4 +558,4 @@ declare function action<F extends AnyFunction, D extends {
514
558
  */
515
559
  declare function until<T>(source: UntilMethod<T> | Stream<T> | (() => T)): Yielding<T>;
516
560
 
517
- export { CircularDependency, type Configurable, type GenericMethodDecorator, type RuleFactory, type SchedulerFn, type Signal, type Writable, WriteConflict, action, cached, peek, rule, runRules, unchangedIf, until, value };
561
+ export { CircularDependency, type Configurable, type GenericMethodDecorator, type RuleFactory, type SchedulerFn, type Signal, type Writable, WriteConflict, action, cached, peek, rule, runRules, stabilizer, stable, stableArray, stableJSON, unchangedIf, until, value };
package/dist/signals.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { F as pushCtx, G as popCtx, w as makeJob, c as root, f as isError, e as markHandled, g as getJob, x as detached, k as currentCell, o as reject, b as resolve } from './jobutils-Dvu-e99o.mjs';
2
2
  import { b as backpressure, I as IsStream, a as callOrWait } from './call-or-wait-EzXJX5Dq.mjs';
3
- import { e as batch, f as arrayEq, d as defer, c as apply, s as setMap, C as CallableObject } from './utils-cyEhnyp7.mjs';
3
+ import { e as batch, d as defer, c as apply, s as setMap, C as CallableObject, f as arrayEq } from './utils-cyEhnyp7.mjs';
4
4
 
5
5
  const ruleQueues = /* @__PURE__ */ new WeakMap();
6
6
  function ruleQueue(scheduleFn = defer) {
@@ -478,6 +478,9 @@ class Cell {
478
478
  this.flags &= ~8 /* Peeking */;
479
479
  }
480
480
  }
481
+ unchangedIf(newVal, equals) {
482
+ return this.flags & 16 /* Error */ || !equals(this.value, newVal) ? newVal : this.value;
483
+ }
481
484
  recalcWhen(fnOrKey, fn) {
482
485
  if (this.flags & 8 /* Peeking */)
483
486
  return;
@@ -514,13 +517,12 @@ class Cell {
514
517
  return stop;
515
518
  }
516
519
  }
517
- function unchangedIf(newVal, equals = arrayEq) {
518
- const cell = currentCell;
519
- if (cell) {
520
- return cell.flags & 16 /* Error */ || !equals(cell.value, newVal) ? newVal : cell.value;
521
- } else {
522
- throw new Error("unchangedIf() must be called from a reactive expression");
523
- }
520
+ function getCell(f = "") {
521
+ if (!currentCell || currentCell.flags & 8 /* Peeking */)
522
+ throw new Error(
523
+ `${f}must be called from a reactive expression`
524
+ );
525
+ return currentCell;
524
526
  }
525
527
 
526
528
  class RF extends CallableObject {
@@ -685,6 +687,17 @@ function action(fn, _ctx, desc) {
685
687
  return currentCell ? currentCell.peek(fn, this, args) : apply(fn, this, args);
686
688
  };
687
689
  }
690
+ function stable(equals, newVal) {
691
+ return getCell("stabilizers ").unchangedIf(newVal, equals);
692
+ }
693
+ function unchangedIf(newVal, equals = arrayEq) {
694
+ return stable(equals, newVal);
695
+ }
696
+ function stabilizer(equals) {
697
+ return stable.bind(null, equals);
698
+ }
699
+ const stableArray = /* @__PURE__ */ stabilizer(arrayEq);
700
+ const stableJSON = /* @__PURE__ */ stabilizer((a, b) => JSON.stringify(a) === JSON.stringify(b));
688
701
  function until(source) {
689
702
  return callOrWait(source, "uneventful.until", waitTruthy, recache);
690
703
  }
@@ -695,4 +708,4 @@ function waitTruthy(job, v) {
695
708
  v && job.return(v);
696
709
  }
697
710
 
698
- export { CircularDependency, ConfigurableImpl, SignalImpl, WritableImpl, WriteConflict, action, cached, peek, rule, runRules, unchangedIf, until, value };
711
+ export { CircularDependency, ConfigurableImpl, SignalImpl, WritableImpl, WriteConflict, action, cached, peek, rule, runRules, stabilizer, stable, stableArray, stableJSON, unchangedIf, until, value };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uneventful",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Declarative, event-driven reactivity: signals, streams, structured concurrency, and easy resource cleanup",
5
5
  "keywords": [
6
6
  "signals",
@@ -93,7 +93,7 @@
93
93
  "ts-expect": "^1.3.0",
94
94
  "ts-node": "^10.9.1",
95
95
  "tsx": "^4.6.1",
96
- "typedoc": "0.28.1",
96
+ "typedoc": "0.28.2",
97
97
  "typescript": "^5.4.5"
98
98
  }
99
99
  }