@oomol-lab/open-flow 0.1.0-beta.77 → 0.1.0-beta.79

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 (64) 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-DkovGogv.js} +3 -3
  5. package/dist/browser/{dialog-BcFBfRB1.js → dialog-CFFuRM8s.js} +37 -86
  6. package/dist/browser/{exportSvg-CG8hEUNk.js → exportSvg-CYwyYiuH.js} +2 -2
  7. package/dist/browser/{flowChanges-BGo_4Ypo.js → flowChanges-CN2R-m1M.js} +1258 -422
  8. package/dist/browser/{flowWorkspace-BFHGev7s.js → flowWorkspace-CXVL1FNv.js} +634 -628
  9. package/dist/browser/licenses.md +1 -1
  10. package/dist/browser/preview.css +512 -462
  11. package/dist/browser/preview.js +8183 -6384
  12. package/dist/browser/{tooltip-BAWSUexx.js → tooltip-BDcVTNpu.js} +65 -2
  13. package/dist/browser/{typeScriptSession-CRoezJPN.js → typeScriptSession-CoEIptNC.js} +1 -1
  14. package/dist/browser/ui.css +5 -24
  15. package/dist/browser/workbench.css +512 -462
  16. package/dist/browser/workbench.d.ts +2 -3
  17. package/dist/browser/workbench.js +65 -690
  18. package/dist/browser/{workbenchSelect-CjK-xdm3.js → workbenchSelect-CkOS95gX.js} +2 -2
  19. package/dist/command/index.js +31 -31
  20. package/dist/common/authoring.d.ts +3 -3
  21. package/dist/common/authoringExamples.d.ts +3 -3
  22. package/dist/common/authoringSchema.d.ts +14 -22
  23. package/dist/common/control-api-conformance.js +91 -25
  24. package/dist/common/control-api.d.ts +8 -3
  25. package/dist/common/control-api.js +51 -25
  26. package/dist/common/control-requests.d.ts +9 -3
  27. package/dist/common/control-requests.js +54 -42
  28. package/dist/common/flow-authoring.js +32 -19
  29. package/dist/common/flow-change.d.ts +1 -1
  30. package/dist/common/flow-change.js +32 -19
  31. package/dist/common/flow-encoding.js +31 -18
  32. package/dist/common/flow-semantics.js +41 -26
  33. package/dist/common/flow-state.d.ts +49 -43
  34. package/dist/common/flow-state.js +95 -110
  35. package/dist/common/mcp.d.ts +35 -4
  36. package/dist/common/mcp.js +51 -26
  37. package/dist/common/run-events.js +19 -9
  38. package/dist/common/runtime-contract.d.ts +4 -1
  39. package/dist/common/runtime-contract.js +66 -17
  40. package/dist/common/runtime-nodejs-compat.js +11 -12
  41. package/dist/common/scheduler.d.ts +2 -1
  42. package/dist/common/scheduler.js +82 -55
  43. package/dist/common/state-conformance.d.ts +3 -3
  44. package/dist/index.d.ts +1 -0
  45. package/package.json +1 -1
  46. package/src/control/common/api.ts +36 -7
  47. package/src/control/common/authoring.ts +6 -5
  48. package/src/control/common/authoringExamples.ts +6 -5
  49. package/src/control/common/authoringSchema.ts +7 -7
  50. package/src/control/common/conformance.ts +31 -0
  51. package/src/control/common/mcp.ts +7 -2
  52. package/src/control/common/requests.ts +10 -1
  53. package/src/execution/common/events.ts +22 -10
  54. package/src/execution/common/node-execution.ts +1 -1
  55. package/src/execution/common/scheduler.ts +28 -14
  56. package/src/flow/common/change.ts +1 -1
  57. package/src/flow/common/changeSchema.ts +4 -4
  58. package/src/flow/common/errorHandling.ts +1 -1
  59. package/src/flow/common/semantics.ts +12 -6
  60. package/src/runtime/common/nodejsGuest.generated.json +11 -12
  61. package/src/runtime/common/runtime.ts +45 -12
  62. package/src/state/common/conformance.ts +32 -21
  63. package/src/state/common/state.ts +85 -80
  64. package/src/types/index.ts +1 -0
