@kiberon-labs/behave-graph-suspendable 1.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 +13 -0
- package/package.json +41 -0
- package/src/engine.ts +237 -0
- package/src/fiber.ts +162 -0
- package/src/index.ts +1 -0
- package/src/types.ts +67 -0
- package/tests/asyncNodeSuspension.test.ts +124 -0
- package/tests/coreNodeSuspension.test.ts +143 -0
- package/tests/fixtures/testNodes.ts +222 -0
- package/tests/loopSuspension.test.ts +137 -0
- package/tests/nestedLoopSuspension.test.ts +125 -0
- package/tests/nodes/suspender.ts +31 -0
- package/tests/testUtils.ts +37 -0
- package/tests/tsconfig.json +15 -0
- package/tests/variableSerialization.test.ts +96 -0
- package/tsconfig.json +55 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { GraphJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { makeTestEngine } from './testUtils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* onStart -> variable/set writes 1000 into the `counter` variable
|
|
7
|
+
* (initial value -1).
|
|
8
|
+
*/
|
|
9
|
+
const variableGraph: GraphJSON = {
|
|
10
|
+
variables: [
|
|
11
|
+
{
|
|
12
|
+
valueTypeName: 'float',
|
|
13
|
+
name: 'counter',
|
|
14
|
+
id: 0,
|
|
15
|
+
initialValue: -1
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
customEvents: [],
|
|
19
|
+
nodes: [
|
|
20
|
+
{
|
|
21
|
+
type: 'lifecycle/onStart',
|
|
22
|
+
id: '0',
|
|
23
|
+
flows: { flow: { nodeId: '1', socket: 'flow' } }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'variable/set',
|
|
27
|
+
id: '1',
|
|
28
|
+
configuration: { variableId: 0 },
|
|
29
|
+
parameters: { value: { value: 1000 } }
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
} as unknown as GraphJSON;
|
|
33
|
+
|
|
34
|
+
describe('variable serialization across suspension', () => {
|
|
35
|
+
it('serializes the updated variable value, not the initial value', async () => {
|
|
36
|
+
const { engine, graph, start } = makeTestEngine(variableGraph);
|
|
37
|
+
|
|
38
|
+
start();
|
|
39
|
+
await engine.executeAllSync();
|
|
40
|
+
expect(graph.variables['0']!.get()).toBe(1000);
|
|
41
|
+
|
|
42
|
+
const suspension = engine.suspend();
|
|
43
|
+
|
|
44
|
+
expect(suspension.variables['0']).toEqual({
|
|
45
|
+
type: 'float',
|
|
46
|
+
value: 1000
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('serializes the latest value when a variable is updated multiple times', async () => {
|
|
51
|
+
const { engine, graph, start } = makeTestEngine(variableGraph);
|
|
52
|
+
|
|
53
|
+
start();
|
|
54
|
+
await engine.executeAllSync();
|
|
55
|
+
|
|
56
|
+
// a later runtime update (e.g. another node firing) must win
|
|
57
|
+
graph.variables['0']!.set(7);
|
|
58
|
+
|
|
59
|
+
const suspension = engine.suspend();
|
|
60
|
+
expect(suspension.variables['0']).toEqual({ type: 'float', value: 7 });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('restores the suspended variable value into a fresh engine', async () => {
|
|
64
|
+
const first = makeTestEngine(variableGraph);
|
|
65
|
+
first.start();
|
|
66
|
+
await first.engine.executeAllSync();
|
|
67
|
+
|
|
68
|
+
const suspension = first.engine.suspend();
|
|
69
|
+
const persisted = JSON.parse(JSON.stringify(suspension));
|
|
70
|
+
|
|
71
|
+
// fresh graph starts back at the initial value...
|
|
72
|
+
const second = makeTestEngine(variableGraph);
|
|
73
|
+
expect(second.graph.variables['0']!.get()).toBe(-1);
|
|
74
|
+
|
|
75
|
+
// ...until the suspension is rehydrated
|
|
76
|
+
second.engine.unsuspend(persisted, undefined);
|
|
77
|
+
expect(second.graph.variables['0']!.get()).toBe(1000);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('throws when the suspension references a variable missing from the graph', async () => {
|
|
81
|
+
const first = makeTestEngine(variableGraph);
|
|
82
|
+
first.start();
|
|
83
|
+
await first.engine.executeAllSync();
|
|
84
|
+
const suspension = first.engine.suspend();
|
|
85
|
+
|
|
86
|
+
const graphWithoutVariables = {
|
|
87
|
+
...variableGraph,
|
|
88
|
+
variables: []
|
|
89
|
+
} as unknown as GraphJSON;
|
|
90
|
+
const second = makeTestEngine(graphWithoutVariables);
|
|
91
|
+
|
|
92
|
+
expect(() => second.engine.unsuspend(suspension, undefined)).toThrow(
|
|
93
|
+
/missing variable/
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"display": "Monorepo shared library",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
/* Base Options: */
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"target": "esnext",
|
|
9
|
+
"lib": [
|
|
10
|
+
"esnext",
|
|
11
|
+
"ES6"
|
|
12
|
+
],
|
|
13
|
+
"allowJs": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"verbatimModuleSyntax": true,
|
|
18
|
+
/* Strictness */
|
|
19
|
+
"strict": true,
|
|
20
|
+
"noUncheckedIndexedAccess": true,
|
|
21
|
+
"noImplicitOverride": true,
|
|
22
|
+
"erasableSyntaxOnly": true,
|
|
23
|
+
/* Opinion */
|
|
24
|
+
"incremental": true,
|
|
25
|
+
"tsBuildInfoFile": "./tsconfig.tsbuildinfo",
|
|
26
|
+
"module": "preserve",
|
|
27
|
+
"outDir": "./dist",
|
|
28
|
+
"baseUrl": ".",
|
|
29
|
+
"rootDir": "./src",
|
|
30
|
+
"paths": {
|
|
31
|
+
"~/*": [
|
|
32
|
+
"./src/*"
|
|
33
|
+
],
|
|
34
|
+
"@/*": [
|
|
35
|
+
"./src/*"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
/* Required for project references, which provide go-to-definition in your
|
|
39
|
+
IDE without first having to build the module, which is essential during development. */
|
|
40
|
+
"composite": true,
|
|
41
|
+
/* Assuming your bundler will output everything, but we can not have noEmit
|
|
42
|
+
enabled because it is not compatible with composite / project references. Also declaration might be required for references to work fully. Not sure yet... */
|
|
43
|
+
"declaration": true,
|
|
44
|
+
"declarationMap": true,
|
|
45
|
+
},
|
|
46
|
+
"include": [
|
|
47
|
+
"./src",
|
|
48
|
+
"./src/**/*.json"
|
|
49
|
+
],
|
|
50
|
+
"references": [
|
|
51
|
+
{
|
|
52
|
+
"path": "../core"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
}
|
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'tsdown';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['./src/index.ts'],
|
|
5
|
+
outDir: 'dist',
|
|
6
|
+
target: 'es2022',
|
|
7
|
+
sourcemap: true,
|
|
8
|
+
format: ['esm'],
|
|
9
|
+
dts: true,
|
|
10
|
+
logLevel: 'warn',
|
|
11
|
+
unbundle: true,
|
|
12
|
+
platform: 'neutral'
|
|
13
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { configDefaults, defineConfig } from 'vitest/config';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
exclude: [...configDefaults.exclude],
|
|
7
|
+
watch: false
|
|
8
|
+
},
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
'~': path.resolve(__dirname, './src'),
|
|
12
|
+
'@': path.resolve(__dirname, './src')
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
});
|