@hyperfrontend/list-utils 0.0.4 → 0.1.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/CHANGELOG.md +24 -6
- package/README.md +7 -4
- package/SECURITY.md +50 -15
- package/_dependencies/@hyperfrontend/data-utils/index.cjs.js +214 -0
- package/_dependencies/@hyperfrontend/data-utils/index.esm.js +202 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/array/index.cjs.js +7 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/array/index.esm.js +5 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/date/index.cjs.js +8 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/date/index.esm.js +7 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/error/index.cjs.js +6 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/error/index.esm.js +5 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/map/index.cjs.js +6 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/map/index.esm.js +5 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/math/index.cjs.js +7 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/math/index.esm.js +5 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/object/index.cjs.js +7 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/object/index.esm.js +5 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/set/index.cjs.js +6 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/set/index.esm.js +5 -0
- package/bundle/index.iife.js +10 -134
- package/bundle/index.iife.min.js +1 -2
- package/bundle/index.umd.js +10 -134
- package/bundle/index.umd.min.js +1 -2
- package/index.cjs.js +88 -122
- package/index.d.ts +172 -8
- package/index.d.ts.map +1 -1
- package/index.esm.js +76 -110
- package/package.json +12 -7
- package/bundle/index.iife.js.map +0 -1
- package/bundle/index.iife.min.js.map +0 -1
- package/bundle/index.umd.js.map +0 -1
- package/bundle/index.umd.min.js.map +0 -1
- package/create-fifo-list.d.ts +0 -18
- package/create-fifo-list.d.ts.map +0 -1
- package/create-lifo-list.d.ts +0 -18
- package/create-lifo-list.d.ts.map +0 -1
- package/create-range.d.ts +0 -9
- package/create-range.d.ts.map +0 -1
- package/create-value-picker.d.ts +0 -12
- package/create-value-picker.d.ts.map +0 -1
- package/get-last-key-in-map.d.ts +0 -2
- package/get-last-key-in-map.d.ts.map +0 -1
- package/index.cjs.js.map +0 -1
- package/index.esm.js.map +0 -1
- package/non-empty-strings.d.ts +0 -8
- package/non-empty-strings.d.ts.map +0 -1
- package/unique-strings.d.ts +0 -8
- package/unique-strings.d.ts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [0.0
|
|
5
|
+
## [0.1.0](https://github.com/AndrewRedican/hyperfrontend/compare/5f116abb8ba6355dfb283fa03b7481e5eb029480...cc16961a470190848f71a7cce41f7377bc4511a4) - 2026-08-25
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### Breaking Changes
|
|
8
8
|
|
|
9
|
+
- **BREAKING** type fifo pull as T | undefined, reword throws
|
|
9
10
|
|
|
10
11
|
### Bug Fixes
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
- **BREAKING** type fifo pull as T | undefined, reword throws
|
|
13
14
|
|
|
14
|
-
## [0.0.
|
|
15
|
+
## [0.0.5](https://github.com/AndrewRedican/hyperfrontend/compare/c8db08be8b183addd26caf81fdd17fb3693f296f...466c0388c4cd516b9c704214140b4df1004098e6) - 2026-06-23
|
|
16
|
+
|
|
17
|
+
### Other
|
|
18
|
+
|
|
19
|
+
- **@hyperfrontend/workspace:** remove lib-builder and tool-package as implicit dependencies for all lib projects
|
|
20
|
+
|
|
21
|
+
## [0.0.4](https://github.com/AndrewRedican/hyperfrontend/compare/lib-list-utils@0.0.3...lib-list-utils@0.0.4) - 2026-03-08
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [0.0.3](https://github.com/AndrewRedican/hyperfrontend/compare/lib-list-utils@0.0.2...lib-list-utils@0.0.3) - 2026-03-02
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
- **lib-list-utils:** correct package exports ([5966537](https://github.com/AndrewRedican/hyperfrontend/commit/5966537bda3addd6d3bc22bb7b2e9a5cfef7c247))
|
|
29
|
+
|
|
30
|
+
## [0.0.2](https://github.com/AndrewRedican/hyperfrontend/compare/lib-list-utils@0.0.1...lib-list-utils@0.0.2) - 2026-02-26
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## 0.0.1 - 2026-02-15
|
|
15
34
|
|
|
16
|
-
## 0.0.1 (2026-02-15)
|
package/README.md
CHANGED
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
|
|
36
36
|
Purpose-built collection utilities for queue management, filtering, and iteration patterns.
|
|
37
37
|
|
|
38
|
+
• 👉 See [**documentation**](https://www.hyperfrontend.dev/docs/libraries/utils/list/)
|
|
39
|
+
• 👉 See [**guides & tutorials**](https://www.hyperfrontend.dev/docs/guides/?package=%40hyperfrontend%2Flist-utils)
|
|
40
|
+
|
|
38
41
|
## What is @hyperfrontend/list-utils?
|
|
39
42
|
|
|
40
43
|
@hyperfrontend/list-utils provides specialized collection utilities focused on common programming patterns that native JavaScript arrays don't handle elegantly. Rather than replicating lodash, this library targets specific use cases: FIFO/LIFO queue management with object reference tracking, cyclical value iteration, and string array sanitization.
|
|
@@ -58,11 +61,11 @@ Queue implementations return frozen objects to prevent external mutation while u
|
|
|
58
61
|
|
|
59
62
|
### Prevents Queue Bugs in Event-Driven Systems
|
|
60
63
|
|
|
61
|
-
Native arrays don't enforce uniqueness, making it easy to accidentally register the same event handler, task, or subscription multiple times.
|
|
64
|
+
Native arrays don't enforce uniqueness, making it easy to accidentally register the same event handler, task, or subscription multiple times. Both list kinds reject duplicates based on reference equality, eliminating a common source of memory leaks and duplicate processing in event loops, job queues, and observer patterns. A FIFO list throws on a duplicate push; a LIFO list ignores it.
|
|
62
65
|
|
|
63
66
|
### Simplifies Round-Robin and Cyclical Patterns
|
|
64
67
|
|
|
65
|
-
The value picker solves the boilerplate problem of cycling through options (load balancer endpoints, color schemes, retry strategies). No manual index tracking or modulo math
|
|
68
|
+
The value picker solves the boilerplate problem of cycling through options (load balancer endpoints, color schemes, retry strategies). No manual index tracking or modulo math, just call `next()`. Particularly useful for testing scenarios where you need predictable value rotation.
|
|
66
69
|
|
|
67
70
|
### Type-Safe String Sanitization
|
|
68
71
|
|
|
@@ -87,7 +90,7 @@ import { createFifoList, createValuePicker, nonEmptyStrings, uniqueStrings } fro
|
|
|
87
90
|
const taskQueue = createFifoList<{ id: string; execute: () => void }>()
|
|
88
91
|
taskQueue.push({ id: 'task1', execute: () => console.log('Task 1') })
|
|
89
92
|
taskQueue.push({ id: 'task2', execute: () => console.log('Task 2') })
|
|
90
|
-
const nextTask = taskQueue.pull() // Gets task1 (first in)
|
|
93
|
+
const nextTask = taskQueue.pull() // Gets task1 (first in), or undefined once the queue is drained
|
|
91
94
|
|
|
92
95
|
// Round-robin value picker
|
|
93
96
|
const colorPicker = createValuePicker(['red', 'blue', 'green'])
|
|
@@ -166,4 +169,4 @@ This library is part of the [hyperfrontend](https://github.com/AndrewRedican/hyp
|
|
|
166
169
|
|
|
167
170
|
## License
|
|
168
171
|
|
|
169
|
-
MIT
|
|
172
|
+
[MIT](https://github.com/AndrewRedican/hyperfrontend/blob/main/LICENSE.md)
|
package/SECURITY.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Security Policy
|
|
2
2
|
|
|
3
|
+
## The Security Model
|
|
4
|
+
|
|
5
|
+
Before reporting or reviewing, read the model this project is built on:
|
|
6
|
+
**[Security Model](https://www.hyperfrontend.dev/docs/core-concepts/security)**. It is the canonical
|
|
7
|
+
statement of what hyperfrontend defends against and what it does not, and it is the reference every
|
|
8
|
+
other security claim in these docs points back to. In short:
|
|
9
|
+
|
|
10
|
+
- **The named adversary is co-resident script**: an analytics snippet, tag manager, compromised
|
|
11
|
+
dependency, or unknown page that embeds a feature URL. A host that deliberately installs a feature
|
|
12
|
+
is trusting it, the way it trusts any dependency; the controls exist to bound a trusted
|
|
13
|
+
feature's bad day, not to treat its authors as hostile.
|
|
14
|
+
- **Origin checks authenticate rooms, not speakers.** Once arbitrary script runs inside a page, no
|
|
15
|
+
message check distinguishes it from the application. Threats inside a page need Content Security
|
|
16
|
+
Policy, Trusted Types, dependency provenance, and server-side authorisation. That is a different
|
|
17
|
+
treatment, deliberately outside this model.
|
|
18
|
+
- **Three parties carry the security of an integration.** The browser enforces document isolation
|
|
19
|
+
and the frame's capability attributes; the protocol enforces the relationship (pinned
|
|
20
|
+
counterparts, gated handshake, validated payloads, versioned contracts, an optional encrypted
|
|
21
|
+
envelope); and **you** decide authorisation: `frame-ancestors`, backend checks, HTTPS, the
|
|
22
|
+
envelope you choose, and the containment posture you set.
|
|
23
|
+
|
|
24
|
+
A vulnerability report is most useful when it names which of those three the issue defeats.
|
|
25
|
+
|
|
3
26
|
## Reporting a Vulnerability
|
|
4
27
|
|
|
5
28
|
We take the security of hyperfrontend seriously. If you discover a security vulnerability, please help us protect our users by following responsible disclosure practices.
|
|
@@ -58,14 +81,30 @@ Thank you for helping keep hyperfrontend and its users safe!
|
|
|
58
81
|
|
|
59
82
|
## Security Best Practices
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
These are the decisions the SDK cannot make for you. Everything the protocol already enforces —
|
|
85
|
+
origin pinning, window binding, instance identity, the gated handshake, payload validation on both
|
|
86
|
+
ends — is on by default and is not something you should be re-implementing by hand.
|
|
87
|
+
|
|
88
|
+
1. **Restrict who may embed the feature.** Send
|
|
89
|
+
`Content-Security-Policy: frame-ancestors <hosts>` on the response that serves the feature
|
|
90
|
+
document. Origin pinning keeps a conversation consistent; only `frame-ancestors` decides whether
|
|
91
|
+
a page was ever allowed to frame you.
|
|
92
|
+
2. **Authorise on the server.** A message that crossed the boundary is not an authorised operation.
|
|
93
|
+
Protected work needs credentials the feature's own backend validates.
|
|
94
|
+
3. **Choose the envelope deliberately.** `v2` with a pre-shared key is the confidentiality control;
|
|
95
|
+
`v1` is time-window obfuscation and buys deterrence only. Provision and rotate the `v2` key
|
|
96
|
+
yourself; a key is never baked into a built artifact. A handshake that cannot agree on an
|
|
97
|
+
encrypted transport falls back to plaintext; where that would be unacceptable, drive
|
|
98
|
+
`@hyperfrontend/nexus` directly and set `security.mode: 'fail-closed'` on the channel so the
|
|
99
|
+
connection is denied instead.
|
|
100
|
+
4. **Declare schemas and a contract version.** Actions without a schema pass unvalidated, and a side
|
|
101
|
+
without a version always passes the compatibility gate. Both are how drift is caught early.
|
|
102
|
+
5. **Grant capability narrowly.** Delegate only the Permissions-Policy features the integration
|
|
103
|
+
needs, and price a `sandbox` posture against what the product actually requires.
|
|
104
|
+
6. **Serve everything over HTTPS**, host and feature alike.
|
|
105
|
+
7. **Keep dependencies updated** on both sides of the boundary, and pair that with the page-integrity
|
|
106
|
+
controls this model deliberately leaves to you: Content Security Policy, Trusted Types,
|
|
107
|
+
Subresource Integrity, and dependency provenance.
|
|
69
108
|
|
|
70
109
|
## Security Updates
|
|
71
110
|
|
|
@@ -73,10 +112,6 @@ Security updates will be released as patch versions and documented in the [CHANG
|
|
|
73
112
|
|
|
74
113
|
## Supported Versions
|
|
75
114
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
| ------- | ------------------ |
|
|
80
|
-
| 0.0.x | :white_check_mark: |
|
|
81
|
-
|
|
82
|
-
As the project matures, we will update this table to reflect our long-term support policy.
|
|
115
|
+
Security updates are provided for the latest published version of each `@hyperfrontend/*` package.
|
|
116
|
+
Older releases receive no backports. A long-term support policy will replace this section once the
|
|
117
|
+
packages settle on a stable release cadence.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isArray } = require('../immutable-api-utils/built-in-copy/array/index.cjs.js');
|
|
4
|
+
const { createError } = require('../immutable-api-utils/built-in-copy/error/index.cjs.js');
|
|
5
|
+
const { freeze, keys: keys$1 } = require('../immutable-api-utils/built-in-copy/object/index.cjs.js');
|
|
6
|
+
const { createMap } = require('../immutable-api-utils/built-in-copy/map/index.cjs.js');
|
|
7
|
+
const { createDate } = require('../immutable-api-utils/built-in-copy/date/index.cjs.js');
|
|
8
|
+
const { random, round } = require('../immutable-api-utils/built-in-copy/math/index.cjs.js');
|
|
9
|
+
|
|
10
|
+
const isMarker = (text) => {
|
|
11
|
+
if (typeof text !== 'string' || !text.startsWith('__$'))
|
|
12
|
+
return false;
|
|
13
|
+
return /^__\$[0-9]+$/.test(text);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const registeredClasses = [];
|
|
17
|
+
const registeredIterableClasses = [
|
|
18
|
+
{
|
|
19
|
+
classRef: Array,
|
|
20
|
+
instantiate: () => [],
|
|
21
|
+
getKeys: (target) => {
|
|
22
|
+
const keysArray = keys$1(target);
|
|
23
|
+
if (getConfig().detectCircularReferences) {
|
|
24
|
+
return keysArray.filter((key) => !isMarker(key));
|
|
25
|
+
}
|
|
26
|
+
return keysArray;
|
|
27
|
+
},
|
|
28
|
+
read: (target, key) => target[key],
|
|
29
|
+
write: (target, value, key) => (target[key] = value),
|
|
30
|
+
remove: (target, value) => target.splice(value, 1),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
classRef: Object,
|
|
34
|
+
instantiate: () => ({}),
|
|
35
|
+
getKeys: (target) => {
|
|
36
|
+
const keysArray = keys$1(target);
|
|
37
|
+
if (getConfig().detectCircularReferences) {
|
|
38
|
+
return keysArray.filter((key) => !isMarker(key));
|
|
39
|
+
}
|
|
40
|
+
return keysArray;
|
|
41
|
+
},
|
|
42
|
+
read: (target, key) => target[key],
|
|
43
|
+
write: (target, value, key) => (target[key] = value),
|
|
44
|
+
remove: (target, value) => delete target[value],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
let samePositionOfOwnProperties = false;
|
|
48
|
+
let detectCircularReferences = false;
|
|
49
|
+
const getConfig = () => ({
|
|
50
|
+
samePositionOfOwnProperties,
|
|
51
|
+
detectCircularReferences,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const getKeysFromIterable = (target, dataType) => {
|
|
55
|
+
if (dataType === 'array')
|
|
56
|
+
dataType = Array.name;
|
|
57
|
+
if (dataType === 'object')
|
|
58
|
+
dataType = Object.name;
|
|
59
|
+
const iterableClass = registeredIterableClasses.find(({ classRef }) => dataType === classRef.name);
|
|
60
|
+
if (iterableClass === undefined)
|
|
61
|
+
return [];
|
|
62
|
+
return iterableClass.getKeys(target);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const getType = (target) => {
|
|
66
|
+
if (target === null)
|
|
67
|
+
return 'null';
|
|
68
|
+
const nativeDataType = typeof target;
|
|
69
|
+
if (nativeDataType === 'object') {
|
|
70
|
+
if (isArray(target))
|
|
71
|
+
return 'array';
|
|
72
|
+
for (const registeredClass of registeredClasses) {
|
|
73
|
+
if (target instanceof registeredClass)
|
|
74
|
+
return registeredClass.name;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return nativeDataType;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const getIterableTypes = () => registeredIterableClasses.map(({ classRef }) => {
|
|
81
|
+
const name = classRef.name;
|
|
82
|
+
if (name === Object.name)
|
|
83
|
+
return 'object';
|
|
84
|
+
if (name === Array.name)
|
|
85
|
+
return 'array';
|
|
86
|
+
return name;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const isIterableType = (dataType) => getIterableTypes().includes(dataType);
|
|
90
|
+
|
|
91
|
+
const isIterable = (target) => isIterableType(getType(target));
|
|
92
|
+
|
|
93
|
+
const marker = () => {
|
|
94
|
+
const randomValue = round(random() * 10000000000000);
|
|
95
|
+
const sequential = createDate().getTime();
|
|
96
|
+
const unique = `${randomValue}${sequential}`;
|
|
97
|
+
const prefix = `__$`;
|
|
98
|
+
return `${prefix}${unique}`;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const referenceStack = () => {
|
|
102
|
+
const records = createMap();
|
|
103
|
+
const flag = marker();
|
|
104
|
+
const exists = (ref) => (isIterable(ref) ? flag in ref && records.has(ref[flag]) : false);
|
|
105
|
+
const add = (ref) => {
|
|
106
|
+
if (!isIterable(ref) || exists(ref))
|
|
107
|
+
return;
|
|
108
|
+
ref[flag] = Symbol();
|
|
109
|
+
records.set(ref[flag], [flag, ref, records.size]);
|
|
110
|
+
};
|
|
111
|
+
const lastSeen = (ref) => {
|
|
112
|
+
if (!isIterable(ref))
|
|
113
|
+
return null;
|
|
114
|
+
const record = records.get(ref[flag]);
|
|
115
|
+
return record ? record[2] - records.size : null;
|
|
116
|
+
};
|
|
117
|
+
const clear = () => (records.forEach(([key, ref]) => {
|
|
118
|
+
delete ref[key];
|
|
119
|
+
}),
|
|
120
|
+
records.clear());
|
|
121
|
+
return {
|
|
122
|
+
add: (ref) => add(ref),
|
|
123
|
+
exists: (ref) => exists(ref),
|
|
124
|
+
lastSeen: (ref) => lastSeen(ref),
|
|
125
|
+
clear: () => clear(),
|
|
126
|
+
get size() {
|
|
127
|
+
return records.size;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const errorMessage = (thing, type) => `Expected ${thing} to be ${type}.`;
|
|
133
|
+
const nextIterationDetails = (path, key, value) => ({
|
|
134
|
+
nextPath: [...path, key],
|
|
135
|
+
nextValue: value[key],
|
|
136
|
+
});
|
|
137
|
+
const circularDependencyTraversal = (condition, callback, config, key, path, value, parent, state, stack, root = false) => {
|
|
138
|
+
if (stack.exists(value))
|
|
139
|
+
return state;
|
|
140
|
+
const ok = condition(config, key, value, path, parent);
|
|
141
|
+
if (config.exitEarly)
|
|
142
|
+
return state;
|
|
143
|
+
if (ok)
|
|
144
|
+
callback(key, value, path, state, parent);
|
|
145
|
+
stack.add(value);
|
|
146
|
+
const type = getType(value);
|
|
147
|
+
if (!isIterableType(type))
|
|
148
|
+
return state;
|
|
149
|
+
const keys = getKeysFromIterable(value, type);
|
|
150
|
+
keys.forEach((key) => {
|
|
151
|
+
const { nextPath, nextValue } = nextIterationDetails(path, key, value);
|
|
152
|
+
circularDependencyTraversal(condition, callback, config, key, nextPath, nextValue, value, state, stack);
|
|
153
|
+
});
|
|
154
|
+
if (root)
|
|
155
|
+
stack.clear();
|
|
156
|
+
return state;
|
|
157
|
+
};
|
|
158
|
+
const nonCircularDependencyTraversal = (condition, callback, config, key, path, value, parent, state) => {
|
|
159
|
+
const ok = condition(config, key, value, path, parent);
|
|
160
|
+
if (config.exitEarly)
|
|
161
|
+
return state;
|
|
162
|
+
if (ok)
|
|
163
|
+
callback(key, value, path, state, parent);
|
|
164
|
+
const type = getType(value);
|
|
165
|
+
if (!isIterableType(type))
|
|
166
|
+
return state;
|
|
167
|
+
const keys = getKeysFromIterable(value, type);
|
|
168
|
+
keys.forEach((key) => {
|
|
169
|
+
const { nextPath, nextValue } = nextIterationDetails(path, key, value);
|
|
170
|
+
nonCircularDependencyTraversal(condition, callback, config, key, nextPath, nextValue, value, state);
|
|
171
|
+
});
|
|
172
|
+
return state;
|
|
173
|
+
};
|
|
174
|
+
const traversal = (target, condition, callback, options, state) => {
|
|
175
|
+
if (typeof callback !== 'function')
|
|
176
|
+
throw createError(errorMessage('callback', 'a function'));
|
|
177
|
+
if (!(typeof options === 'object' && !isArray(options)))
|
|
178
|
+
throw createError(errorMessage('options', 'an object'));
|
|
179
|
+
if (!isArray(options.depth))
|
|
180
|
+
throw createError(errorMessage('options.depth', 'an array'));
|
|
181
|
+
const [startDepth, maxDepth] = options.depth;
|
|
182
|
+
if (startDepth !== void 0 && typeof startDepth !== 'number')
|
|
183
|
+
throw createError(errorMessage('options.depth.0', 'a number'));
|
|
184
|
+
if (maxDepth !== void 0) {
|
|
185
|
+
const maxDepthType = typeof maxDepth;
|
|
186
|
+
if (!['number', 'string'].includes(maxDepthType))
|
|
187
|
+
throw createError(errorMessage('options.depth.1', 'a number or a string'));
|
|
188
|
+
if (maxDepthType === 'string' && maxDepth !== '*')
|
|
189
|
+
throw createError("Only valid string value in options.depth.1 is '*'.");
|
|
190
|
+
}
|
|
191
|
+
const config = {
|
|
192
|
+
depth: freeze([options.depth[0] ?? 0, options.depth[1] ?? '*']),
|
|
193
|
+
exitEarly: false,
|
|
194
|
+
};
|
|
195
|
+
const initialArgs = [condition, callback, config, '', [], target, void 0, state];
|
|
196
|
+
if (getConfig().detectCircularReferences)
|
|
197
|
+
return circularDependencyTraversal(...initialArgs, referenceStack(), true);
|
|
198
|
+
return nonCircularDependencyTraversal(...initialArgs);
|
|
199
|
+
};
|
|
200
|
+
const createTraversal = (condition) => (target, callback, options, state) => traversal(target, condition, callback, options ?? { depth: [0, '*'] }, state ?? {});
|
|
201
|
+
const condition = (config, key, value, path) => !(path.length < config.depth[0] || config.depth[1] < path.length);
|
|
202
|
+
const traverseBetweenDepthRange = createTraversal(condition);
|
|
203
|
+
exports.createTraversal = createTraversal;
|
|
204
|
+
exports.getConfig = getConfig;
|
|
205
|
+
exports.getIterableTypes = getIterableTypes;
|
|
206
|
+
exports.getKeysFromIterable = getKeysFromIterable;
|
|
207
|
+
exports.getType = getType;
|
|
208
|
+
exports.isIterable = isIterable;
|
|
209
|
+
exports.isIterableType = isIterableType;
|
|
210
|
+
exports.isMarker = isMarker;
|
|
211
|
+
exports.marker = marker;
|
|
212
|
+
exports.referenceStack = referenceStack;
|
|
213
|
+
exports.registeredClasses = registeredClasses;
|
|
214
|
+
exports.registeredIterableClasses = registeredIterableClasses;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { isArray } from '../immutable-api-utils/built-in-copy/array/index.esm.js';
|
|
2
|
+
import { createError } from '../immutable-api-utils/built-in-copy/error/index.esm.js';
|
|
3
|
+
import { keys, freeze } from '../immutable-api-utils/built-in-copy/object/index.esm.js';
|
|
4
|
+
import { createMap } from '../immutable-api-utils/built-in-copy/map/index.esm.js';
|
|
5
|
+
import { createDate } from '../immutable-api-utils/built-in-copy/date/index.esm.js';
|
|
6
|
+
import { round, random } from '../immutable-api-utils/built-in-copy/math/index.esm.js';
|
|
7
|
+
|
|
8
|
+
const isMarker = (text) => {
|
|
9
|
+
if (typeof text !== 'string' || !text.startsWith('__$'))
|
|
10
|
+
return false;
|
|
11
|
+
return /^__\$[0-9]+$/.test(text);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const registeredClasses = [];
|
|
15
|
+
const registeredIterableClasses = [
|
|
16
|
+
{
|
|
17
|
+
classRef: Array,
|
|
18
|
+
instantiate: () => [],
|
|
19
|
+
getKeys: (target) => {
|
|
20
|
+
const keysArray = keys(target);
|
|
21
|
+
if (getConfig().detectCircularReferences) {
|
|
22
|
+
return keysArray.filter((key) => !isMarker(key));
|
|
23
|
+
}
|
|
24
|
+
return keysArray;
|
|
25
|
+
},
|
|
26
|
+
read: (target, key) => target[key],
|
|
27
|
+
write: (target, value, key) => (target[key] = value),
|
|
28
|
+
remove: (target, value) => target.splice(value, 1),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
classRef: Object,
|
|
32
|
+
instantiate: () => ({}),
|
|
33
|
+
getKeys: (target) => {
|
|
34
|
+
const keysArray = keys(target);
|
|
35
|
+
if (getConfig().detectCircularReferences) {
|
|
36
|
+
return keysArray.filter((key) => !isMarker(key));
|
|
37
|
+
}
|
|
38
|
+
return keysArray;
|
|
39
|
+
},
|
|
40
|
+
read: (target, key) => target[key],
|
|
41
|
+
write: (target, value, key) => (target[key] = value),
|
|
42
|
+
remove: (target, value) => delete target[value],
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
let samePositionOfOwnProperties = false;
|
|
46
|
+
let detectCircularReferences = false;
|
|
47
|
+
const getConfig = () => ({
|
|
48
|
+
samePositionOfOwnProperties,
|
|
49
|
+
detectCircularReferences,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const getKeysFromIterable = (target, dataType) => {
|
|
53
|
+
if (dataType === 'array')
|
|
54
|
+
dataType = Array.name;
|
|
55
|
+
if (dataType === 'object')
|
|
56
|
+
dataType = Object.name;
|
|
57
|
+
const iterableClass = registeredIterableClasses.find(({ classRef }) => dataType === classRef.name);
|
|
58
|
+
if (iterableClass === undefined)
|
|
59
|
+
return [];
|
|
60
|
+
return iterableClass.getKeys(target);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getType = (target) => {
|
|
64
|
+
if (target === null)
|
|
65
|
+
return 'null';
|
|
66
|
+
const nativeDataType = typeof target;
|
|
67
|
+
if (nativeDataType === 'object') {
|
|
68
|
+
if (isArray(target))
|
|
69
|
+
return 'array';
|
|
70
|
+
for (const registeredClass of registeredClasses) {
|
|
71
|
+
if (target instanceof registeredClass)
|
|
72
|
+
return registeredClass.name;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return nativeDataType;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getIterableTypes = () => registeredIterableClasses.map(({ classRef }) => {
|
|
79
|
+
const name = classRef.name;
|
|
80
|
+
if (name === Object.name)
|
|
81
|
+
return 'object';
|
|
82
|
+
if (name === Array.name)
|
|
83
|
+
return 'array';
|
|
84
|
+
return name;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const isIterableType = (dataType) => getIterableTypes().includes(dataType);
|
|
88
|
+
|
|
89
|
+
const isIterable = (target) => isIterableType(getType(target));
|
|
90
|
+
|
|
91
|
+
const marker = () => {
|
|
92
|
+
const randomValue = round(random() * 10000000000000);
|
|
93
|
+
const sequential = createDate().getTime();
|
|
94
|
+
const unique = `${randomValue}${sequential}`;
|
|
95
|
+
const prefix = `__$`;
|
|
96
|
+
return `${prefix}${unique}`;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const referenceStack = () => {
|
|
100
|
+
const records = createMap();
|
|
101
|
+
const flag = marker();
|
|
102
|
+
const exists = (ref) => (isIterable(ref) ? flag in ref && records.has(ref[flag]) : false);
|
|
103
|
+
const add = (ref) => {
|
|
104
|
+
if (!isIterable(ref) || exists(ref))
|
|
105
|
+
return;
|
|
106
|
+
ref[flag] = Symbol();
|
|
107
|
+
records.set(ref[flag], [flag, ref, records.size]);
|
|
108
|
+
};
|
|
109
|
+
const lastSeen = (ref) => {
|
|
110
|
+
if (!isIterable(ref))
|
|
111
|
+
return null;
|
|
112
|
+
const record = records.get(ref[flag]);
|
|
113
|
+
return record ? record[2] - records.size : null;
|
|
114
|
+
};
|
|
115
|
+
const clear = () => (records.forEach(([key, ref]) => {
|
|
116
|
+
delete ref[key];
|
|
117
|
+
}),
|
|
118
|
+
records.clear());
|
|
119
|
+
return {
|
|
120
|
+
add: (ref) => add(ref),
|
|
121
|
+
exists: (ref) => exists(ref),
|
|
122
|
+
lastSeen: (ref) => lastSeen(ref),
|
|
123
|
+
clear: () => clear(),
|
|
124
|
+
get size() {
|
|
125
|
+
return records.size;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const errorMessage = (thing, type) => `Expected ${thing} to be ${type}.`;
|
|
131
|
+
const nextIterationDetails = (path, key, value) => ({
|
|
132
|
+
nextPath: [...path, key],
|
|
133
|
+
nextValue: value[key],
|
|
134
|
+
});
|
|
135
|
+
const circularDependencyTraversal = (condition, callback, config, key, path, value, parent, state, stack, root = false) => {
|
|
136
|
+
if (stack.exists(value))
|
|
137
|
+
return state;
|
|
138
|
+
const ok = condition(config, key, value, path, parent);
|
|
139
|
+
if (config.exitEarly)
|
|
140
|
+
return state;
|
|
141
|
+
if (ok)
|
|
142
|
+
callback(key, value, path, state, parent);
|
|
143
|
+
stack.add(value);
|
|
144
|
+
const type = getType(value);
|
|
145
|
+
if (!isIterableType(type))
|
|
146
|
+
return state;
|
|
147
|
+
const keys = getKeysFromIterable(value, type);
|
|
148
|
+
keys.forEach((key) => {
|
|
149
|
+
const { nextPath, nextValue } = nextIterationDetails(path, key, value);
|
|
150
|
+
circularDependencyTraversal(condition, callback, config, key, nextPath, nextValue, value, state, stack);
|
|
151
|
+
});
|
|
152
|
+
if (root)
|
|
153
|
+
stack.clear();
|
|
154
|
+
return state;
|
|
155
|
+
};
|
|
156
|
+
const nonCircularDependencyTraversal = (condition, callback, config, key, path, value, parent, state) => {
|
|
157
|
+
const ok = condition(config, key, value, path, parent);
|
|
158
|
+
if (config.exitEarly)
|
|
159
|
+
return state;
|
|
160
|
+
if (ok)
|
|
161
|
+
callback(key, value, path, state, parent);
|
|
162
|
+
const type = getType(value);
|
|
163
|
+
if (!isIterableType(type))
|
|
164
|
+
return state;
|
|
165
|
+
const keys = getKeysFromIterable(value, type);
|
|
166
|
+
keys.forEach((key) => {
|
|
167
|
+
const { nextPath, nextValue } = nextIterationDetails(path, key, value);
|
|
168
|
+
nonCircularDependencyTraversal(condition, callback, config, key, nextPath, nextValue, value, state);
|
|
169
|
+
});
|
|
170
|
+
return state;
|
|
171
|
+
};
|
|
172
|
+
const traversal = (target, condition, callback, options, state) => {
|
|
173
|
+
if (typeof callback !== 'function')
|
|
174
|
+
throw createError(errorMessage('callback', 'a function'));
|
|
175
|
+
if (!(typeof options === 'object' && !isArray(options)))
|
|
176
|
+
throw createError(errorMessage('options', 'an object'));
|
|
177
|
+
if (!isArray(options.depth))
|
|
178
|
+
throw createError(errorMessage('options.depth', 'an array'));
|
|
179
|
+
const [startDepth, maxDepth] = options.depth;
|
|
180
|
+
if (startDepth !== void 0 && typeof startDepth !== 'number')
|
|
181
|
+
throw createError(errorMessage('options.depth.0', 'a number'));
|
|
182
|
+
if (maxDepth !== void 0) {
|
|
183
|
+
const maxDepthType = typeof maxDepth;
|
|
184
|
+
if (!['number', 'string'].includes(maxDepthType))
|
|
185
|
+
throw createError(errorMessage('options.depth.1', 'a number or a string'));
|
|
186
|
+
if (maxDepthType === 'string' && maxDepth !== '*')
|
|
187
|
+
throw createError("Only valid string value in options.depth.1 is '*'.");
|
|
188
|
+
}
|
|
189
|
+
const config = {
|
|
190
|
+
depth: freeze([options.depth[0] ?? 0, options.depth[1] ?? '*']),
|
|
191
|
+
exitEarly: false,
|
|
192
|
+
};
|
|
193
|
+
const initialArgs = [condition, callback, config, '', [], target, void 0, state];
|
|
194
|
+
if (getConfig().detectCircularReferences)
|
|
195
|
+
return circularDependencyTraversal(...initialArgs, referenceStack(), true);
|
|
196
|
+
return nonCircularDependencyTraversal(...initialArgs);
|
|
197
|
+
};
|
|
198
|
+
const createTraversal = (condition) => (target, callback, options, state) => traversal(target, condition, callback, options ?? { depth: [0, '*'] }, state ?? {});
|
|
199
|
+
const condition = (config, key, value, path) => !(path.length < config.depth[0] || config.depth[1] < path.length);
|
|
200
|
+
const traverseBetweenDepthRange = createTraversal(condition);
|
|
201
|
+
|
|
202
|
+
export { createTraversal, getConfig, getIterableTypes, getKeysFromIterable, getType, isIterable, isIterableType, isMarker, marker, referenceStack, registeredClasses, registeredIterableClasses };
|