@@ -27,11 +27,20 @@ export function createState(invoke: (kind: string, payload: JsonValue) => Promis
27
27
  throw invalid('Identities must be strings or finite numbers.')
28
28
  return ids
29
29
  }
30
+ function get(key: string): Promise<JsonValue>
31
+ function get(keys: readonly string[]): Promise<JsonValue[]>
32
+ async function get(key: unknown) {
33
+ if (Array.isArray(key)) {
34
+ if (key.length === 0 || key.length > 16) throw invalid('State reads require 1 to 16 keys.')
35
+ const keys = Array.from(key, keyOf)
36
+ if (new Set(keys).size !== keys.length) throw invalid('State keys must be distinct.')
37
+ return ((await invoke('state.get', { key: keys })).body as { values: JsonValue[] }).values
38
+ }
39
+ return ((await invoke('state.get', { key: keyOf(key) })).body as { value: JsonValue }).value
40
+ }
30
41
  return {
31
42
  state: Object.freeze({
32
- async get(key: unknown) {
33
- return ((await invoke('state.get', { key: keyOf(key) })).body as { value: JsonValue }).value
34
- },
43
+ get,
35
44
  async set(key: unknown, value: unknown) {
36
45
  await invoke('state.set', { key: keyOf(key), value: valueOf(value) })
37
46
  },
@@ -443,6 +452,23 @@ async function rejects(operation: Promise<unknown>, pattern: RegExp, message: st
443
452
  }
444
453
 
445
454
  export const runtimeConformanceCases: readonly RuntimeConformanceCase[] = [
455
+ {
456
+ name: 'reads multiple State keys in one Host call while preserving input order',
457
+ async verify(harness) {
458
+ const calls: { kind: string; payload: JsonValue }[] = []
459
+ const result = await harness.invoke({
460
+ input: null,
461
+ invocationId: 'state-batch-read',
462
+ capability: async ({ kind, payload }) => {
463
+ calls.push({ kind, payload })
464
+ return { status: 200, body: { values: [0, null, false, ''] } }
465
+ },
466
+ program: program(harness, `export default async (_, context) => context.state.get(['zero', 'missing', 'false', 'empty'])`),
467
+ })
468
+ equal(result, [0, null, false, ''], 'State batch values')
469
+ equal(calls, [{ kind: 'state.get', payload: { key: ['zero', 'missing', 'false', 'empty'] } }], 'State batch dispatch')
470
+ },
471
+ },
446
472
  {
447
473
  name: 'cancels detached State work when its Task finishes',
448
474
  async verify(harness) {
@@ -492,7 +518,15 @@ export const runtimeConformanceCases: readonly RuntimeConformanceCase[] = [
492
518
  const invalid = [undefined, NaN, Infinity, new Date(), { x: undefined }, cyclic, [undefined], { x: () => 1 }];
493
519
  const errors = [];
494
520
  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))]) {
521
+ for (const call of [
522
+ () => context.state.get(''), () => context.state.get('x'.repeat(257)),
523
+ () => context.state.get([]), () => context.state.get(['x', 'x']),
524
+ () => context.state.get(Array.from({length: 17}, (_, index) => String(index))),
525
+ () => context.state.get(['']), () => context.state.get(['汉'.repeat(86)]),
526
+ () => context.state.get(new Array(1)), () => context.state.get(null), () => context.state.get(),
527
+ () => context.state.insert('x', null), () => context.dedupe.insert([NaN]),
528
+ () => context.dedupe.has([null]), () => context.dedupe.insert(new Array(1))
529
+ ]) {
496
530
  try { await call(); errors.push('accepted'); } catch (e) { errors.push(e.code); }
497
531
  }
498
532
  return errors;
@@ -501,7 +535,7 @@ export const runtimeConformanceCases: readonly RuntimeConformanceCase[] = [
501
535
  })
502
536
  equal(
503
537
  result,
504
- Array.from({ length: 14 }, () => 'capability.invalid'),
538
+ Array.from({ length: 22 }, () => 'capability.invalid'),
505
539
  'State boundary validation',
506
540
  )
507
541
  equal(calls, 0, 'State invalid dispatch')
@@ -1051,14 +1085,13 @@ export function createBase64() {
1051
1085
  const padding = text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0
1052
1086
  const bytes = new Uint8Array((text.length / 4) * 3 - padding)
1053
1087
  for (let index = 0, offset = 0; index < text.length; index += 4) {
1054
- const digits = [0, 1, 2, 3].map((position) => {
1088
+ let value = 0
1089
+ for (let position = 0; position < 4; position++) {
1055
1090
  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]!
1091
+ const digit = code == 61 && index + position >= text.length - padding ? 0 : code < 128 ? values[code]! : 255
1092
+ if (digit == 255) throw new TypeError('Base64 content is invalid.')
1093
+ value = (value << 6) | digit
1094
+ }
1062
1095
  if (offset < bytes.length) bytes[offset++] = value >> 16
1063
1096
  if (offset < bytes.length) bytes[offset++] = (value >> 8) & 255
1064
1097
  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,42 @@ 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: 'reads selected State keys in input order with missing, falsy and independent values',
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 result = await harness.run(`export default async (_, context) => {
19
+ await context.state.set('object', { nested: [1] });
20
+ await context.state.set('zero', 0);
21
+ await context.state.set('false', false);
22
+ await context.state.set('empty', '');
23
+ const values = await context.state.get(['false', 'missing', 'object', 'zero', 'empty']);
24
+ values[2].nested.push(2);
25
+ return {result: {values, retained: await context.state.get('object'), zero: await context.state.get('zero')}};
26
+ }`)
27
+ if (!dequal(result, { values: [false, null, { nested: [1, 2] }, 0, ''], retained: { nested: [1] }, zero: 0 }))
28
+ throw new Error('State batch reads must preserve order, missing keys and independent JSON values.')
29
+ },
30
+ },
31
+ {
32
+ name: 'skips composite identities across successful Runs without depending on field order',
33
+ async verify(harness) {
34
+ const items = [
35
+ { company: 'a', id: 1 },
36
+ { company: 'b', id: 1 },
37
+ { company: 'a', id: 1 },
38
+ ]
39
+ const first = await harness.skipDuplicates({ fields: ['company', 'id'] }, items)
40
+ if (!dequal(first, { items: items.slice(0, 2), duplicates: [items[2]] }))
41
+ throw new Error('Composite identities must retain the first item in input order.')
42
+ const second = await harness.skipDuplicates({ fields: ['id', 'company'] }, items)
43
+ if (!dequal(second, { items: [], duplicates: items })) throw new Error('Successful Run history must persist independently of field order.')
44
+ const changed = await harness.skipDuplicates({ fields: ['id'] }, items)
45
+ if (!dequal(changed, { items: [items[0]], duplicates: items.slice(1) })) throw new Error('The field definition must participate in identity.')
35
46
  },
36
47
  },
37
48
  {
@@ -5,32 +5,46 @@ import { z } from 'zod'
5
5
  export { stateConformanceCases, type StateConformanceHarness } from './conformance.ts'
6
6
 
7
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({
8
+ export const stateReadLimits = { maxKeys: 16, maxBytes: 1024 * 1024 }
9
+ const keySchema = z
10
+ .string()
11
+ .min(1)
12
+ .refine((value) => new TextEncoder().encode(value).length <= 256)
13
+ export const stateQuerySchema = z.strictObject({ scope: z.enum(['draft', 'live']), values: z.literal(false).optional() })
14
+ export const stateReadSchema = z.strictObject({
10
15
  scope: z.enum(['draft', 'live']),
11
- key: z
12
- .string()
16
+ keys: z
17
+ .array(keySchema)
13
18
  .min(1)
14
- .refine((key) => new TextEncoder().encode(key).length <= 256)
15
- .optional(),
19
+ .max(stateReadLimits.maxKeys)
20
+ .refine((keys) => new Set(keys).size === keys.length, 'State keys must be distinct.'),
21
+ })
22
+ export type StateRead = z.infer<typeof stateReadSchema>
23
+ export const stateClearSchema = z.strictObject({
24
+ scope: z.enum(['draft', 'live']),
25
+ key: keySchema.optional(),
16
26
  })
17
27
  export const dedupeClearSchema = z.strictObject({ scope: z.enum(['draft', 'live']), space: z.string().regex(/^(flow|node:.+)$/) })
18
28
  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'),
29
+ export const skipDuplicatesConfigSchema = z.strictObject({
30
+ fields: z
31
+ .array(z.string().refine((field) => field.length > 0 && field.split('.').every((part) => part.length > 0), 'Use a nonempty dot-separated field path.'))
32
+ .refine((fields) => new Set(fields).size === fields.length, 'Identity fields must be distinct.')
33
+ .default([]),
23
34
  })
24
- export type DedupeConfig = z.infer<typeof dedupeConfigSchema>
25
- export interface DedupeExecutor extends DedupeConfig {
26
- readonly kind: 'dedupe'
35
+ export type SkipDuplicatesConfig = z.infer<typeof skipDuplicatesConfigSchema>
36
+ export interface SkipDuplicatesExecutor extends SkipDuplicatesConfig {
37
+ readonly kind: 'skip-duplicates'
27
38
  }
28
- export const dedupeExecutorSchema = dedupeConfigSchema.extend({ kind: z.literal('dedupe') })
39
+ export const skipDuplicatesExecutorSchema = skipDuplicatesConfigSchema.extend({ kind: z.literal('skip-duplicates') })
29
40
 
30
- export function dedupeTask(config: DedupeConfig = dedupeConfigSchema.parse({}), name = 'Remove Duplicates'): ManagedTaskDefinitionOf<DedupeExecutor> {
41
+ export function skipDuplicatesTask(
42
+ config: SkipDuplicatesConfig = skipDuplicatesConfigSchema.parse({}),
43
+ name = 'Skip Duplicates',
44
+ ): ManagedTaskDefinitionOf<SkipDuplicatesExecutor> {
31
45
  return {
32
46
  name,
33
- executor: { ...config, kind: 'dedupe' },
47
+ executor: { ...config, kind: 'skip-duplicates' },
34
48
  inputs: [{ handle: 'items', jsonSchema: { type: 'array' }, nullable: false }],
35
49
  outputs: ['items', 'duplicates'].map((handle) => ({ handle, jsonSchema: { type: 'array' }, nullable: false })),
36
50
  }
@@ -39,13 +53,13 @@ export function dedupeTask(config: DedupeConfig = dedupeConfigSchema.parse({}),
39
53
  export class StateError extends Error {
40
54
  readonly retryable = false
41
55
  readonly catchable: boolean
42
- readonly code: 'capability.invalid' | 'capability.denied' | 'dedupe.full' | 'limit-exceeded'
56
+ readonly code: 'capability.invalid' | 'capability.denied' | 'dedupe.full' | 'limit-exceeded' | 'node.input-invalid'
43
57
 
44
58
  constructor(code: StateError['code'], message: string) {
45
59
  super(message)
46
60
  this.name = 'StateError'
47
61
  this.code = code
48
- this.catchable = code === 'dedupe.full'
62
+ this.catchable = code === 'dedupe.full' || code === 'node.input-invalid'
49
63
  }
50
64
  }
51
65
 
@@ -67,63 +81,31 @@ export async function hashIdentities(ids: readonly DedupeIdentity[]): Promise<Ui
67
81
  )
68
82
  }
69
83
 
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
84
+ /** Canonical identities include the field definition and preserve scalar types. */
85
+ export function skipDuplicatesIdentities(items: JsonValue, fields: readonly string[]): string[] {
86
+ if (!Array.isArray(items)) throw new StateError('node.input-invalid', 'Skip Duplicates requires an array of items.')
87
+ const paths = fields.toSorted()
88
+ return items.map((item, index) => {
89
+ const values = (paths.length === 0 ? [''] : paths).map((field) => {
90
+ const value =
91
+ field === ''
92
+ ? item
93
+ : field
94
+ .split('.')
95
+ .reduce<JsonValue | undefined>(
96
+ (current, key) =>
97
+ current != null && typeof current === 'object' && Object.hasOwn(current, key) ? (current as Record<string, JsonValue>)[key] : undefined,
98
+ item,
99
+ )
100
+ if (typeof value !== 'string' && !(typeof value === 'number' && Number.isFinite(value)))
101
+ throw new StateError(
102
+ 'node.input-invalid',
103
+ `Item ${index + 1}, ${field === '' ? 'the item itself' : `field ${JSON.stringify(field)}`} must be a string or finite number.`,
104
+ )
105
+ return value
106
+ })
107
+ return JSON.stringify([paths, values])
112
108
  })
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
109
  }
128
110
 
129
111
  export interface FlowStateView {
@@ -136,12 +118,12 @@ export interface FlowStateView {
136
118
  readonly maxIdentities: number
137
119
  readonly maxRunWrites: number
138
120
  }
139
- readonly values: readonly { readonly key: string; readonly size: number; readonly updatedAt: number; readonly value?: JsonValue }[]
121
+ readonly values: readonly { readonly key: string; readonly size: number; readonly updatedAt: number }[]
140
122
  readonly spaces: readonly {
141
123
  readonly space: string
142
- readonly mode: 'new' | 'increasing'
143
124
  readonly count: number
144
- readonly watermark: DedupeIdentity | null
125
+ readonly pendingCount: number
126
+ readonly reservedCount: number
145
127
  readonly updatedAt: number
146
128
  }[]
147
129
  }
@@ -156,14 +138,37 @@ export const flowStateViewSchema = z.object({
156
138
  maxIdentities: z.number().positive(),
157
139
  maxRunWrites: z.number().positive(),
158
140
  }),
159
- values: z.array(z.object({ key: z.string(), size: z.number().nonnegative(), updatedAt: z.number(), value: z.json().optional() })),
141
+ values: z.array(z.strictObject({ key: z.string(), size: z.number().nonnegative(), updatedAt: z.number() })),
160
142
  spaces: z.array(
161
143
  z.object({
162
144
  space: z.string(),
163
- mode: z.enum(['new', 'increasing']),
164
145
  count: z.number().nonnegative(),
165
- watermark: z.union([z.string(), z.number(), z.null()]),
146
+ pendingCount: z.number().nonnegative(),
147
+ reservedCount: z.number().nonnegative(),
166
148
  updatedAt: z.number(),
167
149
  }),
168
150
  ),
169
151
  })
152
+
153
+ export interface FlowStateValues {
154
+ readonly version: 1
155
+ readonly scope: StateScope
156
+ readonly values: readonly { readonly key: string; readonly value: JsonValue }[]
157
+ }
158
+
159
+ export const flowStateValuesSchema = z
160
+ .object({
161
+ version: z.literal(1),
162
+ scope: z.enum(['draft', 'live']),
163
+ values: z
164
+ .array(z.strictObject({ key: keySchema, value: z.json() }))
165
+ .min(1)
166
+ .max(stateReadLimits.maxKeys),
167
+ })
168
+ .refine((response) => {
169
+ const encoder = new TextEncoder()
170
+ return (
171
+ response.values.reduce((bytes, item) => bytes + (item.value === null ? 0 : encoder.encode(JSON.stringify(item.value)).byteLength), 0) <=
172
+ stateReadLimits.maxBytes
173
+ )
174
+ }, 'State values exceed the 1 MiB read limit.')
@@ -121,6 +121,7 @@ export interface TaskContext<
121
121
  readonly blockId: string
122
122
  readonly state: {
123
123
  get(key: string): Promise<JsonValue | null>
124
+ get(keys: readonly string[]): Promise<(JsonValue | null)[]>
124
125
  set(key: string, value: JsonValue): Promise<void>
125
126
  insert(key: string, value: Exclude<JsonValue, null>): Promise<boolean>
126
127
  delete(key: string): Promise<void>