@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.
Files changed (58) hide show
  1. package/README.md +2 -0
  2. package/dist/browser/{ContentIcon-VZQO8VZU.js → ContentIcon-CMn0c-kl.js} +93 -3
  3. package/dist/browser/{FlowCanvas-BNGDMaWr.js → FlowCanvas-BQGBcfoL.js} +1675 -591
  4. package/dist/browser/{createResourceDialog-BmtakfVg.js → createResourceDialog-DCGxfKFM.js} +3 -3
  5. package/dist/browser/{dialog-BcFBfRB1.js → dialog-WzR4CRFH.js} +11 -81
  6. package/dist/browser/{exportSvg-CG8hEUNk.js → exportSvg-CYwyYiuH.js} +2 -2
  7. package/dist/browser/{flowChanges-BGo_4Ypo.js → flowChanges-Bz4dbDN_.js} +1204 -387
  8. package/dist/browser/{flowWorkspace-BFHGev7s.js → flowWorkspace-BI6L6a4r.js} +557 -597
  9. package/dist/browser/licenses.md +1 -1
  10. package/dist/browser/preview.css +512 -462
  11. package/dist/browser/preview.js +8457 -6732
  12. package/dist/browser/{tooltip-BAWSUexx.js → tooltip-BDcVTNpu.js} +65 -2
  13. package/dist/browser/ui.css +5 -24
  14. package/dist/browser/workbench.css +512 -462
  15. package/dist/browser/workbench.d.ts +2 -3
  16. package/dist/browser/workbench.js +65 -690
  17. package/dist/browser/{workbenchSelect-CjK-xdm3.js → workbenchSelect-DtAd0zc4.js} +2 -2
  18. package/dist/command/index.js +25 -25
  19. package/dist/common/authoring.d.ts +3 -3
  20. package/dist/common/authoringExamples.d.ts +3 -3
  21. package/dist/common/authoringSchema.d.ts +14 -22
  22. package/dist/common/control-api-conformance.js +11 -18
  23. package/dist/common/control-api.d.ts +5 -1
  24. package/dist/common/control-api.js +11 -18
  25. package/dist/common/control-requests.d.ts +4 -3
  26. package/dist/common/control-requests.js +26 -37
  27. package/dist/common/flow-authoring.js +6 -14
  28. package/dist/common/flow-change.d.ts +1 -1
  29. package/dist/common/flow-change.js +6 -14
  30. package/dist/common/flow-encoding.js +5 -13
  31. package/dist/common/flow-semantics.js +15 -21
  32. package/dist/common/flow-state.d.ts +16 -39
  33. package/dist/common/flow-state.js +47 -105
  34. package/dist/common/mcp.d.ts +3 -3
  35. package/dist/common/mcp.js +11 -18
  36. package/dist/common/run-events.js +19 -9
  37. package/dist/common/runtime-contract.js +6 -12
  38. package/dist/common/runtime-nodejs-compat.js +11 -12
  39. package/dist/common/scheduler.d.ts +2 -1
  40. package/dist/common/scheduler.js +56 -50
  41. package/dist/common/state-conformance.d.ts +3 -3
  42. package/package.json +1 -1
  43. package/src/control/common/api.ts +20 -3
  44. package/src/control/common/authoring.ts +6 -5
  45. package/src/control/common/authoringExamples.ts +6 -5
  46. package/src/control/common/authoringSchema.ts +7 -7
  47. package/src/control/common/requests.ts +7 -1
  48. package/src/execution/common/events.ts +22 -10
  49. package/src/execution/common/node-execution.ts +1 -1
  50. package/src/execution/common/scheduler.ts +28 -14
  51. package/src/flow/common/change.ts +1 -1
  52. package/src/flow/common/changeSchema.ts +4 -4
  53. package/src/flow/common/errorHandling.ts +1 -1
  54. package/src/flow/common/semantics.ts +12 -6
  55. package/src/runtime/common/nodejsGuest.generated.json +11 -12
  56. package/src/runtime/common/runtime.ts +6 -7
  57. package/src/state/common/conformance.ts +16 -21
  58. 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
