@gradio/core 1.0.0-dev.0 → 1.0.0

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 (109) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/src/Blocks.svelte +534 -1001
  5. package/dist/src/Blocks.svelte.d.ts +32 -45
  6. package/dist/src/Embed.svelte +82 -55
  7. package/dist/src/Embed.svelte.d.ts +39 -30
  8. package/dist/src/Login.svelte +33 -29
  9. package/dist/src/Login.svelte.d.ts +21 -19
  10. package/dist/src/MountComponents.svelte +19 -25
  11. package/dist/src/MountComponents.svelte.d.ts +5 -28
  12. package/dist/src/{init.d.ts → _init.d.ts} +5 -4
  13. package/dist/src/{init.js → _init.js} +31 -108
  14. package/dist/src/api_docs/ApiBanner.svelte +12 -8
  15. package/dist/src/api_docs/ApiBanner.svelte.d.ts +22 -20
  16. package/dist/src/api_docs/ApiDocs.svelte +356 -247
  17. package/dist/src/api_docs/ApiDocs.svelte.d.ts +27 -24
  18. package/dist/src/api_docs/ApiRecorder.svelte +6 -3
  19. package/dist/src/api_docs/ApiRecorder.svelte.d.ts +19 -17
  20. package/dist/src/api_docs/CodeSnippet.svelte +122 -48
  21. package/dist/src/api_docs/CodeSnippet.svelte.d.ts +29 -25
  22. package/dist/src/api_docs/CopyButton.svelte +69 -13
  23. package/dist/src/api_docs/CopyButton.svelte.d.ts +18 -16
  24. package/dist/src/api_docs/CopyMarkdown.svelte +734 -0
  25. package/dist/src/api_docs/CopyMarkdown.svelte.d.ts +37 -0
  26. package/dist/src/api_docs/EndpointDetail.svelte +81 -23
  27. package/dist/src/api_docs/EndpointDetail.svelte.d.ts +23 -18
  28. package/dist/src/api_docs/IconArrowUpRight.svelte +34 -0
  29. package/dist/src/api_docs/IconArrowUpRight.svelte.d.ts +20 -0
  30. package/dist/src/api_docs/IconCaret.svelte +39 -0
  31. package/dist/src/api_docs/IconCaret.svelte.d.ts +20 -0
  32. package/dist/src/api_docs/IconHuggingChat.svelte +62 -0
  33. package/dist/src/api_docs/IconHuggingChat.svelte.d.ts +20 -0
  34. package/dist/src/api_docs/InputPayload.svelte +17 -11
  35. package/dist/src/api_docs/InputPayload.svelte.d.ts +25 -23
  36. package/dist/src/api_docs/InstallSnippet.svelte +9 -6
  37. package/dist/src/api_docs/InstallSnippet.svelte.d.ts +18 -16
  38. package/dist/src/api_docs/MCPSnippet.svelte +139 -126
  39. package/dist/src/api_docs/MCPSnippet.svelte.d.ts +60 -58
  40. package/dist/src/api_docs/NoApi.svelte +7 -4
  41. package/dist/src/api_docs/NoApi.svelte.d.ts +20 -18
  42. package/dist/src/api_docs/ParametersSnippet.svelte +8 -6
  43. package/dist/src/api_docs/ParametersSnippet.svelte.d.ts +21 -19
  44. package/dist/src/api_docs/PercentileChart.svelte +125 -0
  45. package/dist/src/api_docs/PercentileChart.svelte.d.ts +22 -0
  46. package/dist/src/api_docs/RecordingSnippet.svelte +124 -110
  47. package/dist/src/api_docs/RecordingSnippet.svelte.d.ts +24 -22
  48. package/dist/src/api_docs/ResponseSnippet.svelte +7 -5
  49. package/dist/src/api_docs/ResponseSnippet.svelte.d.ts +21 -19
  50. package/dist/src/api_docs/Settings.svelte +73 -62
  51. package/dist/src/api_docs/Settings.svelte.d.ts +25 -23
  52. package/dist/src/api_docs/SettingsBanner.svelte +11 -8
  53. package/dist/src/api_docs/SettingsBanner.svelte.d.ts +20 -18
  54. package/dist/src/api_docs/TryButton.svelte +5 -3
  55. package/dist/src/api_docs/TryButton.svelte.d.ts +19 -17
  56. package/dist/src/api_docs/img/IconCheck.svelte +33 -0
  57. package/dist/src/api_docs/img/IconCheck.svelte.d.ts +26 -0
  58. package/dist/src/api_docs/img/IconCopy.svelte +40 -0
  59. package/dist/src/api_docs/img/IconCopy.svelte.d.ts +26 -0
  60. package/dist/src/api_docs/img/clear.svelte.d.ts +22 -21
  61. package/dist/src/dependency.d.ts +145 -0
  62. package/dist/src/dependency.js +668 -0
  63. package/dist/src/init.svelte.d.ts +78 -0
  64. package/dist/src/init.svelte.js +469 -0
  65. package/dist/src/init_utils.d.ts +32 -0
  66. package/dist/src/init_utils.js +73 -0
  67. package/dist/src/lang/en.json +10 -1
  68. package/dist/src/lang/get_lang_names.js +0 -3
  69. package/dist/src/lang/ru.json +10 -1
  70. package/dist/src/stores.d.ts +0 -21
  71. package/dist/src/stories/I18nMultiLanguageTestComponent.svelte +5 -3
  72. package/dist/src/stories/I18nMultiLanguageTestComponent.svelte.d.ts +16 -14
  73. package/dist/src/stories/I18nTestSetup.svelte +14 -10
  74. package/dist/src/stories/I18nTestSetup.svelte.d.ts +18 -16
  75. package/dist/src/types.d.ts +31 -26
  76. package/index.ts +1 -1
  77. package/package.json +62 -63
  78. package/src/Blocks.svelte +360 -1063
  79. package/src/MountComponents.svelte +17 -27
  80. package/src/{init.ts → _init.ts} +49 -126
  81. package/src/api_docs/ApiDocs.svelte +84 -62
  82. package/src/api_docs/CodeSnippet.svelte +83 -24
  83. package/src/api_docs/CopyButton.svelte +61 -7
  84. package/src/api_docs/CopyMarkdown.svelte +734 -0
  85. package/src/api_docs/EndpointDetail.svelte +73 -17
  86. package/src/api_docs/IconArrowUpRight.svelte +34 -0
  87. package/src/api_docs/IconCaret.svelte +39 -0
  88. package/src/api_docs/IconHuggingChat.svelte +62 -0
  89. package/src/api_docs/MCPSnippet.svelte +44 -73
  90. package/src/api_docs/ParametersSnippet.svelte +1 -1
  91. package/src/api_docs/PercentileChart.svelte +125 -0
  92. package/src/api_docs/ResponseSnippet.svelte +1 -1
  93. package/src/api_docs/Settings.svelte +11 -11
  94. package/src/api_docs/img/IconCheck.svelte +33 -0
  95. package/src/api_docs/img/IconCopy.svelte +40 -0
  96. package/src/dependency.ts +909 -0
  97. package/src/init.svelte.ts +717 -0
  98. package/src/init_utils.ts +99 -0
  99. package/src/lang/en.json +10 -1
  100. package/src/lang/get_lang_names.js +0 -3
  101. package/src/lang/ru.json +10 -1
  102. package/src/stores.ts +22 -22
  103. package/src/types.ts +55 -43
  104. package/dist/src/Render.svelte +0 -105
  105. package/dist/src/Render.svelte.d.ts +0 -31
  106. package/dist/src/RenderComponent.svelte +0 -72
  107. package/dist/src/RenderComponent.svelte.d.ts +0 -33
  108. package/src/Render.svelte +0 -126
  109. package/src/RenderComponent.svelte +0 -91
