@marimo-team/islands 0.19.8-dev5 → 0.19.8-dev50

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 (147) hide show
  1. package/dist/{Combination-Bg-xN8JV.js → Combination-BTMrlhzT.js} +11 -10
  2. package/dist/{ConnectedDataExplorerComponent-DewsKLl2.js → ConnectedDataExplorerComponent-BAeQ8DWw.js} +11 -11
  3. package/dist/{ImageComparisonComponent-Bijp8beW.js → ImageComparisonComponent-DkEXPki_.js} +2 -2
  4. package/dist/{any-language-editor-DZc6NCTp.js → any-language-editor-D0UQItkS.js} +6 -6
  5. package/dist/{architectureDiagram-VXUJARFQ--NkyBn9Y.js → architectureDiagram-VXUJARFQ-DPPYVq8H.js} +4 -4
  6. package/dist/assets/__vite-browser-external-WSlCcXn_.js +1 -0
  7. package/dist/assets/worker-DUYMdbtA.js +73 -0
  8. package/dist/{blockDiagram-VD42YOAC-DEZZaTW0.js → blockDiagram-VD42YOAC-BA5N05Y9.js} +4 -4
  9. package/dist/{button-BWvsJ2Wr.js → button-Cy0ElmIm.js} +2 -2
  10. package/dist/{c4Diagram-YG6GDRKO-Bj7hwWCO.js → c4Diagram-YG6GDRKO-DJLzuGJJ.js} +3 -3
  11. package/dist/{channel-B_QrFrGg.js → channel-Dob5kWXR.js} +1 -1
  12. package/dist/{check-CM_kewwn.js → check-DkNR52Mm.js} +1 -1
  13. package/dist/{chunk-5FQGJX7Z-D5VFKHmt.js → chunk-5FQGJX7Z-BEb20Lzt.js} +3 -3
  14. package/dist/{chunk-ABZYJK2D-SZPYmRzN.js → chunk-ABZYJK2D-BXTC53mt.js} +1 -1
  15. package/dist/{chunk-ATLVNIR6-BI_WwH1o.js → chunk-ATLVNIR6-BJDjUR_c.js} +1 -1
  16. package/dist/{chunk-B4BG7PRW-BlI9Gm1l.js → chunk-B4BG7PRW-DzmUUpfH.js} +4 -4
  17. package/dist/{chunk-DI55MBZ5-BXxemMn5.js → chunk-DI55MBZ5-gTd3J8Tu.js} +4 -4
  18. package/dist/{chunk-EXTU4WIE-CzWtDV99.js → chunk-EXTU4WIE-DyoOs5QX.js} +1 -1
  19. package/dist/{chunk-JA3XYJ7Z-DQ-2ARfa.js → chunk-JA3XYJ7Z-BGnAIbOP.js} +2 -2
  20. package/dist/{chunk-JZLCHNYA-CVfjf2vv.js → chunk-JZLCHNYA-CIRgweVQ.js} +4 -4
  21. package/dist/{chunk-N4CR4FBY-BCZvQ7Jq.js → chunk-N4CR4FBY-DKSvXAIS.js} +5 -5
  22. package/dist/{chunk-QN33PNHL-DY_2Q2zl.js → chunk-QN33PNHL-B6zC8BTi.js} +1 -1
  23. package/dist/{chunk-QXUST7PY-BMCjAVR_.js → chunk-QXUST7PY-C7750n_u.js} +5 -5
  24. package/dist/{chunk-S3R3BYOJ-Ddu0H4Qa.js → chunk-S3R3BYOJ-CBkH6JZZ.js} +1 -1
  25. package/dist/{chunk-TZMSLE5B-C2wVlbMl.js → chunk-TZMSLE5B-DObGL7xi.js} +1 -1
  26. package/dist/{classDiagram-2ON5EDUG-D-g7zbyO.js → classDiagram-2ON5EDUG-B9pkKjjc.js} +9 -9
  27. package/dist/{classDiagram-v2-WZHVMYZB-C7v5zNRD.js → classDiagram-v2-WZHVMYZB-CRhhA0tV.js} +9 -9
  28. package/dist/{click-outside-container-BCN5BtVO.js → click-outside-container-DNfggvIW.js} +1 -1
  29. package/dist/{code-block-37QAKDTI-eUgXqGNG.js → code-block-37QAKDTI-u5kgjqmr.js} +2 -2
  30. package/dist/{compiler-runtime-DHFVbq0b.js → compiler-runtime-B_OLMU9S.js} +1 -1
  31. package/dist/{copy-B59Bw3-w.js → copy-DRaXIb_a.js} +3 -3
  32. package/dist/{dagre-6UL2VRFP-DKIPL74O.js → dagre-6UL2VRFP-C2C2XxsB.js} +6 -6
  33. package/dist/{data-grid-overlay-editor-COyFwFmE.js → data-grid-overlay-editor-BXqtz1ia.js} +4 -4
  34. package/dist/{diagram-PSM6KHXK-CVTrAZaP.js → diagram-PSM6KHXK-DHBY-94p.js} +5 -5
  35. package/dist/{diagram-QEK2KX5R-BqHBzu3x.js → diagram-QEK2KX5R-CgMshOwn.js} +3 -3
  36. package/dist/{diagram-S2PKOQOG-CJD6owcg.js → diagram-S2PKOQOG-F1KPva3Y.js} +3 -3
  37. package/dist/{dist-Co5PD8Fb.js → dist-BBYTEAvO.js} +1 -1
  38. package/dist/{erDiagram-Q2GNP2WA-CqOceSf9.js → erDiagram-Q2GNP2WA-18gGng8V.js} +9 -9
  39. package/dist/{error-banner-C7KLpECd.js → error-banner-D2zjeN_a.js} +5 -5
  40. package/dist/{esm-D4WO8J3G.js → esm-CgRNPmz8.js} +6 -6
  41. package/dist/{flowDiagram-NV44I4VS-K7-DUifo.js → flowDiagram-NV44I4VS-iHFiHYe0.js} +9 -9
  42. package/dist/{ganttDiagram-JELNMOA3-BwUFY9Nu.js → ganttDiagram-JELNMOA3-D7GixxiF.js} +2 -2
  43. package/dist/{gitGraphDiagram-NY62KEGX-CjGRtLb1.js → gitGraphDiagram-NY62KEGX-CJFHytRK.js} +2 -2
  44. package/dist/{glide-data-editor-C3T7HsLi.js → glide-data-editor-BYwb17Bf.js} +13 -13
  45. package/dist/{infoDiagram-WHAUD3N6-DNhmDn-6.js → infoDiagram-WHAUD3N6-B5Lkh3A9.js} +2 -2
  46. package/dist/{journeyDiagram-XKPGCS4Q-BOdK47P8.js → journeyDiagram-XKPGCS4Q-CV_9R9iP.js} +2 -2
  47. package/dist/{kanban-definition-3W4ZIXB7-A0JC9d0g.js → kanban-definition-3W4ZIXB7-Dp21D5Ym.js} +6 -6
  48. package/dist/{katex-DJyOeQ91.js → katex-CX2BKujk.js} +1 -1
  49. package/dist/{katex-Dm9nZf6A.js → katex-Db0k5oV_.js} +1 -1
  50. package/dist/{label-C4PtQcza.js → label-CxU5JNBW.js} +6 -6
  51. package/dist/main.js +2000 -1887
  52. package/dist/mermaid-4DMBBIKO-BhDCqnO1.js +6 -0
  53. package/dist/{mermaid-Bqp2Xw99.js → mermaid-B__BZSXU.js} +39 -39
  54. package/dist/{mhchem-BqdXeZVX.js → mhchem-w1tkUnWr.js} +1 -1
  55. package/dist/{mindmap-definition-VGOIOE7T-CS6nKN_L.js → mindmap-definition-VGOIOE7T-B_5mfdYp.js} +8 -8
  56. package/dist/{number-overlay-editor-Bz_bDJQb.js → number-overlay-editor-D-4WQAGX.js} +2 -2
  57. package/dist/{pieDiagram-ADFJNKIX-DSa60Grk.js → pieDiagram-ADFJNKIX-B-DGEopK.js} +3 -3
  58. package/dist/{quadrantDiagram-AYHSOK5B-CFnMbP2J.js → quadrantDiagram-AYHSOK5B-M_yRSIZn.js} +1 -1
  59. package/dist/{react-DdA8EBol.js → react-Bs6Z0kvn.js} +1 -1
  60. package/dist/{react-dom-DJW8xUDg.js → react-dom-CqtLRVZP.js} +2 -2
  61. package/dist/{react-plotly-jVjTu07w.js → react-plotly-BuRa9xtI.js} +1 -1
  62. package/dist/{react-vega-DgHpnZ04.js → react-vega-3WcLHYC7.js} +2 -2
  63. package/dist/{react-vega-CjiPWyw0.js → react-vega-DLFvGrpJ.js} +1 -1
  64. package/dist/{requirementDiagram-UZGBJVZJ-ytLQrFTk.js → requirementDiagram-UZGBJVZJ-9Wt82hOZ.js} +8 -8
  65. package/dist/{sankeyDiagram-TZEHDZUN-KQqXDoky.js → sankeyDiagram-TZEHDZUN-x_aTXZeN.js} +1 -1
  66. package/dist/{sequenceDiagram-WL72ISMW-ByLI04T5.js → sequenceDiagram-WL72ISMW-CXXmJqiQ.js} +3 -3
  67. package/dist/{slides-component-BVjvNo92.js → slides-component-Dp-y50K9.js} +4 -4
  68. package/dist/{spec-Dmb1KfK3.js → spec-HoYHAQo2.js} +6 -6
  69. package/dist/{stateDiagram-FKZM4ZOC-Dfz8vBbP.js → stateDiagram-FKZM4ZOC-CiSKS_Mx.js} +9 -9
  70. package/dist/{stateDiagram-v2-4FDKWEC3-DRYoLdT5.js → stateDiagram-v2-4FDKWEC3-A43Itnjp.js} +9 -9
  71. package/dist/style.css +1 -1
  72. package/dist/{timeline-definition-IT6M3QCI-CO48XU1B.js → timeline-definition-IT6M3QCI-DR26eWb4.js} +1 -1
  73. package/dist/{types-CzEZ3EWT.js → types-Bb-6p8hv.js} +8 -8
  74. package/dist/{useAsyncData-BjNwqCfS.js → useAsyncData-Dyq3DyOF.js} +3 -3
  75. package/dist/{useDeepCompareMemoize-CfoxVor3.js → useDeepCompareMemoize-BhZZsis0.js} +12 -8
  76. package/dist/{useIframeCapabilities-BBO_R0ww.js → useIframeCapabilities-DurI5SJh.js} +2 -2
  77. package/dist/{useTheme-BYG2SH8J.js → useTheme-SlKl8MlS.js} +5 -6
  78. package/dist/{vega-component-rDX7xwxH.js → vega-component-DCxUyPnb.js} +10 -10
  79. package/dist/{xychartDiagram-PRI3JC2R-CUIfjNVD.js → xychartDiagram-PRI3JC2R-BcVxCRox.js} +4 -4
  80. package/dist/{zod-DITCj31F.js → zod-bjADtMKr.js} +3 -3
  81. package/package.json +18 -18
  82. package/src/components/app-config/ai-config.tsx +11 -2
  83. package/src/components/app-config/optional-features.tsx +1 -1
  84. package/src/components/app-config/user-config-form.tsx +0 -54
  85. package/src/components/chat/__tests__/useFileState.test.tsx +93 -0
  86. package/src/components/chat/acp/__tests__/state.test.ts +69 -0
  87. package/src/components/chat/acp/agent-panel.tsx +26 -77
  88. package/src/components/chat/acp/state.ts +6 -6
  89. package/src/components/chat/chat-components.tsx +114 -1
  90. package/src/components/chat/chat-panel.tsx +79 -134
  91. package/src/components/chat/chat-utils.ts +42 -0
  92. package/src/components/data-table/__tests__/data-table.test.tsx +94 -2
  93. package/src/components/editor/actions/useCellActionButton.tsx +14 -1
  94. package/src/components/editor/ai/add-cell-with-ai.tsx +85 -53
  95. package/src/components/editor/ai/ai-completion-editor.tsx +15 -38
  96. package/src/components/editor/cell/CreateCellButton.tsx +2 -1
  97. package/src/components/editor/cell/code/cell-editor.tsx +12 -0
  98. package/src/components/editor/chrome/panels/packages-panel.tsx +12 -9
  99. package/src/components/editor/database/__tests__/__snapshots__/as-code.test.ts.snap +15 -0
  100. package/src/components/editor/database/__tests__/as-code.test.ts +8 -0
  101. package/src/components/editor/database/as-code.ts +3 -0
  102. package/src/components/editor/database/schemas.ts +9 -0
  103. package/src/components/editor/renderers/cell-array.tsx +2 -1
  104. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -0
  105. package/src/components/pages/gallery-page.tsx +37 -6
  106. package/src/core/MarimoApp.tsx +12 -8
  107. package/src/core/ai/context/providers/file.ts +1 -1
  108. package/src/core/cells/__tests__/cells.test.ts +120 -0
  109. package/src/core/cells/__tests__/session.test.ts +37 -1
  110. package/src/core/cells/cells.ts +14 -0
  111. package/src/core/cells/session.ts +20 -8
  112. package/src/core/codemirror/language/languages/markdown.ts +7 -0
  113. package/src/core/config/feature-flag.tsx +0 -4
  114. package/src/core/dom/uiregistry.ts +4 -1
  115. package/src/core/islands/__tests__/bridge.test.ts +7 -2
  116. package/src/core/islands/bridge.ts +1 -1
  117. package/src/core/islands/main.ts +7 -0
  118. package/src/core/network/types.ts +2 -2
  119. package/src/core/run-app.tsx +11 -4
  120. package/src/core/static/__tests__/files.test.ts +195 -1
  121. package/src/core/static/files.ts +39 -9
  122. package/src/core/wasm/bridge.ts +1 -1
  123. package/src/core/websocket/useMarimoKernelConnection.tsx +5 -15
  124. package/src/plugins/core/registerReactComponent.tsx +9 -1
  125. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +164 -0
  126. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +93 -168
  127. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +37 -123
  128. package/src/plugins/impl/anywidget/__tests__/model.test.ts +128 -122
  129. package/src/{utils/__tests__/data-views.test.ts → plugins/impl/anywidget/__tests__/serialization.test.ts} +42 -96
  130. package/src/plugins/impl/anywidget/model.ts +348 -223
  131. package/src/plugins/impl/anywidget/schemas.ts +32 -0
  132. package/src/{utils/data-views.ts → plugins/impl/anywidget/serialization.ts} +13 -36
  133. package/src/plugins/impl/anywidget/types.ts +27 -0
  134. package/src/plugins/impl/chat/chat-ui.tsx +22 -20
  135. package/src/utils/Deferred.ts +21 -0
  136. package/src/utils/__tests__/blob.test.ts +3 -3
  137. package/src/utils/__tests__/id-tree.test.ts +22 -7
  138. package/src/utils/__tests__/mime-types.test.ts +8 -10
  139. package/src/utils/__tests__/url-parser.test.ts +22 -0
  140. package/src/utils/blob.ts +14 -27
  141. package/src/utils/id-tree.tsx +11 -19
  142. package/src/utils/json/base64.ts +38 -8
  143. package/src/utils/mime-types.ts +5 -5
  144. package/src/utils/url-parser.ts +1 -1
  145. package/dist/assets/__vite-browser-external-DRa9CT_O.js +0 -1
  146. package/dist/assets/worker-SqntmiwV.js +0 -73
  147. package/dist/mermaid-4DMBBIKO-o3xNphpD.js +0 -6