- const digits = [0, 1, 2, 3].map((position) => {
1054
+ let value = 0
1055
+ for (let position = 0; position < 4; position++) {
1055
1056
  const code = text.charCodeAt(index + position)
1056
- if (code == 61 && index + position >= text.length - padding) return 0
1057
- const value = code < 128 ? values[code]! : 255
1058
- if (value == 255) throw new TypeError('Base64 content is invalid.')
1059
- return value
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 { DedupeConfig } from './state.ts'
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 dedupe node across Runs, updating its business config and items. */
11
- dedupe(config: DedupeConfig, items: JsonValue[]): Promise<{ items: JsonValue[]; duplicates: 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
12
  }
13
13
 
14
14
  export const stateConformanceCases: readonly { name: string; verify(harness: StateConformanceHarness): Promise<void> }[] = [
15
15
  {
16
- name: 'persists watermarks by instant and resets changed modes atomically',
16
+ name: 'skips composite identities across successful Runs without depending on field order',
17
17
  async verify(harness) {
18
- const config: DedupeConfig = { field: '', mode: 'increasing', space: 'node' }
19
- const first = await harness.dedupe(config, ['2026-10-30T09:00:00+08:00', '2026-10-30T01:00:00Z', '2026-10-30T02:00:00Z'])
20
- if (!dequal(first, { items: ['2026-10-30T09:00:00+08:00', '2026-10-30T02:00:00Z'], duplicates: ['2026-10-30T01:00:00Z'] }))
21
- throw new Error('Watermarks must compare instants and preserve item order.')
22
- let rejected = false
23
- try {
24
- await harness.dedupe(config, [1])
25
- } catch {
26
- rejected = true
27
- }
28
- if (!rejected) throw new Error('Mixed watermark types must fail.')
29
- const second = await harness.dedupe(config, ['2026-10-30T02:00:00Z', '2026-10-30T03:00:00Z'])
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 dedupeConfigSchema = z.strictObject({
20
- field: z.string().default(''),
21
- mode: z.enum(['new', 'increasing']).default('new'),
22
- space: z.enum(['node', 'flow']).default('node'),
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 DedupeConfig = z.infer<typeof dedupeConfigSchema>
25
- export interface DedupeExecutor extends DedupeConfig {
26
- readonly kind: 'dedupe'
25
+ export type SkipDuplicatesConfig = z.infer<typeof skipDuplicatesConfigSchema>
26
+ export interface SkipDuplicatesExecutor extends SkipDuplicatesConfig {
27
+ readonly kind: 'skip-duplicates'
27
28
  }
28
- export const dedupeExecutorSchema = dedupeConfigSchema.extend({ kind: z.literal('dedupe') })
29
+ export const skipDuplicatesExecutorSchema = skipDuplicatesConfigSchema.extend({ kind: z.literal('skip-duplicates') })
29
30
 
30
- export function dedupeTask(config: DedupeConfig = dedupeConfigSchema.parse({}), name = 'Remove Duplicates'): ManagedTaskDefinitionOf<DedupeExecutor> {
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: 'dedupe' },
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
- export function watermarkValue(id: DedupeIdentity): number {
71
- if (typeof id === 'number') {
72
- if (!Number.isFinite(id)) throw new StateError('capability.invalid', 'Watermarks must be finite.')
73
- return id
74
- }
75
- const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|[+-]\d{2}:\d{2})$/.exec(id)
76
- if (match == null) throw new StateError('capability.invalid', 'Watermarks require RFC 3339 timestamps with an explicit timezone.')
77
- const [, year, month, day, hour, minute, second, , zone] = match
78
- const y = Number(year)
79
- const days = [31, y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][Number(month) - 1] ?? 0
80
- const result = Date.parse(id)
81
- if (
82
- Number(month) < 1 ||
83
- Number(month) > 12 ||
84
- Number(day) < 1 ||
85
- Number(day) > days ||
86
- Number(hour) > 23 ||
87
- Number(minute) > 59 ||
88
- Number(second) > 59 ||
89
- (zone !== 'Z' && (Number(zone!.slice(1, 3)) > 23 || Number(zone!.slice(4)) > 59)) ||
90
- !Number.isFinite(result)
91
- )
92
- throw new StateError('capability.invalid', 'Watermarks require valid calendar timestamps.')
93
- return result
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 watermark: DedupeIdentity | null
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
- watermark: z.union([z.string(), z.number(), z.null()]),
136
+ pendingCount: z.number().nonnegative(),
137
+ reservedCount: z.number().nonnegative(),
166
138
  updatedAt: z.number(),
167
139
  }),
168
140
  ),