@oomol-lab/open-flow 0.1.0-beta.76 → 0.1.0-beta.78
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 +3 -0
- package/dist/browser/{ContentIcon-Cm1hbVzW.js → ContentIcon-CMn0c-kl.js} +99 -6
- package/dist/browser/{FlowCanvas-NzKtla_c.js → FlowCanvas-BQGBcfoL.js} +1709 -613
- package/dist/browser/{createResourceDialog-BgdedQqu.js → createResourceDialog-DCGxfKFM.js} +63 -57
- package/dist/browser/{dialog-aSElFFw2.js → dialog-WzR4CRFH.js} +76 -77
- package/dist/browser/{exportSvg-Dcz4HTo_.js → exportSvg-CYwyYiuH.js} +2 -2
- package/dist/browser/{flowChanges-NZyS7HSp.js → flowChanges-Bz4dbDN_.js} +6875 -11091
- package/dist/browser/{flowWorkspace-Bl0SDehy.js → flowWorkspace-BI6L6a4r.js} +1415 -3037
- package/dist/browser/licenses.md +1 -1
- package/dist/browser/preview.css +619 -462
- package/dist/browser/preview.js +9016 -14956
- package/dist/browser/theme.css +4 -2
- package/dist/browser/{tooltip-3XMK9jkr.js → tooltip-BDcVTNpu.js} +65 -2
- package/dist/browser/{typeScriptSession-C9sBGOKZ.js → typeScriptSession-CRoezJPN.js} +1 -1
- package/dist/browser/ui.css +112 -24
- package/dist/browser/ui.js +3 -2
- package/dist/browser/workbench.css +10020 -9842
- package/dist/browser/workbench.d.ts +5 -2
- package/dist/browser/workbench.js +2382 -2668
- package/dist/browser/{workbenchSelect-DBGP7Y4Z.js → workbenchSelect-DtAd0zc4.js} +2 -2
- package/dist/command/index.js +48 -48
- package/dist/common/authoring.d.ts +4 -4
- package/dist/common/authoringExamples.d.ts +3 -2
- package/dist/common/authoringSchema.d.ts +15 -4
- package/dist/common/control-api-conformance.js +155 -15
- package/dist/common/control-api-errors.d.ts +12 -0
- package/dist/common/control-api.d.ts +39 -2
- package/dist/common/control-api.js +180 -37
- package/dist/common/control-requests.d.ts +23 -3
- package/dist/common/control-requests.js +141 -14
- package/dist/common/engine-contract.d.ts +3 -2
- package/dist/common/flow-authoring.js +47 -7
- package/dist/common/flow-change-schema.d.ts +1 -1
- package/dist/common/flow-change.d.ts +2 -2
- package/dist/common/flow-change.js +48 -8
- package/dist/common/flow-encoding.js +47 -7
- package/dist/common/flow-semantics.js +86 -8
- package/dist/common/flow-state.d.ts +100 -0
- package/dist/common/flow-state.js +5794 -0
- package/dist/common/mcp.d.ts +96 -3
- package/dist/common/mcp.js +158 -13
- package/dist/common/run-events.js +19 -8
- package/dist/common/runtime-contract.d.ts +14 -0
- package/dist/common/runtime-contract.js +124 -17
- package/dist/common/runtime-nodejs-compat.js +11 -12
- package/dist/common/scheduler-node-execution.d.ts +1 -0
- package/dist/common/scheduler.d.ts +2 -1
- package/dist/common/scheduler.js +95 -40
- package/dist/common/state-conformance.d.ts +16 -0
- package/dist/common/tags.d.ts +4 -0
- package/dist/index.d.ts +11 -0
- package/package.json +5 -1
- package/src/control/common/api.ts +101 -11
- package/src/control/common/authoring.ts +7 -1
- package/src/control/common/authoringExamples.ts +9 -0
- package/src/control/common/authoringSchema.ts +18 -12
- package/src/control/common/errors.ts +6 -0
- package/src/control/common/flowDecoders.ts +1 -0
- package/src/control/common/mcp.ts +16 -0
- package/src/control/common/requests.ts +16 -2
- package/src/control/common/tags.ts +20 -0
- package/src/execution/common/events.ts +22 -7
- package/src/execution/common/node-execution.ts +3 -0
- package/src/execution/common/scheduler.ts +29 -13
- package/src/flow/common/change.ts +2 -1
- package/src/flow/common/changeSchema.ts +11 -1
- package/src/flow/common/errorHandling.ts +1 -1
- package/src/flow/common/semantics.ts +27 -1
- package/src/runtime/common/engineContract.ts +6 -3
- package/src/runtime/common/nodejsGuest.generated.json +11 -12
- package/src/runtime/common/runtime.ts +122 -7
- package/src/state/common/conformance.ts +81 -0
- package/src/state/common/state.ts +141 -0
- package/src/types/index.ts +11 -0
|
@@ -5,6 +5,58 @@ import { currentEngineContract } from './engineContract.ts'
|
|
|
5
5
|
|
|
6
6
|
export { currentEngineContract, nodejsEngineContract, findEngineContract, type EngineContract } from './engineContract.ts'
|
|
7
7
|
|
|
8
|
+
/** Serialized into the guest; keep validation and helpers self-contained. */
|
|
9
|
+
export function createState(invoke: (kind: string, payload: JsonValue) => Promise<RuntimeCapabilityResponse>) {
|
|
10
|
+
// Serialized into the guest together with this factory.
|
|
11
|
+
// oxlint-disable-next-line unicorn/consistent-function-scoping
|
|
12
|
+
const invalid = (message: string) => Object.assign(new TypeError(message), { code: 'capability.invalid' })
|
|
13
|
+
const keyOf = (key: unknown) => {
|
|
14
|
+
if (typeof key !== 'string' || key.length === 0 || new TextEncoder().encode(key).length > 256) throw invalid('State keys require 1 to 256 UTF-8 bytes.')
|
|
15
|
+
return key
|
|
16
|
+
}
|
|
17
|
+
const valueOf = (value: unknown): JsonValue => {
|
|
18
|
+
if (value === undefined) throw invalid('State values must be JSON.')
|
|
19
|
+
try {
|
|
20
|
+
return (snapshotRuntimeResult({ value }) as { value: JsonValue }).value
|
|
21
|
+
} catch {
|
|
22
|
+
throw invalid('State values must be JSON.')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const idsOf = (ids: unknown): (string | number)[] => {
|
|
26
|
+
if (!Array.isArray(ids) || !Array.from(ids).every((id) => typeof id === 'string' || (typeof id === 'number' && Number.isFinite(id))))
|
|
27
|
+
throw invalid('Identities must be strings or finite numbers.')
|
|
28
|
+
return ids
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
state: Object.freeze({
|
|
32
|
+
async get(key: unknown) {
|
|
33
|
+
return ((await invoke('state.get', { key: keyOf(key) })).body as { value: JsonValue }).value
|
|
34
|
+
},
|
|
35
|
+
async set(key: unknown, value: unknown) {
|
|
36
|
+
await invoke('state.set', { key: keyOf(key), value: valueOf(value) })
|
|
37
|
+
},
|
|
38
|
+
async insert(key: unknown, value: unknown) {
|
|
39
|
+
if (value === null) throw invalid('State insert does not accept null.')
|
|
40
|
+
return ((await invoke('state.insert', { key: keyOf(key), value: valueOf(value) })).body as { inserted: boolean }).inserted
|
|
41
|
+
},
|
|
42
|
+
async delete(key: unknown) {
|
|
43
|
+
await invoke('state.delete', { key: keyOf(key) })
|
|
44
|
+
},
|
|
45
|
+
async list() {
|
|
46
|
+
return ((await invoke('state.list', {})).body as { keys: string[] }).keys
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
dedupe: Object.freeze({
|
|
50
|
+
async insert(ids: unknown) {
|
|
51
|
+
return ((await invoke('dedupe.insert', { ids: idsOf(ids) })).body as { inserted: boolean[] }).inserted
|
|
52
|
+
},
|
|
53
|
+
async has(ids: unknown) {
|
|
54
|
+
return ((await invoke('dedupe.has', { ids: idsOf(ids) })).body as { present: boolean[] }).present
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
8
60
|
export type RuntimeModule = Pick<CodeModule, 'imports' | 'source'>
|
|
9
61
|
|
|
10
62
|
/** Snapshot a Task result before JSON transport can discard or coerce invalid values. */
|
|
@@ -391,6 +443,70 @@ async function rejects(operation: Promise<unknown>, pattern: RegExp, message: st
|
|
|
391
443
|
}
|
|
392
444
|
|
|
393
445
|
export const runtimeConformanceCases: readonly RuntimeConformanceCase[] = [
|
|
446
|
+
{
|
|
447
|
+
name: 'cancels detached State work when its Task finishes',
|
|
448
|
+
async verify(harness) {
|
|
449
|
+
let aborted = false
|
|
450
|
+
const result = await harness.invoke({
|
|
451
|
+
input: null,
|
|
452
|
+
invocationId: 'state-detached',
|
|
453
|
+
capability: async ({ signal }) =>
|
|
454
|
+
await new Promise((resolve) => {
|
|
455
|
+
signal.addEventListener(
|
|
456
|
+
'abort',
|
|
457
|
+
() => {
|
|
458
|
+
aborted = true
|
|
459
|
+
resolve({ status: 200, body: null })
|
|
460
|
+
},
|
|
461
|
+
{ once: true },
|
|
462
|
+
)
|
|
463
|
+
}),
|
|
464
|
+
program: program(
|
|
465
|
+
harness,
|
|
466
|
+
`export default async (_, context) => {
|
|
467
|
+
void context.state.set('detached', 1);
|
|
468
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
469
|
+
return null;
|
|
470
|
+
}`,
|
|
471
|
+
),
|
|
472
|
+
})
|
|
473
|
+
equal(result, null, 'Detached State Task result')
|
|
474
|
+
equal(aborted, true, 'Detached State cancellation')
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: 'rejects non-JSON State values and invalid identities before Host dispatch',
|
|
479
|
+
async verify(harness) {
|
|
480
|
+
let calls = 0
|
|
481
|
+
const result = await harness.invoke({
|
|
482
|
+
input: null,
|
|
483
|
+
invocationId: 'state-invalid',
|
|
484
|
+
capability: async () => {
|
|
485
|
+
calls += 1
|
|
486
|
+
return { status: 200, body: null }
|
|
487
|
+
},
|
|
488
|
+
program: program(
|
|
489
|
+
harness,
|
|
490
|
+
`export default async (_, context) => {
|
|
491
|
+
const cyclic = {}; cyclic.self = cyclic;
|
|
492
|
+
const invalid = [undefined, NaN, Infinity, new Date(), { x: undefined }, cyclic, [undefined], { x: () => 1 }];
|
|
493
|
+
const errors = [];
|
|
494
|
+
for (const value of invalid) { try { await context.state.set('key', value); errors.push('accepted'); } catch (e) { errors.push(e.code); } }
|
|
495
|
+
for (const call of [() => context.state.get(''), () => context.state.get('x'.repeat(257)), () => context.state.insert('x', null), () => context.dedupe.insert([NaN]), () => context.dedupe.has([null]), () => context.dedupe.insert(new Array(1))]) {
|
|
496
|
+
try { await call(); errors.push('accepted'); } catch (e) { errors.push(e.code); }
|
|
497
|
+
}
|
|
498
|
+
return errors;
|
|
499
|
+
}`,
|
|
500
|
+
),
|
|
501
|
+
})
|
|
502
|
+
equal(
|
|
503
|
+
result,
|
|
504
|
+
Array.from({ length: 14 }, () => 'capability.invalid'),
|
|
505
|
+
'State boundary validation',
|
|
506
|
+
)
|
|
507
|
+
equal(calls, 0, 'State invalid dispatch')
|
|
508
|
+
},
|
|
509
|
+
},
|
|
394
510
|
{
|
|
395
511
|
name: 'rejects Task results that JSON transport would discard or coerce',
|
|
396
512
|
async verify(harness) {
|
|
@@ -935,14 +1051,13 @@ export function createBase64() {
|
|
|
935
1051
|
const padding = text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0
|
|
936
1052
|
const bytes = new Uint8Array((text.length / 4) * 3 - padding)
|
|
937
1053
|
for (let index = 0, offset = 0; index < text.length; index += 4) {
|
|
938
|
-
|
|
1054
|
+
let value = 0
|
|
1055
|
+
for (let position = 0; position < 4; position++) {
|
|
939
1056
|
const code = text.charCodeAt(index + position)
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
})
|
|
945
|
-
const value = (digits[0]! << 18) | (digits[1]! << 12) | (digits[2]! << 6) | digits[3]!
|
|
1057
|
+
const digit = code == 61 && index + position >= text.length - padding ? 0 : code < 128 ? values[code]! : 255
|
|
1058
|
+
if (digit == 255) throw new TypeError('Base64 content is invalid.')
|
|
1059
|
+
value = (value << 6) | digit
|
|
1060
|
+
}
|
|
946
1061
|
if (offset < bytes.length) bytes[offset++] = value >> 16
|
|
947
1062
|
if (offset < bytes.length) bytes[offset++] = (value >> 8) & 255
|
|
948
1063
|
if (offset < bytes.length) bytes[offset++] = value & 255
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { JsonValue } from '../../flow/common/model.ts'
|
|
2
|
+
import type { SkipDuplicatesConfig } from './state.ts'
|
|
3
|
+
|
|
4
|
+
import { dequal } from 'dequal/lite'
|
|
5
|
+
|
|
6
|
+
export interface StateConformanceHarness {
|
|
7
|
+
readonly maxIdentities: number
|
|
8
|
+
/** Executes Code on the same initially empty Flow and scope on every call; returns its `result` output. */
|
|
9
|
+
run(source: string): Promise<JsonValue>
|
|
10
|
+
/** Executes the same Skip Duplicates node across Runs, updating its business config and items. */
|
|
11
|
+
skipDuplicates(config: SkipDuplicatesConfig, items: JsonValue[]): Promise<{ items: JsonValue[]; duplicates: JsonValue[] }>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const stateConformanceCases: readonly { name: string; verify(harness: StateConformanceHarness): Promise<void> }[] = [
|
|
15
|
+
{
|
|
16
|
+
name: 'skips composite identities across successful Runs without depending on field order',
|
|
17
|
+
async verify(harness) {
|
|
18
|
+
const items = [
|
|
19
|
+
{ company: 'a', id: 1 },
|
|
20
|
+
{ company: 'b', id: 1 },
|
|
21
|
+
{ company: 'a', id: 1 },
|
|
22
|
+
]
|
|
23
|
+
const first = await harness.skipDuplicates({ fields: ['company', 'id'] }, items)
|
|
24
|
+
if (!dequal(first, { items: items.slice(0, 2), duplicates: [items[2]] }))
|
|
25
|
+
throw new Error('Composite identities must retain the first item in input order.')
|
|
26
|
+
const second = await harness.skipDuplicates({ fields: ['id', 'company'] }, items)
|
|
27
|
+
if (!dequal(second, { items: [], duplicates: items })) throw new Error('Successful Run history must persist independently of field order.')
|
|
28
|
+
const changed = await harness.skipDuplicates({ fields: ['id'] }, items)
|
|
29
|
+
if (!dequal(changed, { items: [items[0]], duplicates: items.slice(1) })) throw new Error('The field definition must participate in identity.')
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'round-trips independent JSON values and deletes without exposing stored references',
|
|
34
|
+
async verify(harness) {
|
|
35
|
+
const result = await harness.run(`export default async (_, context) => {
|
|
36
|
+
await context.state.set('b', { nested: [1, null, false] });
|
|
37
|
+
const copy = await context.state.get('b');
|
|
38
|
+
copy.nested.push(2);
|
|
39
|
+
const retained = await context.state.get('b');
|
|
40
|
+
const inserted = await context.state.insert('b', 'replacement');
|
|
41
|
+
await context.state.set('a', 0);
|
|
42
|
+
const keys = await context.state.list();
|
|
43
|
+
await context.state.delete('a');
|
|
44
|
+
await context.state.set('b', null);
|
|
45
|
+
return {result: {retained, inserted, keys, missing: await context.state.get('a'), remaining: await context.state.list()}};
|
|
46
|
+
}`)
|
|
47
|
+
if (!dequal(result, { retained: { nested: [1, null, false] }, inserted: false, keys: ['a', 'b'], missing: null, remaining: [] }))
|
|
48
|
+
throw new Error('State JSON round-trip, conditional insertion or deletion differs from the contract.')
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'persists explicit values and dedupe history across Runs',
|
|
53
|
+
async verify(harness) {
|
|
54
|
+
const source = `export default async (_, context) => {
|
|
55
|
+
const first = await context.state.insert('count', 0);
|
|
56
|
+
const count = await context.state.get('count');
|
|
57
|
+
await context.state.set('count', count + 1);
|
|
58
|
+
return { result: { first, count, ids: await context.dedupe.insert(['a', 'a']) } };
|
|
59
|
+
}`
|
|
60
|
+
if (
|
|
61
|
+
!dequal(await harness.run(source), { first: true, count: 0, ids: [true, false] }) ||
|
|
62
|
+
!dequal(await harness.run(source), { first: false, count: 1, ids: [false, false] })
|
|
63
|
+
)
|
|
64
|
+
throw new Error('State must persist across Runs without replacing existing values on insert.')
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'rejects a full batch atomically while permitting duplicates at capacity',
|
|
69
|
+
async verify(harness) {
|
|
70
|
+
const result = await harness.run(`export default async (_, context) => {
|
|
71
|
+
const ids = Array.from({length: ${harness.maxIdentities}}, (_, index) => index);
|
|
72
|
+
await context.dedupe.insert(ids);
|
|
73
|
+
let error;
|
|
74
|
+
try { await context.dedupe.insert([0, 'extra']); } catch (failure) { error = failure.code; }
|
|
75
|
+
return { result: { error, extra: await context.dedupe.has(['extra']), duplicate: await context.dedupe.insert([0, 0]) } };
|
|
76
|
+
}`)
|
|
77
|
+
if (!dequal(result, { error: 'dedupe.full', extra: [false], duplicate: [false, false] }))
|
|
78
|
+
throw new Error('Dedupe capacity must fail atomically and preserve duplicate membership.')
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { JsonValue, ManagedTaskDefinitionOf } from '../../flow/common/model.ts'
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
export { stateConformanceCases, type StateConformanceHarness } from './conformance.ts'
|
|
6
|
+
|
|
7
|
+
export type StateScope = 'draft' | 'live'
|
|
8
|
+
export const stateQuerySchema = z.strictObject({ scope: z.enum(['draft', 'live']), values: z.boolean().optional() })
|
|
9
|
+
export const stateClearSchema = z.strictObject({
|
|
10
|
+
scope: z.enum(['draft', 'live']),
|
|
11
|
+
key: z
|
|
12
|
+
.string()
|
|
13
|
+
.min(1)
|
|
14
|
+
.refine((key) => new TextEncoder().encode(key).length <= 256)
|
|
15
|
+
.optional(),
|
|
16
|
+
})
|
|
17
|
+
export const dedupeClearSchema = z.strictObject({ scope: z.enum(['draft', 'live']), space: z.string().regex(/^(flow|node:.+)$/) })
|
|
18
|
+
export type DedupeIdentity = string | number
|
|
19
|
+
export const skipDuplicatesConfigSchema = z.strictObject({
|
|
20
|
+
fields: z
|
|
21
|
+
.array(z.string().refine((field) => field.length > 0 && field.split('.').every((part) => part.length > 0), 'Use a nonempty dot-separated field path.'))
|
|
22
|
+
.refine((fields) => new Set(fields).size === fields.length, 'Identity fields must be distinct.')
|
|
23
|
+
.default([]),
|
|
24
|
+
})
|
|
25
|
+
export type SkipDuplicatesConfig = z.infer<typeof skipDuplicatesConfigSchema>
|
|
26
|
+
export interface SkipDuplicatesExecutor extends SkipDuplicatesConfig {
|
|
27
|
+
readonly kind: 'skip-duplicates'
|
|
28
|
+
}
|
|
29
|
+
export const skipDuplicatesExecutorSchema = skipDuplicatesConfigSchema.extend({ kind: z.literal('skip-duplicates') })
|
|
30
|
+
|
|
31
|
+
export function skipDuplicatesTask(
|
|
32
|
+
config: SkipDuplicatesConfig = skipDuplicatesConfigSchema.parse({}),
|
|
33
|
+
name = 'Skip Duplicates',
|
|
34
|
+
): ManagedTaskDefinitionOf<SkipDuplicatesExecutor> {
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
executor: { ...config, kind: 'skip-duplicates' },
|
|
38
|
+
inputs: [{ handle: 'items', jsonSchema: { type: 'array' }, nullable: false }],
|
|
39
|
+
outputs: ['items', 'duplicates'].map((handle) => ({ handle, jsonSchema: { type: 'array' }, nullable: false })),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class StateError extends Error {
|
|
44
|
+
readonly retryable = false
|
|
45
|
+
readonly catchable: boolean
|
|
46
|
+
readonly code: 'capability.invalid' | 'capability.denied' | 'dedupe.full' | 'limit-exceeded' | 'node.input-invalid'
|
|
47
|
+
|
|
48
|
+
constructor(code: StateError['code'], message: string) {
|
|
49
|
+
super(message)
|
|
50
|
+
this.name = 'StateError'
|
|
51
|
+
this.code = code
|
|
52
|
+
this.catchable = code === 'dedupe.full' || code === 'node.input-invalid'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function stateKey(key: unknown): string {
|
|
57
|
+
if (typeof key !== 'string' || key.length === 0 || new TextEncoder().encode(key).length > 256)
|
|
58
|
+
throw new StateError('capability.invalid', 'State keys require 1 to 256 UTF-8 bytes.')
|
|
59
|
+
return key
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function dedupeIdentities(value: unknown): DedupeIdentity[] {
|
|
63
|
+
if (!Array.isArray(value) || !Array.from(value).every((id) => typeof id === 'string' || (typeof id === 'number' && Number.isFinite(id))))
|
|
64
|
+
throw new StateError('capability.invalid', 'Identities must be strings or finite numbers.')
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function hashIdentities(ids: readonly DedupeIdentity[]): Promise<Uint8Array[]> {
|
|
69
|
+
return Promise.all(
|
|
70
|
+
ids.map(async (id) => new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${typeof id}:${String(id)}`))).slice(0, 16)),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Canonical identities include the field definition and preserve scalar types. */
|
|
75
|
+
export function skipDuplicatesIdentities(items: JsonValue, fields: readonly string[]): string[] {
|
|
76
|
+
if (!Array.isArray(items)) throw new StateError('node.input-invalid', 'Skip Duplicates requires an array of items.')
|
|
77
|
+
const paths = fields.toSorted()
|
|
78
|
+
return items.map((item, index) => {
|
|
79
|
+
const values = (paths.length === 0 ? [''] : paths).map((field) => {
|
|
80
|
+
const value =
|
|
81
|
+
field === ''
|
|
82
|
+
? item
|
|
83
|
+
: field
|
|
84
|
+
.split('.')
|
|
85
|
+
.reduce<JsonValue | undefined>(
|
|
86
|
+
(current, key) =>
|
|
87
|
+
current != null && typeof current === 'object' && Object.hasOwn(current, key) ? (current as Record<string, JsonValue>)[key] : undefined,
|
|
88
|
+
item,
|
|
89
|
+
)
|
|
90
|
+
if (typeof value !== 'string' && !(typeof value === 'number' && Number.isFinite(value)))
|
|
91
|
+
throw new StateError(
|
|
92
|
+
'node.input-invalid',
|
|
93
|
+
`Item ${index + 1}, ${field === '' ? 'the item itself' : `field ${JSON.stringify(field)}`} must be a string or finite number.`,
|
|
94
|
+
)
|
|
95
|
+
return value
|
|
96
|
+
})
|
|
97
|
+
return JSON.stringify([paths, values])
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface FlowStateView {
|
|
102
|
+
readonly version: 1
|
|
103
|
+
readonly scope: StateScope
|
|
104
|
+
readonly limits: {
|
|
105
|
+
readonly maxValueBytes: number
|
|
106
|
+
readonly maxValues: number
|
|
107
|
+
readonly maxSpaces: number
|
|
108
|
+
readonly maxIdentities: number
|
|
109
|
+
readonly maxRunWrites: number
|
|
110
|
+
}
|
|
111
|
+
readonly values: readonly { readonly key: string; readonly size: number; readonly updatedAt: number; readonly value?: JsonValue }[]
|
|
112
|
+
readonly spaces: readonly {
|
|
113
|
+
readonly space: string
|
|
114
|
+
readonly count: number
|
|
115
|
+
readonly pendingCount: number
|
|
116
|
+
readonly reservedCount: number
|
|
117
|
+
readonly updatedAt: number
|
|
118
|
+
}[]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const flowStateViewSchema = z.object({
|
|
122
|
+
version: z.literal(1),
|
|
123
|
+
scope: z.enum(['draft', 'live']),
|
|
124
|
+
limits: z.object({
|
|
125
|
+
maxValueBytes: z.number().positive(),
|
|
126
|
+
maxValues: z.number().int().positive(),
|
|
127
|
+
maxSpaces: z.number().int().positive(),
|
|
128
|
+
maxIdentities: z.number().positive(),
|
|
129
|
+
maxRunWrites: z.number().positive(),
|
|
130
|
+
}),
|
|
131
|
+
values: z.array(z.object({ key: z.string(), size: z.number().nonnegative(), updatedAt: z.number(), value: z.json().optional() })),
|
|
132
|
+
spaces: z.array(
|
|
133
|
+
z.object({
|
|
134
|
+
space: z.string(),
|
|
135
|
+
count: z.number().nonnegative(),
|
|
136
|
+
pendingCount: z.number().nonnegative(),
|
|
137
|
+
reservedCount: z.number().nonnegative(),
|
|
138
|
+
updatedAt: z.number(),
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
})
|
package/src/types/index.ts
CHANGED
|
@@ -119,6 +119,17 @@ export interface TaskContext<
|
|
|
119
119
|
readonly inputs: Readonly<Record<string, unknown>>
|
|
120
120
|
readonly runId: string
|
|
121
121
|
readonly blockId: string
|
|
122
|
+
readonly state: {
|
|
123
|
+
get(key: string): Promise<JsonValue | null>
|
|
124
|
+
set(key: string, value: JsonValue): Promise<void>
|
|
125
|
+
insert(key: string, value: Exclude<JsonValue, null>): Promise<boolean>
|
|
126
|
+
delete(key: string): Promise<void>
|
|
127
|
+
list(): Promise<string[]>
|
|
128
|
+
}
|
|
129
|
+
readonly dedupe: {
|
|
130
|
+
insert(ids: readonly (string | number)[]): Promise<boolean[]>
|
|
131
|
+
has(ids: readonly (string | number)[]): Promise<boolean[]>
|
|
132
|
+
}
|
|
122
133
|
readonly artifact: ArtifactCapability
|
|
123
134
|
/** Reads the selected direct predecessors, sorted by node ID; empty for a graph root. */
|
|
124
135
|
getPrevious(): Promise<readonly PreviousNode[]>
|