@mark1russell7/collections 0.5.0 → 0.7.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/collections",
4
- "version": "0.5.0",
4
+ "version": "0.7.0",
5
5
  "description": "Collections with no dependencies: lists, maps, queues, a summary tree, channels, lazy operations and dependency graphs.",
6
6
  "keywords": [
7
7
  "collections",
@@ -78,6 +78,9 @@
78
78
  "typescript": "^7.0.2",
79
79
  "vitest": "^5.0.3"
80
80
  },
81
+ "dependencies": {
82
+ "@mark1russell7/optional": "^1.2.0"
83
+ },
81
84
  "scripts": {
82
85
  "build": "tsdown",
83
86
  "test": "vitest run",
@@ -1,35 +1,29 @@
1
1
  /**
2
- * Safe behavior - Gives Option/Result-based error handling.
3
- *
4
- * This module wraps collection operations that throw errors to instead return
5
- * Option<T> or Result<T, E>, enabling functional error handling.
2
+ * The safe middleware. Each operation that throws on a missing element has a safe form in the property `safe`. The safe
3
+ * form gives an `Optional` or a `Result` of `@mark1russell7/optional`, and does not throw.
6
4
  *
7
5
  * @example
8
- * const list = compose(
9
- * safeList<number>()
10
- * )(arrayList<number>())
6
+ * import { isSome } from "@mark1russell7/optional";
11
7
  *
12
- * const value = list.safe.get(10) // Option<number> instead of throwing
13
- * if (isSome(value)) {
14
- * console.log(value.value)
15
- * }
8
+ * const list = compose(safeList<number>())(arrayList<number>());
9
+ * const value = list.safe.get(10); // an Optional<number>, not an exception
10
+ * if (isSome(value)) console.log(value.value);
16
11
  */
17
12
 
18
13
  import type { Middleware } from "../core/middleware.js";
19
14
  import type { List } from "../interfaces/list.js";
20
15
  import type { Queue, Deque } from "../interfaces/queue.js";
21
16
  import type { MapLike } from "../interfaces/map.js";
22
- import { type Option, type Result } from "../core/effects.js";
23
- import { None, Some, tryCatch } from "../core/effects.js";
17
+ import { none, some, toOptional, tryCatch, type Optional, type Result } from "@mark1russell7/optional";
24
18
 
25
19
  /**
26
20
  * Safe operations for List.
27
21
  */
28
22
  export interface SafeListOperations<T> {
29
23
  /**
30
- * Get element at index, giving Option.
24
+ * Get element at index, as an optional.
31
25
  */
32
- get(index: number): Option<T>;
26
+ get(index: number): Optional<T>;
33
27
 
34
28
  /**
35
29
  * Set element at index, giving Result with old value or error.
@@ -42,24 +36,24 @@ export interface SafeListOperations<T> {
42
36
  removeAt(index: number): Result<T, string>;
43
37
 
44
38
  /**
45
- * Get first element as Option.
39
+ * Get first element as an optional.
46
40
  */
47
- first(): Option<T>;
41
+ first(): Optional<T>;
48
42
 
49
43
  /**
50
- * Get last element as Option.
44
+ * Get last element as an optional.
51
45
  */
52
- last(): Option<T>;
46
+ last(): Optional<T>;
53
47
 
54
48
  /**
55
- * Pop element, giving Option.
49
+ * Pop element, as an optional.
56
50
  */
57
- pop(): Option<T>;
51
+ pop(): Optional<T>;
58
52
 
59
53
  /**
60
- * Shift element, giving Option.
54
+ * Shift element, as an optional.
61
55
  */
62
- shift(): Option<T>;
56
+ shift(): Optional<T>;
63
57
  }
64
58
 
65
59
  /**
@@ -67,19 +61,19 @@ export interface SafeListOperations<T> {
67
61
  */
68
62
  export interface SafeQueueOperations<T> {
69
63
  /**
70
- * Peek at head element as Option.
64
+ * Peek at head element as an optional.
71
65
  */
72
- peek(): Option<T>;
66
+ peek(): Optional<T>;
73
67
 
74
68
  /**
75
- * Poll element as Option.
69
+ * Poll element as an optional.
76
70
  */
77
- poll(): Option<T>;
71
+ poll(): Optional<T>;
78
72
 
79
73
  /**
80
- * Dequeue element as Option.
74
+ * Dequeue element as an optional.
81
75
  */
82
- dequeue(): Option<T>;
76
+ dequeue(): Optional<T>;
83
77
  }
84
78
 
85
79
  /**
@@ -87,29 +81,29 @@ export interface SafeQueueOperations<T> {
87
81
  */
88
82
  export interface SafeDequeOperations<T> extends SafeQueueOperations<T> {
89
83
  /**
90
- * Peek at first element as Option.
84
+ * Peek at first element as an optional.
91
85
  */
92
- peekFirst(): Option<T>;
86
+ peekFirst(): Optional<T>;
93
87
 
94
88
  /**
95
- * Peek at last element as Option.
89
+ * Peek at last element as an optional.
96
90
  */
97
- peekLast(): Option<T>;
91
+ peekLast(): Optional<T>;
98
92
 
99
93
  /**
100
- * Poll first element as Option.
94
+ * Poll first element as an optional.
101
95
  */
102
- pollFirst(): Option<T>;
96
+ pollFirst(): Optional<T>;
103
97
 
104
98
  /**
105
- * Poll last element as Option.
99
+ * Poll last element as an optional.
106
100
  */
107
- pollLast(): Option<T>;
101
+ pollLast(): Optional<T>;
108
102
 
109
103
  /**
110
- * Pop element as Option.
104
+ * Pop element as an optional.
111
105
  */
112
- pop(): Option<T>;
106
+ pop(): Optional<T>;
113
107
  }
114
108
 
115
109
  /**
@@ -117,9 +111,9 @@ export interface SafeDequeOperations<T> extends SafeQueueOperations<T> {
117
111
  */
118
112
  export interface SafeMapOperations<K, V> {
119
113
  /**
120
- * Get value for key as Option.
114
+ * Get value for key as an optional.
121
115
  */
122
- get(key: K): Option<V>;
116
+ get(key: K): Optional<V>;
123
117
 
124
118
  /**
125
119
  * Set value for key, giving Result with old value or success.
@@ -135,7 +129,7 @@ export interface SafeMapOperations<K, V> {
135
129
  /**
136
130
  * This function makes a safe List middleware.
137
131
  *
138
- * This function exposes a `safe` property with Option/Result-based operations.
132
+ * This function exposes a `safe` property with Optional/Result-based operations.
139
133
  */
140
134
  export function safeList<T>(): Middleware<
141
135
  List<T>,
@@ -143,14 +137,8 @@ export function safeList<T>(): Middleware<
143
137
  > {
144
138
  return (next: List<T>): List<T> & { safe: SafeListOperations<T> } => {
145
139
  const safe: SafeListOperations<T> = {
146
- get(index: number): Option<T> {
147
- return tryCatch(
148
- () => next.get(index),
149
- () => "Index out of bounds"
150
- ).match<Option<T>>({
151
- Ok: (value) => Some(value),
152
- Err: () => None,
153
- }) as Option<T>;
140
+ get(index: number): Optional<T> {
141
+ return toOptional(tryCatch(() => next.get(index)));
154
142
  },
155
143
 
156
144
  set(index: number, element: T): Result<T, string> {
@@ -167,34 +155,22 @@ export function safeList<T>(): Middleware<
167
155
  );
168
156
  },
169
157
 
170
- first(): Option<T> {
171
- return next.isEmpty ? None : Some(next.first());
158
+ first(): Optional<T> {
159
+ return next.isEmpty ? none : some(next.first());
172
160
  },
173
161
 
174
- last(): Option<T> {
175
- return next.isEmpty ? None : Some(next.last());
162
+ last(): Optional<T> {
163
+ return next.isEmpty ? none : some(next.last());
176
164
  },
177
165
 
178
- pop(): Option<T> {
179
- if (next.isEmpty) return None;
180
- return tryCatch(
181
- () => next.pop(),
182
- () => "List is empty"
183
- ).match<Option<T>>({
184
- Ok: (value) => Some(value),
185
- Err: () => None,
186
- }) as Option<T>;
166
+ pop(): Optional<T> {
167
+ if (next.isEmpty) return none;
168
+ return toOptional(tryCatch(() => next.pop()));
187
169
  },
188
170
 
189
- shift(): Option<T> {
190
- if (next.isEmpty) return None;
191
- return tryCatch(
192
- () => next.shift(),
193
- () => "List is empty"
194
- ).match<Option<T>>({
195
- Ok: (value) => Some(value),
196
- Err: () => None,
197
- }) as Option<T>;
171
+ shift(): Optional<T> {
172
+ if (next.isEmpty) return none;
173
+ return toOptional(tryCatch(() => next.shift()));
198
174
  },
199
175
  };
200
176
 
@@ -212,37 +188,19 @@ export function safeQueue<T>(): Middleware<
212
188
  > {
213
189
  return (next: Queue<T>): Queue<T> & { safe: SafeQueueOperations<T> } => {
214
190
  const safe: SafeQueueOperations<T> = {
215
- peek(): Option<T> {
216
- if (next.isEmpty) return None;
217
- return tryCatch(
218
- () => next.peek(),
219
- () => "Queue is empty"
220
- ).match<Option<T>>({
221
- Ok: (value) => Some(value),
222
- Err: () => None,
223
- }) as Option<T>;
191
+ peek(): Optional<T> {
192
+ if (next.isEmpty) return none;
193
+ return toOptional(tryCatch(() => next.peek()));
224
194
  },
225
195
 
226
- poll(): Option<T> {
227
- if (next.isEmpty) return None;
228
- return tryCatch(
229
- () => next.poll(),
230
- () => "Queue is empty"
231
- ).match<Option<T>>({
232
- Ok: (value) => Some(value),
233
- Err: () => None,
234
- }) as Option<T>;
196
+ poll(): Optional<T> {
197
+ if (next.isEmpty) return none;
198
+ return toOptional(tryCatch(() => next.poll()));
235
199
  },
236
200
 
237
- dequeue(): Option<T> {
238
- if (next.isEmpty) return None;
239
- return tryCatch(
240
- () => next.dequeue(),
241
- () => "Queue is empty"
242
- ).match<Option<T>>({
243
- Ok: (value) => Some(value),
244
- Err: () => None,
245
- }) as Option<T>;
201
+ dequeue(): Optional<T> {
202
+ if (next.isEmpty) return none;
203
+ return toOptional(tryCatch(() => next.dequeue()));
246
204
  },
247
205
  };
248
206
 
@@ -259,55 +217,43 @@ export function safeDeque<T>(): Middleware<
259
217
  > {
260
218
  return (next: Deque<T>): Deque<T> & { safe: SafeDequeOperations<T> } => {
261
219
  const safe: SafeDequeOperations<T> = {
262
- peek(): Option<T> {
263
- return next.isEmpty ? None : Some(next.peekFirst());
220
+ peek(): Optional<T> {
221
+ return next.isEmpty ? none : some(next.peekFirst());
264
222
  },
265
223
 
266
- poll(): Option<T> {
224
+ poll(): Optional<T> {
267
225
  // One poll: before, the test and the result each called pollFirst(), so poll() removed
268
226
  // two elements and returned the second (BUGS-2026-07 L12)
269
- if (next.isEmpty) return None;
270
- return Some(next.pollFirst() as T);
227
+ if (next.isEmpty) return none;
228
+ return some(next.pollFirst() as T);
271
229
  },
272
230
 
273
- dequeue(): Option<T> {
274
- if (next.isEmpty) return None;
275
- return tryCatch(
276
- () => next.dequeue(),
277
- () => "Deque is empty"
278
- ).match<Option<T>>({
279
- Ok: (value) => Some(value),
280
- Err: () => None,
281
- }) as Option<T>;
231
+ dequeue(): Optional<T> {
232
+ if (next.isEmpty) return none;
233
+ return toOptional(tryCatch(() => next.dequeue()));
282
234
  },
283
235
 
284
- peekFirst(): Option<T> {
285
- return next.isEmpty ? None : Some(next.peekFirst());
236
+ peekFirst(): Optional<T> {
237
+ return next.isEmpty ? none : some(next.peekFirst());
286
238
  },
287
239
 
288
- peekLast(): Option<T> {
289
- return next.isEmpty ? None : Some(next.peekLast());
240
+ peekLast(): Optional<T> {
241
+ return next.isEmpty ? none : some(next.peekLast());
290
242
  },
291
243
 
292
- pollFirst(): Option<T> {
244
+ pollFirst(): Optional<T> {
293
245
  const result = next.pollFirst();
294
- return result !== undefined ? Some(result) : None;
246
+ return result !== undefined ? some(result) : none;
295
247
  },
296
248
 
297
- pollLast(): Option<T> {
249
+ pollLast(): Optional<T> {
298
250
  const result = next.pollLast();
299
- return result !== undefined ? Some(result) : None;
251
+ return result !== undefined ? some(result) : none;
300
252
  },
301
253
 
302
- pop(): Option<T> {
303
- if (next.isEmpty) return None;
304
- return tryCatch(
305
- () => next.pop(),
306
- () => "Deque is empty"
307
- ).match<Option<T>>({
308
- Ok: (value) => Some(value),
309
- Err: () => None,
310
- }) as Option<T>;
254
+ pop(): Optional<T> {
255
+ if (next.isEmpty) return none;
256
+ return toOptional(tryCatch(() => next.pop()));
311
257
  },
312
258
  };
313
259
 
@@ -326,8 +272,8 @@ export function safeMap<K, V>(): Middleware<
326
272
  next: MapLike<K, V>
327
273
  ): MapLike<K, V> & { safe: SafeMapOperations<K, V> } => {
328
274
  const safe: SafeMapOperations<K, V> = {
329
- get(key: K): Option<V> {
330
- return next.has(key) ? Some(next.get(key)) : None;
275
+ get(key: K): Optional<V> {
276
+ return next.has(key) ? some(next.get(key)) : none;
331
277
  },
332
278
 
333
279
  set(key: K, value: V): Result<V | undefined, string> {
@@ -68,6 +68,9 @@ function notify<T>(name: string, listener: ((arg: T) => void) | undefined, arg:
68
68
  * A processor that throws does NOT reject the whole run. The run records the throw
69
69
  * as a failed NodeResult, so partial results are always given.
70
70
  *
71
+ * With `skipDependents` (the default), a node whose dependency failed or was skipped does not start. Its result has
72
+ * `skipped: true` and an error that names those dependencies. A dependency outside the DAG does not count.
73
+ *
71
74
  * `failFast` operates at level granularity. When a node in a level fails, each
72
75
  * node already in flight in that level continues to completion. No later level
73
76
  * starts. (There is no mid-level cancellation.)
@@ -80,20 +83,37 @@ export async function executeDAG<TNode extends DAGNode>(
80
83
  const {
81
84
  concurrency = 4,
82
85
  failFast = true,
86
+ skipDependents = true,
83
87
  onNodeStart,
84
88
  onNodeComplete,
85
89
  } = options;
86
90
 
87
91
  const results = new Map<string, NodeResult<TNode>>();
88
92
  const failedNodes: string[] = [];
93
+ const skippedNodes: string[] = [];
94
+ // The ids of the nodes that failed or were skipped
95
+ const blocked = new Set<string>();
89
96
  const startTime = Date.now();
90
97
  let shouldStop = false;
91
98
 
92
99
  for (const level of dag.levels) {
93
100
  if (shouldStop) break;
94
101
 
102
+ const runnable: TNode[] = [];
103
+ for (const node of level) {
104
+ const blockers = skipDependents ? node.dependencies.filter((dep) => blocked.has(dep)) : [];
105
+ if (blockers.length === 0) {
106
+ runnable.push(node);
107
+ continue;
108
+ }
109
+ const message = "Skipped " + node.id + ": the dependencies " + blockers.join(", ") + " failed or were skipped";
110
+ results.set(node.id, { node, success: false, skipped: true, error: new Error(message), duration: 0, logs: [message] });
111
+ skippedNodes.push(node.id);
112
+ blocked.add(node.id);
113
+ }
114
+
95
115
  const levelResults = await executeWithConcurrency(
96
- level,
116
+ runnable,
97
117
  async (node) => {
98
118
  const startError = notify("onNodeStart", onNodeStart, node);
99
119
  const nodeStart = Date.now();
@@ -127,6 +147,7 @@ export async function executeDAG<TNode extends DAGNode>(
127
147
  results.set(result.node.id, result);
128
148
  if (!result.success) {
129
149
  failedNodes.push(result.node.id);
150
+ blocked.add(result.node.id);
130
151
  if (failFast) {
131
152
  shouldStop = true;
132
153
  }
@@ -135,9 +156,10 @@ export async function executeDAG<TNode extends DAGNode>(
135
156
  }
136
157
 
137
158
  return {
138
- success: failedNodes.length === 0,
159
+ success: failedNodes.length === 0 && skippedNodes.length === 0,
139
160
  results,
140
161
  failedNodes,
162
+ skippedNodes,
141
163
  totalDuration: Date.now() - startTime,
142
164
  };
143
165
  }
package/src/dag/types.ts CHANGED
@@ -41,6 +41,11 @@ export interface DAGExecutionOptions<TNode extends DAGNode = DAGNode> {
41
41
  concurrency?: number;
42
42
  /** When this value is true, the run stops on the first error. The default is true. */
43
43
  failFast?: boolean;
44
+ /**
45
+ * When this value is true, a node whose dependency failed or was skipped does not start. Its result has `skipped: true`.
46
+ * The default is true. Give false only when the nodes do not use the work of their dependencies.
47
+ */
48
+ skipDependents?: boolean;
44
49
  /** Callback for progress reporting */
45
50
  onNodeStart?: (node: TNode) => void;
46
51
  /** Callback when a node completes */
@@ -63,6 +68,8 @@ export interface NodeResult<TNode extends DAGNode = DAGNode> {
63
68
  logs: string[];
64
69
  /** Optional output from the processor */
65
70
  output?: unknown | undefined;
71
+ /** This value is true when the node did not start, because a dependency failed or was skipped. */
72
+ skipped?: boolean | undefined;
66
73
  }
67
74
 
68
75
  /**
@@ -73,8 +80,10 @@ export interface DAGResult<TNode extends DAGNode = DAGNode> {
73
80
  success: boolean;
74
81
  /** Results for each node */
75
82
  results: Map<string, NodeResult<TNode>>;
76
- /** Names of failed nodes */
83
+ /** The ids of the nodes whose processor failed */
77
84
  failedNodes: string[];
85
+ /** The ids of the nodes that did not start, because a dependency failed or was skipped */
86
+ skippedNodes: string[];
78
87
  /** Total duration in milliseconds */
79
88
  totalDuration: number;
80
89
  }
@@ -136,46 +136,36 @@ export class HashMap<K, V> implements MapLike<K, V> {
136
136
  // ========================================================================
137
137
 
138
138
  has(key: K): boolean {
139
- const hash = this._keyHash(key);
140
- const index = this.bucketIndex(hash);
141
- let node = this._buckets[index] ?? null;
142
-
143
- while (node !== null) {
144
- if (node.hash === hash && this._keyEq(node.key, key)) {
145
- return true;
146
- }
147
- node = node.next;
148
- }
149
-
150
- return false;
139
+ return this.findNode(key) !== null;
151
140
  }
152
141
 
153
142
  get(key: K): V {
154
- const value = this.getOrUndefined(key);
155
- if (value === undefined) {
143
+ // The node, not the value, tells if the key is present: a key can have the value undefined (C8)
144
+ const node = this.findNode(key);
145
+ if (node === null) {
156
146
  throw new Error(`Key not found: ${key}`);
157
147
  }
158
- return value;
148
+ return node.value;
159
149
  }
160
150
 
161
151
  getOrUndefined(key: K): V | undefined {
162
- const hash = this._keyHash(key);
163
- const index = this.bucketIndex(hash);
164
- let node = this._buckets[index] ?? null;
152
+ return this.findNode(key)?.value;
153
+ }
165
154
 
155
+ getOrDefault(key: K, defaultValue: V): V {
156
+ const node = this.findNode(key);
157
+ return node !== null ? node.value : defaultValue;
158
+ }
159
+
160
+ /** This method finds the node of a key, or gives `null`. */
161
+ private findNode(key: K): Node<K, V> | null {
162
+ const hash = this._keyHash(key);
163
+ let node = this._buckets[this.bucketIndex(hash)] ?? null;
166
164
  while (node !== null) {
167
- if (node.hash === hash && this._keyEq(node.key, key)) {
168
- return node.value;
169
- }
165
+ if (node.hash === hash && this._keyEq(node.key, key)) return node;
170
166
  node = node.next;
171
167
  }
172
-
173
- return undefined;
174
- }
175
-
176
- getOrDefault(key: K, defaultValue: V): V {
177
- const value = this.getOrUndefined(key);
178
- return value !== undefined ? value : defaultValue;
168
+ return null;
179
169
  }
180
170
 
181
171
  containsValue(value: V): boolean {
package/src/index.ts CHANGED
@@ -10,7 +10,6 @@
10
10
  // ============================================================================
11
11
 
12
12
  export * from "./core/traits.js";
13
- export * from "./core/effects.js";
14
13
  export * from "./core/policies.js";
15
14
  export * from "./core/middleware.js";
16
15
  export * from "./core/events.js";
@@ -220,18 +219,3 @@ export {
220
219
  groupBy,
221
220
  } from "./utils/factories.js";
222
221
 
223
- // Effects
224
- export {
225
- None,
226
- Some,
227
- type Option,
228
- Ok,
229
- Err,
230
- type Result,
231
- isSome,
232
- isNone,
233
- isOk,
234
- isErr,
235
- getOrElse,
236
- unwrap,
237
- } from "./core/effects.js";