@@ -0,0 +1,909 @@
1
+ import type {
2
+ ComponentMeta,
3
+ DependencyTypes,
4
+ Dependency as IDependency,
5
+ LayoutNode,
6
+ Payload
7
+ } from "./types.js";
8
+ import { AsyncFunction } from "./init_utils";
9
+ import { Client, type client_return } from "@gradio/client";
10
+ import { LoadingStatus, type LoadingStatusArgs } from "@gradio/statustracker";
11
+ import type { ToastMessage } from "@gradio/statustracker";
12
+ import type { StatusMessage, ValidationError } from "@gradio/client";
13
+ const MESSAGE_QUOTE_RE = /^'([^]+)'$/;
14
+
15
+ const NOVALUE = Symbol("NOVALUE");
16
+ /**
17
+ * A dependency as used by the frontend
18
+ * This class represents a discrete dependency that can be triggered by an event
19
+ * It is responsible calling the appropriate functions and reporting back results
20
+ */
21
+ export class Dependency {
22
+ id: number;
23
+ inputs: number[];
24
+ outputs: number[];
25
+ cancels: number[];
26
+ pending = false;
27
+ trigger_modes: "once" | "multiple" | "always_last";
28
+ event_args: Record<string, unknown> = {};
29
+ targets: [number, string][] = [];
30
+ connection_type: "stream" | "sse";
31
+
32
+ // if this dependency has any then, success or failure triggers
33
+ triggers: [number, "success" | "failure" | "all"][] = [];
34
+
35
+ // the id of the original event_id that caused this dependency to run
36
+ // in the case of chained events, it would be the id of the initial trigger
37
+ original_trigger_id: number | null = null;
38
+ show_progress_on: number[] | null = null;
39
+ component_prop_inputs: number[] = [];
40
+ show_progress: "full" | "minimal" | "hidden";
41
+
42
+ functions: {
43
+ frontend?: (...args: unknown[]) => Promise<unknown[]>;
44
+ backend: boolean;
45
+ backend_js?: (...args: unknown[]) => Promise<unknown[]>;
46
+ };
47
+
48
+ constructor(dep_config: IDependency) {
49
+ this.id = dep_config.id;
50
+ this.original_trigger_id = dep_config.id;
51
+ this.inputs = dep_config.inputs;
52
+ this.outputs = dep_config.outputs;
53
+ this.connection_type = dep_config.connection;
54
+ this.show_progress = dep_config.show_progress;
55
+ this.functions = {
56
+ frontend: dep_config.js
57
+ ? process_frontend_fn(
58
+ dep_config.js,
59
+ dep_config.backend_fn,
60
+ dep_config.inputs.length,
61
+ dep_config.outputs.length
62
+ )
63
+ : undefined,
64
+ backend: dep_config.backend_fn,
65
+ backend_js: dep_config.js_implementation
66
+ ? new AsyncFunction(
67
+ `let result = await (${dep_config.js_implementation})(...arguments);
68
+ return (!Array.isArray(result)) ? [result] : result;`
69
+ )
70
+ : undefined
71
+ };
72
+ this.targets = dep_config.targets;
73
+ this.cancels = dep_config.cancels;
74
+ this.trigger_modes = dep_config.trigger_mode;
75
+ this.show_progress_on = dep_config.show_progress_on || null;
76
+ this.component_prop_inputs = dep_config.component_prop_inputs || [];
77
+
78
+ for (let i = 0; i < dep_config.event_specific_args?.length || 0; i++) {
79
+ const key = dep_config.event_specific_args[i];
80
+ this.event_args[key] = dep_config[key] ?? null;
81
+ }
82
+ }
83
+
84
+ async run(
85
+ client: client_return,
86
+ data_payload: unknown[],
87
+ event_data: unknown,
88
+ target_id: number | null | undefined
89
+ ): Promise<
90
+ | { type: "data"; data: unknown[] }
91
+ | { type: "void"; data: null }
92
+ | { type: "submit"; data: ReturnType<client_return["submit"]> }
93
+ > {
94
+ let _data_payload = data_payload;
95
+
96
+ // if the function is backend_js, then it's the entire event
97
+ // no need to chain frontend and backend
98
+ if (this.functions.backend_js) {
99
+ const data = await this.functions.backend_js(..._data_payload);
100
+ return { type: "data", data };
101
+ }
102
+
103
+ // If it has a js implementation, the correct behavior
104
+ // is to run that and pass the output to the backend
105
+ if (this.functions.frontend) {
106
+ _data_payload = await this.functions.frontend(data_payload);
107
+ }
108
+
109
+ if (this.functions.backend) {
110
+ return {
111
+ type: "submit",
112
+ data: client.submit(this.id, _data_payload, event_data, target_id)
113
+ };
114
+ } else if (this.functions.frontend) {
115
+ return { type: "data", data: _data_payload };
116
+ }
117
+ return { type: "void", data: null };
118
+ }
119
+
120
+ add_trigger(dep_id: number, condition: "success" | "failure" | "all") {
121
+ this.triggers.push([dep_id, condition]);
122
+ }
123
+
124
+ get_triggers(): { success: number[]; failure: number[]; all: number[] } {
125
+ return {
126
+ success: this.triggers
127
+ .filter(([, condition]) => condition === "success")
128
+ .map(([id]) => id),
129
+ failure: this.triggers
130
+ .filter(([, condition]) => condition === "failure")
131
+ .map(([id]) => id),
132
+ all: this.triggers
133
+ .filter(([, condition]) => condition === "all")
134
+ .map(([id]) => id)
135
+ };
136
+ }
137
+ }
138
+
139
+ interface DispatchFunction {
140
+ type: "fn";
141
+ event_data: unknown;
142
+ fn_index?: number;
143
+ target_id?: number;
144
+ }
145
+
146
+ interface DispatchEvent {
147
+ type: "event";
148
+ event_name?: string;
149
+ target_id?: number;
150
+ event_data: unknown;
151
+ }
152
+
153
+ /**
154
+ * Manages all dependencies for an app acting as a bridge between app state and Dependencies
155
+ * Responsible for registering dependencies and dispatching events to them
156
+ * It is also responsible for orchestrating dependencies based on the follwing:
157
+ * - Cancelling dependencies
158
+ * - Ensuring individual dependencies respect `trigger_mode`
159
+ * - Managing then, success and failure events
160
+ * - Ensuring that dependencies bound to the same id are treated a single unit
161
+ * - updating loading states
162
+ * - updating component states
163
+ */
164
+ export class DependencyManager {
165
+ dependencies_by_fn: Map<number, Dependency> = new Map();
166
+ dependencies_by_event: Map<string, Dependency[]> = new Map();
167
+ render_id_deps = new Map<number, Set<number>>();
168
+
169
+ submissions: Map<number, ReturnType<Client["submit"]>> = new Map();
170
+ client: Client;
171
+ queue: Set<number> = new Set();
172
+ add_to_api_calls: (payload: Payload) => void;
173
+
174
+ update_state_cb: (
175
+ id: number,
176
+ state: Record<string, unknown>,
177
+ check_visibility?: boolean
178
+ ) => Promise<void>;
179
+ get_state_cb: (id: number) => Promise<Record<string, unknown> | null>;
180
+ rerender_cb: (components: ComponentMeta[], layout: LayoutNode) => void;
181
+ log_cb: (
182
+ title: string,
183
+ message: string,
184
+ fn_index: number,
185
+ type: ToastMessage["type"],
186
+ duration?: number | null,
187
+ visible?: boolean
188
+ ) => void;
189
+ loading_stati = new LoadingStatus();
190
+
191
+ constructor(
192
+ dependencies: IDependency[],
193
+ client: Client,
194
+ update_state_cb: (
195
+ id: number,
196
+ state: Record<string, unknown>,
197
+ check_visibility?: boolean
198
+ ) => Promise<void>,
199
+ get_state_cb: (id: number) => Promise<Record<string, unknown> | null>,
200
+ rerender_cb: (components: ComponentMeta[], layout: LayoutNode) => void,
201
+ log_cb: (
202
+ title: string,
203
+ message: string,
204
+ fn_index: number,
205
+ type: ToastMessage["type"],
206
+ duration?: number | null,
207
+ visible?: boolean
208
+ ) => void,
209
+ add_to_api_calls: (payload: Payload) => void
210
+ ) {
211
+ this.add_to_api_calls = add_to_api_calls;
212
+ this.log_cb = log_cb;
213
+ this.update_state_cb = update_state_cb;
214
+ this.get_state_cb = get_state_cb;
215
+ this.rerender_cb = rerender_cb;
216
+ this.reload(
217
+ dependencies,
218
+ update_state_cb,
219
+ get_state_cb,
220
+ rerender_cb,
221
+ client
222
+ );
223
+ }
224
+
225
+ reload(
226
+ dependencies: IDependency[],
227
+ update_state,
228
+ get_state,
229
+ rerender,
230
+ client
231
+ ) {
232
+ const { by_id, by_event } = this.create(dependencies);
233
+ this.dependencies_by_event = by_event;
234
+ this.dependencies_by_fn = by_id;
235
+ this.client = client;
236
+ this.update_state_cb = update_state;
237
+ this.get_state_cb = get_state;
238
+ this.rerender_cb = rerender;
239
+ for (const [dep_id, dep] of this.dependencies_by_fn) {
240
+ for (const [output_id] of dep.targets) {
241
+ this.set_event_args(output_id, dep.event_args);
242
+ }
243
+ }
244
+ this.register_loading_stati(by_id);
245
+ }
246
+ register_loading_stati(deps: Map<number, Dependency>): void {
247
+ for (const [_, dep] of deps) {
248
+ this.loading_stati.register(
249
+ dep.id,
250
+ dep.show_progress_on || dep.outputs,
251
+ dep.inputs,
252
+ dep.show_progress
253
+ );
254
+ }
255
+ }
256
+
257
+ clear_loading_status(component_id: number): void {
258
+ this.loading_stati.clear(component_id);
259
+ }
260
+
261
+ async update_loading_stati_state() {
262
+ for (const [component_id, loading_status] of Object.entries(
263
+ this.loading_stati.current
264
+ )) {
265
+ this.update_state_cb(
266
+ Number(component_id),
267
+ {
268
+ loading_status: loading_status
269
+ },
270
+ false
271
+ );
272
+ }
273
+ }
274
+
275
+ dispatch_state_change_events(result: StatusMessage): void {
276
+ if (result.changed_state_ids) {
277
+ for (const changed_id of result.changed_state_ids) {
278
+ const change_dep = this.dependencies_by_event.get(
279
+ "change-" + changed_id
280
+ );
281
+ change_dep?.forEach((dep) => {
282
+ this.dispatch({
283
+ type: "fn",
284
+ fn_index: dep.id,
285
+ target_id: changed_id,
286
+ event_data: null
287
+ });
288
+ });
289
+ }
290
+ }
291
+ }
292
+
293
+ /** Dispatches an event to the appropriate dependency
294
+ * @param event_name the name of the event
295
+ * @param target_id the id of the component that triggered the event
296
+ * @param event_data any additional data to pass to the dependency
297
+ * @returns a value if there is no backend fn, a 'submission' if there is a backend fn, or null if there is no dependency
298
+ */
299
+ async dispatch(event_meta: DispatchFunction | DispatchEvent): Promise<void> {
300
+ let deps: Dependency[] | undefined;
301
+ if (event_meta.type === "fn") {
302
+ const dep = this.dependencies_by_fn.get(event_meta.fn_index!);
303
+ if (dep) deps = [dep];
304
+ } else {
305
+ deps = this.dependencies_by_event.get(
306
+ `${event_meta.event_name}-${event_meta.target_id}`
307
+ );
308
+ }
309
+
310
+ for (let i = 0; i < (deps?.length || 0); i++) {
311
+ const dep = deps ? deps[i] : undefined;
312
+ if (dep) {
313
+ this.cancel(dep.cancels);
314
+
315
+ const dispatch_status = should_dispatch(
316
+ dep.trigger_modes,
317
+ this.submissions.has(dep.id)
318
+ );
319
+
320
+ if (dispatch_status === "skip") {
321
+ continue;
322
+ } else if (dispatch_status === "defer") {
323
+ this.queue.add(dep.id);
324
+ continue;
325
+ }
326
+
327
+ // No loading status for js-only deps
328
+ if (dep.functions.backend) {
329
+ this.loading_stati.update({
330
+ status: "pending",
331
+ fn_index: dep.id,
332
+ stream_state: null
333
+ });
334
+ this.update_loading_stati_state();
335
+ }
336
+
337
+ const data_payload = await this.gather_state(
338
+ dep.inputs,
339
+ dep.component_prop_inputs
340
+ );
341
+ const unset_args = await Promise.all(
342
+ dep.targets.map(([output_id]) =>
343
+ this.set_event_args(output_id, dep.event_args)
344
+ )
345
+ );
346
+
347
+ const { success, failure, all } = dep.get_triggers();
348
+
349
+ try {
350
+ let target_id: number | null = null;
351
+ if (
352
+ event_meta.target_id !== undefined ||
353
+ event_meta.type === "event"
354
+ ) {
355
+ target_id = event_meta.target_id || null;
356
+ } else {
357
+ target_id = dep.original_trigger_id;
358
+ }
359
+
360
+ if (
361
+ dep.connection_type === "stream" &&
362
+ this.submissions.has(dep.id)
363
+ ) {
364
+ const submission = this.submissions.get(dep.id);
365
+ let payload: Payload = {
366
+ fn_index: dep.id,
367
+ data: data_payload,
368
+ event_data: event_meta.event_data
369
+ };
370
+ submission!.send_chunk(payload);
371
+ unset_args.forEach((fn) => fn());
372
+ continue;
373
+ }
374
+
375
+ this.add_to_api_calls({
376
+ fn_index: dep.id,
377
+ data: data_payload,
378
+ event_data: event_meta.event_data,
379
+ trigger_id: target_id
380
+ });
381
+ const dep_submission = await dep.run(
382
+ this.client,
383
+ data_payload,
384
+ event_meta.event_data,
385
+ target_id
386
+ );
387
+
388
+ if (dep_submission.type === "void") {
389
+ unset_args.forEach((fn) => fn());
390
+ } else if (dep_submission.type === "data") {
391
+ await this.handle_data(dep.outputs, dep_submission.data);
392
+ unset_args.forEach((fn) => fn());
393
+ } else {
394
+ let stream_state: "open" | "closed" | "waiting" | null = null;
395
+
396
+ if (
397
+ dep.connection_type === "stream" &&
398
+ !this.submissions.has(dep.id)
399
+ ) {
400
+ stream_state = "waiting";
401
+ }
402
+
403
+ this.submissions.set(dep.id, dep_submission.data);
404
+ let index = 0;
405
+ // fn for this?
406
+ submit_loop: for await (const result of dep_submission.data) {
407
+ if (index === 0) {
408
+ // Clear out previously set validation errors
409
+ dep.inputs.forEach((input_id) => {
410
+ this.update_state_cb(
411
+ input_id,
412
+ {
413
+ loading_status: {
414
+ validation_error: null
415
+ }
416
+ },
417
+ false
418
+ );
419
+ });
420
+ }
421
+ index += 1;
422
+ if (result === null) continue;
423
+ if (result.type === "data") {
424
+ await this.handle_data(dep.outputs, result.data);
425
+ }
426
+ if (result.type === "status") {
427
+ if (
428
+ result.original_msg === "process_starts" &&
429
+ dep.connection_type === "stream"
430
+ ) {
431
+ stream_state = "open";
432
+ }
433
+ const { fn_index, ...status } = result;
434
+
435
+ // handle status updates here
436
+ if (result.stage === "complete") {
437
+ stream_state = "closed";
438
+ success.forEach((dep_id) => {
439
+ this.dispatch({
440
+ type: "fn",
441
+ fn_index: dep_id,
442
+ event_data: null,
443
+ target_id: target_id as number | undefined
444
+ });
445
+ });
446
+ this.dispatch_state_change_events(result);
447
+ // @ts-ignore
448
+ this.loading_stati.update({
449
+ ...status,
450
+ status: status.stage,
451
+ fn_index: dep.id,
452
+ stream_state
453
+ });
454
+ this.update_loading_stati_state();
455
+ break submit_loop;
456
+ } else if (result.stage === "generating") {
457
+ this.dispatch_state_change_events(result);
458
+ // @ts-ignore
459
+ this.loading_stati.update({
460
+ ...status,
461
+ status: status.stage,
462
+ fn_index: dep.id,
463
+ stream_state
464
+ });
465
+ this.update_loading_stati_state();
466
+ } else if (result.stage === "error") {
467
+ if (Array.isArray(result?.message)) {
468
+ result.message.forEach((m: ValidationError, i) => {
469
+ this.update_state_cb(
470
+ dep.inputs[i],
471
+ {
472
+ loading_status: {
473
+ validation_error: !m.is_valid ? m.message : null,
474
+ show_validation_error: true
475
+ }
476
+ },
477
+ false
478
+ );
479
+ });
480
+
481
+ // Manually set the output statuses to null
482
+ // Doing this in update_loading_stati_state would
483
+ // validation errors set above
484
+ // For example, if the input component is an output component (chatinterface)
485
+ dep.outputs.forEach((output_id) => {
486
+ if (dep.inputs.includes(output_id)) return;
487
+ this.update_state_cb(
488
+ output_id,
489
+ {
490
+ loading_status: {
491
+ status: null
492
+ }
493
+ },
494
+ false
495
+ );
496
+ });
497
+ unset_args.forEach((fn) => fn());
498
+ this.submissions.delete(dep.id);
499
+ if (this.queue.has(dep.id)) {
500
+ this.queue.delete(dep.id);
501
+ this.dispatch(event_meta);
502
+ }
503
+ return;
504
+ }
505
+
506
+ const _message = result?.message?.replace(
507
+ MESSAGE_QUOTE_RE,
508
+ (_, b) => b
509
+ );
510
+ this.log_cb(
511
+ result._title ?? "Error",
512
+ _message,
513
+ fn_index,
514
+ "error",
515
+ status.duration,
516
+ status.visible
517
+ );
518
+ throw new Error("Dependency function failed");
519
+ } else {
520
+ // @ts-ignore
521
+ this.loading_stati.update({
522
+ ...status,
523
+ status: status.stage,
524
+ fn_index: dep.id,
525
+ stream_state
526
+ });
527
+ this.update_loading_stati_state();
528
+ }
529
+ }
530
+
531
+ if (result.type === "render") {
532
+ this.loading_stati.update({
533
+ status: "complete",
534
+ fn_index: dep.id,
535
+ stream_state: null
536
+ });
537
+ this.update_loading_stati_state();
538
+ const { layout, components, render_id, dependencies } =
539
+ result.data;
540
+
541
+ this.rerender_cb(components, layout);
542
+ // update dependencies
543
+ const { by_id, by_event } = this.create(
544
+ dependencies as unknown as IDependency[]
545
+ );
546
+ this.register_loading_stati(by_id);
547
+
548
+ by_id.forEach((dep) =>
549
+ this.dependencies_by_fn.set(dep.id, dep)
550
+ );
551
+ by_event.forEach((dep, key) =>
552
+ this.dependencies_by_event.set(key, dep)
553
+ );
554
+ const current_deps = this.render_id_deps.get(render_id);
555
+ if (current_deps) {
556
+ current_deps.forEach((old_dep_id) => {
557
+ if (!by_id.has(old_dep_id)) {
558
+ this.dependencies_by_fn.delete(old_dep_id);
559
+ }
560
+ });
561
+ }
562
+ this.render_id_deps.set(
563
+ render_id,
564
+ new Set(Array.from(by_id.keys()))
565
+ );
566
+ this.register_loading_stati(by_id);
567
+ break submit_loop;
568
+ }
569
+
570
+ if (result.type === "log") {
571
+ this.handle_log(result);
572
+ }
573
+ }
574
+ all.forEach((dep_id) => {
575
+ this.dispatch({
576
+ type: "fn",
577
+ fn_index: dep_id,
578
+ event_data: null,
579
+ target_id: target_id as number | undefined
580
+ });
581
+ });
582
+ unset_args.forEach((fn) => fn());
583
+ this.submissions.delete(dep.id);
584
+
585
+ if (this.queue.has(dep.id)) {
586
+ this.queue.delete(dep.id);
587
+ this.dispatch(event_meta);
588
+ }
589
+ }
590
+ } catch (error) {
591
+ this.loading_stati.update({
592
+ status: "error",
593
+ fn_index: dep.id,
594
+ eta: 0,
595
+ queue: false,
596
+ stream_state: null
597
+ });
598
+ this.update_loading_stati_state();
599
+ this.submissions.delete(dep.id);
600
+ failure.forEach((dep_id) => {
601
+ this.dispatch({
602
+ type: "fn",
603
+ fn_index: dep_id,
604
+ event_data: null
605
+ });
606
+ });
607
+ }
608
+ }
609
+ }
610
+ return;
611
+ }
612
+
613
+ /**
614
+ * Creates a map of dependencies for easy lookup
615
+ *
616
+ * @param dependencies the list of dependencies from the backend
617
+ * @returns a map of dependencies keyed by `${event_name}-${target_id}`
618
+ * */
619
+ create(dependencies: IDependency[]): {
620
+ by_id: Map<number, Dependency>;
621
+ by_event: Map<string, Dependency[]>;
622
+ } {
623
+ const _deps_by_id = new Map<number, Dependency>();
624
+ const _deps_by_event = new Map<string, Dependency[]>();
625
+ const then_triggers: [number, number, "success" | "failure" | "all"][] = [];
626
+
627
+ for (const dep_config of dependencies) {
628
+ const dependency = new Dependency(dep_config);
629
+
630
+ for (const [target_id, event_name] of dep_config.targets) {
631
+ // if the key is already present, add it to the list. Otherwise, create a new element with the list
632
+ if (!_deps_by_event.has(`${event_name}-${target_id}`)) {
633
+ _deps_by_event.set(`${event_name}-${target_id}`, []);
634
+ }
635
+ _deps_by_event.get(`${event_name}-${target_id}`)?.push(dependency);
636
+ }
637
+
638
+ _deps_by_id.set(dep_config.id, dependency);
639
+
640
+ if (dep_config.trigger_after !== undefined) {
641
+ const then_mode = dep_config.trigger_only_on_failure
642
+ ? "failure"
643
+ : dep_config.trigger_only_on_success
644
+ ? "success"
645
+ : "all";
646
+
647
+ then_triggers.push([
648
+ dep_config.id,
649
+ dep_config.trigger_after,
650
+ then_mode
651
+ ]);
652
+ }
653
+ }
654
+
655
+ for (const [dep_id, trigger_after, condition] of then_triggers) {
656
+ const dependency = _deps_by_id.get(trigger_after);
657
+ if (dependency) {
658
+ dependency.add_trigger(dep_id, condition);
659
+ dependency.original_trigger_id = walk_after_to_original(
660
+ dependencies,
661
+ trigger_after
662
+ );
663
+ }
664
+ }
665
+
666
+ return { by_id: _deps_by_id, by_event: _deps_by_event };
667
+ }
668
+
669
+ handle_log(msg: LogMessage): void {
670
+ const { title, log, fn_index, level, duration, visible } = msg;
671
+
672
+ this.log_cb(title, log, fn_index, level, duration, visible);
673
+ }
674
+
675
+ /**
676
+ * Updates the state of the outputs based on the data received from the dependency
677
+ *
678
+ * @param outputs the ids of the output components
679
+ * @param data the data to update the components with
680
+ * */
681
+ async handle_data(outputs: number[], data: unknown[]) {
682
+ await Promise.all(
683
+ outputs.map(async (output_id, i) => {
684
+ const _data = data[i] === undefined ? NOVALUE : data[i];
685
+ if (_data === NOVALUE) return;
686
+
687
+ if (is_prop_update(_data)) {
688
+ let pending_visibility_update = false;
689
+ let pending_visibility_value = null;
690
+ for (const [update_key, update_value] of Object.entries(_data)) {
691
+ if (update_key === "__type__") continue;
692
+ if (update_key === "visible") {
693
+ pending_visibility_update = true;
694
+ pending_visibility_value = update_value;
695
+ continue;
696
+ }
697
+ await this.update_state_cb(
698
+ outputs[i],
699
+ {
700
+ [update_key]: update_value
701
+ },
702
+ false
703
+ );
704
+ }
705
+ if (pending_visibility_update) {
706
+ await this.update_state_cb(
707
+ outputs[i],
708
+ {
709
+ visible: pending_visibility_value
710
+ },
711
+ true
712
+ );
713
+ }
714
+ } else {
715
+ await this.update_state_cb(output_id, { value: _data }, false);
716
+ }
717
+ })
718
+ );
719
+ }
720
+
721
+ /**
722
+ * Gathers the current state of the inputs
723
+ *
724
+ * @param ids the ids of the components to gather state from
725
+ * @param prop_indices the indices (relative to ids array) that should return all component props instead of just the value
726
+ * @returns an array of the current state of the components, in the same order as the ids
727
+ */
728
+ async gather_state(
729
+ ids: number[],
730
+ prop_indices: number[] = []
731
+ ): Promise<(unknown | null)[]> {
732
+ return (await Promise.all(ids.map((id) => this.get_state_cb(id)))).map(
733
+ (state, index) => {
734
+ if (prop_indices.includes(index)) {
735
+ return state ?? null;
736
+ }
737
+ return state?.value ?? null;
738
+ }
739
+ );
740
+ }
741
+
742
+ /** Sets the event arguments for a specific component
743
+ *
744
+ * @param id the id of the component to set the event arguments for
745
+ * @param args the event arguments to set
746
+ * @returns a function that can be called to reset the event arguments to their previous values
747
+ */
748
+ async set_event_args(
749
+ id: number,
750
+ args: Record<string, unknown>
751
+ ): Promise<() => void> {
752
+ let current_args: Record<string, unknown> = {};
753
+ const current_state = await this.get_state_cb?.(id);
754
+ if (!current_state) return () => {};
755
+ for (const [key] of Object.entries(args)) {
756
+ current_args[key] = current_state?.[key] ?? null;
757
+ }
758
+
759
+ if (Object.keys(args).length === 0) {
760
+ return () => {
761
+ // do nothing
762
+ };
763
+ }
764
+
765
+ await this.update_state_cb(id, args, false);
766
+
767
+ return () => {
768
+ this.update_state_cb(id, current_args, false);
769
+ };
770
+ }
771
+
772
+ async cancel(ids: number[] | undefined): Promise<void> {
773
+ if (!ids) return;
774
+
775
+ for (const id of ids) {
776
+ const submission = this.submissions.get(id);
777
+ if (submission) {
778
+ await submission.cancel();
779
+ this.submissions.delete(id);
780
+ }
781
+ }
782
+ }
783
+
784
+ dispatch_load_events() {
785
+ this.dependencies_by_fn.forEach((dep) => {
786
+ dep.targets.forEach(([target_id, event_name]) => {
787
+ if (event_name === "load") {
788
+ this.dispatch({
789
+ type: "fn",
790
+ fn_index: dep.id,
791
+ event_data: null,
792
+ target_id: target_id
793
+ });
794
+ }
795
+ });
796
+ });
797
+ }
798
+
799
+ get_fns_from_targets(target_id: number): number[] {
800
+ const fn_indices: number[] = [];
801
+ this.dependencies_by_event.forEach((deps, key) => {
802
+ const [, dep_target_id] = key.split("-");
803
+ if (Number(dep_target_id) === target_id) {
804
+ deps.forEach((dep) => {
805
+ fn_indices.push(dep.id);
806
+ });
807
+ }
808
+ });
809
+ return fn_indices;
810
+ }
811
+
812
+ close_stream(id: number): void {
813
+ const fn_ids = this.get_fns_from_targets(id);
814
+
815
+ for (const fn_id of fn_ids) {
816
+ const submission = this.submissions.get(fn_id);
817
+ if (submission) {
818
+ submission.close_stream();
819
+ this.submissions.delete(fn_id);
820
+ }
821
+
822
+ this.loading_stati.update({
823
+ status: "complete",
824
+ fn_index: fn_id,
825
+ eta: 0,
826
+ queue: false,
827
+ stream_state: "closed"
828
+ });
829
+ }
830
+
831
+ this.update_loading_stati_state();
832
+ }
833
+ }
834
+
835
+ function is_prop_update(payload: unknown): payload is Record<string, unknown> {
836
+ return (
837
+ typeof payload === "object" &&
838
+ payload !== null &&
839
+ "__type__" in payload &&
840
+ payload?.__type__ === "update"
841
+ );
842
+ }
843
+
844
+ function should_dispatch(
845
+ mode: Dependency["trigger_modes"],
846
+ is_running: boolean
847
+ ): "run" | "skip" | "defer" {
848
+ if (!is_running) return "run";
849
+
850
+ if (mode === "always_last") {
851
+ return "defer";
852
+ } else if (mode === "multiple") {
853
+ return "run";
854
+ } else if (mode === "once") {
855
+ return "skip";
856
+ }
857
+ return "run";
858
+ }
859
+
860
+ /**
861
+ * Takes a string of source code and returns a function that can be called with arguments
862
+ * @param source the source code
863
+ * @param backend_fn if there is also a backend function
864
+ * @param input_length the number of inputs
865
+ * @param output_length the number of outputs
866
+ * @returns The function, or null if the source code is invalid or missing
867
+ */
868
+ export function process_frontend_fn(
869
+ source: string,
870
+ backend_fn: boolean,
871
+ input_length: number,
872
+ output_length: number
873
+ ): (...args: unknown[]) => Promise<unknown[]> {
874
+ const wrap = backend_fn ? input_length === 1 : output_length === 1;
875
+ try {
876
+ return new AsyncFunction(
877
+ "__fn_args",
878
+ ` let result = await (${source})(...__fn_args);
879
+ if (typeof result === "undefined") return [];
880
+ return (${wrap} && !Array.isArray(result)) ? [result] : result;`
881
+ );
882
+ } catch (e) {
883
+ throw e;
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Walks the dependency graph to find the original trigger ID for a given dependency.
889
+ * @param dependency_map The map of all dependencies.
890
+ * @param dep_id The ID of the dependency to start from.
891
+ * @returns The ID of the original trigger dependency, or the input ID if not found.
892
+ */
893
+ function walk_after_to_original(dependency_map: IDependency[], dep_id: number) {
894
+ // TODO: hoist this cache later so it is useful across multiple calls
895
+ let cache = new Map<number, IDependency>();
896
+ let current_id = dep_id;
897
+ let safety_counter = 0;
898
+ while (safety_counter < 100) {
899
+ const dep =
900
+ cache.get(current_id) || dependency_map.find((d) => d.id === current_id);
901
+ if (!dep) break;
902
+ cache.set(dep.id, dep);
903
+ if (dep.trigger_after === null || dep.trigger_after === undefined) break;
904
+
905
+ current_id = dep.trigger_after;
906
+ safety_counter += 1;
907
+ }
908
+ return current_id;
909
+ }