@zeix/cause-effect 0.17.3 → 0.18.0

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 (89) hide show
  1. package/.ai-context.md +163 -232
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/ARCHITECTURE.md +274 -0
  5. package/CLAUDE.md +199 -143
  6. package/COLLECTION_REFACTORING.md +161 -0
  7. package/GUIDE.md +298 -0
  8. package/README.md +232 -197
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/index.dev.js +1325 -997
  12. package/index.js +1 -1
  13. package/index.ts +58 -74
  14. package/package.json +4 -1
  15. package/src/errors.ts +118 -74
  16. package/src/graph.ts +601 -0
  17. package/src/nodes/collection.ts +474 -0
  18. package/src/nodes/effect.ts +149 -0
  19. package/src/nodes/list.ts +588 -0
  20. package/src/nodes/memo.ts +120 -0
  21. package/src/nodes/sensor.ts +139 -0
  22. package/src/nodes/state.ts +135 -0
  23. package/src/nodes/store.ts +383 -0
  24. package/src/nodes/task.ts +146 -0
  25. package/src/signal.ts +112 -66
  26. package/src/util.ts +26 -57
  27. package/test/batch.test.ts +96 -62
  28. package/test/benchmark.test.ts +473 -487
  29. package/test/collection.test.ts +466 -706
  30. package/test/effect.test.ts +293 -696
  31. package/test/list.test.ts +335 -592
  32. package/test/memo.test.ts +380 -0
  33. package/test/regression.test.ts +156 -0
  34. package/test/scope.test.ts +191 -0
  35. package/test/sensor.test.ts +454 -0
  36. package/test/signal.test.ts +220 -213
  37. package/test/state.test.ts +217 -265
  38. package/test/store.test.ts +346 -446
  39. package/test/task.test.ts +395 -0
  40. package/test/untrack.test.ts +167 -0
  41. package/types/index.d.ts +13 -15
  42. package/types/src/errors.d.ts +73 -17
  43. package/types/src/graph.d.ts +208 -0
  44. package/types/src/nodes/collection.d.ts +64 -0
  45. package/types/src/nodes/effect.d.ts +48 -0
  46. package/types/src/nodes/list.d.ts +65 -0
  47. package/types/src/nodes/memo.d.ts +57 -0
  48. package/types/src/nodes/sensor.d.ts +75 -0
  49. package/types/src/nodes/state.d.ts +78 -0
  50. package/types/src/nodes/store.d.ts +51 -0
  51. package/types/src/nodes/task.d.ts +73 -0
  52. package/types/src/signal.d.ts +43 -29
  53. package/types/src/util.d.ts +9 -16
  54. package/archive/benchmark.ts +0 -683
  55. package/archive/collection.ts +0 -253
  56. package/archive/composite.ts +0 -85
  57. package/archive/computed.ts +0 -195
  58. package/archive/list.ts +0 -483
  59. package/archive/memo.ts +0 -139
  60. package/archive/state.ts +0 -90
  61. package/archive/store.ts +0 -298
  62. package/archive/task.ts +0 -189
  63. package/src/classes/collection.ts +0 -245
  64. package/src/classes/computed.ts +0 -349
  65. package/src/classes/list.ts +0 -343
  66. package/src/classes/ref.ts +0 -70
  67. package/src/classes/state.ts +0 -102
  68. package/src/classes/store.ts +0 -262
  69. package/src/diff.ts +0 -138
  70. package/src/effect.ts +0 -93
  71. package/src/match.ts +0 -45
  72. package/src/resolve.ts +0 -49
  73. package/src/system.ts +0 -257
  74. package/test/computed.test.ts +0 -1108
  75. package/test/diff.test.ts +0 -955
  76. package/test/match.test.ts +0 -388
  77. package/test/ref.test.ts +0 -353
  78. package/test/resolve.test.ts +0 -154
  79. package/types/src/classes/collection.d.ts +0 -45
  80. package/types/src/classes/computed.d.ts +0 -94
  81. package/types/src/classes/list.d.ts +0 -43
  82. package/types/src/classes/ref.d.ts +0 -35
  83. package/types/src/classes/state.d.ts +0 -49
  84. package/types/src/classes/store.d.ts +0 -52
  85. package/types/src/diff.d.ts +0 -28
  86. package/types/src/effect.d.ts +0 -15
  87. package/types/src/match.d.ts +0 -21
  88. package/types/src/resolve.d.ts +0 -29
  89. package/types/src/system.d.ts +0 -78
