@layerzerolabs/dfs 0.2.68 → 0.2.70
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 +7 -4
- package/.turbo/turbo-build.log +0 -19
- package/.turbo/turbo-lint.log +0 -8
- package/.turbo/turbo-test.log +0 -19
- package/src/index.ts +0 -162
- package/test/dfs.test.ts +0 -330
- package/tsconfig.json +0 -20
- package/tsup.config.ts +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@layerzerolabs/dfs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.70",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
"main": "./dist/index.cjs",
|
|
14
14
|
"module": "./dist/index.js",
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/**/*"
|
|
18
|
+
],
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@layerzerolabs/dependency-graph": "0.2.
|
|
20
|
+
"@layerzerolabs/dependency-graph": "0.2.70"
|
|
18
21
|
},
|
|
19
22
|
"devDependencies": {
|
|
20
23
|
"tsup": "^8.4.0",
|
|
21
24
|
"vitest": "^3.2.3",
|
|
22
|
-
"@layerzerolabs/
|
|
23
|
-
"@layerzerolabs/
|
|
25
|
+
"@layerzerolabs/typescript-configuration": "0.2.70",
|
|
26
|
+
"@layerzerolabs/tsup-configuration": "0.2.70"
|
|
24
27
|
},
|
|
25
28
|
"publishConfig": {
|
|
26
29
|
"access": "public",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
WARN Issue while reading "/home/runner/work/monorepo-internal/monorepo-internal/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
|
2
|
-
|
|
3
|
-
> @layerzerolabs/dfs@0.0.2 build /home/runner/work/monorepo-internal/monorepo-internal/packages/framework/dfs
|
|
4
|
-
> tsup
|
|
5
|
-
|
|
6
|
-
[34mCLI[39m Building entry: src/index.ts
|
|
7
|
-
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
8
|
-
[34mCLI[39m tsup v8.5.1
|
|
9
|
-
[34mCLI[39m Using tsup config: /home/runner/work/monorepo-internal/monorepo-internal/packages/framework/dfs/tsup.config.ts
|
|
10
|
-
[34mCLI[39m Target: ES2023
|
|
11
|
-
[34mCLI[39m Cleaning output folder
|
|
12
|
-
[34mCJS[39m Build start
|
|
13
|
-
[34mESM[39m Build start
|
|
14
|
-
[32mCJS[39m [1mdist/index.cjs [22m[32m3.43 KB[39m
|
|
15
|
-
[32mCJS[39m [1mdist/index.cjs.map [22m[32m11.79 KB[39m
|
|
16
|
-
[32mCJS[39m ⚡️ Build success in 103ms
|
|
17
|
-
[32mESM[39m [1mdist/index.js [22m[32m3.42 KB[39m
|
|
18
|
-
[32mESM[39m [1mdist/index.js.map [22m[32m11.78 KB[39m
|
|
19
|
-
[32mESM[39m ⚡️ Build success in 104ms
|
package/.turbo/turbo-lint.log
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @layerzerolabs/dfs@0.0.2 lint /home/runner/work/monorepo-internal/monorepo-internal/packages/framework/dfs
|
|
3
|
-
> eslint . --max-warnings 0 || (eslint . --fix --max-warnings 0 && false)
|
|
4
|
-
|
|
5
|
-
(node:66152) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/monorepo-internal/monorepo-internal/eslint.config.js?mtime=1775777605148 is not specified and it doesn't parse as CommonJS.
|
|
6
|
-
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
|
|
7
|
-
To eliminate this warning, add "type": "module" to /home/runner/work/monorepo-internal/monorepo-internal/package.json.
|
|
8
|
-
(Use `node --trace-warnings ...` to show where the warning was created)
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @layerzerolabs/dfs@0.0.2 test /home/runner/work/monorepo-internal/monorepo-internal/packages/framework/dfs
|
|
3
|
-
> vitest --run --pass-with-no-tests --typecheck
|
|
4
|
-
|
|
5
|
-
[33mTesting types with tsc and vue-tsc is an experimental feature.
|
|
6
|
-
Breaking changes might not follow SemVer, please pin Vitest's version when using it.[39m
|
|
7
|
-
|
|
8
|
-
[1m[46m RUN [49m[22m [36mv3.2.4 [39m[90m/home/runner/work/monorepo-internal/monorepo-internal/packages/framework/dfs[39m
|
|
9
|
-
|
|
10
|
-
[32m✓[39m test/dfs.test.ts [2m([22m[2m7 tests[22m[2m)[22m[33m 675[2mms[22m[39m
|
|
11
|
-
[33m[2m✓[22m[39m DI Depth-first-search[2m > [22mThe handlers for each of a node's dependencies should be completed before that node [33m 304[2mms[22m[39m
|
|
12
|
-
[33m[2m✓[22m[39m DI Ancestry - randomized DAG property[2m > [22mrandom DAGs: ancestry includes all ancestors and distances are non-decreasing [33m 350[2mms[22m[39m
|
|
13
|
-
|
|
14
|
-
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
15
|
-
[2m Tests [22m [1m[32m7 passed[39m[22m[90m (7)[39m
|
|
16
|
-
[2mType Errors [22m [2mno errors[22m
|
|
17
|
-
[2m Start at [22m 23:35:39
|
|
18
|
-
[2m Duration [22m 1.12s[2m (transform 98ms, setup 0ms, collect 88ms, tests 675ms, environment 0ms, prepare 107ms)[22m
|
|
19
|
-
|
package/src/index.ts
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import type { DependencyNode } from '@layerzerolabs/dependency-graph';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* <!-- anchor:Registrar -->
|
|
5
|
-
* A registrar is a simple interface for an object that provides the ability to traverse the dependency graph.
|
|
6
|
-
* It is implicit in this definition that the registrar should also *register* values adhering to the schemata
|
|
7
|
-
* of the graph.
|
|
8
|
-
*/
|
|
9
|
-
export interface Registrar<ReturnType> {
|
|
10
|
-
traverseDependencies: (rootNode: DependencyNode) => Promise<ReturnType>;
|
|
11
|
-
}
|
|
12
|
-
export type NodeHandlerFunction = (
|
|
13
|
-
node: DependencyNode,
|
|
14
|
-
ancestry: DependencyNode[],
|
|
15
|
-
) => Promise<{ key: string; value: any }>;
|
|
16
|
-
export type NodePreHandlerFunction = (node: DependencyNode) => DependencyNode;
|
|
17
|
-
|
|
18
|
-
// Map of ancestor node -> minimal hop distance
|
|
19
|
-
type AncestorDistanceByNode = Map<DependencyNode, number>;
|
|
20
|
-
// Index from node name -> its ancestor distance map
|
|
21
|
-
type AncestryDistanceIndex = Map<string, AncestorDistanceByNode>;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* In-place merge of minimal ancestry distances.
|
|
25
|
-
*
|
|
26
|
-
* childDistances holds minimal distances from the child to each ancestor (keyed by DependencyNode).
|
|
27
|
-
* For every entry in parentDistances, we update the child's map with (parentDist + 1),
|
|
28
|
-
* keeping the minimal value if the ancestor already exists.
|
|
29
|
-
*/
|
|
30
|
-
const mergeAncestorDistances = (
|
|
31
|
-
parentDistances: AncestorDistanceByNode,
|
|
32
|
-
childDistances: AncestorDistanceByNode,
|
|
33
|
-
) => {
|
|
34
|
-
for (const [ancestor, parentDist] of parentDistances) {
|
|
35
|
-
const candidate = parentDist + 1;
|
|
36
|
-
const current = childDistances.get(ancestor) || Infinity;
|
|
37
|
-
childDistances.set(ancestor, Math.min(current, candidate));
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Builds a minimal ancestry distance index for all nodes reachable from the root.
|
|
43
|
-
*
|
|
44
|
-
* Returns a Map: node.name -> Map<DependencyNode, distance> where distance is the minimal hop count
|
|
45
|
-
* from the node to that ancestor. We perform a Kahn-style BFS over the DAG, and for each edge
|
|
46
|
-
* curNode -> dep we merge curNode's minimal distances into the dependency with +1 hop and take the minimum.
|
|
47
|
-
*/
|
|
48
|
-
const buildAncestryDistanceIndex = (
|
|
49
|
-
node: DependencyNode,
|
|
50
|
-
prehandler: NodePreHandlerFunction,
|
|
51
|
-
): AncestryDistanceIndex => {
|
|
52
|
-
const ancestryDistanceIndex: AncestryDistanceIndex = new Map();
|
|
53
|
-
const inDegreeByNodeName = new Map<string, number>();
|
|
54
|
-
|
|
55
|
-
// If A depends on B and C, B depends on C, we initialize with:
|
|
56
|
-
// ancestryDistanceIndex: { A: Map(), B: Map(), C: Map() }
|
|
57
|
-
// inDegreeByNodeName: { B: 1, C: 2 }
|
|
58
|
-
const initializeMaps = (cur: DependencyNode) => {
|
|
59
|
-
ancestryDistanceIndex.set(cur.name, new Map());
|
|
60
|
-
for (const dep of Object.values(cur.dependencies)) {
|
|
61
|
-
const handledDep = prehandler(dep);
|
|
62
|
-
const inDegree = inDegreeByNodeName.get(handledDep.name) || 0;
|
|
63
|
-
inDegreeByNodeName.set(handledDep.name, inDegree + 1);
|
|
64
|
-
if (!ancestryDistanceIndex.has(handledDep.name)) initializeMaps(handledDep);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const handled = prehandler(node);
|
|
69
|
-
|
|
70
|
-
initializeMaps(handled);
|
|
71
|
-
|
|
72
|
-
// Kahn-style topological BFS accumulating minimal distance ancestors
|
|
73
|
-
let queue = [handled];
|
|
74
|
-
while (queue.length > 0) {
|
|
75
|
-
const curNode = queue.shift()!;
|
|
76
|
-
// Include self with distance 0, then extend with the already known minimal distances
|
|
77
|
-
const currentMinimalDistances: AncestorDistanceByNode = new Map([
|
|
78
|
-
[curNode, 0],
|
|
79
|
-
...ancestryDistanceIndex.get(curNode.name)!,
|
|
80
|
-
]);
|
|
81
|
-
// Add the new processable dependencies to the queue, update their state.
|
|
82
|
-
for (const dep of Object.values(curNode.dependencies)) {
|
|
83
|
-
const handledDep = prehandler(dep);
|
|
84
|
-
const inDegree = inDegreeByNodeName.get(handledDep.name)!;
|
|
85
|
-
// We are the last edge missing in the graph for this dependency -> we can process it after us.
|
|
86
|
-
if (inDegree === 1) {
|
|
87
|
-
queue.push(handledDep);
|
|
88
|
-
}
|
|
89
|
-
// Reduce the in-degree of the dependency -> it basically means that this edge got removed from the graph.
|
|
90
|
-
inDegreeByNodeName.set(handledDep.name, inDegree - 1);
|
|
91
|
-
|
|
92
|
-
// Merge curNode's minimal distances (+1 hop) into the dependency's minimal distances
|
|
93
|
-
const childDistances = ancestryDistanceIndex.get(handledDep.name)!;
|
|
94
|
-
mergeAncestorDistances(currentMinimalDistances, childDistances);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
for (const [nodeName, inDegree] of inDegreeByNodeName) {
|
|
99
|
-
if (inDegree !== 0) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
`node ${nodeName} has in-degree ${inDegree}, this indicates a cycle in the graph containing the node`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return ancestryDistanceIndex;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Performs a depth-first-search on a tree of dependency nodes, and returns a function
|
|
111
|
-
* that will call the handler for each node in the tree, ordered s.t. the handler of N
|
|
112
|
-
* will be called only after the handlers of dependencies(N) have been called.
|
|
113
|
-
* The node's ancestors are sorted by non-decreasing minimal distance.
|
|
114
|
-
* The resolver function will only call the handler once for each unique definition node.
|
|
115
|
-
* The resolver function returns an object whose keys are the keys defined
|
|
116
|
-
* by each of the handlers, and whose values are objects whose keys are the names
|
|
117
|
-
* of the nodes resolved and whose values are the values defined by the handlers.
|
|
118
|
-
* @param node the root node of the tree
|
|
119
|
-
* @param handler a function that accepts a node and registers it
|
|
120
|
-
* @param prehandler a function that accepts a node and returns a node. Will be used to pre-process the graph
|
|
121
|
-
* @returns a resolver function
|
|
122
|
-
*/
|
|
123
|
-
export const dfs = <ReturnTypes>(
|
|
124
|
-
node: DependencyNode,
|
|
125
|
-
handler: NodeHandlerFunction,
|
|
126
|
-
prehandler: NodePreHandlerFunction = (node) => node,
|
|
127
|
-
_returns: ReturnTypes = {} as any,
|
|
128
|
-
): (() => Promise<ReturnTypes>) => {
|
|
129
|
-
const ancestryDistanceIndex = buildAncestryDistanceIndex(node, prehandler);
|
|
130
|
-
|
|
131
|
-
// Maintains Map<node.name, Promise<void>> -> the promise to resolve the node, for all nodes.
|
|
132
|
-
const nodeResolverPromises = new Map<string, Promise<void>>();
|
|
133
|
-
const resolveNode = async (node: DependencyNode) => {
|
|
134
|
-
const prehandledNode = prehandler(node);
|
|
135
|
-
// first wait for all its children
|
|
136
|
-
const childrenPromises = [];
|
|
137
|
-
for (const dependencyValue of Object.values(prehandledNode.dependencies)) {
|
|
138
|
-
// Grab the cached promise or create it, we want to have only 1 handler promise for each node.
|
|
139
|
-
if (!nodeResolverPromises.has(dependencyValue.name)) {
|
|
140
|
-
nodeResolverPromises.set(dependencyValue.name, resolveNode(dependencyValue));
|
|
141
|
-
}
|
|
142
|
-
childrenPromises.push(nodeResolverPromises.get(dependencyValue.name)!);
|
|
143
|
-
}
|
|
144
|
-
await Promise.all(childrenPromises);
|
|
145
|
-
|
|
146
|
-
// Then, you can process yourself. Build ancestry sorted by non-decreasing minimal distance.
|
|
147
|
-
const minimalDistances = ancestryDistanceIndex.get(prehandledNode.name)!;
|
|
148
|
-
const sortedAncestors = Array.from(minimalDistances.entries()).sort((a, b) => a[1] - b[1]);
|
|
149
|
-
const regRes = await handler(
|
|
150
|
-
prehandledNode,
|
|
151
|
-
sortedAncestors.map(([node, _]) => node),
|
|
152
|
-
);
|
|
153
|
-
if (regRes) {
|
|
154
|
-
((_returns as any)[regRes.key] ??= {})[prehandledNode.name] = regRes.value;
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
return async () => {
|
|
159
|
-
await resolveNode(node);
|
|
160
|
-
return _returns;
|
|
161
|
-
};
|
|
162
|
-
};
|
package/test/dfs.test.ts
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { Dependencies, DependencyNode } from '@layerzerolabs/dependency-graph';
|
|
4
|
-
|
|
5
|
-
import { dfs } from '../src';
|
|
6
|
-
|
|
7
|
-
const SimpleClassA = class<
|
|
8
|
-
Name extends string,
|
|
9
|
-
_Dependencies extends Dependencies,
|
|
10
|
-
> extends DependencyNode<Name, _Dependencies> {};
|
|
11
|
-
|
|
12
|
-
const mySimpleClassA = new SimpleClassA({ name: 'MySimpleClassA' });
|
|
13
|
-
|
|
14
|
-
const SimpleClassB = class<
|
|
15
|
-
Name extends string,
|
|
16
|
-
_Dependencies extends Dependencies,
|
|
17
|
-
> extends DependencyNode<Name, _Dependencies> {};
|
|
18
|
-
|
|
19
|
-
const mySimpleClassB = new SimpleClassB({
|
|
20
|
-
name: 'MySimpleClassB',
|
|
21
|
-
dependencies: { mySimpleClassA },
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const SimpleClassC = class<
|
|
25
|
-
Name extends string,
|
|
26
|
-
_Dependencies extends Dependencies,
|
|
27
|
-
> extends DependencyNode<Name, _Dependencies> {};
|
|
28
|
-
|
|
29
|
-
const mySimpleClassC = new SimpleClassC({
|
|
30
|
-
name: 'MySimpleClassC',
|
|
31
|
-
dependencies: { mySimpleClassB },
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const SimpleClassD = class<
|
|
35
|
-
Name extends string,
|
|
36
|
-
_Dependencies extends Dependencies,
|
|
37
|
-
> extends DependencyNode<Name, _Dependencies> {};
|
|
38
|
-
|
|
39
|
-
const mySimpleClassD = new SimpleClassD({
|
|
40
|
-
name: 'MySimpleClassD',
|
|
41
|
-
dependencies: { mySimpleClassC, mySimpleClassB, mySimpleClassA },
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('DI Depth-first-search', () => {
|
|
45
|
-
test('Initial DFS and resolution should happen separately and return the expected structure', async () => {
|
|
46
|
-
let wasCalled = false;
|
|
47
|
-
const resolve = dfs(mySimpleClassA, async () => {
|
|
48
|
-
wasCalled = true;
|
|
49
|
-
return { key: 'nodeKey', value: 'nodeValue' };
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
//when called, dfs should traverse the tree and collect all the nodes,
|
|
53
|
-
//returning a function that will call the handler on each node
|
|
54
|
-
expect(resolve).toBeTypeOf('function');
|
|
55
|
-
|
|
56
|
-
//it shouldn't call the handler until the resolve function is called
|
|
57
|
-
expect(wasCalled).toBe(false);
|
|
58
|
-
const res = await resolve();
|
|
59
|
-
//the resolve method should await all of the handlers
|
|
60
|
-
expect(wasCalled).toBe(true);
|
|
61
|
-
//the resolve method should return an object whose keys are the keys defined
|
|
62
|
-
//by each of the handlers, and whose values are objects whose keys are the names
|
|
63
|
-
//of the nodes resolved and whose values are the values defined by the handlers
|
|
64
|
-
expect(res).toStrictEqual({ nodeKey: { MySimpleClassA: 'nodeValue' } });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test(`The handlers for each of a node's dependencies should be completed before that node`, async () => {
|
|
68
|
-
let order: string[] = [];
|
|
69
|
-
|
|
70
|
-
const handler: Parameters<typeof dfs>[1] = async (node) => {
|
|
71
|
-
const handlerRet = { key: '_', value: '_' };
|
|
72
|
-
if (node.name === mySimpleClassA.name) {
|
|
73
|
-
order.push('A');
|
|
74
|
-
await new Promise((res) => setTimeout(res, 50));
|
|
75
|
-
return handlerRet;
|
|
76
|
-
} else if (node.name === mySimpleClassB.name) {
|
|
77
|
-
order.push('B');
|
|
78
|
-
await new Promise((res) => setTimeout(res, 100));
|
|
79
|
-
return handlerRet;
|
|
80
|
-
} else if (node.name === mySimpleClassC.name) {
|
|
81
|
-
order.push('C');
|
|
82
|
-
return handlerRet;
|
|
83
|
-
}
|
|
84
|
-
throw new Error(`Unexpected node ${JSON.stringify(node)}`);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
await dfs(mySimpleClassB, handler)();
|
|
88
|
-
expect(order).toStrictEqual(['A', 'B']);
|
|
89
|
-
|
|
90
|
-
order = [];
|
|
91
|
-
|
|
92
|
-
await dfs(mySimpleClassC, handler)();
|
|
93
|
-
expect(order).toStrictEqual(['A', 'B', 'C']);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('The handler should be called only once for each unique node', async () => {
|
|
97
|
-
let count = 0;
|
|
98
|
-
|
|
99
|
-
const handler: Parameters<typeof dfs>[1] = async (_node) => {
|
|
100
|
-
count++;
|
|
101
|
-
return { key: '_', value: '_' };
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
await dfs(mySimpleClassB, handler)();
|
|
105
|
-
expect(count).toBe(2);
|
|
106
|
-
|
|
107
|
-
count = 0;
|
|
108
|
-
|
|
109
|
-
await dfs(mySimpleClassD, handler)();
|
|
110
|
-
expect(count).toBe(4);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('DI Ancestry - inclusion and distance order', () => {
|
|
115
|
-
test('C -> B -> A', async () => {
|
|
116
|
-
const { ancestry } = await dfs<{ ancestry: Record<string, string[]> }>(
|
|
117
|
-
mySimpleClassC,
|
|
118
|
-
async (_node, ancestry) => {
|
|
119
|
-
return { key: 'ancestry', value: ancestry.map((n) => n.name) };
|
|
120
|
-
},
|
|
121
|
-
)();
|
|
122
|
-
|
|
123
|
-
expect(ancestry.MySimpleClassC).toStrictEqual([]);
|
|
124
|
-
expect(ancestry.MySimpleClassB).toStrictEqual(['MySimpleClassC']);
|
|
125
|
-
expect(ancestry.MySimpleClassA).toStrictEqual(['MySimpleClassB', 'MySimpleClassC']);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('D -> (C, B, A) with C -> B -> A', async () => {
|
|
129
|
-
const { ancestry } = await dfs<{ ancestry: Record<string, string[]> }>(
|
|
130
|
-
mySimpleClassD,
|
|
131
|
-
async (_node, ancestry) => {
|
|
132
|
-
return { key: 'ancestry', value: ancestry.map((n) => n.name) };
|
|
133
|
-
},
|
|
134
|
-
)();
|
|
135
|
-
|
|
136
|
-
// D is the root
|
|
137
|
-
expect(ancestry.MySimpleClassD).toStrictEqual([]);
|
|
138
|
-
|
|
139
|
-
// C has only D above it
|
|
140
|
-
expect(ancestry.MySimpleClassC).toStrictEqual(['MySimpleClassD']);
|
|
141
|
-
|
|
142
|
-
// B has both C and D at distance 1 (order between them is not asserted)
|
|
143
|
-
expect(new Set(ancestry.MySimpleClassB)).toStrictEqual(
|
|
144
|
-
new Set(['MySimpleClassC', 'MySimpleClassD']),
|
|
145
|
-
);
|
|
146
|
-
expect(ancestry.MySimpleClassB.length).toBe(2);
|
|
147
|
-
|
|
148
|
-
// A must include B and D (distance 1, order between them not asserted), then C (distance 2)
|
|
149
|
-
expect(new Set(ancestry.MySimpleClassA.slice(0, 2))).toStrictEqual(
|
|
150
|
-
new Set(['MySimpleClassB', 'MySimpleClassD']),
|
|
151
|
-
);
|
|
152
|
-
expect(ancestry.MySimpleClassA[2]).toBe('MySimpleClassC');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('ancestry matches expected distance layers', async () => {
|
|
156
|
-
const { ancestry } = await dfs<{ ancestry: Record<string, string[]> }>(
|
|
157
|
-
mySimpleClassD,
|
|
158
|
-
async (_node, ancestry) => {
|
|
159
|
-
return { key: 'ancestry', value: ancestry.map((n) => n.name) };
|
|
160
|
-
},
|
|
161
|
-
)();
|
|
162
|
-
|
|
163
|
-
const expectedLayers: Record<string, string[][]> = {
|
|
164
|
-
MySimpleClassD: [],
|
|
165
|
-
MySimpleClassC: [['MySimpleClassD']],
|
|
166
|
-
MySimpleClassB: [['MySimpleClassC', 'MySimpleClassD']],
|
|
167
|
-
MySimpleClassA: [['MySimpleClassB', 'MySimpleClassD'], ['MySimpleClassC']],
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
for (const [nodeName, layers] of Object.entries(expectedLayers)) {
|
|
171
|
-
const actual = ancestry[nodeName];
|
|
172
|
-
let idx = 0;
|
|
173
|
-
for (const layer of layers) {
|
|
174
|
-
const segment = actual.slice(idx, idx + layer.length);
|
|
175
|
-
expect(new Set(segment)).toStrictEqual(new Set(layer));
|
|
176
|
-
idx += layer.length;
|
|
177
|
-
}
|
|
178
|
-
expect(idx).toBe(actual.length);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('DI Ancestry - randomized DAG property', () => {
|
|
184
|
-
const GNode = class<
|
|
185
|
-
Name extends string,
|
|
186
|
-
_Dependencies extends Dependencies,
|
|
187
|
-
> extends DependencyNode<Name, _Dependencies> {};
|
|
188
|
-
|
|
189
|
-
const makeRng = (seed: number) => {
|
|
190
|
-
let state = seed >>> 0;
|
|
191
|
-
return () => {
|
|
192
|
-
state = (state * 1664525 + 1013904223) >>> 0; // LCG
|
|
193
|
-
return state / 2 ** 32;
|
|
194
|
-
};
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const buildRandomDag = (
|
|
198
|
-
numNodes: number,
|
|
199
|
-
edgeProbability: number,
|
|
200
|
-
seed: number,
|
|
201
|
-
): DependencyNode<any, any>[] => {
|
|
202
|
-
const rng = makeRng(seed);
|
|
203
|
-
const nodes: DependencyNode<any, any>[] = [];
|
|
204
|
-
for (let i = 0; i < numNodes; i++) {
|
|
205
|
-
const deps: Record<string, DependencyNode<any, any>> = {};
|
|
206
|
-
for (let j = 0; j < i; j++) {
|
|
207
|
-
if (rng() < edgeProbability) {
|
|
208
|
-
deps[`d${j}`] = nodes[j];
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (i > 0 && Object.keys(deps).length === 0) {
|
|
212
|
-
const j = Math.floor(rng() * i);
|
|
213
|
-
deps[`d${j}`] = nodes[j];
|
|
214
|
-
}
|
|
215
|
-
nodes.push(new GNode({ name: `N${i}`, dependencies: deps }));
|
|
216
|
-
}
|
|
217
|
-
return nodes;
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const collectReachable = (root: DependencyNode<any, any>) => {
|
|
221
|
-
const map = new Map<string, DependencyNode<any, any>>();
|
|
222
|
-
const queue: DependencyNode<any, any>[] = [root];
|
|
223
|
-
while (queue.length > 0) {
|
|
224
|
-
const cur = queue.shift()!;
|
|
225
|
-
if (map.has(cur.name)) continue;
|
|
226
|
-
map.set(cur.name, cur);
|
|
227
|
-
for (const dep of Object.values(cur.dependencies) as DependencyNode<any, any>[]) {
|
|
228
|
-
queue.push(dep);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return map;
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const buildDependentsIndex = (nodes: Map<string, DependencyNode<any, any>>) => {
|
|
235
|
-
const dependentsIndex = new Map<string, Set<string>>();
|
|
236
|
-
for (const node of nodes.values()) dependentsIndex.set(node.name, new Set());
|
|
237
|
-
for (const node of nodes.values()) {
|
|
238
|
-
for (const dep of Object.values(node.dependencies) as DependencyNode<any, any>[]) {
|
|
239
|
-
dependentsIndex.get(dep.name)!.add(node.name);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return dependentsIndex;
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const computeDependentDistances = (
|
|
246
|
-
start: string,
|
|
247
|
-
dependentsIndex: Map<string, Set<string>>,
|
|
248
|
-
) => {
|
|
249
|
-
const distances = new Map<string, number>();
|
|
250
|
-
const queue: Array<{ name: string; dist: number }> = [{ name: start, dist: 0 }];
|
|
251
|
-
distances.set(start, 0);
|
|
252
|
-
while (queue.length > 0) {
|
|
253
|
-
const { name, dist } = queue.shift()!;
|
|
254
|
-
for (const next of dependentsIndex.get(name) || [])
|
|
255
|
-
if (!distances.has(next)) {
|
|
256
|
-
distances.set(next, dist + 1);
|
|
257
|
-
queue.push({ name: next, dist: dist + 1 });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
distances.delete(start);
|
|
261
|
-
return distances;
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
test('random DAGs: ancestry includes all ancestors and distances are non-decreasing', async () => {
|
|
265
|
-
// increase/play with these values if you're making changes :D
|
|
266
|
-
const iterations = 2;
|
|
267
|
-
const numNodes = 200;
|
|
268
|
-
|
|
269
|
-
// Edge probability p: node i has E[in-degree(i)] ≈ p*i (edges only to earlier nodes).
|
|
270
|
-
// Average per node ≈ p*(n-1)/2. For n=500 and p=0.25, avg ≈ 62; the last node ≈ 125.
|
|
271
|
-
const edgeProbability = 0.25;
|
|
272
|
-
|
|
273
|
-
// Sampling probability: fraction of reachable nodes validated.
|
|
274
|
-
// E[sampled] ≈ reachableCount * sampleProbability (≈100 if ~500 reachable).
|
|
275
|
-
const sampleProbability = 0.2;
|
|
276
|
-
|
|
277
|
-
const seeds = Array.from({ length: iterations }, () =>
|
|
278
|
-
Math.floor(Math.random() * 0x7fffffff),
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
for (const seed of seeds) {
|
|
282
|
-
try {
|
|
283
|
-
const nodes = buildRandomDag(numNodes, edgeProbability, seed);
|
|
284
|
-
const root = nodes[nodes.length - 1];
|
|
285
|
-
const res = await dfs<{ ancestry: Record<string, string[]> }>(
|
|
286
|
-
root,
|
|
287
|
-
async (_node, ancestry) => {
|
|
288
|
-
return { key: 'ancestry', value: ancestry.map((n) => n.name) };
|
|
289
|
-
},
|
|
290
|
-
)();
|
|
291
|
-
const reachable = collectReachable(root);
|
|
292
|
-
const dependentsIndex = buildDependentsIndex(reachable);
|
|
293
|
-
|
|
294
|
-
// dfs should traverse exactly the reachable set
|
|
295
|
-
expect(new Set(Object.keys(res.ancestry))).toStrictEqual(new Set(reachable.keys()));
|
|
296
|
-
|
|
297
|
-
const sample: { nodeName: string; list: string[] }[] = [];
|
|
298
|
-
for (const [nodeName, list] of Object.entries(res.ancestry)) {
|
|
299
|
-
if (Math.random() < sampleProbability) {
|
|
300
|
-
sample.push({ nodeName, list });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
for (const { nodeName, list } of sample) {
|
|
305
|
-
// No self in ancestry
|
|
306
|
-
expect(list.includes(nodeName)).toBe(false);
|
|
307
|
-
// Inclusion: exactly the set of reachable dependents
|
|
308
|
-
const distMap = computeDependentDistances(nodeName, dependentsIndex);
|
|
309
|
-
expect(new Set(list)).toStrictEqual(new Set(distMap.keys()));
|
|
310
|
-
|
|
311
|
-
// No duplicates
|
|
312
|
-
expect(new Set(list).size).toBe(list.length);
|
|
313
|
-
|
|
314
|
-
// Distance non-decreasing
|
|
315
|
-
for (let i = 1; i < list.length; i++) {
|
|
316
|
-
const prev = distMap.get(list[i - 1])!;
|
|
317
|
-
const cur = distMap.get(list[i])!;
|
|
318
|
-
expect(prev <= cur).toBe(true);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
} catch (err: any) {
|
|
322
|
-
if (err && typeof err === 'object' && 'message' in err) {
|
|
323
|
-
err.message = `failed random dag test with seed ${seed}: ${err.message}`;
|
|
324
|
-
throw err;
|
|
325
|
-
}
|
|
326
|
-
throw new Error(`failed random dag test with seed ${seed}: ${String(err)}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "@layerzerolabs/typescript-configuration/tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "./src",
|
|
5
|
-
"outDir": "./dist",
|
|
6
|
-
"strictPropertyInitialization": false,
|
|
7
|
-
"noUnusedLocals": false,
|
|
8
|
-
"noUnusedParameters": false,
|
|
9
|
-
"jsx": "react-jsx"
|
|
10
|
-
},
|
|
11
|
-
"exclude": [
|
|
12
|
-
"node_modules",
|
|
13
|
-
"**/__mocks__/*",
|
|
14
|
-
"**/__tests__/*",
|
|
15
|
-
"**/*.spec.ts",
|
|
16
|
-
"**/*.test.ts",
|
|
17
|
-
"dist"
|
|
18
|
-
],
|
|
19
|
-
"include": ["src/**/*"]
|
|
20
|
-
}
|