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

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 (146) hide show
  1. package/README.md +3 -1
  2. package/dist/browser/{ContentIcon-FCpKiwjp.js → ContentIcon-VZQO8VZU.js} +9 -6
  3. package/dist/browser/{FlowCanvas-dzRBnlUT.js → FlowCanvas-BNGDMaWr.js} +75 -63
  4. package/dist/browser/{createResourceDialog-BeYDdUl-.js → createResourceDialog-BmtakfVg.js} +63 -57
  5. package/dist/browser/{dialog-ZtpsFbl4.js → dialog-BcFBfRB1.js} +126 -32
  6. package/dist/browser/{exportSvg-DqXw1yf-.js → exportSvg-CG8hEUNk.js} +2 -2
  7. package/dist/browser/{flowChanges-b_5UobFj.js → flowChanges-BGo_4Ypo.js} +6063 -11285
  8. package/dist/browser/{flowWorkspace-ItojZbO4.js → flowWorkspace-BFHGev7s.js} +1065 -2570
  9. package/dist/browser/preview.css +626 -653
  10. package/dist/browser/preview.js +1907 -9663
  11. package/dist/browser/theme.css +117 -263
  12. package/dist/browser/{tooltip-wZJRB9oP.js → tooltip-BAWSUexx.js} +2 -2
  13. package/dist/browser/{typeScriptSession-CSRwWmZm.js → typeScriptSession-CRoezJPN.js} +1 -1
  14. package/dist/browser/ui.css +464 -463
  15. package/dist/browser/ui.js +4 -3
  16. package/dist/browser/workbench.css +9891 -9897
  17. package/dist/browser/workbench.d.ts +6 -2
  18. package/dist/browser/workbench.js +838 -499
  19. package/dist/browser/{workbenchSelect-M441Lz0d.js → workbenchSelect-CjK-xdm3.js} +2 -2
  20. package/dist/command/index.js +48 -50
  21. package/dist/common/authoring.d.ts +5 -5
  22. package/dist/common/authoringExamples.d.ts +3 -2
  23. package/dist/common/authoringSchema.d.ts +26 -4
  24. package/dist/common/connector-proxy.d.ts +1 -1
  25. package/dist/common/control-api-conformance.js +352 -144
  26. package/dist/common/control-api-errors.d.ts +12 -0
  27. package/dist/common/control-api.d.ts +38 -4
  28. package/dist/common/control-api.js +373 -162
  29. package/dist/common/control-requests.d.ts +22 -3
  30. package/dist/common/control-requests.js +268 -28
  31. package/dist/common/cron-trigger.d.ts +2 -1
  32. package/dist/common/decision.d.ts +4 -4
  33. package/dist/common/decision.js +5 -2
  34. package/dist/common/engine-contract.d.ts +6 -1
  35. package/dist/common/event-source-definition.d.ts +1 -1
  36. package/dist/common/event-source-view.d.ts +22 -0
  37. package/dist/common/event-source.d.ts +1 -1
  38. package/dist/common/event-source.js +24 -65
  39. package/dist/common/event-sources.d.ts +0 -21
  40. package/dist/common/flow-authoring.js +90 -20
  41. package/dist/common/flow-change-schema.d.ts +9 -1
  42. package/dist/common/flow-change.d.ts +8 -202
  43. package/dist/common/flow-change.js +96 -24
  44. package/dist/common/flow-encoding.js +89 -21
  45. package/dist/common/flow-input-value.d.ts +1 -1
  46. package/dist/common/flow-model.d.ts +121 -0
  47. package/dist/common/flow-notifications.d.ts +1 -1
  48. package/dist/common/flow-retry.d.ts +2 -4
  49. package/dist/common/flow-semantics.js +161 -80
  50. package/dist/common/flow-state.d.ts +123 -0
  51. package/dist/common/flow-state.js +5852 -0
  52. package/dist/common/flow-workflowReferences.d.ts +8 -0
  53. package/dist/common/flowInspection.d.ts +1 -1
  54. package/dist/common/http-hosts.d.ts +6 -0
  55. package/dist/common/http-request.d.ts +13 -5
  56. package/dist/common/http-request.js +23 -5
  57. package/dist/common/integration-trigger.d.ts +2 -1
  58. package/dist/common/mcp.d.ts +96 -3
  59. package/dist/common/mcp.js +355 -142
  60. package/dist/common/openapi.d.ts +4 -4
  61. package/dist/common/openapi.js +4 -1
  62. package/dist/common/poll-trigger.d.ts +2 -1
  63. package/dist/common/run-events.js +1 -0
  64. package/dist/common/runtime-contract.d.ts +37 -1
  65. package/dist/common/runtime-contract.js +717 -72
  66. package/dist/common/runtime-nodejs-compat.d.ts +44 -0
  67. package/dist/common/runtime-nodejs-compat.js +5870 -0
  68. package/dist/common/scheduler-checkpoint.d.ts +1 -1
  69. package/dist/common/scheduler-node-execution.d.ts +2 -1
  70. package/dist/common/scheduler.d.ts +2 -2
  71. package/dist/common/scheduler.js +76 -6
  72. package/dist/common/state-conformance.d.ts +16 -0
  73. package/dist/common/tags.d.ts +4 -0
  74. package/dist/common/trigger-catalog.d.ts +1 -7
  75. package/dist/common/trigger-config-options.d.ts +1 -1
  76. package/dist/common/trigger-config.d.ts +1 -1
  77. package/dist/common/trigger-contract.d.ts +2 -1
  78. package/dist/common/trigger-localization.d.ts +8 -2
  79. package/dist/common/trigger-model.d.ts +101 -0
  80. package/dist/common/webhook-response.d.ts +1 -1
  81. package/dist/common/webhook-trigger.d.ts +3 -1
  82. package/dist/common/workflow.d.ts +3 -12
  83. package/dist/common/workflow.js +25 -23
  84. package/dist/index.d.ts +52 -14
  85. package/package.json +9 -1
  86. package/src/base/common/json.ts +1 -1
  87. package/src/connector/common/proxy.ts +1 -1
  88. package/src/control/common/api.ts +90 -16
  89. package/src/control/common/authoring.ts +6 -1
  90. package/src/control/common/authoringExamples.ts +8 -0
  91. package/src/control/common/authoringSchema.ts +26 -13
  92. package/src/control/common/authoringTools.ts +1 -1
  93. package/src/control/common/conformance.ts +1 -1
  94. package/src/control/common/connectorDecoders.ts +1 -1
  95. package/src/control/common/errors.ts +6 -0
  96. package/src/control/common/eventSources.ts +2 -29
  97. package/src/control/common/flowDecoders.ts +1 -0
  98. package/src/control/common/mcp.ts +16 -0
  99. package/src/control/common/requests.ts +10 -2
  100. package/src/control/common/tags.ts +20 -0
  101. package/src/control/common/triggerCatalog.ts +1 -8
  102. package/src/decision/common/decision.ts +15 -7
  103. package/src/event-source/common/definition.ts +1 -1
  104. package/src/event-source/common/index.ts +1 -1
  105. package/src/event-source/common/view.ts +20 -0
  106. package/src/execution/common/checkpoint.ts +1 -1
  107. package/src/execution/common/events.ts +3 -0
  108. package/src/execution/common/node-execution.ts +4 -1
  109. package/src/execution/common/scheduler.ts +3 -1
  110. package/src/flow/common/change.ts +60 -193
  111. package/src/flow/common/changeSchema.ts +39 -4
  112. package/src/flow/common/connectionUsage.ts +1 -1
  113. package/src/flow/common/errorHandling.ts +1 -1
  114. package/src/flow/common/inputValue.ts +1 -1
  115. package/src/flow/common/model.ts +130 -0
  116. package/src/flow/common/modules.ts +1 -1
  117. package/src/flow/common/portChanges.ts +1 -1
  118. package/src/flow/common/retry.ts +2 -4
  119. package/src/flow/common/semantics.ts +31 -4
  120. package/src/flow/common/webhookResponse.ts +1 -1
  121. package/src/flow/common/workflowReferences.ts +31 -0
  122. package/src/http/common/authBindings.ts +6 -9
  123. package/src/http/common/hosts.ts +13 -0
  124. package/src/http/common/httpRequest.ts +30 -15
  125. package/src/openapi/common/openapi.ts +9 -5
  126. package/src/{execution → runtime}/common/engineContract.ts +23 -15
  127. package/src/runtime/common/nodejsCompat.ts +275 -0
  128. package/src/runtime/common/nodejsGuest.generated.json +47 -0
  129. package/src/runtime/common/runtime.ts +1254 -0
  130. package/src/state/common/conformance.ts +86 -0
  131. package/src/state/common/state.ts +169 -0
  132. package/src/trigger/common/config.ts +1 -1
  133. package/src/trigger/common/configOptions.ts +1 -1
  134. package/src/trigger/common/contract.ts +2 -1
  135. package/src/trigger/common/cron.ts +2 -1
  136. package/src/trigger/common/integration.ts +2 -1
  137. package/src/trigger/common/model.ts +87 -0
  138. package/src/trigger/common/poll.ts +2 -1
  139. package/src/trigger/common/webhook.ts +3 -1
  140. package/src/trigger/providers/fieldDescriptions.ts +1 -1
  141. package/src/trigger/providers/localization.ts +9 -2
  142. package/src/trigger/providers/remote.ts +1 -1
  143. package/src/types/index.ts +55 -15
  144. package/src/workflow/common/workflow.ts +3 -44
  145. package/src/execution/common/runtime.ts +0 -563
  146. /package/src/{control → connector}/common/providerIconSprite.ts +0 -0
