@oomol-lab/open-flow 0.1.0-beta.77 → 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 +2 -0
- package/dist/browser/{ContentIcon-VZQO8VZU.js → ContentIcon-CMn0c-kl.js} +93 -3
- package/dist/browser/{FlowCanvas-BNGDMaWr.js → FlowCanvas-BQGBcfoL.js} +1675 -591
- package/dist/browser/{createResourceDialog-BmtakfVg.js → createResourceDialog-DCGxfKFM.js} +3 -3
- package/dist/browser/{dialog-BcFBfRB1.js → dialog-WzR4CRFH.js} +11 -81
- package/dist/browser/{exportSvg-CG8hEUNk.js → exportSvg-CYwyYiuH.js} +2 -2
- package/dist/browser/{flowChanges-BGo_4Ypo.js → flowChanges-Bz4dbDN_.js} +1204 -387
- package/dist/browser/{flowWorkspace-BFHGev7s.js → flowWorkspace-BI6L6a4r.js} +557 -597
- package/dist/browser/licenses.md +1 -1
- package/dist/browser/preview.css +512 -462
- package/dist/browser/preview.js +8457 -6732
- package/dist/browser/{tooltip-BAWSUexx.js → tooltip-BDcVTNpu.js} +65 -2
- package/dist/browser/ui.css +5 -24
- package/dist/browser/workbench.css +512 -462
- package/dist/browser/workbench.d.ts +2 -3
- package/dist/browser/workbench.js +65 -690
- package/dist/browser/{workbenchSelect-CjK-xdm3.js → workbenchSelect-DtAd0zc4.js} +2 -2
- package/dist/command/index.js +25 -25
- package/dist/common/authoring.d.ts +3 -3
- package/dist/common/authoringExamples.d.ts +3 -3
- package/dist/common/authoringSchema.d.ts +14 -22
- package/dist/common/control-api-conformance.js +11 -18
- package/dist/common/control-api.d.ts +5 -1
- package/dist/common/control-api.js +11 -18
- package/dist/common/control-requests.d.ts +4 -3
- package/dist/common/control-requests.js +26 -37
- package/dist/common/flow-authoring.js +6 -14
- package/dist/common/flow-change.d.ts +1 -1
- package/dist/common/flow-change.js +6 -14
- package/dist/common/flow-encoding.js +5 -13
- package/dist/common/flow-semantics.js +15 -21
- package/dist/common/flow-state.d.ts +16 -39
- package/dist/common/flow-state.js +47 -105
- package/dist/common/mcp.d.ts +3 -3
- package/dist/common/mcp.js +11 -18
- package/dist/common/run-events.js +19 -9
- package/dist/common/runtime-contract.js +6 -12
- package/dist/common/runtime-nodejs-compat.js +11 -12
- package/dist/common/scheduler.d.ts +2 -1
- package/dist/common/scheduler.js +56 -50
- package/dist/common/state-conformance.d.ts +3 -3
- package/package.json +1 -1
- package/src/control/common/api.ts +20 -3
- package/src/control/common/authoring.ts +6 -5
- package/src/control/common/authoringExamples.ts +6 -5
- package/src/control/common/authoringSchema.ts +7 -7
- package/src/control/common/requests.ts +7 -1
- package/src/execution/common/events.ts +22 -10
- package/src/execution/common/node-execution.ts +1 -1
- package/src/execution/common/scheduler.ts +28 -14
- package/src/flow/common/change.ts +1 -1
- package/src/flow/common/changeSchema.ts +4 -4
- package/src/flow/common/errorHandling.ts +1 -1
- package/src/flow/common/semantics.ts +12 -6
- package/src/runtime/common/nodejsGuest.generated.json +11 -12
- package/src/runtime/common/runtime.ts +6 -7
- package/src/state/common/conformance.ts +16 -21
- package/src/state/common/state.ts +44 -72
|
@@ -1051,14 +1051,13 @@ export function createBase64() {
|
|
|
1051
1051
|
const padding = text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0
|
|
1052
1052
|
const bytes = new Uint8Array((text.length / 4) * 3 - padding)
|
|
1053
1053
|
for (let index = 0, offset = 0; index < text.length; index += 4) {
|
|
1054
|
-
|
|
1054
|
+
let value = 0
|
|
1055
|
+
for (let position = 0; position < 4; position++) {
|
|
1055
1056
|
const code = text.charCodeAt(index + position)
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
})
|
|
1061
|
-
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
|
+
}
|
|
1062
1061
|
if (offset < bytes.length) bytes[offset++] = value >> 16
|
|
1063
1062
|
if (offset < bytes.length) bytes[offset++] = (value >> 8) & 255
|
|
1064
1063
|
if (offset < bytes.length) bytes[offset++] = value & 255
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JsonValue } from '../../flow/common/model.ts'
|
|
2
|
-
import type {
|
|
2
|
+
import type { SkipDuplicatesConfig } from './state.ts'
|
|
3
3
|
|
|
4
4
|
import { dequal } from 'dequal/lite'
|
|
5
5
|
|
|
@@ -7,31 +7,26 @@ export interface StateConformanceHarness {
|
|
|
7
7
|
readonly maxIdentities: number
|
|
8
8
|
/** Executes Code on the same initially empty Flow and scope on every call; returns its `result` output. */
|
|
9
9
|
run(source: string): Promise<JsonValue>
|
|
10
|
-
/** Executes the same
|
|
11
|
-
|
|
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
12
|
}
|
|
13
13
|
|
|
14
14
|
export const stateConformanceCases: readonly { name: string; verify(harness: StateConformanceHarness): Promise<void> }[] = [
|
|
15
15
|
{
|
|
16
|
-
name: '
|
|
16
|
+
name: 'skips composite identities across successful Runs without depending on field order',
|
|
17
17
|
async verify(harness) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!dequal(second, { items: ['2026-10-30T03:00:00Z'], duplicates: ['2026-10-30T02:00:00Z'] }))
|
|
31
|
-
throw new Error('A failed batch must preserve the previous watermark.')
|
|
32
|
-
await harness.dedupe({ ...config, mode: 'new' }, [])
|
|
33
|
-
const reset = await harness.dedupe(config, [5, 3, 5])
|
|
34
|
-
if (!dequal(reset, { items: [5, 3], duplicates: [5] })) throw new Error('A mode reset must discard previous watermarks, including on empty batches.')
|
|
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.')
|
|
35
30
|
},
|
|
36
31
|
},
|
|
37
32
|
{
|
|
@@ -16,21 +16,25 @@ export const stateClearSchema = z.strictObject({
|
|
|
16
16
|
})
|
|
17
17
|
export const dedupeClearSchema = z.strictObject({ scope: z.enum(['draft', 'live']), space: z.string().regex(/^(flow|node:.+)$/) })
|
|
18
18
|
export type DedupeIdentity = string | number
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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([]),
|
|
23
24
|
})
|
|
24
|
-
export type
|
|
25
|
-
export interface
|
|
26
|
-
readonly kind: '
|
|
25
|
+
export type SkipDuplicatesConfig = z.infer<typeof skipDuplicatesConfigSchema>
|
|
26
|
+
export interface SkipDuplicatesExecutor extends SkipDuplicatesConfig {
|
|
27
|
+
readonly kind: 'skip-duplicates'
|
|
27
28
|
}
|
|
28
|
-
export const
|
|
29
|
+
export const skipDuplicatesExecutorSchema = skipDuplicatesConfigSchema.extend({ kind: z.literal('skip-duplicates') })
|
|
29
30
|
|
|
30
|
-
export function
|
|
31
|
+
export function skipDuplicatesTask(
|
|
32
|
+
config: SkipDuplicatesConfig = skipDuplicatesConfigSchema.parse({}),
|
|
33
|
+
name = 'Skip Duplicates',
|
|
34
|
+
): ManagedTaskDefinitionOf<SkipDuplicatesExecutor> {
|
|
31
35
|
return {
|
|
32
36
|
name,
|
|
33
|
-
executor: { ...config, kind: '
|
|
37
|
+
executor: { ...config, kind: 'skip-duplicates' },
|
|
34
38
|
inputs: [{ handle: 'items', jsonSchema: { type: 'array' }, nullable: false }],
|
|
35
39
|
outputs: ['items', 'duplicates'].map((handle) => ({ handle, jsonSchema: { type: 'array' }, nullable: false })),
|
|
36
40
|
}
|
|
@@ -39,13 +43,13 @@ export function dedupeTask(config: DedupeConfig = dedupeConfigSchema.parse({}),
|
|
|
39
43
|
export class StateError extends Error {
|
|
40
44
|
readonly retryable = false
|
|
41
45
|
readonly catchable: boolean
|
|
42
|
-
readonly code: 'capability.invalid' | 'capability.denied' | 'dedupe.full' | 'limit-exceeded'
|
|
46
|
+
readonly code: 'capability.invalid' | 'capability.denied' | 'dedupe.full' | 'limit-exceeded' | 'node.input-invalid'
|
|
43
47
|
|
|
44
48
|
constructor(code: StateError['code'], message: string) {
|
|
45
49
|
super(message)
|
|
46
50
|
this.name = 'StateError'
|
|
47
51
|
this.code = code
|
|
48
|
-
this.catchable = code === 'dedupe.full'
|
|
52
|
+
this.catchable = code === 'dedupe.full' || code === 'node.input-invalid'
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -67,63 +71,31 @@ export async function hashIdentities(ids: readonly DedupeIdentity[]): Promise<Ui
|
|
|
67
71
|
)
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function increasingIdentities(ids: readonly DedupeIdentity[], watermark: DedupeIdentity | null) {
|
|
97
|
-
const type = typeof (watermark ?? ids[0])
|
|
98
|
-
if (ids.some((id) => typeof id !== type)) throw new StateError('capability.invalid', 'Watermark types must not be mixed.')
|
|
99
|
-
const initial = watermark == null ? -Infinity : watermarkValue(watermark)
|
|
100
|
-
const seen = new Set<number>()
|
|
101
|
-
let maximum = initial
|
|
102
|
-
let next = watermark
|
|
103
|
-
const inserted = ids.map((id) => {
|
|
104
|
-
const value = watermarkValue(id)
|
|
105
|
-
const fresh = value > initial && !seen.has(value)
|
|
106
|
-
seen.add(value)
|
|
107
|
-
if (value > maximum) {
|
|
108
|
-
maximum = value
|
|
109
|
-
next = id
|
|
110
|
-
}
|
|
111
|
-
return fresh
|
|
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])
|
|
112
98
|
})
|
|
113
|
-
return { inserted, watermark: next }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function itemIdentities(items: JsonValue, field: string): DedupeIdentity[] {
|
|
117
|
-
if (!Array.isArray(items)) throw new StateError('capability.invalid', 'Deduplication requires an array of items.')
|
|
118
|
-
const path = field === '' ? [] : field.split('.')
|
|
119
|
-
return dedupeIdentities(
|
|
120
|
-
items.map((item) =>
|
|
121
|
-
path.reduce<JsonValue | undefined>(
|
|
122
|
-
(value, key) => (value != null && typeof value === 'object' && Object.hasOwn(value, key) ? (value as Record<string, JsonValue>)[key] : undefined),
|
|
123
|
-
item,
|
|
124
|
-
),
|
|
125
|
-
),
|
|
126
|
-
)
|
|
127
99
|
}
|
|
128
100
|
|
|
129
101
|
export interface FlowStateView {
|
|
@@ -139,9 +111,9 @@ export interface FlowStateView {
|
|
|
139
111
|
readonly values: readonly { readonly key: string; readonly size: number; readonly updatedAt: number; readonly value?: JsonValue }[]
|
|
140
112
|
readonly spaces: readonly {
|
|
141
113
|
readonly space: string
|
|
142
|
-
readonly mode: 'new' | 'increasing'
|
|
143
114
|
readonly count: number
|
|
144
|
-
readonly
|
|
115
|
+
readonly pendingCount: number
|
|
116
|
+
readonly reservedCount: number
|
|
145
117
|
readonly updatedAt: number
|
|
146
118
|
}[]
|
|
147
119
|
}
|
|
@@ -160,9 +132,9 @@ export const flowStateViewSchema = z.object({
|
|
|
160
132
|
spaces: z.array(
|
|
161
133
|
z.object({
|
|
162
134
|
space: z.string(),
|
|
163
|
-
mode: z.enum(['new', 'increasing']),
|
|
164
135
|
count: z.number().nonnegative(),
|
|
165
|
-
|
|
136
|
+
pendingCount: z.number().nonnegative(),
|
|
137
|
+
reservedCount: z.number().nonnegative(),
|
|
166
138
|
updatedAt: z.number(),
|
|
167
139
|
}),
|
|
168
140
|
),
|