package/src/graph.ts ADDED
@@ -0,0 +1,601 @@
1
+ import { CircularDependencyError, type Guard } from './errors'
2
+
3
+ /* === Internal Types === */
4
+
5
+ type SourceFields<T extends {}> = {
6
+ value: T
7
+ sinks: Edge | null
8
+ sinksTail: Edge | null
9
+ stop?: Cleanup
10
+ }
11
+
12
+ type OptionsFields<T extends {}> = {
13
+ equals: (a: T, b: T) => boolean
14
+ guard?: Guard<T>
15
+ }
16
+
17
+ type SinkFields = {
18
+ fn: unknown
19
+ flags: number
20
+ sources: Edge | null
21
+ sourcesTail: Edge | null
22
+ }
23
+
24
+ type OwnerFields = {
25
+ cleanup: Cleanup | Cleanup[] | null
26
+ }
27
+
28
+ type AsyncFields = {
29
+ controller: AbortController | undefined
30
+ error: Error | undefined
31
+ }
32
+
33
+ type StateNode<T extends {}> = SourceFields<T> & OptionsFields<T>
34
+
35
+ type MemoNode<T extends {}> = SourceFields<T> &
36
+ OptionsFields<T> &
37
+ SinkFields & {
38
+ fn: MemoCallback<T>
39
+ error: Error | undefined
40
+ }
41
+
42
+ type TaskNode<T extends {}> = SourceFields<T> &
43
+ OptionsFields<T> &
44
+ SinkFields &
45
+ AsyncFields & {
46
+ fn: (prev: T, abort: AbortSignal) => Promise<T>
47
+ }
48
+
49
+ type EffectNode = SinkFields &
50
+ OwnerFields & {
51
+ fn: EffectCallback
52
+ }
53
+
54
+ type Scope = OwnerFields
55
+
56
+ type SourceNode = SourceFields<unknown & {}>
57
+ type SinkNode = MemoNode<unknown & {}> | TaskNode<unknown & {}> | EffectNode
58
+ type OwnerNode = EffectNode | Scope
59
+
60
+ type Edge = {
61
+ source: SourceNode
62
+ sink: SinkNode
63
+ nextSource: Edge | null
64
+ prevSink: Edge | null
65
+ nextSink: Edge | null
66
+ }
67
+
68
+ /* === Public API Types === */
69
+
70
+ type Signal<T extends {}> = {
71
+ get(): T
72
+ }
73
+
74
+ /**
75
+ * A cleanup function that can be called to dispose of resources.
76
+ */
77
+ type Cleanup = () => void
78
+
79
+ // biome-ignore lint/suspicious/noConfusingVoidType: optional Cleanup return type
80
+ type MaybeCleanup = Cleanup | undefined | void
81
+
82
+ /**
83
+ * Options for configuring signal behavior.
84
+ *
85
+ * @template T - The type of value in the signal
86
+ */
87
+ type SignalOptions<T extends {}> = {
88
+ /**
89
+ * Optional type guard to validate values.
90
+ * If provided, will throw an error if an invalid value is set.
91
+ */
92
+ guard?: Guard<T>
93
+
94
+ /**
95
+ * Optional custom equality function.
96
+ * Used to determine if a new value is different from the old value.
97
+ * Defaults to reference equality (===).
98
+ */
99
+ equals?: (a: T, b: T) => boolean
100
+ }
101
+
102
+ type ComputedOptions<T extends {}> = SignalOptions<T> & {
103
+ /**
104
+ * Optional initial value.
105
+ * Useful for reducer patterns so that calculations start with a value of correct type.
106
+ */
107
+ value?: T
108
+ }
109
+
110
+ /**
111
+ * A callback function for memos that computes a value based on the previous value.
112
+ *
113
+ * @template T - The type of value computed
114
+ * @param prev - The previous computed value
115
+ * @returns The new computed value
116
+ */
117
+ type MemoCallback<T extends {}> = (prev: T | undefined) => T
118
+
119
+ /**
120
+ * A callback function for tasks that asynchronously computes a value.
121
+ *
122
+ * @template T - The type of value computed
123
+ * @param prev - The previous computed value
124
+ * @param signal - An AbortSignal that will be triggered if the task is aborted
125
+ * @returns A promise that resolves to the new computed value
126
+ */
127
+ type TaskCallback<T extends {}> = (
128
+ prev: T | undefined,
129
+ signal: AbortSignal,
130
+ ) => Promise<T>
131
+
132
+ /**
133
+ * A callback function for effects that can perform side effects.
134
+ *
135
+ * @param match - A function to register cleanup callbacks that will be called before the effect re-runs or is disposed
136
+ */
137
+ type EffectCallback = () => MaybeCleanup
138
+
139
+ /* === Constants === */
140
+
141
+ const TYPE_STATE = 'State'
142
+ const TYPE_MEMO = 'Memo'
143
+ const TYPE_TASK = 'Task'
144
+ const TYPE_SENSOR = 'Sensor'
145
+ const TYPE_LIST = 'List'
146
+ const TYPE_COLLECTION = 'Collection'
147
+ const TYPE_STORE = 'Store'
148
+
149
+ const FLAG_CLEAN = 0
150
+ const FLAG_CHECK = 1 << 0
151
+ const FLAG_DIRTY = 1 << 1
152
+ const FLAG_RUNNING = 1 << 2
153
+
154
+ /* === Module State === */
155
+
156
+ let activeSink: SinkNode | null = null
157
+ let activeOwner: OwnerNode | null = null
158
+ const queuedEffects: EffectNode[] = []
159
+ let batchDepth = 0
160
+ let flushing = false
161
+
162
+ /* === Utility Functions === */
163
+
164
+ const DEFAULT_EQUALITY = <T extends {}>(a: T, b: T): boolean => a === b
165
+
166
+ /**
167
+ * Equality function that always returns false, causing propagation on every update.
168
+ * Use with `createSensor` for observing mutable objects where the reference stays the same
169
+ * but internal state changes (e.g., DOM elements observed via MutationObserver).
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * const el = createSensor<HTMLElement>((set) => {
174
+ * const node = document.getElementById('box')!;
175
+ * set(node);
176
+ * const obs = new MutationObserver(() => set(node));
177
+ * obs.observe(node, { attributes: true });
178
+ * return () => obs.disconnect();
179
+ * }, { value: node, equals: SKIP_EQUALITY });
180
+ * ```
181
+ */
182
+ const SKIP_EQUALITY = (_a?: unknown, _b?: unknown): boolean => false
183
+
184
+ /* === Link Management === */
185
+
186
+ function isValidEdge(checkEdge: Edge, node: SinkNode): boolean {
187
+ const sourcesTail = node.sourcesTail
188
+ if (sourcesTail) {
189
+ let edge = node.sources
190
+ while (edge) {
191
+ if (edge === checkEdge) return true
192
+ if (edge === sourcesTail) break
193
+ edge = edge.nextSource
194
+ }
195
+ }
196
+ return false
197
+ }
198
+
199
+ function link(source: SourceNode, sink: SinkNode): void {
200
+ const prevSource = sink.sourcesTail
201
+ if (prevSource?.source === source) return
202
+
203
+ let nextSource: Edge | null = null
204
+ const isRecomputing = sink.flags & FLAG_RUNNING
205
+ if (isRecomputing) {
206
+ nextSource = prevSource ? prevSource.nextSource : sink.sources
207
+ if (nextSource?.source === source) {
208
+ sink.sourcesTail = nextSource
209
+ return
210
+ }
211
+ }
212
+
213
+ const prevSink = source.sinksTail
214
+ if (
215
+ prevSink?.sink === sink &&
216
+ (!isRecomputing || isValidEdge(prevSink, sink))
217
+ )
218
+ return
219
+
220
+ const newEdge = { source, sink, nextSource, prevSink, nextSink: null }
221
+ sink.sourcesTail = source.sinksTail = newEdge
222
+ if (prevSource) prevSource.nextSource = newEdge
223
+ else sink.sources = newEdge
224
+ if (prevSink) prevSink.nextSink = newEdge
225
+ else source.sinks = newEdge
226
+ }
227
+
228
+ function unlink(edge: Edge): Edge | null {
229
+ const { source, nextSource, nextSink, prevSink } = edge
230
+
231
+ if (nextSink) nextSink.prevSink = prevSink
232
+ else source.sinksTail = prevSink
233
+ if (prevSink) prevSink.nextSink = nextSink
234
+ else source.sinks = nextSink
235
+
236
+ if (!source.sinks && source.stop) {
237
+ source.stop()
238
+ source.stop = undefined
239
+ }
240
+
241
+ return nextSource
242
+ }
243
+
244
+ function trimSources(node: SinkNode): void {
245
+ const tail = node.sourcesTail
246
+ let source = tail ? tail.nextSource : node.sources
247
+ while (source) source = unlink(source)
248
+ if (tail) tail.nextSource = null
249
+ else node.sources = null
250
+ }
251
+
252
+ /* === Propagation === */
253
+
254
+ function propagate(node: SinkNode, newFlag = FLAG_DIRTY): void {
255
+ const flags = node.flags
256
+
257
+ if ('sinks' in node) {
258
+ if ((flags & (FLAG_DIRTY | FLAG_CHECK)) >= newFlag) return
259
+
260
+ node.flags = flags | newFlag
261
+
262
+ // Abort in-flight work when sources change
263
+ if ('controller' in node && node.controller) {
264
+ node.controller.abort()
265
+ node.controller = undefined
266
+ }
267
+
268
+ // Propagate Check to sinks
269
+ for (let e = node.sinks; e; e = e.nextSink)
270
+ propagate(e.sink, FLAG_CHECK)
271
+ } else {
272
+ if (flags & FLAG_DIRTY) return
273
+
274
+ // Enqueue effect for later execution
275
+ node.flags = FLAG_DIRTY
276
+ queuedEffects.push(node as EffectNode)
277
+ }
278
+ }
279
+
280
+ /* === State Management === */
281
+
282
+ function setState<T extends {}>(node: StateNode<T>, next: T): void {
283
+ if (node.equals(node.value, next)) return
284
+
285
+ node.value = next
286
+ for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
287
+ if (batchDepth === 0) flush()
288
+ }
289
+
290
+ /* === Cleanup Management === */
291
+
292
+ function registerCleanup(owner: OwnerNode, fn: Cleanup): void {
293
+ if (!owner.cleanup) owner.cleanup = fn
294
+ else if (Array.isArray(owner.cleanup)) owner.cleanup.push(fn)
295
+ else owner.cleanup = [owner.cleanup, fn]
296
+ }
297
+
298
+ function runCleanup(owner: OwnerNode): void {
299
+ if (!owner.cleanup) return
300
+
301
+ if (Array.isArray(owner.cleanup))
302
+ for (let i = 0; i < owner.cleanup.length; i++) owner.cleanup[i]()
303
+ else owner.cleanup()
304
+ owner.cleanup = null
305
+ }
306
+
307
+ /* === Recomputation === */
308
+
309
+ function recomputeMemo(node: MemoNode<unknown & {}>): void {
310
+ const prevWatcher = activeSink
311
+ activeSink = node
312
+ node.sourcesTail = null
313
+ node.flags = FLAG_RUNNING
314
+
315
+ let changed = false
316
+ try {
317
+ const next = node.fn(node.value)
318
+ if (node.error || !node.equals(next, node.value)) {
319
+ node.value = next
320
+ node.error = undefined
321
+ changed = true
322
+ }
323
+ } catch (err: unknown) {
324
+ changed = true
325
+ node.error = err instanceof Error ? err : new Error(String(err))
326
+ } finally {
327
+ activeSink = prevWatcher
328
+ trimSources(node)
329
+ }
330
+
331
+ if (changed) {
332
+ for (let e = node.sinks; e; e = e.nextSink)
333
+ if (e.sink.flags & FLAG_CHECK) e.sink.flags |= FLAG_DIRTY
334
+ }
335
+
336
+ node.flags = FLAG_CLEAN
337
+ }
338
+
339
+ function recomputeTask(node: TaskNode<unknown & {}>): void {
340
+ node.controller?.abort()
341
+
342
+ const controller = new AbortController()
343
+ node.controller = controller
344
+ node.error = undefined
345
+
346
+ const prevWatcher = activeSink
347
+ activeSink = node
348
+ node.sourcesTail = null
349
+ node.flags = FLAG_RUNNING
350
+
351
+ let promise: Promise<unknown & {}>
352
+ try {
353
+ promise = node.fn(node.value, controller.signal)
354
+ } catch (err) {
355
+ node.controller = undefined
356
+ node.error = err instanceof Error ? err : new Error(String(err))
357
+ return
358
+ } finally {
359
+ activeSink = prevWatcher
360
+ trimSources(node)
361
+ }
362
+
363
+ promise.then(
364
+ next => {
365
+ if (controller.signal.aborted) return
366
+
367
+ node.controller = undefined
368
+ if (node.error || !node.equals(next, node.value)) {
369
+ node.value = next
370
+ node.error = undefined
371
+ for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
372
+ if (batchDepth === 0) flush()
373
+ }
374
+ },
375
+ (err: unknown) => {
376
+ if (controller.signal.aborted) return
377
+
378
+ node.controller = undefined
379
+ const error = err instanceof Error ? err : new Error(String(err))
380
+ if (
381
+ !node.error ||
382
+ error.name !== node.error.name ||
383
+ error.message !== node.error.message
384
+ ) {
385
+ // We don't clear old value on errors
386
+ node.error = error
387
+ for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
388
+ if (batchDepth === 0) flush()
389
+ }
390
+ },
391
+ )
392
+
393
+ node.flags = FLAG_CLEAN
394
+ }
395
+
396
+ function runEffect(node: EffectNode): void {
397
+ runCleanup(node)
398
+ const prevContext = activeSink
399
+ const prevOwner = activeOwner
400
+ activeSink = activeOwner = node
401
+ node.sourcesTail = null
402
+ node.flags = FLAG_RUNNING
403
+
404
+ try {
405
+ const out = node.fn()
406
+ if (typeof out === 'function') registerCleanup(node, out)
407
+ } finally {
408
+ activeSink = prevContext
409
+ activeOwner = prevOwner
410
+ trimSources(node)
411
+ }
412
+
413
+ node.flags = FLAG_CLEAN
414
+ }
415
+
416
+ function refresh(node: SinkNode): void {
417
+ if (node.flags & FLAG_CHECK) {
418
+ for (let e = node.sources; e; e = e.nextSource) {
419
+ if ('fn' in e.source) refresh(e.source as SinkNode)
420
+ if (node.flags & FLAG_DIRTY) break
421
+ }
422
+ }
423
+
424
+ if (node.flags & FLAG_RUNNING) {
425
+ throw new CircularDependencyError(
426
+ 'controller' in node
427
+ ? TYPE_TASK
428
+ : 'value' in node
429
+ ? TYPE_MEMO
430
+ : 'Effect',
431
+ )
432
+ }
433
+
434
+ if (node.flags & FLAG_DIRTY) {
435
+ if ('controller' in node) recomputeTask(node)
436
+ else if ('value' in node) recomputeMemo(node)
437
+ else runEffect(node)
438
+ } else {
439
+ node.flags = FLAG_CLEAN
440
+ }
441
+ }
442
+
443
+ /* === Batching === */
444
+
445
+ function flush(): void {
446
+ if (flushing) return
447
+ flushing = true
448
+ try {
449
+ for (let i = 0; i < queuedEffects.length; i++) {
450
+ const effect = queuedEffects[i]
451
+ if (effect.flags & FLAG_DIRTY) refresh(effect)
452
+ }
453
+ queuedEffects.length = 0
454
+ } finally {
455
+ flushing = false
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Batches multiple signal updates together.
461
+ * Effects will not run until the batch completes.
462
+ * Batches can be nested; effects run when the outermost batch completes.
463
+ *
464
+ * @param fn - The function to execute within the batch
465
+ *
466
+ * @example
467
+ * ```ts
468
+ * const count = createState(0);
469
+ * const double = createMemo(() => count.get() * 2);
470
+ *
471
+ * batch(() => {
472
+ * count.set(1);
473
+ * count.set(2);
474
+ * count.set(3);
475
+ * // Effects run only once at the end with count = 3
476
+ * });
477
+ * ```
478
+ */
479
+ function batch(fn: () => void): void {
480
+ batchDepth++
481
+ try {
482
+ fn()
483
+ } finally {
484
+ batchDepth--
485
+ if (batchDepth === 0) flush()
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Runs a callback without tracking dependencies.
491
+ * Any signal reads inside the callback will not create edges to the current active sink.
492
+ *
493
+ * @param fn - The function to execute without tracking
494
+ * @returns The return value of the function
495
+ *
496
+ * @example
497
+ * ```ts
498
+ * const count = createState(0);
499
+ * const label = createState('Count');
500
+ *
501
+ * createEffect(() => {
502
+ * // Only re-runs when count changes, not when label changes
503
+ * const name = untrack(() => label.get());
504
+ * console.log(`${name}: ${count.get()}`);
505
+ * });
506
+ * ```
507
+ */
508
+ function untrack<T>(fn: () => T): T {
509
+ const prev = activeSink
510
+ activeSink = null
511
+ try {
512
+ return fn()
513
+ } finally {
514
+ activeSink = prev
515
+ }
516
+ }
517
+
518
+ /* === Scope Management === */
519
+
520
+ /**
521
+ * Creates a new ownership scope for managing cleanup of nested effects and resources.
522
+ * All effects created within the scope will be automatically disposed when the scope is disposed.
523
+ * Scopes can be nested - disposing a parent scope disposes all child scopes.
524
+ *
525
+ * @param fn - The function to execute within the scope, may return a cleanup function
526
+ * @returns A dispose function that cleans up the scope
527
+ *
528
+ * @example
529
+ * ```ts
530
+ * const dispose = createScope(() => {
531
+ * const count = createState(0);
532
+ *
533
+ * createEffect(() => {
534
+ * console.log(count.get());
535
+ * });
536
+ *
537
+ * return () => console.log('Scope disposed');
538
+ * });
539
+ *
540
+ * dispose(); // Cleans up the effect and runs cleanup callbacks
541
+ * ```
542
+ */
543
+ function createScope(fn: () => MaybeCleanup): Cleanup {
544
+ const prevOwner = activeOwner
545
+ const scope: Scope = { cleanup: null }
546
+ activeOwner = scope
547
+
548
+ try {
549
+ const out = fn()
550
+ if (typeof out === 'function') registerCleanup(scope, out)
551
+ const dispose = () => runCleanup(scope)
552
+ if (prevOwner) registerCleanup(prevOwner, dispose)
553
+ return dispose
554
+ } finally {
555
+ activeOwner = prevOwner
556
+ }
557
+ }
558
+
559
+ export {
560
+ type Cleanup,
561
+ type ComputedOptions,
562
+ type EffectCallback,
563
+ type EffectNode,
564
+ type MaybeCleanup,
565
+ type MemoCallback,
566
+ type MemoNode,
567
+ type Scope,
568
+ type Signal,
569
+ type SignalOptions,
570
+ type SinkNode,
571
+ type StateNode,
572
+ type TaskCallback,
573
+ type TaskNode,
574
+ activeOwner,
575
+ activeSink,
576
+ batch,
577
+ batchDepth,
578
+ createScope,
579
+ DEFAULT_EQUALITY as defaultEquals,
580
+ SKIP_EQUALITY,
581
+ FLAG_CLEAN,
582
+ FLAG_DIRTY,
583
+ flush,
584
+ link,
585
+ propagate,
586
+ refresh,
587
+ registerCleanup,
588
+ runCleanup,
589
+ runEffect,
590
+ setState,
591
+ trimSources,
592
+ TYPE_COLLECTION,
593
+ TYPE_LIST,
594
+ TYPE_MEMO,
595
+ TYPE_SENSOR,
596
+ TYPE_STATE,
597
+ TYPE_STORE,
598
+ TYPE_TASK,
599
+ unlink,
600
+ untrack,
601
+ }