@just-io/utils 1.5.0 → 2.0.1

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/README.md CHANGED
@@ -1,12 +1,245 @@
1
- Utils
2
- =====
1
+ @just-io/utils
2
+ ==============
3
3
 
4
- This package contains some utilities.
4
+ Lightweight TypeScript utility functions.
5
5
 
6
6
  ## Installation
7
7
 
8
- This is a Typescript module so you need to use Typescript.
8
+ ```bash
9
+ npm install @just-io/utils
10
+ ```
9
11
 
10
- ## Documentation
12
+ ## Utilities
11
13
 
12
- See tests.
14
+ ### debounce / debounceByKey
15
+
16
+ Debounce function calls with optional key-based grouping.
17
+
18
+ ```typescript
19
+ import { debounce, debounceByKey } from '@just-io/utils';
20
+
21
+ // Basic debounce - executes after delay with last arguments
22
+ function setFilterValue(values: string[]): void {
23
+ console.log('Filter:', values);
24
+ }
25
+
26
+ const debouncedSetFilterValue = debounce(setFilterValue, 1000);
27
+
28
+ debouncedSetFilterValue(['first']);
29
+ // wait 300ms...
30
+ debouncedSetFilterValue(['second']); // Only ['second'] executes after 1000ms
31
+
32
+ // Cancel pending execution
33
+ debouncedSetFilterValue.teardown();
34
+
35
+ // Key-based debounce - separate timers per extracted key
36
+ function notify(event: { type: string; at: number }): void {
37
+ console.log(event.type, event.at);
38
+ }
39
+
40
+ const debouncedNotify = debounceByKey(notify, (event) => event.type, 1000);
41
+
42
+ debouncedNotify({ type: 'change', at: 0 });
43
+ debouncedNotify({ type: 'update', at: 0 }); // Both execute - different keys
44
+ debouncedNotify({ type: 'change', at: 10 }); // Replaces first 'change', only at:10 executes
45
+
46
+ // Cancel by key or all
47
+ debouncedNotify.teardown({ type: 'change', at: 0 });
48
+ debouncedNotify.teardownAll();
49
+ ```
50
+
51
+ ### memo / memoByKey
52
+
53
+ Memoize function calls - skip execution if arguments haven't changed.
54
+
55
+ ```typescript
56
+ import { memo, memoByKey } from '@just-io/utils';
57
+
58
+ // Basic memo - only calls when extracted values change
59
+ function notify(event: { type: string; at: number }): void {
60
+ console.log(event.type, event.at);
61
+ }
62
+
63
+ const memoNotify = memo(notify, (event) => [event.type, event.at]);
64
+
65
+ memoNotify({ type: 'change', at: 0 }); // Executes
66
+ memoNotify({ type: 'change', at: 0 }); // Skipped - same values
67
+ memoNotify({ type: 'change', at: 1 }); // Executes - 'at' changed
68
+
69
+ // Only trigger on changes (skip first call)
70
+ function setFilterValue(values: string[]): void {
71
+ console.log('Filters changed:', values);
72
+ }
73
+
74
+ const memoSetFilterValue = memo(setFilterValue, (values) => values, true);
75
+
76
+ memoSetFilterValue([]); // Skipped (initial call)
77
+ memoSetFilterValue(['first']); // Executes (values changed)
78
+
79
+ // Key-based memo - separate memo state per key
80
+ function notifyChannel(channel: string, event: { type: string; at: number }): void {
81
+ console.log(channel, event);
82
+ }
83
+
84
+ const memoNotifyChannel = memoByKey(
85
+ notifyChannel,
86
+ (channel) => channel,
87
+ (channel, event) => [event.type, event.at],
88
+ );
89
+
90
+ memoNotifyChannel('channel1', { type: 'change', at: 0 }); // Executes
91
+ memoNotifyChannel('channel2', { type: 'change', at: 0 }); // Executes (different key)
92
+ memoNotifyChannel('channel1', { type: 'change', at: 0 }); // Skipped
93
+ memoNotifyChannel('channel1', { type: 'change', at: 10 }); // Executes (values changed)
94
+
95
+ // Key-based with onlyChanges
96
+ function setFilter(filter: string, values: string[]): void {
97
+ console.log(filter, values);
98
+ }
99
+
100
+ const memoSetFilter = memoByKey(
101
+ setFilter,
102
+ (filter) => filter,
103
+ (filter, values) => values,
104
+ true,
105
+ );
106
+
107
+ memoSetFilter('filter1', []); // Skipped (initial)
108
+ memoSetFilter('filter1', ['1']); // Executes (changed)
109
+ ```
110
+
111
+ ### DeepMap
112
+
113
+ Map with array keys (nested key paths). Useful for hierarchical data.
114
+
115
+ ```typescript
116
+ import { DeepMap } from '@just-io/utils';
117
+
118
+ // Create empty or with initial entries
119
+ const deepMap = new DeepMap<string, string>();
120
+ const withEntries = new DeepMap<string, string>([
121
+ [['one'], 'str'],
122
+ [['one', 'two'], 'str'],
123
+ ]);
124
+
125
+ // Copy from another DeepMap
126
+ const copy = new DeepMap(withEntries);
127
+
128
+ // Set and get with array keys
129
+ deepMap.set([], 'root'); // Empty key path
130
+ deepMap.set(['one'], 'first');
131
+ deepMap.set(['one', 'two'], 'nested');
132
+
133
+ deepMap.get(['one']); // 'first'
134
+ deepMap.get(['one', 'two']); // 'nested'
135
+ deepMap.get(['missing']); // undefined
136
+
137
+ // Check existence
138
+ deepMap.has(['one']); // true
139
+ deepMap.has(['missing']); // false
140
+
141
+ // Delete entries
142
+ deepMap.delete(['one', 'two']); // returns true
143
+ deepMap.delete(['missing']); // returns false
144
+
145
+ // Take: get and delete in one operation
146
+ const value = deepMap.take(['one']); // 'first', now deleted
147
+ deepMap.take(['missing']); // undefined
148
+
149
+ // Size and clear
150
+ deepMap.size; // number of entries
151
+ deepMap.clear(); // remove all
152
+
153
+ // Iteration
154
+ deepMap.forEach((value, key) => console.log(key, value));
155
+
156
+ for (const [key, value] of deepMap) {
157
+ console.log(key, value);
158
+ }
159
+
160
+ Array.from(deepMap.entries()); // [[key, value], ...]
161
+ Array.from(deepMap.keys()); // [key, ...]
162
+ Array.from(deepMap.values()); // [value, ...]
163
+
164
+ // Clone subtree (non-destructive)
165
+ deepMap.set(['one'], 'str');
166
+ deepMap.set(['one', 'two'], 'str');
167
+ const cloned = deepMap.clone(['one']);
168
+ // cloned: [[], 'str'], [['two'], 'str']
169
+
170
+ // Extract subtree (removes from original)
171
+ const extracted = deepMap.extract(['one']);
172
+ // extracted has the subtree, deepMap no longer has it
173
+
174
+ // Append entries under a prefix
175
+ deepMap.append(['one'], [[['two'], 'str']]);
176
+ // Result: [['one', 'two'], 'str']
177
+ ```
178
+
179
+ ### EventEmitter / Notifier
180
+
181
+ Type-safe event handling.
182
+
183
+ ```typescript
184
+ import { EventEmitter, EventTuple, Notifier } from '@just-io/utils';
185
+
186
+ // Notifier - single event type
187
+ type Event = [first: string, second: string];
188
+
189
+ const notifier = new Notifier<Event>();
190
+
191
+ const handler = (first: string, second: string) => console.log(first, second);
192
+ notifier.subscribe(handler);
193
+ notifier.notify('a', 'b'); // logs: a b
194
+
195
+ // One-time subscription
196
+ notifier.subscribe((a, b) => console.log('once:', a, b), { once: true });
197
+ notifier.notify('x', 'y'); // Both handlers called
198
+ notifier.notify('x', 'y'); // Only permanent handler
199
+
200
+ notifier.unsubscribe(handler); // Remove specific
201
+ notifier.unsubscribeAll(); // Remove all
202
+
203
+ // EventEmitter - multiple named events
204
+ type EventMap = {
205
+ one: [first: number];
206
+ two: [first: string, second: number];
207
+ };
208
+
209
+ const emitter = new EventEmitter<EventMap>();
210
+
211
+ emitter.on('one', (first) => console.log('one:', first));
212
+ emitter.on('two', (first, second) => console.log('two:', first, second));
213
+
214
+ emitter.emit('one', 1);
215
+ emitter.emit('two', '1', 2);
216
+
217
+ // One-time listener
218
+ emitter.once('one', (first) => console.log('once:', first));
219
+
220
+ // Emit from tuple (useful for batching)
221
+ type ETuple = EventTuple<EventMap>;
222
+ const events: ETuple[] = [
223
+ ['one', 1],
224
+ ['two', '2', 1],
225
+ ];
226
+ for (const event of events) {
227
+ emitter.emit(...event);
228
+ }
229
+
230
+ // Unsubscribe
231
+ emitter.off('one', handler);
232
+ emitter.unsubscribeAll('one'); // Remove all 'one' handlers
233
+ emitter.unsubscribeAll(); // Remove all handlers
234
+
235
+ // Event store - batch events and emit later
236
+ const store = emitter.makeStore();
237
+ store.add('one', 1);
238
+ store.add('one', 2);
239
+ store.emit(); // Emits both events
240
+ store.emit(); // Does nothing (already emitted)
241
+ ```
242
+
243
+ ## License
244
+
245
+ MIT
@@ -33,7 +33,7 @@ class DeepMap extends Map {
33
33
  }