@@ -1,35 +1,49 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
 
4
- import type { AnyModel } from "@anywidget/types";
4
+ import type { AnyModel, AnyWidget, Experimental } from "@anywidget/types";
5
5
  import { debounce } from "lodash-es";
6
- import { z } from "zod";
6
+ import type { NotificationMessageData } from "@/core/kernel/messages";
7
7
  import { getRequestClient } from "@/core/network/requests";
8
+ import {
9
+ decodeFromWire,
10
+ serializeBuffersToBase64,
11
+ } from "@/plugins/impl/anywidget/serialization";
8
12
  import { assertNever } from "@/utils/assertNever";
9
13
  import { Deferred } from "@/utils/Deferred";
10
- import { decodeFromWire, serializeBuffersToBase64 } from "@/utils/data-views";
11
- import { throwNotImplemented } from "@/utils/functions";
12
- import type { Base64String } from "@/utils/json/base64";
14
+ import {
15
+ type Base64String,
16
+ base64ToDataView,
17
+ dataViewToBase64,
18
+ } from "@/utils/json/base64";
13
19
  import { Logger } from "@/utils/Logger";
14
-
15
- export type EventHandler = (...args: any[]) => void;
20
+ import { repl } from "@/utils/repl";
21
+ import type { AnyWidgetMessage } from "./schemas";
22
+ import type { EventHandler, ModelState, WidgetModelId } from "./types";
16
23
 
