@gesslar/toolkit 4.0.0 → 4.2.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 (38) hide show
  1. package/README.md +1 -0
  2. package/package.json +4 -4
  3. package/src/browser/lib/Data.js +4 -4
  4. package/src/browser/lib/TypeSpec.js +115 -39
  5. package/src/node/index.js +2 -1
  6. package/src/node/lib/Cache.js +105 -35
  7. package/src/node/lib/Data.js +49 -0
  8. package/src/node/lib/DirectoryObject.js +4 -7
  9. package/src/node/lib/FileObject.js +89 -53
  10. package/src/node/lib/FileSystem.js +47 -2
  11. package/src/node/lib/Font.js +1 -1
  12. package/src/node/lib/Notify.js +26 -6
  13. package/src/node/lib/Term.js +1 -1
  14. package/src/node/lib/Util.js +47 -5
  15. package/src/node/lib/Watcher.js +118 -0
  16. package/types/browser/lib/Data.d.ts +2 -8
  17. package/types/browser/lib/Data.d.ts.map +1 -1
  18. package/types/browser/lib/TypeSpec.d.ts +21 -36
  19. package/types/browser/lib/TypeSpec.d.ts.map +1 -1
  20. package/types/node/index.d.ts +2 -1
  21. package/types/node/lib/Cache.d.ts +36 -5
  22. package/types/node/lib/Cache.d.ts.map +1 -1
  23. package/types/node/lib/Data.d.ts +19 -0
  24. package/types/node/lib/Data.d.ts.map +1 -0
  25. package/types/node/lib/DirectoryObject.d.ts +6 -5
  26. package/types/node/lib/DirectoryObject.d.ts.map +1 -1
  27. package/types/node/lib/FileObject.d.ts +54 -26
  28. package/types/node/lib/FileObject.d.ts.map +1 -1
  29. package/types/node/lib/FileSystem.d.ts +19 -0
  30. package/types/node/lib/FileSystem.d.ts.map +1 -1
  31. package/types/node/lib/Notify.d.ts +23 -10
  32. package/types/node/lib/Notify.d.ts.map +1 -1
  33. package/types/node/lib/Term.d.ts +2 -2
  34. package/types/node/lib/Term.d.ts.map +1 -1
  35. package/types/node/lib/Util.d.ts +20 -6
  36. package/types/node/lib/Util.d.ts.map +1 -1
  37. package/types/node/lib/Watcher.d.ts +38 -0
  38. package/types/node/lib/Watcher.d.ts.map +1 -0
@@ -4,14 +4,13 @@
4
4
  * resolution and existence checks.
5
5
  */
6
6
 
7
- import JSON5 from "json5"
7
+ import {Buffer} from "node:buffer"
8
8
  import fs from "node:fs/promises"
9
- import YAML from "yaml"
10
9
  import {URL} from "node:url"
11
- import {Buffer} from "node:buffer"
12
10
  import {inspect} from "node:util"
13
11
 
14
- import Data from "../../browser/lib/Data.js"
12
+ import Cache from "./Cache.js"
13
+ import Data from "./Data.js"
15
14
  import DirectoryObject from "./DirectoryObject.js"
16
15
  import FS from "./FileSystem.js"
17
16
  import Sass from "./Sass.js"
@@ -33,19 +32,6 @@ import Valid from "./Valid.js"
33
32
  */
34
33
 