34
34
  set(keys, value) {
35
35
  let node = __classPrivateFieldGet(this, _DeepMap_rootNode, "f");
36
- for (const key of (Array.isArray(keys) ? keys : [keys])) {
36
+ for (const key of Array.isArray(keys) ? keys : [keys]) {
37
37
  if (!node.children.has(key)) {
38
38
  node.children.set(key, {
39
39
  children: new Map(),
@@ -50,7 +50,7 @@ class DeepMap extends Map {
50
50
  }
51
51
  has(keys) {
52
52
  let node = __classPrivateFieldGet(this, _DeepMap_rootNode, "f");
53
- for (const key of (Array.isArray(keys) ? keys : [keys])) {
53
+ for (const key of Array.isArray(keys) ? keys : [keys]) {
54
54
  if (!node.children.has(key)) {
55
55
  return false;
56
56
  }
@@ -61,7 +61,7 @@ class DeepMap extends Map {
61
61
  delete(keys) {
62
62
  let node = __classPrivateFieldGet(this, _DeepMap_rootNode, "f");
63
63
  const entries = [[undefined, __classPrivateFieldGet(this, _DeepMap_rootNode, "f")]];
64
- for (const key of (Array.isArray(keys) ? keys : [keys])) {
64
+ for (const key of Array.isArray(keys) ? keys : [keys]) {
65
65
  if (!node.children.has(key)) {
66
66
  return false;
67
67
  }
@@ -129,7 +129,7 @@ class DeepMap extends Map {
129
129
  }
130
130
  [(_DeepMap_rootNode = new WeakMap(), _DeepMap_instances = new WeakSet(), _DeepMap_node = function _DeepMap_node(keys) {
131
131
  let node = __classPrivateFieldGet(this, _DeepMap_rootNode, "f");
132
- for (const key of (Array.isArray(keys) ? keys : [keys])) {
132
+ for (const key of Array.isArray(keys) ? keys : [keys]) {
133
133
  if (!node.children.has(key)) {
134
134
  return undefined;
135
135
  }
@@ -10,9 +10,9 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
11
11
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
12
12
  };
13
- var _Notifier_subscribers, _Notifier_onceSubscribers, _EventEmitter_subscribers, _EventEmitter_onceSubscribers;
13
+ var _Notifier_subscribers, _Notifier_onceSubscribers, _EventStore_eventEmitter, _EventStore_eventTuples, _EventEmitter_subscribers, _EventEmitter_onceSubscribers;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.EventEmitter = exports.Notifier = void 0;
15
+ exports.EventEmitter = exports.EventStore = exports.Notifier = void 0;
16
16
  class Notifier {
17
17
  constructor() {
18
18
  _Notifier_subscribers.set(this, new Set());
@@ -40,12 +40,38 @@ class Notifier {
40
40
  }
41
41
  });
42
42
  }
43
- clear() {
43
+ unsubscribeAll() {
44
44
  __classPrivateFieldSet(this, _Notifier_subscribers, new Set(), "f");
45
+ __classPrivateFieldSet(this, _Notifier_onceSubscribers, new WeakSet(), "f");
45
46
  }
46
47
  }
47
48
  exports.Notifier = Notifier;
48
49
  _Notifier_subscribers = new WeakMap(), _Notifier_onceSubscribers = new WeakMap();
50
+ class EventStore {
51
+ constructor(eventEmitter) {
52
+ _EventStore_eventEmitter.set(this, void 0);
53
+ _EventStore_eventTuples.set(this, []);
54
+ __classPrivateFieldSet(this, _EventStore_eventEmitter, eventEmitter, "f");
55
+ }
56
+ add(event, ...args) {
57
+ __classPrivateFieldGet(this, _EventStore_eventTuples, "f").push([event, ...args]);
58
+ }
59
+ emit() {
60
+ for (const eventTuple of __classPrivateFieldGet(this, _EventStore_eventTuples, "f")) {
61
+ __classPrivateFieldGet(this, _EventStore_eventEmitter, "f").emit(...eventTuple);
62
+ }
63
+ __classPrivateFieldSet(this, _EventStore_eventTuples, [], "f");
64
+ }
65
+ consume(eventStore) {
66
+ __classPrivateFieldGet(this, _EventStore_eventTuples, "f").push(...__classPrivateFieldGet(eventStore, _EventStore_eventTuples, "f"));
67
+ __classPrivateFieldSet(eventStore, _EventStore_eventTuples, [], "f");
68
+ }
69
+ clean() {
70
+ __classPrivateFieldSet(this, _EventStore_eventTuples, [], "f");
71
+ }
72
+ }
73
+ exports.EventStore = EventStore;
74
+ _EventStore_eventEmitter = new WeakMap(), _EventStore_eventTuples = new WeakMap();
49
75
  class EventEmitter {
50
76
  constructor() {
51
77
  _EventEmitter_subscribers.set(this, {});
@@ -86,9 +112,18 @@ class EventEmitter {
86
112
  }
87
113
  });
88
114
  }
89
- clear() {
90
- __classPrivateFieldSet(this, _EventEmitter_subscribers, {}, "f");
91
- __classPrivateFieldSet(this, _EventEmitter_onceSubscribers, {}, "f");
115
+ unsubscribeAll(event) {
116
+ if (event) {
117
+ __classPrivateFieldGet(this, _EventEmitter_subscribers, "f")[event] = new Set();
118
+ __classPrivateFieldGet(this, _EventEmitter_onceSubscribers, "f")[event] = new WeakSet();
119
+ }
120
+ else {
121
+ __classPrivateFieldSet(this, _EventEmitter_subscribers, {}, "f");
122
+ __classPrivateFieldSet(this, _EventEmitter_onceSubscribers, {}, "f");
123
+ }
124
+ }
125
+ makeStore() {
126
+ return new EventStore(this);
92
127
  }
93
128
  }
94
129
  exports.EventEmitter = EventEmitter;
@@ -12,7 +12,7 @@ export class DeepMap extends Map {
12
12
  }
13
13
  #node(keys) {
14
14
  let node = this.#rootNode;
15
- for (const key of (Array.isArray(keys) ? keys : [keys])) {
15
+ for (const key of Array.isArray(keys) ? keys : [keys]) {
16
16
  if (!node.children.has(key)) {
17
17
  return undefined;
18
18
  }
@@ -35,7 +35,7 @@ export class DeepMap extends Map {
35
35
  }
36
36
  set(keys, value) {
37
37
  let node = this.#rootNode;
38
- for (const key of (Array.isArray(keys) ? keys : [keys])) {
38
+ for (const key of Array.isArray(keys) ? keys : [keys]) {
39
39
  if (!node.children.has(key)) {
40
40
  node.children.set(key, {
41
41
  children: new Map(),
@@ -51,7 +51,7 @@ export class DeepMap extends Map {
51
51
  }
52
52
  has(keys) {
53
53
  let node = this.#rootNode;
54
- for (const key of (Array.isArray(keys) ? keys : [keys])) {
54
+ for (const key of Array.isArray(keys) ? keys : [keys]) {
55
55
  if (!node.children.has(key)) {
56
56
  return false;
57
57
  }
@@ -62,7 +62,7 @@ export class DeepMap extends Map {
62
62
  delete(keys) {
63
63
  let node = this.#rootNode;
64
64
  const entries = [[undefined, this.#rootNode]];
65
- for (const key of (Array.isArray(keys) ? keys : [keys])) {
65
+ for (const key of Array.isArray(keys) ? keys : [keys]) {
66
66
  if (!node.children.has(key)) {
67
67
  return false;
68
68
  }
@@ -23,8 +23,32 @@ export class Notifier {
23
23
  }
24
24
  });
25
25
  }
26
- clear() {
26
+ unsubscribeAll() {
27
27
  this.#subscribers = new Set();
28
+ this.#onceSubscribers = new WeakSet();
29
+ }
30
+ }
31
+ export class EventStore {
32
+ #eventEmitter;
33
+ #eventTuples = [];
34
+ constructor(eventEmitter) {
35
+ this.#eventEmitter = eventEmitter;
36
+ }
37
+ add(event, ...args) {
38
+ this.#eventTuples.push([event, ...args]);
39
+ }
40
+ emit() {
41
+ for (const eventTuple of this.#eventTuples) {
42
+ this.#eventEmitter.emit(...eventTuple);
43
+ }
44
+ this.#eventTuples = [];
45
+ }
46
+ consume(eventStore) {
47
+ this.#eventTuples.push(...eventStore.#eventTuples);
48
+ eventStore.#eventTuples = [];
49
+ }
50
+ clean() {
51
+ this.#eventTuples = [];
28
52
  }
29
53
  }
30
54
  export class EventEmitter {
@@ -63,8 +87,17 @@ export class EventEmitter {
63
87
  }
64
88
  });
65
89
  }
66
- clear() {
67
- this.#subscribers = {};
68
- this.#onceSubscribers = {};
90
+ unsubscribeAll(event) {
91
+ if (event) {
92
+ this.#subscribers[event] = new Set();
93
+ this.#onceSubscribers[event] = new WeakSet();
94
+ }
95
+ else {
96
+ this.#subscribers = {};
97
+ this.#onceSubscribers = {};
98
+ }
99
+ }
100
+ makeStore() {
101
+ return new EventStore(this);
69
102
  }
70
103
  }
@@ -13,7 +13,7 @@ export declare class Notifier<E extends unknown[]> implements Notifiable<E> {
13
13
  }): this;
14
14
  unsubscribe(subscriber: Subscriber<E>): boolean;
15
15
  notify(...args: E): void;
16
- clear(): void;
16
+ unsubscribeAll(): void;
17
17
  }
18
18
  export type EventMap = Record<string, unknown[]>;
19
19
  export interface Eventable<E extends EventMap> {
@@ -21,9 +21,18 @@ export interface Eventable<E extends EventMap> {
21
21
  once<K extends keyof E>(event: K, subscriber: Subscriber<E[K]>): this;
22
22
  off<K extends keyof E>(event: K, subscriber: Subscriber<E[K]>): boolean;
23
23
  }
24
- export type EventList<E extends EventMap> = {
24
+ export type EventTuple<E extends EventMap> = {
25
25
  [K in keyof E]: [K, ...E[K]];
26
26
  }[keyof E];
27
+ export declare class EventStore<E extends EventMap> {
28
+ #private;
29
+ constructor(eventEmitter: EventEmitter<E>);
30
+ add<K extends keyof E>(event: K, ...args: E[K]): void;
31
+ add(...eventTuple: EventTuple<E>): void;
32
+ emit(): void;
33
+ consume(eventStore: EventStore<E>): void;
34
+ clean(): void;
35
+ }
27
36
  export declare class EventEmitter<E extends EventMap> implements Eventable<E> {
28
37
  #private;
29
38
  getSuscribers<K extends keyof E>(event: K): Set<Subscriber<E[K]>>;
@@ -31,6 +40,8 @@ export declare class EventEmitter<E extends EventMap> implements Eventable<E> {
31
40
  once<K extends keyof E>(event: K, subscriber: Subscriber<E[K]>): this;
32
41
  off<K extends keyof E>(event: K, subscriber: Subscriber<E[K]>): boolean;
33
42
  emit<K extends keyof E>(event: K, ...args: E[K]): void;
34
- emit(...eventList: EventList<E>): void;
35
- clear(): void;
43
+ emit(...eventTuple: EventTuple<E>): void;
44
+ unsubscribeAll(): void;
45
+ unsubscribeAll<K extends keyof E>(event: K): void;
46
+ makeStore(): EventStore<E>;
36
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@just-io/utils",
3
- "version": "1.5.0",
3
+ "version": "2.0.1",
4
4
  "description": "Utility functional",
5
5
  "types": "dist/types",
6
6
  "main": "dist/cjs/index.js",