@@ -0,0 +1,275 @@
1
+ import type { EngineContract } from './engineContract.ts'
2
+
3
+ import * as z from 'zod'
4
+ import guestModules from './nodejsGuest.generated.json' with { type: 'json' }
5
+
6
+ /**
7
+ * Guest modules of the Node compatibility engine, keyed by engine module path.
8
+ *
9
+ * A Host evaluates `engine/web.mjs` after installing the guest bridge, then `engine/globals.mjs`, then the Task.
10
+ * The bridge consists of `__hostPrimitive(request, buffer?, extra?)`, which forwards to {@link NodejsPrimitives.call};
11
+ * `__hostTimer(callback, delay, repeat)` and `__hostClearTimer(id)`, which schedule guest callbacks; and
12
+ * `__hostBuiltin(group)`, which synchronously evaluates {@link nodejsBuiltinGroupModule} and returns its namespace.
13
+ */
14
+ export const nodejsGuestModules: Readonly<Record<string, string>> = guestModules
15
+ export const nodejsWebModule = 'engine/web.mjs'
16
+ export const nodejsGlobalsModule = 'engine/globals.mjs'
17
+
18
+ const builtinGroups: ReadonlySet<string> = new Set(['abort', 'buffer', 'crypto', 'process', 'streams', 'url'])
19
+
20
+ /** Returns the engine module of a lazily loaded builtin group requested through `__hostBuiltin`. */
21
+ export function nodejsBuiltinGroupModule(group: string): string {
22
+ if (!builtinGroups.has(group)) throw new TypeError('Unknown builtin group.')
23
+ return group == 'crypto' ? 'engine/deferred/crypto.mjs' : `engine/globals/${group}.mjs`
24
+ }
25
+
26
+ /** Resolves a builtin specifier or an import between engine modules; other specifiers belong to the Host closure. */
27
+ export function resolveNodejsModule(contract: EngineContract, specifier: string, referrer: string): string | undefined {
28
+ if (contract.builtinModules?.has(specifier)) return `engine/builtin/${specifier.replace(/^node:/, '')}.mjs`
29
+ if (!referrer.startsWith('engine/')) return undefined
30
+ const resolved = new URL(specifier, `https://modules.invalid/${referrer}`).pathname.slice(1)
31
+ return Object.hasOwn(nodejsGuestModules, resolved) ? resolved : undefined
32
+ }
33
+
34
+ type Encoding = 'utf8' | 'utf-8' | 'hex' | 'base64' | 'ascii' | 'latin1' | 'binary' | 'ucs2' | 'ucs-2' | 'utf16le' | 'utf-16le'
35
+ type ZlibMethod = 'gzipSync' | 'gunzipSync' | 'deflateSync' | 'inflateSync' | 'deflateRawSync' | 'inflateRawSync' | 'unzipSync'
36
+
37
+ interface PlatformHash {
38
+ update(data: Uint8Array): unknown
39
+ digest(): Uint8Array
40
+ destroy(): unknown
41
+ }
42
+
43
+ /** Platform implementations behind the primitives; Node.js and Workers with `nodejs_compat` supply their own modules. */
44
+ export interface NodejsPrimitivePlatform {
45
+ readonly Buffer: {
46
+ from(text: string, encoding: Encoding): Uint8Array
47
+ from(buffer: ArrayBuffer, offset: number, length: number): { toString(encoding: Encoding): string }
48
+ byteLength(text: string, encoding: Encoding): number
49
+ }
50
+ createHash(algorithm: string): PlatformHash
51
+ createHmac(algorithm: string, key: Uint8Array): PlatformHash
52
+ readonly zlib: Readonly<Record<ZlibMethod, (input: Uint8Array, options: object) => Uint8Array>>
53
+ }
54
+
55
+ export interface NodejsPrimitives {
56
+ /** Serves one `__hostPrimitive` request; failures are returned as a serialized guest error. */
57
+ call(request: string, buffer?: ArrayBuffer, extra?: ArrayBuffer): string | ArrayBuffer
58
+ close(): void
59
+ }
60
+
61
+ const binaryLimit = 1024 * 1024
62
+ const range = { offset: z.number().int().nonnegative(), length: z.number().int().min(0).max(binaryLimit) }
63
+ const encoding = z.enum(['utf8', 'utf-8', 'hex', 'base64', 'ascii', 'latin1', 'binary', 'ucs2', 'ucs-2', 'utf16le', 'utf-16le'])
64
+ const operation = z.discriminatedUnion('operation', [
65
+ z.object({
66
+ operation: z.literal('url'),
67
+ input: z.string(),
68
+ base: z.string().optional(),
69
+ change: z
70
+ .object({ key: z.enum(['href', 'protocol', 'username', 'password', 'host', 'hostname', 'port', 'pathname', 'search', 'hash']), value: z.string() })
71
+ .optional(),
72
+ entries: z.array(z.tuple([z.string(), z.string()])).optional(),
73
+ }),
74
+ z.object({ operation: z.literal('params.parse'), input: z.string() }),
75
+ z.object({ operation: z.literal('params.stringify'), entries: z.array(z.tuple([z.string(), z.string()])) }),
76
+ z.object({ operation: z.literal('encode'), text: z.string() }),
77
+ z.object({ operation: z.literal('decoder'), label: z.string(), fatal: z.boolean(), ignoreBOM: z.boolean() }),
78
+ z.object({
79
+ operation: z.literal('decode'),
80
+ id: z.number().int().positive().optional(),
81
+ label: z.string(),
82
+ fatal: z.boolean(),
83
+ ignoreBOM: z.boolean(),
84
+ ...range,
85
+ stream: z.boolean(),
86
+ }),
87
+ z.object({ operation: z.enum(['atob', 'btoa']), text: z.string() }),
88
+ z.object({ operation: z.literal('random'), length: z.number().int().min(0).max(65536) }),
89
+ z.object({ operation: z.literal('uuid') }),
90
+ z.object({ operation: z.enum(['buffer.from', 'buffer.byteLength']), text: z.string(), encoding }),
91
+ z.object({ operation: z.literal('buffer.toString'), encoding, ...range }),
92
+ z.object({ operation: z.literal('hash.create'), algorithm: z.string(), hmac: z.boolean(), ...range, length: z.number().int().min(0).max(65536) }),
93
+ z.object({ operation: z.literal('hash.update'), id: z.number().int().positive(), ...range }),
94
+ z.object({ operation: z.enum(['hash.digest', 'hash.dispose']), id: z.number().int().positive() }),
95
+ z.object({
96
+ operation: z.literal('zlib'),
97
+ method: z.enum(['gzipSync', 'gunzipSync', 'deflateSync', 'inflateSync', 'deflateRawSync', 'inflateRawSync', 'unzipSync']),
98
+ ...range,
99
+ dictionary: z.object(range).optional(),
100
+ options: z.object({
101
+ level: z.number().optional(),
102
+ windowBits: z.number().optional(),
103
+ memLevel: z.number().optional(),
104
+ strategy: z.number().optional(),
105
+ chunkSize: z.number().int().min(64).max(binaryLimit).optional(),
106
+ flush: z.number().optional(),
107
+ finishFlush: z.number().optional(),
108
+ maxOutputLength: z.number().int().min(1).max(binaryLimit).optional(),
109
+ }),
110
+ }),
111
+ ])
112
+
113
+ /** Creates the per-Task primitive state behind the guest bridge, bounded by call count, bytes, and live instances. */
114
+ export function createNodejsPrimitives(platform: NodejsPrimitivePlatform): NodejsPrimitives {
115
+ // Only streaming decodes keep a host decoder, so Tasks may create any number of guest TextDecoders.
116
+ const decoders = new Map<number, TextDecoder>()
117
+ let nextDecoder = 0
118
+ const hashes = new Map<number, PlatformHash>()
119
+ let nextHash = 0
120
+ let remainingBytes = 64 * binaryLimit
121
+ let calls = 0
122
+ let closed = false
123
+ const spend = (bytes: number): void => {
124
+ remainingBytes -= bytes
125
+ if (remainingBytes < 0) throw new Error('Native builtin byte limit exceeded.')
126
+ }
127
+ const binaryResult = (bytes: Uint8Array): ArrayBuffer => {
128
+ if (bytes.byteLength > binaryLimit) throw new RangeError('Builtin result too large.')
129
+ spend(bytes.byteLength)
130
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
131
+ }
132
+ const binaryView = (buffer: ArrayBuffer | undefined, offset: number, length: number): Uint8Array => {
133
+ const byteLength = buffer?.byteLength ?? 0
134
+ if (byteLength > binaryLimit || offset + length > byteLength) throw new RangeError('Invalid buffer range.')
135
+ spend(length)
136
+ return new Uint8Array(buffer ?? new ArrayBuffer(0), offset, length)
137
+ }
138
+ const serve = (source: string, buffer: ArrayBuffer | undefined, extra: ArrayBuffer | undefined): unknown => {
139
+ const request = operation.parse(JSON.parse(source))
140
+ switch (request.operation) {
141
+ case 'buffer.from':
142
+ spend(request.text.length * 2)
143
+ return binaryResult(platform.Buffer.from(request.text, request.encoding))
144
+ case 'buffer.byteLength':
145
+ spend(request.text.length * 2)
146
+ return { value: platform.Buffer.byteLength(request.text, request.encoding) }
147
+ case 'buffer.toString': {
148
+ const view = binaryView(buffer, request.offset, request.length)
149
+ const value = platform.Buffer.from(view.buffer as ArrayBuffer, view.byteOffset, view.byteLength).toString(request.encoding)
150
+ spend(value.length * 2)
151
+ return { value }
152
+ }
153
+ case 'hash.create': {
154
+ if (hashes.size >= 256) throw new Error('Hash instance limit exceeded.')
155
+ const key = binaryView(buffer, request.offset, request.length)
156
+ hashes.set(++nextHash, request.hmac ? platform.createHmac(request.algorithm, key) : platform.createHash(request.algorithm))
157
+ return { value: nextHash }
158
+ }
159
+ case 'hash.update': {
160
+ const hash = hashes.get(request.id)
161
+ if (hash == null) throw new Error('Digest already called or hash destroyed.')
162
+ try {
163
+ hash.update(binaryView(buffer, request.offset, request.length))
164
+ } catch (error) {
165
+ hashes.delete(request.id)
166
+ hash.destroy()
167
+ throw error
168
+ }
169
+ return { value: undefined }
170
+ }
171
+ case 'hash.digest': {
172
+ const hash = hashes.get(request.id)
173
+ if (hash == null) throw new Error('Digest already called or hash destroyed.')
174
+ hashes.delete(request.id)
175
+ try {
176
+ return binaryResult(hash.digest())
177
+ } finally {
178
+ hash.destroy()
179
+ }
180
+ }
181
+ case 'hash.dispose':
182
+ hashes.get(request.id)?.destroy()
183
+ hashes.delete(request.id)
184
+ return { value: undefined }
185
+ case 'zlib': {
186
+ const input = binaryView(buffer, request.offset, request.length)
187
+ const options = {
188
+ ...request.options,
189
+ maxOutputLength: request.options.maxOutputLength ?? binaryLimit,
190
+ dictionary: request.dictionary == null ? undefined : binaryView(extra, request.dictionary.offset, request.dictionary.length),
191
+ }
192
+ return binaryResult(platform.zlib[request.method](input, options))
193
+ }
194
+ case 'url': {
195
+ let url: URL
196
+ try {
197
+ url = new URL(request.input, request.base)
198
+ } catch (error) {
199
+ if (!(error instanceof TypeError)) throw error
200
+ return { value: null }
201
+ }
202
+ if (request.change != null) url[request.change.key] = request.change.value
203
+ if (request.entries != null) url.search = new URLSearchParams(request.entries).toString()
204
+ const { hash, host, hostname, href, origin, password, pathname, port, protocol, search, username } = url
205
+ return { value: { hash, host, hostname, href, origin, password, pathname, port, protocol, search, username, entries: [...url.searchParams] } }
206
+ }
207
+ case 'params.parse':
208
+ return { value: [...new URLSearchParams(request.input)] }
209
+ case 'params.stringify':
210
+ return { value: new URLSearchParams(request.entries).toString() }
211
+ case 'encode': {
212
+ const encoded = new TextEncoder().encode(request.text)
213
+ if (encoded.byteLength > binaryLimit) throw new Error('Builtin result too large.')
214
+ return encoded.buffer
215
+ }
216
+ case 'decoder':
217
+ return { value: { encoding: new TextDecoder(request.label, { fatal: request.fatal, ignoreBOM: request.ignoreBOM }).encoding } }
218
+ case 'decode': {
219
+ const byteLength = buffer?.byteLength ?? 0
220
+ if (request.offset + request.length > byteLength) throw new RangeError('Invalid buffer range.')
221
+ const stored = request.id == null ? undefined : decoders.get(request.id)
222
+ if (request.id != null && stored == null) throw new TypeError('Unknown TextDecoder.')
223
+ const decoder = stored ?? new TextDecoder(request.label, { fatal: request.fatal, ignoreBOM: request.ignoreBOM })
224
+ let text: string
225
+ try {
226
+ text = decoder.decode(new Uint8Array(buffer ?? new ArrayBuffer(0), request.offset, request.length), { stream: request.stream })
227
+ } catch (error) {
228
+ if (request.id != null) decoders.delete(request.id)
229
+ throw error
230
+ }
231
+ if (!request.stream) {
232
+ if (request.id != null) decoders.delete(request.id)
233
+ return { value: { text } }
234
+ }
235
+ if (stored != null) return { value: { id: request.id, text } }
236
+ if (decoders.size >= 256) throw new Error('TextDecoder limit exceeded.')
237
+ decoders.set(++nextDecoder, decoder)
238
+ return { value: { id: nextDecoder, text } }
239
+ }
240
+ case 'atob':
241
+ return { value: atob(request.text) }
242
+ case 'btoa':
243
+ return { value: btoa(request.text) }
244
+ case 'random':
245
+ return crypto.getRandomValues(new Uint8Array(request.length)).buffer
246
+ case 'uuid':
247
+ return { value: crypto.randomUUID() }
248
+ }
249
+ }
250
+ return {
251
+ call(source, buffer, extra) {
252
+ if (closed) throw new Error('Task completed.')
253
+ if (++calls > 100_000) throw new Error('Builtin call limit exceeded.')
254
+ if (source.length > binaryLimit) throw new Error('Builtin payload too large.')
255
+ try {
256
+ const result = serve(source, buffer, extra)
257
+ if (result instanceof ArrayBuffer) return result
258
+ const serialized = JSON.stringify(result)
259
+ if (serialized.length > binaryLimit) throw new Error('Builtin result too large.')
260
+ return serialized
261
+ } catch (error) {
262
+ const code = error instanceof Error && 'code' in error && typeof error.code == 'string' ? error.code : undefined
263
+ return JSON.stringify({
264
+ error: { name: error instanceof Error ? error.name : 'Error', message: String(error instanceof Error ? error.message : error), code },
265
+ })
266
+ }
267
+ },
268
+ close() {
269
+ closed = true
270
+ decoders.clear()
271
+ for (const hash of hashes.values()) hash.destroy()
272
+ hashes.clear()
273
+ },
274
+ }
275
+ }