35
34
  export default class FileObject extends FS {
36
- /**
37
- * Configuration mapping data types to their respective parser modules for loadData method.
38
- * Each parser module must have a .parse() method that accepts a string and returns parsed data.
39
- *
40
- * @type {{[key: string]: Array<typeof JSON5 | typeof YAML>}}
41
- */
42
- static dataLoaderConfig = Object.freeze({
43
- json5: [JSON5],
44
- json: [JSON5],
45
- yaml: [YAML],
46
- any: [JSON5, YAML]
47
- })
48
-
49
35
  /**
50
36
  * @type {object}
51
37
  * @property {string|null} supplied - User-supplied path
@@ -70,6 +56,8 @@ export default class FileObject extends FS {
70
56
  real: null,
71
57
  })
72
58
 
59
+ #cache = null
60
+
73
61
  /**
74
62
  * Constructs a FileObject instance.
75
63
  *
@@ -162,6 +150,7 @@ export default class FileObject extends FS {
162
150
  extension: this.extension,
163
151
  isFile: this.isFile,
164
152
  parentPath: this.parentPath,
153
+ cached: this.cached
165
154
  }
166
155
  }
167
156
 
@@ -377,17 +366,74 @@ export default class FileObject extends FS {
377
366
  }
378
367
  }
379
368
 
369
+ /**
370
+ * Whether this FileObject has an active cache attached.
371
+ *
372
+ * @returns {boolean} True if a Cache instance is attached
373
+ */
374
+ get cached() {
375
+ return Data.isType(this.#cache, "Cache")
376
+ }
377
+
378
+ /**
379
+ * Attaches a Cache instance to this FileObject for caching read and
380
+ * loadData results. If no cache is provided, a new Cache is created.
381
+ *
382
+ * @param {Cache} [cache] - The Cache instance to attach
383
+ * @returns {FileObject} This FileObject for chaining
384
+ * @throws {Sass} If a cache is already attached
385
+ */
386
+ withCache(cache) {
387
+ Valid.type(cache, "Undefined|Cache")
388
+ Valid.assert(!this.#cache, "Cache already set. Remove the cache before re-assigning.")
389
+
390
+ cache ??= new Cache()
391
+
392
+ this.#cache = cache
393
+
394
+ return this
395
+ }
396
+
397
+ /**
398
+ * Removes the attached cache, clearing any cached data for this file first.
399
+ *
400
+ * @returns {FileObject} This FileObject for chaining
401
+ */
402
+ removeCache() {
403
+ this.resetCache()
404
+ this.#cache = null
405
+
406
+ return this
407
+ }
408
+
409
+ /**
410
+ * Clears cached data for this file without removing the cache itself.
411
+ *
412
+ * @returns {FileObject} This FileObject for chaining
413
+ */
414
+ resetCache() {
415
+ this.#cache?.resetCache(this)
416
+
417
+ return this
418
+ }
419
+
380
420
  /**
381
421
  * Reads the content of a file asynchronously.
382
422
  *
383
- * @param {string} [encoding] - The encoding to read the file as.
423
+ * @param {object} [options] - Read options
424
+ * @param {string} [options.encoding="utf8"] - The encoding to read the file as
425
+ * @param {boolean} [options.skipCache=false] - If true, bypass the cache
384
426
  * @returns {Promise<string>} The file contents
385
427
  */
386
- async read(encoding="utf8") {
428
+ async read({encoding="utf8", skipCache=false} = {}) {
387
429
  const filePath = this.path
388
430
 
431
+ if(this.#cache && skipCache === false)
432
+ return await this.#cache.loadFromCache(
433
+ this, {encoding})
434
+
389
435
  if(!(await this.exists))
390
- throw Sass.new(`No such file '${filePath}'`)
436
+ throw Sass.new(`No such file '${this}'`)
391
437
 
392
438
  return await fs.readFile(filePath, encoding)
393
439
  }
@@ -419,7 +465,7 @@ export default class FileObject extends FS {
419
465
  *
420
466
  * @param {string} content - The content to write
421
467
  * @param {string} [encoding] - The encoding in which to write (default: "utf8")
422
- * @returns {Promise<void>}
468
+ * @returns {Promise<undefined>}
423
469
  * @throws {Sass} If the file URL is invalid or the parent directory doesn't exist
424
470
  * @example
425
471
  * const file = new FileObject('./output/data.json')
@@ -444,7 +490,7 @@ export default class FileObject extends FS {
444
490
  * Supports ArrayBuffer, TypedArrays (Uint8Array, etc.), Blob, and Node Buffer types.
445
491
  *
446
492
  * @param {ArrayBuffer|Blob|Buffer} data - The binary data to write
447
- * @returns {Promise<void>}
493
+ * @returns {Promise<undefined>}
448
494
  * @throws {Sass} If the file URL is invalid
449
495
  * @throws {Sass} If the parent directory doesn't exist
450
496
  * @throws {Sass} If the data is not a valid binary type
@@ -471,44 +517,34 @@ export default class FileObject extends FS {
471
517
  }
472
518
 
473
519
  /**
474
- * Loads an object from JSON or YAML file.
475
- * Attempts to parse content as JSON5 first, then falls back to YAML if specified.
520
+ * Loads an object from JSON or YAML file. Attempts to parse content as JSON5
521
+ * first, then falls back to YAML if specified.
476
522
  *
477
- * @param {string} [type] - The expected type of data to parse ("json", "json5", "yaml", or "any")
478
- * @param {string} [encoding] - The encoding to read the file as (default: "utf8")
523
+ * @param {object} [options] - Load options
524
+ * @param {string} [options.type="any"] - The expected type of data to parse ("json", "json5", "yaml", or "any")
525
+ * @param {string} [options.encoding="utf8"] - The encoding to read the file as
526
+ * @param {boolean} [options.skipCache=false] - If true, bypass the cache
479
527
  * @returns {Promise<unknown>} The parsed data object
480
528
  * @throws {Sass} If the content cannot be parsed or type is unsupported
481
529
  * @example
482
530
  * const configFile = new FileObject('./config.json5')
483
- * const config = await configFile.loadData('json5')
531
+ * const config = await configFile.loadData({type: 'json5'})
484
532
  *
485
533
  * // Auto-detect format
486
- * const data = await configFile.loadData('any')
487
- */
488
- async loadData(type="any", encoding="utf8") {
489
- const content = await this.read(encoding)
490
- const normalizedType = type.toLowerCase()
491
- const toTry = {
492
- json5: [JSON5],
493
- json: [JSON5],
494
- yaml: [YAML],
495
- any: [JSON5,YAML]
496
- }[normalizedType]
497
-
498
- if(!toTry)
499
- throw Sass.new(`Unsupported data type '${type}'. Supported types: json, json5, yaml.`)
500
-
501
- for(const format of toTry) {
502
- try {
503
- const result = format.parse(content)
504
-
505
- return result
506
- } catch {
507
- // nothing to see here
508
- }
509
- }
534
+ * const data = await configFile.loadData()
535
+ */
536
+ async loadData({type="any", encoding="utf8", skipCache=false} = {}) {
537
+ if(this.#cache && skipCache === false)
538
+ return await this.#cache.loadDataFromCache(
539
+ this, {encoding, type})
540
+
541
+ if(!(await this.exists))
542
+ throw Sass.new(`No such file '${this}'`)
543
+
544
+ const content = await this.read({encoding, skipCache})
545
+ const result = Data.textAsData(content, type)
510
546
 
511
- throw Sass.new(`Content is neither valid JSON5 nor valid YAML:\n'${this.path}'`)
547
+ return result
512
548
  }
513
549
 
514
550
  /**
@@ -610,7 +646,7 @@ export default class FileObject extends FS {
610
646
  /**
611
647
  * Deletes the file from the filesystem.
612
648
  *
613
- * @returns {Promise<void>} Resolves when file is deleted
649
+ * @returns {Promise<undefined>} Resolves when file is deleted
614
650
  * @throws {Sass} If the file URL is invalid
615
651
  * @throws {Sass} If the file does not exist
616
652
  * @example
@@ -11,12 +11,14 @@
11
11
  import path from "node:path"
12
12
  import url from "node:url"
13
13
 
14
+ import Collection from "../../browser/lib/Collection.js"
14
15
  import Data from "../../browser/lib/Data.js"
16
+ import Glog from "./Glog.js"
15
17
  import Valid from "./Valid.js"
16
- import Collection from "../../browser/lib/Collection.js"
18
+ import Watcher from "./Watcher.js"
17
19
 
18
20
  /**
19
- * @import {Sass} from "./Sass.js"
21
+ * @import Sass from "./Sass.js"
20
22
  */
21
23
 
22
24
  const fdTypes = Object.freeze(["file", "directory"])
@@ -33,6 +35,8 @@ export default class FileSystem {
33
35
  static upperFdTypes = upperFdTypes
34
36
  static fdType = fdType
35
37
 
38
+ #watcher = null
39
+
36
40
  /**
37
41
  * Compute the relative path from another file or directory to this instance.
38
42
  *
@@ -53,6 +57,47 @@ export default class FileSystem {
53
57
  return FileSystem.relativeOrAbsolute(fileOrDirectoryObject, this)
54
58
  }
55
59
 
60
+ /**
61
+ * Watch this file or directory for changes.
62
+ *
63
+ * @param {object} [options] - Watch options
64
+ * @param {Function} [options.onChange] - Callback invoked on change
65
+ * @param {number} [options.debounceMs] - Debounce interval in milliseconds
66
+ * @param {boolean} [options.persistent] - Keep the process alive while watching
67
+ * @returns {Promise<undefined>}
68
+ */
69
+ async watch(options={}) {
70
+ Valid.type(options, "Object")
71
+
72
+ const localOptions = Collection.cloneObject(options)
73
+
74
+ const {onChange} = localOptions ?? {}
75
+ Valid.type(onChange, "Undefined|Function")
76
+
77
+ delete localOptions.onChange
78
+
79
+ this.stopWatching()
80
+
81
+ this.#watcher = new Watcher()
82
+
83
+ await this.#watcher.watch(this, Object.assign({},
84
+ localOptions,
85
+ {
86
+ onChange: onChange ?? (() => {
87
+ Glog(`${this} changed somehow.`)
88
+ })
89
+ }
90
+ ))
91
+ }
92
+
93
+ /**
94
+ * Stop watching this file or directory for changes.
95
+ */
96
+ stopWatching() {
97
+ this.#watcher?.stopWatching()
98
+ this.#watcher = null
99
+ }
100
+
56
101
  /**
57
102
  * Fix slashes in a path
58
103
  *
@@ -11,7 +11,7 @@ import DirectoryObject from "./DirectoryObject.js"
11
11
  import FS from "./FileSystem.js"
12
12
 
13
13
  /**
14
- * @import {FileObject} from "./FileObject.js"
14
+ * @import FileObject from "./FileObject.js"
15
15
  */
16
16
 
17
17
  const execFile = promisify(child_process.execFile)
@@ -31,7 +31,7 @@ export class Notify {
31
31
  *
32
32
  * @param {string} type - Event name to dispatch.
33
33
  * @param {unknown} [payload] - Data to send with the event.
34
- * @returns {void}
34
+ * @returns {undefined}
35
35
  */
36
36
  emit(type, payload=undefined) {
37
37
  Valid.type(type, "String", {allowEmpty: false})
@@ -46,7 +46,7 @@ export class Notify {
46
46
  *
47
47
  * @param {string} type - Event name to dispatch.
48
48
  * @param {unknown} [payload] - Data to send with the event.
49
- * @returns {Promise<void>} Resolves when all listeners have completed.
49
+ * @returns {Promise<undefined>} Resolves when all listeners have completed.
50
50
  */
51
51
  async asyncEmit(type, payload) {
52
52
  Valid.type(type, "String", {allowEmpty: false})
@@ -54,6 +54,26 @@ export class Notify {
54
54
  await Util.asyncEmit(this.#emitter, type, payload)
55
55
  }
56
56
 
57
+ /**
58
+ * Fires an event asynchronously without blocking the caller.
59
+ * Listeners run in the background. If any listener throws and an error
60
+ * callback is provided, it receives the error. Otherwise errors are
61
+ * silently discarded.
62
+ *
63
+ * @param {string} type - Event name to dispatch.
64
+ * @param {unknown} [payload] - Data to send with the event.
65
+ * @param {((error: Error) => void)|null} [errorCb] - Optional callback for errors.
66
+ * @param {AbortSignal} [signal] - Optional AbortSignal to cancel the operation.
67
+ * @returns {undefined}
68
+ */
69
+ fire(type, payload, errorCb, signal) {
70
+ Valid.type(type, "String", {allowEmpty: false})
71
+ Valid.type(errorCb, "Undefined|Null|Function")
72
+ Valid.type(signal, "Undefined|Null|AbortSignal")
73
+
74
+ Util.fire(this.#emitter, type, payload, errorCb, signal)
75
+ }
76
+
57
77
  /**
58
78
  * Emits an event and returns the payload for simple request/response flows.
59
79
  * Listeners can mutate the payload object to provide responses.
@@ -74,10 +94,10 @@ export class Notify {
74
94
  * Registers a listener for the given event type.
75
95
  *
76
96
  * @param {string} type - Event name to listen for.
77
- * @param {(payload: unknown) => void} handler - Listener callback.
97
+ * @param {(payload: unknown) => undefined} handler - Listener callback.
78
98
  * @param {EventEmitter} [emitter] - The EventEmitter to attach to. Default is internal emitter.
79
99
  * @param {NotifyEventOptions} [options] - Options for the listener.
80
- * @returns {() => void} Dispose function to unregister the handler.
100
+ * @returns {() => undefined} Dispose function to unregister the handler.
81
101
  */
82
102
  on(type, handler, emitter=this.#emitter, options=undefined) {
83
103
  Valid.type(type, "String", {allowEmpty: false})
@@ -96,9 +116,9 @@ export class Notify {
96
116
  * Removes a previously registered listener for the given event type.
97
117
  *
98
118
  * @param {string} type - Event name to remove.
99
- * @param {(payload: unknown) => void} handler - Listener callback to detach.
119
+ * @param {(payload: unknown) => undefined} handler - Listener callback to detach.
100
120
  * @param {EventEmitter} [emitter] - The EventEmitter from which to remove. Default is internal emitter.
101
- * @returns {void}
121
+ * @returns {undefined}
102
122
  */
103
123
  off(type, handler, emitter=this.#emitter) {
104
124
  Valid.type(type, "String", {allowEmpty: false})
@@ -230,7 +230,7 @@ export default class Term {
230
230
  * @param {string | Array<string | [string, string]>} args - Message or segments.
231
231
  * @param {object} [options] - Behaviour flags.
232
232
  * @param {boolean} options.silent - When true, suppress output.
233
- * @returns {void}
233
+ * @returns {undefined}
234
234
  */
235
235
  static status(args, {silent=false} = {}) {
236
236
  if(silent)
@@ -65,10 +65,10 @@ export default class Util extends BrowserUtil {
65
65
  * @param {object} emitter - The emitter object (already validated)
66
66
  * @param {string} event - The event name to emit
67
67
  * @param {...unknown} args - Arguments to pass to event listeners
68
- * @returns {Promise<void>} Resolves when all listeners have completed
68
+ * @returns {Promise<undefined>} Resolves when all listeners have completed
69
69
  */
70
70
  static async #performAsyncEmit(emitter, event, ...args) {
71
- const listeners = emitter.listeners(event)
71
+ const listeners = emitter.rawListeners(event)
72
72
 
73
73
  if(listeners.length === 0)
74
74
  return // No listeners, nothing to do
@@ -97,7 +97,7 @@ export default class Util extends BrowserUtil {
97
97
  * @param {EventEmitter} emitter - The EventEmitter instance to emit on
98
98
  * @param {string} event - The event name to emit
99
99
  * @param {...unknown} args - Arguments to pass to event listeners
100
- * @returns {Promise<void>} Resolves when all listeners have completed
100
+ * @returns {Promise<undefined>} Resolves when all listeners have completed
101
101
  */
102
102
  static async asyncEmit(emitter, event, ...args) {
103
103
  try {
@@ -115,6 +115,48 @@ export default class Util extends BrowserUtil {
115
115
  }
116
116
  }
117
117
 
118
+ /**
119
+ * Fires an event asynchronously without blocking the caller.
120
+ * Listeners run in the background via asyncEmit. If any listener rejects
121
+ * and an error callback is provided, it receives the error. If no callback
122
+ * is provided, errors are silently discarded.
123
+ *
124
+ * @param {EventEmitter} emitter - The EventEmitter instance to emit on
125
+ * @param {string} event - The event name to emit
126
+ * @param {unknown} [payload] - Data to send with the event
127
+ * @param {((error: Error) => void)|null} [errorCb] - Optional callback for errors
128
+ * @param {AbortSignal} [signal] - Optional AbortSignal to cancel the operation
129
+ * @returns {undefined}
130
+ */
131
+ static fire(emitter, event, payload, errorCb, signal) {
132
+ Valid.type(errorCb, "Undefined|Null|Function")
133
+ Valid.type(signal, "Undefined|Null|AbortSignal")
134
+
135
+ if(signal?.aborted)
136
+ return
137
+
138
+ const promise = this.asyncEmit(emitter, event, payload)
139
+
140
+ if(signal) {
141
+ const onAbort = () => {} // asyncEmit already in flight, nothing to cancel
142
+ signal.addEventListener("abort", onAbort, {once: true})
143
+ promise.then(
144
+ () => signal.removeEventListener("abort", onAbort),
145
+ error => {
146
+ signal.removeEventListener("abort", onAbort)
147
+
148
+ if(!signal.aborted && errorCb) {
149
+ errorCb(error)
150
+ }
151
+ }
152
+ )
153
+ } else if(errorCb) {
154
+ promise.catch(errorCb)
155
+ } else {
156
+ promise.catch(() => {})
157
+ }
158
+ }
159
+
118
160
  /**
119
161
  * Emits an event asynchronously and waits for all listeners to complete.
120
162
  * Like asyncEmit, but uses duck typing for more flexible emitter validation.
@@ -124,12 +166,12 @@ export default class Util extends BrowserUtil {
124
166
  * @param {object} emitter - Any object with EventEmitter-like interface
125
167
  * @param {string} event - The event name to emit
126
168
  * @param {...unknown} args - Arguments to pass to event listeners
127
- * @returns {Promise<void>} Resolves when all listeners have completed, but no grapes.
169
+ * @returns {Promise<undefined>} Resolves when all listeners have completed, but no grapes.
128
170
  */
129
171
  static async asyncEmitQuack(emitter, event, ...args) {
130
172
  try {
131
173
  if(!emitter ||
132
- typeof emitter.listeners !== "function" ||
174
+ typeof emitter.rawListeners !== "function" ||
133
175
  typeof emitter.on !== "function" ||
134
176
  typeof emitter.emit !== "function") {
135
177
  throw Sass.new("First argument must be an EventEmitter-like object")
@@ -0,0 +1,118 @@
1
+ import {watch} from "node:fs/promises"
2
+ import Valid from "./Valid.js"
3
+ import Data from "./Data.js"
4
+ import Time from "../../browser/lib/Time.js"
5
+
6
+ /**
7
+ * @import FileObject from "./FileObject.js"
8
+ * @import DirectoryObject from "./DirectoryObject.js"
9
+ */
10
+
11
+ export const OverFlowBehaviour = Object.freeze({
12
+ IGNORE: "ignore",
13
+ THROW: "throw",
14
+ })
15
+
16
+ export default class Watcher {
17
+ #abortController
18
+ #state = new Map()
19
+
20
+ /**
21
+ * Watch one or more file/directory targets for changes, invoking a callback
22
+ * with debounce protection.
23
+ *
24
+ * @param {FileObject|DirectoryObject|Array.<(FileObject|DirectoryObject)>} targets - The target(s) to watch
25
+ * @param {object} options - Watch options
26
+ * @param {(target: FileObject|DirectoryObject) => void} options.onChange - Callback invoked on change
27
+ * @param {number} [options.debounceMs=50] - Debounce interval in milliseconds
28
+ * @param {boolean} [options.persistent=true] - Keep the process alive while watching
29
+ * @param {boolean} [options.recursive=false] - Watch subdirectories (directories only)
30
+ * @param {string} [options.overflow="ignore"] - Overflow behaviour ("ignore" or "throw")
31
+ * @returns {Promise<undefined>}
32
+ */
33
+ async watch(targets, {
34
+ onChange,
35
+ debounceMs=50,
36
+ persistent=true,
37
+ recursive=false,
38
+ overflow=OverFlowBehaviour.IGNORE
39
+ } = {}) {
40
+ Valid.type(targets, "FileObject|DirectoryObject|(FileObject|DirectoryObject)[]")
41
+ Valid.type(onChange, "Function")
42
+ Valid.type(debounceMs, "Number")
43
+ Valid.type(persistent, "Boolean")
44
+ Valid.type(recursive, "Undefined|Boolean")
45
+ Valid.type(overflow, "String")
46
+ Valid.assert(Object.values(OverFlowBehaviour).includes(overflow), `Overflow must be one of "ignore" or "throw".`)
47
+
48
+ if(!Data.isType(targets, "Array")) {
49
+ targets = [targets]
50
+ }
51
+
52
+ this.#abortController = new AbortController()
53
+
54
+ for(const target of targets) {
55
+ const watcher = watch(target.url, {
56
+ recursive: target.isDirectory ? recursive : false,
57
+ persistent,
58
+ signal: this.#abortController.signal,
59
+ overflow
60
+ })
61
+
62
+ this.#state.set(target, {busy: false, pending: false})
63
+
64
+ ;(async() => {
65
+ try {
66
+ for await(const _ of watcher) {
67
+ const state = this.#state.get(target)
68
+
69
+ if(!state) {
70
+ return
71
+ }
72
+
73
+ if(state.busy) {
74
+ state.pending = true
75
+ continue
76
+ }
77
+
78
+ state.pending = false
79
+ state.busy = true
80
+
81
+ while(true) {
82
+ await Time.after(debounceMs)
83
+
84
+ if(state.pending) {
85
+ state.pending = false
86
+ continue
87
+ }
88
+
89
+ break
90
+ }
91
+
92
+ try {
93
+ await onChange(target)
94
+ } catch(callbackErr) {
95
+ console.error("Watcher onChange callback error:", callbackErr)
96
+ }
97
+ state.busy = false
98
+ }
99
+ } catch(err) {
100
+ if(err.name === "AbortError") {
101
+ return
102
+ }
103
+
104
+ console.error("Watcher error:", err)
105
+ }
106
+ })()
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Stop watching all targets.
112
+ */
113
+ stopWatching() {
114
+ this.#state.clear()
115
+ this.#abortController?.abort()
116
+ this.#abortController = null
117
+ }
118
+ }
@@ -106,15 +106,9 @@ export default class Data {
106
106
  * defining the type of a value and whether an array is expected.
107
107
  *
108
108
  * @param {string} string - The string to parse into a type spec.
109
- * @param {TypeSpecOptions} [options] - Additional options for parsing.
110
- * @returns {Array<object>} An array of type specs.
109
+ * @returns {TypeSpec} A new TypeSpec instance.
111
110
  */
112
- static newTypeSpec(string: string, options?: {
113
- /**
114
- * - The delimiter for union types
115
- */
116
- delimiter?: string;
117
- }): Array<object>;
111
+ static newTypeSpec(string: string): TypeSpec;
118
112
  /**
119
113
  * Checks if a value is of a specified type
120
114
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Data.d.ts","sourceRoot":"","sources":["../../../src/browser/lib/Data.js"],"names":[],"mappings":"AAUA;IACA;;;;;OAKG;IACD,mBAFQ,KAAK,CAAC,MAAM,CAAC,CAgBnB;IAEF;;;;;OAKG;IACH,qBAFU,KAAK,CAAC,MAAM,CAAC,CAmBrB;IAEF;;;;;;;OAOG;IACH,kBAFU,KAAK,CAAC,MAAM,CAAC,CAKrB;IAEF;;;;;OAKG;IACH,uBAFU,KAAK,CAAC,MAAM,CAAC,CAE2D;IAElF;;;;;;OAMG;IACH,sBAJW,MAAM,UACN,MAAM,GACJ,MAAM,CAMlB;IAED;;;;;;OAMG;IACH,uBAJW,MAAM,WACN,MAAM,GACJ,MAAM,CAMlB;IAED;;;;;;;;;;;OAWG;IACH,yBATW,MAAM,UACN,MAAM,oBACN,OAAO,GACL,MAAM,CAWlB;IAED;;;;;;;;;;;OAWG;IACH,wBATW,MAAM,UACN,MAAM,oBACN,OAAO,GACL,MAAM,CAWlB;IAED;;;;;;;OAOG;IACH,yBALW,MAAM,UACN,MAAM,oBACN,OAAO,GACL,MAAM,CAWlB;IAED;;;;;;;OAOG;IACH,0BALW,MAAM,UACN,MAAM,oBACN,OAAO,GACL,MAAM,CAYlB;IAED;;;;;OAKG;IAEH;;;;;OAKG;IAEH;;;;;;;OAOG;IACH,2BAJW,MAAM;;;;oBAdH,MAAM;QAgBP,KAAK,CAAC,MAAM,CAAC,CAIzB;IAED;;;;;;;OAOG;IACH,qBALW,OAAO,QACP,MAAM,GAAC,QAAQ;;;;qBAnBZ,OAAO;QAqBR,OAAO,CAQnB;IAED;;;;;OAKG;IACH,yBAHW,MAAM,GACJ,OAAO,CASnB;IAED;;;;;;;;OAQG;IACH,yBAJW,OAAO,QACP,MAAM,GACJ,OAAO,CAenB;IAED;;;;;;OAMG;IACH,qBAHW,OAAO,GACL,MAAM,CAoBlB;IAED;;;;;OAKG;IACH,wBAHW,OAAO,GACL,OAAO,CAInB;IAED;;;;;;;;OAQG;IACH,sBALW,OAAO,oBACP,OAAO,GAEL,OAAO,CA8BnB;IAED;;;;;OAKG;IACH,6BAHW,MAAM,GACJ,MAAM,CAmBlB;IAED;;;;;;;OAOG;IACH,6BAJW,MAAM,QACN,KAAK,CAAC,MAAM,CAAC,GACX,MAAM,CAiBlB;IAED;;;;;;;OAOG;IACH,2BAJW,MAAM,QACN,KAAK,CAAC,MAAM,CAAC,SACb,OAAO,QAMjB;IAED;;;;;OAKG;IACH,+BAHc,MAAM,EAAA,GACP,MAAM,CAqBlB;IAED;;;;;;;OAOG;IACH,wBAJW,KAAK,CAAC,OAAO,CAAC,aACd,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAClC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAMnC;IAED;;;;;;;OAOG;IACH,kBALW,MAAM,OACN,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;;OAOG;IACH,oBALW,MAAM,OACN,MAAM,OACN,MAAM,GACJ,OAAO,CAInB;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,4BAbW,OAAO,GACL,OAAO,CA+BnB;IAED;;;;;;;;;;;;;;;OAeG;IACH,uBAXW,OAAO,GACL,OAAO,CAgBnB;CACF;qBA5gBoB,eAAe"}
1
+ {"version":3,"file":"Data.d.ts","sourceRoot":"","sources":["../../../src/browser/lib/Data.js"],"names":[],"mappings":"AAUA;IACA;;;;;OAKG;IACD,mBAFQ,KAAK,CAAC,MAAM,CAAC,CAgBnB;IAEF;;;;;OAKG;IACH,qBAFU,KAAK,CAAC,MAAM,CAAC,CAmBrB;IAEF;;;;;;;OAOG;IACH,kBAFU,KAAK,CAAC,MAAM,CAAC,CAKrB;IAEF;;;;;OAKG;IACH,uBAFU,KAAK,CAAC,MAAM,CAAC,CAE2D;IAElF;;;;;;OAMG;IACH,sBAJW,MAAM,UACN,MAAM,GACJ,MAAM,CAMlB;IAED;;;;;;OAMG;IACH,uBAJW,MAAM,WACN,MAAM,GACJ,MAAM,CAMlB;IAED;;;;;;;;;;;OAWG;IACH,yBATW,MAAM,UACN,MAAM,oBACN,OAAO,GACL,MAAM,CAWlB;IAED;;;;;;;;;;;OAWG;IACH,wBATW,MAAM,UACN,MAAM,oBACN,OAAO,GACL,MAAM,CAWlB;IAED;;;;;;;OAOG;IACH,yBALW,MAAM,UACN,MAAM,oBACN,OAAO,GACL,MAAM,CAWlB;IAED;;;;;;;OAOG;IACH,0BALW,MAAM,UACN,MAAM,oBACN,OAAO,GACL,MAAM,CAYlB;IAED;;;;;OAKG;IAEH;;;;;OAKG;IAEH;;;;;;OAMG;IACH,2BAHW,MAAM,GACJ,QAAQ,CAIpB;IAED;;;;;;;OAOG;IACH,qBALW,OAAO,QACP,MAAM,GAAC,QAAQ;;;;qBAlBZ,OAAO;QAoBR,OAAO,CAQnB;IAED;;;;;OAKG;IACH,yBAHW,MAAM,GACJ,OAAO,CASnB;IAED;;;;;;;;OAQG;IACH,yBAJW,OAAO,QACP,MAAM,GACJ,OAAO,CAenB;IAED;;;;;;OAMG;IACH,qBAHW,OAAO,GACL,MAAM,CAoBlB;IAED;;;;;OAKG;IACH,wBAHW,OAAO,GACL,OAAO,CAInB;IAED;;;;;;;;OAQG;IACH,sBALW,OAAO,oBACP,OAAO,GAEL,OAAO,CA8BnB;IAED;;;;;OAKG;IACH,6BAHW,MAAM,GACJ,MAAM,CAmBlB;IAED;;;;;;;OAOG;IACH,6BAJW,MAAM,QACN,KAAK,CAAC,MAAM,CAAC,GACX,MAAM,CAiBlB;IAED;;;;;;;OAOG;IACH,2BAJW,MAAM,QACN,KAAK,CAAC,MAAM,CAAC,SACb,OAAO,QAMjB;IAED;;;;;OAKG;IACH,+BAHc,MAAM,EAAA,GACP,MAAM,CAqBlB;IAED;;;;;;;OAOG;IACH,wBAJW,KAAK,CAAC,OAAO,CAAC,aACd,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAClC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAMnC;IAED;;;;;;;OAOG;IACH,kBALW,MAAM,OACN,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;;OAOG;IACH,oBALW,MAAM,OACN,MAAM,OACN,MAAM,GACJ,OAAO,CAInB;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,4BAbW,OAAO,GACL,OAAO,CA+BnB;IAED;;;;;;;;;;;;;;;OAeG;IACH,uBAXW,OAAO,GACL,OAAO,CAgBnB;CAEF;qBA5gBoB,eAAe"}