17
24
  class ModelManager {
18
- private models = new Map<string, Deferred<Model<any>>>();
19
- private timeout: number;
25
+ /**
26
+ * Map of model ids to deferred promises
27
+ */
28
+ #models = new Map<WidgetModelId, Deferred<Model<ModelState>>>();
29
+ /**
30
+ * Timeout for model lookup
31
+ */
32
+ #timeout: number;
33
+
20
34
  constructor(timeout = 10_000) {
21
- this.timeout = timeout;
35
+ this.#timeout = timeout;
22
36
  }
23
37
 
24
- get(key: string): Promise<Model<any>> {
25
- let deferred = this.models.get(key);
38
+ get(key: WidgetModelId): Promise<Model<any>> {
39
+ let deferred = this.#models.get(key);
26
40
  if (deferred) {
27
41
  return deferred.promise;
28
42
  }
29
43
 
30
44
  // If the model is not yet created, create the new deferred promise without resolving it
31
- deferred = new Deferred<Model<any>>();
32
- this.models.set(key, deferred);
45
+ deferred = new Deferred<Model<ModelState>>();
46
+ this.#models.set(key, deferred);
33
47
 
34
48
  // Add timeout to prevent hanging
35
49
  setTimeout(() => {
@@ -39,90 +53,216 @@ class ModelManager {
39
53
  }
40
54
 
41
55
  deferred.reject(new Error(`Model not found for key: ${key}`));
42
- this.models.delete(key);
43
- }, this.timeout);
56
+ this.#models.delete(key);
57
+ }, this.#timeout);
44
58
 
45
59
  return deferred.promise;
46
60
  }
47
61
 
48
- set(key: string, model: Model<any>): void {
49
- let deferred = this.models.get(key);
62
+ set(key: WidgetModelId, model: Model<any>): void {
63
+ let deferred = this.#models.get(key);
50
64
  if (!deferred) {
51
- deferred = new Deferred<Model<any>>();
52
- this.models.set(key, deferred);
65
+ deferred = new Deferred<Model<ModelState>>();
66
+ this.#models.set(key, deferred);
53
67
  }
54
68
  deferred.resolve(model);
55
69
  }
56
70
 
57
- delete(key: string): void {
58
- this.models.delete(key);
71
+ /**
72
+ * Check if a model exists and has been resolved (not pending).
73
+ * This is useful for checking if a model was already created by the plugin
74
+ * before the 'open' message arrives.
75
+ */
76
+ has(key: WidgetModelId): boolean {
77
+ const deferred = this.#models.get(key);
78
+ return deferred !== undefined && deferred.status === "resolved";
79
+ }
80
+
81
+ /**
82
+ * Get a model synchronously if it exists and has been resolved.
83
+ * Returns undefined if the model doesn't exist or is still pending.
84
+ */
85
+ getSync(key: WidgetModelId): Model<any> | undefined {
86
+ const deferred = this.#models.get(key);
87
+ if (deferred && deferred.status === "resolved") {
88
+ return deferred.value;
89
+ }
90
+ return undefined;
59
91
  }
92
+
93
+ delete(key: WidgetModelId): void {
94
+ this.#models.delete(key);
95
+ }
96
+ }
97
+
98
+ interface MarimoComm<T> {
99
+ sendUpdate: (value: Partial<T>) => Promise<void>;
100
+ sendCustomMessage: (content: unknown, buffers: DataView[]) => Promise<void>;
101
+ }
102
+
103
+ const marimoSymbol = Symbol("marimo");
104
+
105
+ const experimental: Experimental = {
106
+ invoke: async () => {
107
+ const message =
108
+ "anywidget.invoke not supported in marimo. Please file an issue at https://github.com/marimo-team/marimo/issues";
109
+ Logger.warn(message);
110
+ throw new Error(message);
111
+ },
112
+ };
113
+
114
+ type RenderFn = (el: HTMLElement, signal: AbortSignal) => Promise<void>;
115
+
116
+ interface MarimoInternalApi<T extends ModelState> {
117
+ /**
118
+ * Resolve the widget definition and initialize if needed.
119
+ * Returns a render function that can be called for each view.
120
+ *
121
+ * Per AFM spec:
122
+ * - widgetDef() is called once per model
123
+ * - initialize() is called once per model
124
+ * - render() (the returned function) is called once per view
125
+ */
126
+ resolveWidget: (widgetDef: AnyWidget<T>) => Promise<RenderFn>;
127
+ /**
128
+ * Update model state and emit change events for any differences.
129
+ */
130
+ updateAndEmitDiffs: (value: T) => void;
131
+ /**
132
+ * Emit a custom message to listeners.
133
+ */
134
+ emitCustomMessage: (
135
+ message: Extract<AnyWidgetMessage, { method: "custom" }>,
136
+ buffers?: readonly DataView[],
137
+ ) => void;
138
+ /**
139
+ * Destroy the model, triggering initialize cleanup.
140
+ */
141
+ destroy: () => void;
142
+ }
143
+
144
+ /**
145
+ * Get the internal marimo API for a Model instance.
146
+ * These are not part of the public AnyModel interface.
147
+ */
148
+ export function getMarimoInternal<T extends ModelState>(
149
+ model: Model<T>,
150
+ ): MarimoInternalApi<T> {
151
+ return model[marimoSymbol];
60
152
  }
61
153
 
62
154
  export const MODEL_MANAGER = new ModelManager();
63
155
 
64
- export class Model<T extends Record<string, any>> implements AnyModel<T> {
65
- private ANY_CHANGE_EVENT = "change";
66
- private dirtyFields: Map<keyof T, unknown>;
67
- public static _modelManager: ModelManager = MODEL_MANAGER;
68
- private data: T;
69
- private onChange: (value: Partial<T>) => void;
70
- private sendToWidget: (req: {
71
- content: unknown;
72
- buffers: Base64String[];
73
- }) => Promise<null | undefined>;
74
-
75
- constructor(
76
- data: T,
77
- onChange: (value: Partial<T>) => void,
78
- sendToWidget: (req: {
79
- content: unknown;
80
- buffers: Base64String[];
81
- }) => Promise<null | undefined>,
82
- initialDirtyFields: Set<keyof T>,
83
- ) {
84
- this.data = data;
85
- this.onChange = onChange;
86
- this.sendToWidget = sendToWidget;
87
- this.dirtyFields = new Map(
88
- [...initialDirtyFields].map((key) => [key, this.data[key]]),
89
- );
156
+ export class Model<T extends ModelState> implements AnyModel<T> {
157
+ #ANY_CHANGE_EVENT = "change";
158
+ #dirtyFields: Map<keyof T, unknown>;
159
+ #data: T;
160
+ #comm: MarimoComm<T>;
161
+ #listeners: Record<string, Set<EventHandler> | undefined> = {};
162
+ #controller = new AbortController();
163
+ #widgetDef: AnyWidget<T> | undefined;
164
+ #render:
165
+ | ((el: HTMLElement, signal: AbortSignal) => Promise<void>)
166
+ | undefined;
167
+
168
+ static _modelManager: ModelManager = MODEL_MANAGER;
169
+
170
+ constructor(data: T, comm: MarimoComm<T>) {
171
+ this.#data = data;
172
+ this.#comm = comm;
173
+ this.#dirtyFields = new Map();
90
174
  }
91
175
 
92
- private listeners: Record<string, Set<EventHandler>> = {};
176
+ /**
177
+ * Internal marimo API - not part of AnyWidget AFM.
178
+ * Access via getMarimoInternal().
179
+ */
180
+ [marimoSymbol]: MarimoInternalApi<T> = {
181
+ updateAndEmitDiffs: (value: T) => this.#updateAndEmitDiffs(value),
182
+ emitCustomMessage: (
183
+ message: Extract<AnyWidgetMessage, { method: "custom" }>,
184
+ buffers?: readonly DataView[],
185
+ ) => this.#emitCustomMessage(message, buffers),
186
+ resolveWidget: async (widgetDef: AnyWidget<T>): Promise<RenderFn> => {
187
+ // Already initialized with the same widget - return cached render
188
+ if (this.#render && this.#widgetDef === widgetDef) {
189
+ return this.#render;
190
+ }
191
+
192
+ // If widgetDef changed (hot reload), destroy old and re-initialize
193
+ if (this.#render && this.#widgetDef !== widgetDef) {
194
+ this.#controller.abort();
195
+ this.#controller = new AbortController();
196
+ this.#render = undefined;
197
+ }
198
+
199
+ this.#widgetDef = widgetDef;
200
+
201
+ // Resolve the widget definition (call if it's a function)
202
+ const widget =
203
+ typeof widgetDef === "function" ? await widgetDef() : widgetDef;
204
+
205
+ // Call initialize once per model
206
+ const cleanup = await widget.initialize?.({ model: this, experimental });
207
+ if (cleanup) {
208
+ this.#controller.signal.addEventListener("abort", cleanup);
209
+ }
210
+
211
+ // Store and return the render closure
212
+ this.#render = async (el: HTMLElement, signal: AbortSignal) => {
213
+ const renderCleanup = await widget.render?.({
214
+ model: this,
215
+ el,
216
+ experimental,
217
+ });
218
+ if (renderCleanup) {
219
+ // Cleanup when either the view unmounts or the model is destroyed
220
+ AbortSignal.any([signal, this.#controller.signal]).addEventListener(
221
+ "abort",
222
+ renderCleanup,
223
+ );
224
+ }
225
+ };
226
+
227
+ return this.#render;
228
+ },
229
+ destroy: () => {
230
+ this.#controller.abort();
231
+ },
232
+ };
93
233
 
94
234
  off(eventName?: string | null, callback?: EventHandler | null): void {
95
235
  if (!eventName) {
96
- this.listeners = {};
236
+ this.#listeners = {};
97
237
  return;
98
238
  }
99
239
 
100
240
  if (!callback) {
101
- this.listeners[eventName] = new Set();
241
+ this.#listeners[eventName] = new Set();
102
242
  return;
103
243
  }
104
244
 
105
- this.listeners[eventName]?.delete(callback);
245
+ this.#listeners[eventName]?.delete(callback);
106
246
  }
107
247
 
108
248
  send(
109
249
  content: any,
110
250
  callbacks?: any,
111
- _buffers?: ArrayBuffer[] | ArrayBufferView[],
112
- ): void {
113
- const { state, bufferPaths, buffers } = serializeBuffersToBase64(content);
114
- this.sendToWidget({
115
- content: {
116
- state: state,
117
- bufferPaths: bufferPaths,
118
- },
119
- buffers: buffers,
120
- }).then(callbacks);
251
+ buffers?: ArrayBuffer[] | ArrayBufferView[],
252
+ ): Promise<void> {
253
+ const dataViews = (buffers ?? []).map((buf) =>
254
+ buf instanceof ArrayBuffer
255
+ ? new DataView(buf)
256
+ : new DataView(buf.buffer, buf.byteOffset, buf.byteLength),
257
+ );
258
+ return this.#comm
259
+ .sendCustomMessage(content, dataViews)
260
+ .then(() => callbacks?.());
121
261
  }
122
262
 
123
263
  widget_manager = {
124
- async get_model<TT extends Record<string, any>>(
125
- model_id: string,
264
+ async get_model<TT extends ModelState>(
265
+ model_id: WidgetModelId,
126
266
  ): Promise<AnyModel<TT>> {
127
267
  const model = await Model._modelManager.get(model_id);
128
268
  if (!model) {
@@ -135,31 +275,52 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
135
275
  };
136
276
 
137
277
  get<K extends keyof T>(key: K): T[K] {
138
- return this.data[key];
278
+ return this.#data[key];
139
279
  }
140
280
 
141
281
  set<K extends keyof T>(key: K, value: T[K]): void {
142
- this.data = { ...this.data, [key]: value };
143
- this.dirtyFields.set(key, value);
144
- this.emit(`change:${key as K & string}`, value);
145
- this.emitAnyChange();
282
+ this.#data = { ...this.#data, [key]: value };
283
+ this.#dirtyFields.set(key, value);
284
+ this.#emit(`change:${key as K & string}`, value);
285
+ this.#emitAnyChange();
146
286
  }
147
287
 
148
288
  save_changes(): void {
149
- if (this.dirtyFields.size === 0) {
289
+ if (this.#dirtyFields.size === 0) {
150
290
  return;
151
291
  }
152
292
  // Only send the dirty fields, not the entire state.
153
293
  const partialData = Object.fromEntries(
154
- this.dirtyFields.entries(),
294
+ this.#dirtyFields.entries(),
155
295
  ) as Partial<T>;
156
296
 
157
297
  // Clear the dirty fields to avoid sending again.
158
- this.dirtyFields.clear();
159
- this.onChange(partialData);
298
+ this.#dirtyFields.clear();
299
+ this.#comm.sendUpdate(partialData);
160
300
  }
161
301
 
162
- updateAndEmitDiffs(value: T): void {
302
+ on(eventName: string, callback: EventHandler): void {
303
+ if (!this.#listeners[eventName]) {
304
+ this.#listeners[eventName] = new Set();
305
+ }
306
+ this.#listeners[eventName].add(callback);
307
+ }
308
+
309
+ #emit<K extends keyof T>(event: `change:${K & string}`, value: T[K]) {
310
+ if (!this.#listeners[event]) {
311
+ return;
312
+ }
313
+ const listeners = this.#listeners[event];
314
+ for (const listener of listeners) {
315
+ try {
316
+ listener(value);
317
+ } catch (error) {
318
+ Logger.error("Error emitting event", error);
319
+ }
320
+ }
321
+ }
322
+
323
+ #updateAndEmitDiffs(value: T) {
163
324
  if (value == null) {
164
325
  return;
165
326
  }
@@ -167,7 +328,7 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
167
328
  Object.keys(value).forEach((key) => {
168
329
  const k = key as keyof T;
169
330
  // Shallow equal since these can be large objects
170
- if (this.data[k] !== value[k]) {
331
+ if (this.#data[k] !== value[k]) {
171
332
  this.set(k, value[k]);
172
333
  }
173
334
  });
@@ -177,175 +338,139 @@ export class Model<T extends Record<string, any>> implements AnyModel<T> {
177
338
  * When receiving a message from the backend.
178
339
  * We want to notify all listeners with `msg:custom`
179
340
  */
180
- receiveCustomMessage(
181
- message: unknown,
341
+ #emitCustomMessage(
342
+ message: Extract<AnyWidgetMessage, { method: "custom" }>,
182
343
  buffers: readonly DataView[] = [],
183
- ): void {
184
- const response = AnyWidgetMessageSchema.safeParse(message);
185
- if (response.success) {
186
- const data = response.data;
187
- switch (data.method) {
188
- case "update":
189
- this.updateAndEmitDiffs(
190
- decodeFromWire<T>({
191
- state: data.state as T,
192
- bufferPaths: data.buffer_paths ?? [],
193
- buffers,
194
- }),
195
- );
196
- break;
197
- case "custom":
198
- this.listeners["msg:custom"]?.forEach((cb) =>
199
- cb(data.content, buffers),
200
- );
201
- break;
202
- case "open":
203
- this.updateAndEmitDiffs(
204
- decodeFromWire<T>({
205
- state: data.state as T,
206
- bufferPaths: data.buffer_paths ?? [],
207
- buffers,
208
- }),
209
- );
210
- break;
211
- case "echo_update":
212
- // We don't need to do anything with this message
213
- break;
214
- default:
215
- Logger.error("[anywidget] Unknown message method", data.method);
216
- break;
217
- }
218
- } else {
219
- Logger.error("Failed to parse message", response.error);
220
- Logger.error("Message", message);
344
+ ) {
345
+ const listeners = this.#listeners["msg:custom"];
346
+ if (!listeners) {
347
+ return;
221
348
  }
222
- }
223
-
224
- on(eventName: string, callback: EventHandler): void {
225
- if (!this.listeners[eventName]) {
226
- this.listeners[eventName] = new Set();
349
+ for (const listener of listeners) {
350
+ try {
351
+ listener(message.content, buffers);
352
+ } catch (error) {
353
+ Logger.error("Error emitting event", error);
354
+ }
227
355
  }
228
- this.listeners[eventName].add(callback);
229
356
  }
230
357
 
231
- private emit<K extends keyof T>(event: `change:${K & string}`, value: T[K]) {
232
- if (!this.listeners[event]) {
358
+ // Debounce 0 to send off one request in a single frame
359
+ #emitAnyChange = debounce(() => {
360
+ const listeners = this.#listeners[this.#ANY_CHANGE_EVENT];
361
+ if (!listeners) {
233
362
  return;
234
363
  }
235
- this.listeners[event].forEach((cb) => cb(value));
236
- }
237
-
238
- // Debounce 0 to send off one request in a single frame
239
- private emitAnyChange = debounce(() => {
240
- this.listeners[this.ANY_CHANGE_EVENT]?.forEach((cb) => cb());
364
+ for (const listener of listeners) {
365
+ try {
366
+ listener();
367
+ } catch (error) {
368
+ Logger.error("Error emitting event", error);
369
+ }
370
+ }
241
371
  }, 0);
242
372
  }
243
373
 
244
- const BufferPathSchema = z.array(z.array(z.union([z.string(), z.number()])));
245
- const StateSchema = z.record(z.string(), z.any());
246
-
247
- const AnyWidgetMessageSchema = z.discriminatedUnion("method", [
248
- z.object({
249
- method: z.literal("open"),
250
- state: StateSchema,
251
- buffer_paths: BufferPathSchema.optional(),
252
- }),
253
- z.object({
254
- method: z.literal("update"),
255
- state: StateSchema,
256
- buffer_paths: BufferPathSchema.optional(),
257
- }),
258
- z.object({
259
- method: z.literal("custom"),
260
- content: z.any(),
261
- }),
262
- z.object({
263
- method: z.literal("echo_update"),
264
- buffer_paths: BufferPathSchema,
265
- state: StateSchema,
266
- }),
267
- z.object({
268
- method: z.literal("close"),
269
- }),
270
- ]);
271
-
272
- export type AnyWidgetMessage = z.infer<typeof AnyWidgetMessageSchema>;
273
-
274
- export function isMessageWidgetState(msg: unknown): msg is AnyWidgetMessage {
275
- if (msg == null) {
276
- return false;
277
- }
374
+ /**
375
+ * Handle an incoming model lifecycle notification from the backend.
376
+ *
377
+ * Messages are dispatched by method type:
378
+ * - "open": Initialize a new model or update existing one with initial state
379
+ * - "update": Update model state with new values
380
+ * - "custom": Forward custom message to model listeners
381
+ * - "close": Remove model from manager
382
+ */
383
+ export async function handleWidgetMessage(
384
+ modelManager: ModelManager,
385
+ notification: NotificationMessageData<"model-lifecycle">,
386
+ ): Promise<void> {
387
+ const modelId = notification.model_id as WidgetModelId;
388
+ const msg = notification.message;
389
+
390
+ Logger.debug("AnyWidget message", msg);
391
+
392
+ // Decode base64 buffers to DataViews (present in open/update/custom messages)
393
+ const base64Buffers: Base64String[] =
394
+ "buffers" in msg ? (msg.buffers as Base64String[]) : [];
395
+ const buffers = base64Buffers.map(base64ToDataView);
396
+
397
+ switch (msg.method) {
398
+ case "open": {
399
+ const { state, buffer_paths = [] } = msg;
400
+ const stateWithBuffers = decodeFromWire({
401
+ state,
402
+ bufferPaths: buffer_paths,
403
+ buffers,
404
+ });
278
405
 
279
- return AnyWidgetMessageSchema.safeParse(msg).success;
280
- }
406
+ // Check if a model already exists (created by the plugin using model_id reference)
407
+ // If so, just update its state instead of creating a duplicate
408
+ const existingModel = modelManager.getSync(modelId);
409
+ if (existingModel) {
410
+ getMarimoInternal(existingModel).updateAndEmitDiffs(stateWithBuffers);
411
+ return;
412
+ }
281
413
 
282
- export async function handleWidgetMessage({
283
- modelId,
284
- msg,
285
- buffers,
286
- modelManager,
287
- }: {
288
- modelId: string;
289
- msg: AnyWidgetMessage;
290
- buffers: readonly DataView[];
291
- modelManager: ModelManager;
292
- }): Promise<void> {
293
- if (msg.method === "echo_update") {
294
- // We don't need to do anything with this message
295
- return;
296
- }
414
+ const model = new Model(stateWithBuffers, {
415
+ async sendUpdate(changeData) {
416
+ const { state, buffers, bufferPaths } =
417
+ serializeBuffersToBase64(changeData);
418
+ await getRequestClient().sendModelValue({
419
+ modelId,
420
+ message: { method: "update", state, bufferPaths },
421
+ buffers,
422
+ });
423
+ },
424
+ async sendCustomMessage(content, buffers) {
425
+ await getRequestClient().sendModelValue({
426
+ modelId,
427
+ message: { method: "custom", content },
428
+ buffers: buffers.map(dataViewToBase64),
429
+ });
430
+ },
431
+ });
432
+ modelManager.set(modelId, model);
433
+ return;
434
+ }
297
435
 
298
- if (msg.method === "custom") {
299
- const model = await modelManager.get(modelId);
300
- model.receiveCustomMessage(msg, buffers);
301
- return;
302
- }
436
+ case "custom": {
437
+ const model = await modelManager.get(modelId);
438
+ // For custom messages, we need to reconstruct the AnyWidgetMessage format
439
+ getMarimoInternal(model).emitCustomMessage(
440
+ { method: "custom", content: msg.content },
441
+ buffers,
442
+ );
443
+ return;
444
+ }
303
445
 
304
- if (msg.method === "close") {
305
- modelManager.delete(modelId);
306
- return;
307
- }
446
+ case "close": {
447
+ const model = modelManager.getSync(modelId);
448
+ if (model) {
449
+ getMarimoInternal(model).destroy();
450
+ }
451
+ modelManager.delete(modelId);
452
+ return;
453
+ }
308
454
 
309
- const { method, state, buffer_paths = [] } = msg;
310
- const stateWithBuffers = decodeFromWire({
311
- state,
312
- bufferPaths: buffer_paths,
313
- buffers,
314
- });
315
-
316
- if (method === "open") {
317
- const handleDataChange = (changeData: Record<string, any>) => {
318
- const { state, buffers, bufferPaths } =
319
- serializeBuffersToBase64(changeData);
320
- getRequestClient().sendModelValue({
321
- modelId: modelId,
322
- message: {
323
- state,
324
- bufferPaths,
325
- },
455
+ case "update": {
456
+ const { state, buffer_paths = [] } = msg;
457
+ const stateWithBuffers = decodeFromWire({
458
+ state,
459
+ bufferPaths: buffer_paths,
326
460
  buffers,
327
461
  });
328
- };
329
-
330
- const model = new Model(
331
- stateWithBuffers,
332
- handleDataChange,
333
- throwNotImplemented,
334
- new Set(),
335
- );
336
- modelManager.set(modelId, model);
337
- return;
338
- }
462
+ const model = await modelManager.get(modelId);
463
+ getMarimoInternal(model).updateAndEmitDiffs(stateWithBuffers);
464
+ return;
465
+ }
339
466
 
340
- if (method === "update") {
341
- const model = await modelManager.get(modelId);
342
- model.updateAndEmitDiffs(stateWithBuffers);
343
- return;
467
+ default:
468
+ assertNever(msg);
344
469
  }
345
-
346
- assertNever(method);
347
470
  }
348
471
 
472
+ repl(MODEL_MANAGER, "MODEL_MANAGER");
473
+
349
474
  export const visibleForTesting = {
350
475
  ModelManager,
351
476
  };