@1upvision/sdk 0.1.0 → 0.1.2

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.
@@ -1,11 +1,17 @@
1
1
  type RequestHandler = (params: unknown) => unknown | Promise<unknown>;
2
2
  type NotificationHandler = (params: unknown) => void;
3
+ /**
4
+ * Bridge used by the extension sandbox (Worker **or** iframe) to
5
+ * communicate with the host via JSON-RPC over postMessage.
6
+ */
3
7
  export declare class WorkerBridge {
4
8
  private requestHandlers;
5
9
  private notificationHandlers;
6
10
  private pendingRequests;
7
11
  private requestCounter;
12
+ private messageListener;
8
13
  constructor();
14
+ private send;
9
15
  notify(method: string, params?: unknown): void;
10
16
  request(method: string, params?: unknown): Promise<unknown>;
11
17
  onRequest(method: string, handler: RequestHandler): void;
@@ -1 +1 @@
1
- {"version":3,"file":"worker-bridge.d.ts","sourceRoot":"","sources":["../../src/bridge/worker-bridge.ts"],"names":[],"mappings":"AAOA,KAAK,cAAc,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AACtE,KAAK,mBAAmB,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AAErD,qBAAa,YAAY;IACvB,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,oBAAoB,CAA0C;IACtE,OAAO,CAAC,eAAe,CAGnB;IACJ,OAAO,CAAC,cAAc,CAAK;;IAQ3B,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAK9C,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI;IAIxD,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI;IAIlE,OAAO,CAAC,aAAa;IAsDrB,OAAO,IAAI,IAAI;CAKhB"}
1
+ {"version":3,"file":"worker-bridge.d.ts","sourceRoot":"","sources":["../../src/bridge/worker-bridge.ts"],"names":[],"mappings":"AAOA,KAAK,cAAc,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AACtE,KAAK,mBAAmB,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AAUrD;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,oBAAoB,CAA0C;IACtE,OAAO,CAAC,eAAe,CAGnB;IACJ,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,eAAe,CAAgD;;IAqBvE,OAAO,CAAC,IAAI;IAQZ,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAK9C,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI;IAIxD,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI;IAIlE,OAAO,CAAC,aAAa;IAsDrB,OAAO,IAAI,IAAI;CAahB"}
@@ -1,23 +1,55 @@
1
+ /**
2
+ * Detect whether we're running inside a Web Worker or an iframe.
3
+ * In a Worker `self` is a DedicatedWorkerGlobalScope (no `parent`).
4
+ * In an iframe `self` is a Window with `self.parent !== self`.
5
+ */
6
+ const isIframe = typeof window !== "undefined" && window.parent && window.parent !== window;
7
+ /**
8
+ * Bridge used by the extension sandbox (Worker **or** iframe) to
9
+ * communicate with the host via JSON-RPC over postMessage.
10
+ */
1
11
  export class WorkerBridge {
2
12
  requestHandlers = new Map();
3
13
  notificationHandlers = new Map();
4
14
  pendingRequests = new Map();
5
15
  requestCounter = 0;
16
+ messageListener = null;
6
17
  constructor() {
7
- self.addEventListener("message", (event) => {
8
- this.handleMessage(event.data);
9
- });
18
+ this.messageListener = (event) => {
19
+ // In iframe context, ignore messages not from the parent window
20
+ if (isIframe && event.source !== window.parent)
21
+ return;
22
+ const data = event.data;
23
+ // Ignore non-JSONRPC messages (e.g. React DevTools, HMR, etc.)
24
+ if (!data || typeof data !== "object" || data.jsonrpc !== "2.0")
25
+ return;
26
+ this.handleMessage(data);
27
+ };
28
+ if (isIframe) {
29
+ window.addEventListener("message", this.messageListener);
30
+ }
31
+ else {
32
+ self.addEventListener("message", this.messageListener);
33
+ }
34
+ }
35
+ send(message) {
36
+ if (isIframe) {
37
+ window.parent.postMessage(message, "*");
38
+ }
39
+ else {
40
+ self.postMessage(message);
41
+ }
10
42
  }
11
43
  notify(method, params) {
12
44
  const message = { jsonrpc: "2.0", method, params };
13
- self.postMessage(message);
45
+ this.send(message);
14
46
  }
15
47
  request(method, params) {
16
48
  const id = `req_${++this.requestCounter}`;
17
49
  const message = { jsonrpc: "2.0", id, method, params };
18
50
  return new Promise((resolve, reject) => {
19
51
  this.pendingRequests.set(id, { resolve, reject });
20
- self.postMessage(message);
52
+ this.send(message);
21
53
  });
22
54
  }
23
55
  onRequest(method, handler) {
@@ -56,7 +88,7 @@ export class WorkerBridge {
56
88
  id: message.id,
57
89
  result,
58
90
  };
59
- self.postMessage(response);
91
+ this.send(response);
60
92
  })
61
93
  .catch((error) => {
62
94
  const response = {
@@ -64,7 +96,7 @@ export class WorkerBridge {
64
96
  id: message.id,
65
97
  error: { code: -32000, message: String(error) },
66
98
  };
67
- self.postMessage(response);
99
+ this.send(response);
68
100
  });
69
101
  }
70
102
  }
@@ -78,6 +110,15 @@ export class WorkerBridge {
78
110
  }
79
111
  }
80
112
  destroy() {
113
+ if (this.messageListener) {
114
+ if (isIframe) {
115
+ window.removeEventListener("message", this.messageListener);
116
+ }
117
+ else {
118
+ self.removeEventListener("message", this.messageListener);
119
+ }
120
+ this.messageListener = null;
121
+ }
81
122
  this.pendingRequests.clear();
82
123
  this.requestHandlers.clear();
83
124
  this.notificationHandlers.clear();
@@ -0,0 +1,18 @@
1
+ export interface UseMutationResult<T = unknown> {
2
+ mutate: (args?: Record<string, unknown>) => void;
3
+ mutateAsync: (args?: Record<string, unknown>) => Promise<T>;
4
+ data: T | undefined;
5
+ isLoading: boolean;
6
+ error: Error | null;
7
+ }
8
+ /**
9
+ * Call a server mutation function by name.
10
+ *
11
+ * ```tsx
12
+ * const { mutate: addPlayer, isLoading } = useMutation("addPlayer");
13
+ * // ...
14
+ * addPlayer({ userId: "alice", score: 100 });
15
+ * ```
16
+ */
17
+ export declare function useMutation<T = unknown>(name: string): UseMutationResult<T>;
18
+ //# sourceMappingURL=useMutation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMutation.d.ts","sourceRoot":"","sources":["../../src/hooks/useMutation.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,iBAAiB,CAAC,CAAC,GAAG,OAAO;IAC5C,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACjD,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5D,IAAI,EAAE,CAAC,GAAG,SAAS,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,GAAG,OAAO,EACrC,IAAI,EAAE,MAAM,GACX,iBAAiB,CAAC,CAAC,CAAC,CAwDtB"}
@@ -0,0 +1,50 @@
1
+ import { useState, useCallback } from "react";
2
+ /**
3
+ * Call a server mutation function by name.
4
+ *
5
+ * ```tsx
6
+ * const { mutate: addPlayer, isLoading } = useMutation("addPlayer");
7
+ * // ...
8
+ * addPlayer({ userId: "alice", score: 100 });
9
+ * ```
10
+ */
11
+ export function useMutation(name) {
12
+ const [data, setData] = useState(undefined);
13
+ const [isLoading, setIsLoading] = useState(false);
14
+ const [error, setError] = useState(null);
15
+ const bridge = globalThis.__visionBridge;
16
+ const mutateAsync = useCallback(async (args) => {
17
+ if (!bridge) {
18
+ throw new Error("Vision bridge not available");
19
+ }
20
+ setIsLoading(true);
21
+ setError(null);
22
+ try {
23
+ const result = (await bridge.request("server.mutation", {
24
+ name,
25
+ args,
26
+ }));
27
+ setData(result);
28
+ setIsLoading(false);
29
+ // Trigger query invalidation so useQuery hooks refetch
30
+ const invalidate = globalThis
31
+ .__visionQueryInvalidate;
32
+ if (invalidate) {
33
+ invalidate({});
34
+ }
35
+ return result;
36
+ }
37
+ catch (err) {
38
+ const error = err instanceof Error ? err : new Error(String(err));
39
+ setError(error);
40
+ setIsLoading(false);
41
+ throw error;
42
+ }
43
+ }, [bridge, name]);
44
+ const mutate = useCallback((args) => {
45
+ mutateAsync(args).catch(() => {
46
+ // Error is already captured in state; swallow to avoid unhandled rejection
47
+ });
48
+ }, [mutateAsync]);
49
+ return { mutate, mutateAsync, data, isLoading, error };
50
+ }
@@ -0,0 +1,16 @@
1
+ export interface UseQueryResult<T = unknown> {
2
+ data: T | undefined;
3
+ isLoading: boolean;
4
+ error: Error | null;
5
+ refetch: () => void;
6
+ }
7
+ /**
8
+ * Call a server query function by name and reactively return the result.
9
+ *
10
+ * ```tsx
11
+ * const { data: players, isLoading, refetch } = useQuery("getPlayers");
12
+ * const { data: player } = useQuery("getPlayer", { userId: "alice" });
13
+ * ```
14
+ */
15
+ export declare function useQuery<T = unknown>(name: string, args?: Record<string, unknown>): UseQueryResult<T>;
16
+ //# sourceMappingURL=useQuery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useQuery.d.ts","sourceRoot":"","sources":["../../src/hooks/useQuery.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO;IACzC,IAAI,EAAE,CAAC,GAAG,SAAS,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAClC,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,cAAc,CAAC,CAAC,CAAC,CAiEnB"}
@@ -0,0 +1,62 @@
1
+ import { useState, useEffect, useCallback, useRef } from "react";
2
+ /**
3
+ * Call a server query function by name and reactively return the result.
4
+ *
5
+ * ```tsx
6
+ * const { data: players, isLoading, refetch } = useQuery("getPlayers");
7
+ * const { data: player } = useQuery("getPlayer", { userId: "alice" });
8
+ * ```
9
+ */
10
+ export function useQuery(name, args) {
11
+ const [data, setData] = useState(undefined);
12
+ const [isLoading, setIsLoading] = useState(true);
13
+ const [error, setError] = useState(null);
14
+ const argsJson = JSON.stringify(args);
15
+ const mountedRef = useRef(true);
16
+ const bridge = globalThis.__visionBridge;
17
+ const fetchData = useCallback(() => {
18
+ if (!bridge)
19
+ return;
20
+ setIsLoading(true);
21
+ setError(null);
22
+ const parsedArgs = argsJson ? JSON.parse(argsJson) : undefined;
23
+ bridge
24
+ .request("server.query", { name, args: parsedArgs })
25
+ .then((result) => {
26
+ if (mountedRef.current) {
27
+ setData(result);
28
+ setIsLoading(false);
29
+ }
30
+ })
31
+ .catch((err) => {
32
+ if (mountedRef.current) {
33
+ setError(err instanceof Error ? err : new Error(String(err)));
34
+ setIsLoading(false);
35
+ }
36
+ });
37
+ }, [bridge, name, argsJson]);
38
+ useEffect(() => {
39
+ mountedRef.current = true;
40
+ fetchData();
41
+ return () => {
42
+ mountedRef.current = false;
43
+ };
44
+ }, [fetchData]);
45
+ // Listen for server-side invalidation pushed by the host after mutations
46
+ useEffect(() => {
47
+ const handler = (detail) => {
48
+ const payload = detail;
49
+ if (!payload?.queryNames || payload.queryNames.includes(name)) {
50
+ fetchData();
51
+ }
52
+ };
53
+ globalThis.__visionQueryInvalidate = handler;
54
+ return () => {
55
+ if (globalThis.__visionQueryInvalidate ===
56
+ handler) {
57
+ delete globalThis.__visionQueryInvalidate;
58
+ }
59
+ };
60
+ }, [fetchData, name]);
61
+ return { data, isLoading, error, refetch: fetchData };
62
+ }
package/dist/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export { Box } from "./components/Box";
13
13
  export { useExtensionStorage } from "./hooks/useExtensionStorage";
14
14
  export { useExtensionContext } from "./hooks/useExtensionContext";
15
15
  export { useVisionState } from "./hooks/useVisionState";
16
+ export { useQuery } from "./hooks/useQuery";
17
+ export { useMutation } from "./hooks/useMutation";
16
18
  export type { ExtensionContext } from "./hooks/useExtensionContext";
17
19
  export type { ButtonProps } from "./components/Button";
18
20
  export type { TextFieldProps } from "./components/TextField";
@@ -25,5 +27,7 @@ export type { ListProps } from "./components/List";
25
27
  export type { TextProps } from "./components/Text";
26
28
  export type { ImageProps } from "./components/Image";
27
29
  export type { BoxProps } from "./components/Box";
30
+ export type { UseQueryResult } from "./hooks/useQuery";
31
+ export type { UseMutationResult } from "./hooks/useMutation";
28
32
  export type { VisionStyle } from "@1upvision/protocol";
29
33
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAGvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGxD,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAGvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -16,3 +16,5 @@ export { Box } from "./components/Box";
16
16
  export { useExtensionStorage } from "./hooks/useExtensionStorage";
17
17
  export { useExtensionContext } from "./hooks/useExtensionContext";
18
18
  export { useVisionState } from "./hooks/useVisionState";
19
+ export { useQuery } from "./hooks/useQuery";
20
+ export { useMutation } from "./hooks/useMutation";
@@ -3,6 +3,7 @@ export declare class CallbackRegistry {
3
3
  private counter;
4
4
  register(fn: Function): string;
5
5
  invoke(handlerId: string, args: unknown[]): unknown;
6
+ update(handlerId: string, fn: Function): void;
6
7
  remove(handlerId: string): void;
7
8
  clear(): void;
8
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"callback-registry.d.ts","sourceRoot":"","sources":["../../src/renderer/callback-registry.ts"],"names":[],"mappings":"AAAA,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,OAAO,CAAK;IAEpB,QAAQ,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM;IAM9B,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO;IASnD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI/B,KAAK,IAAI,IAAI;CAId"}
1
+ {"version":3,"file":"callback-registry.d.ts","sourceRoot":"","sources":["../../src/renderer/callback-registry.ts"],"names":[],"mappings":"AAAA,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,OAAO,CAAK;IAEpB,QAAQ,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM;IAM9B,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO;IASnD,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,IAAI;IAI7C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI/B,KAAK,IAAI,IAAI;CAId"}
@@ -14,6 +14,9 @@ export class CallbackRegistry {
14
14
  }
15
15
  return handler(...args);
16
16
  }
17
+ update(handlerId, fn) {
18
+ this.handlers.set(handlerId, fn);
19
+ }
17
20
  remove(handlerId) {
18
21
  this.handlers.delete(handlerId);
19
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"reconciler.d.ts","sourceRoot":"","sources":["../../src/renderer/reconciler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAMvD,eAAO,MAAM,gBAAgB,kBAAyB,CAAC;AAMvD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;CAC/B;AAMD,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAsDD;;;GAGG;AACH,iBAAS,aAAa,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,mBAAmB,EAAE,CASzE;AAiBD,KAAK,UAAU,GAAG,GAAG,CAAC;AAEtB,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,CAAC,IAAI,EAAE,mBAAmB,EAAE,KAAK,IAAI,GAC9C,UAAU,CA+TZ;AAED,OAAO,EAAE,aAAa,EAAE,CAAC;AACzB,YAAY,EAAE,mBAAmB,EAAE,CAAC"}
1
+ {"version":3,"file":"reconciler.d.ts","sourceRoot":"","sources":["../../src/renderer/reconciler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAMvD,eAAO,MAAM,gBAAgB,kBAAyB,CAAC;AAMvD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;CAC/B;AAMD,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAoID;;;GAGG;AACH,iBAAS,aAAa,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,mBAAmB,EAAE,CASzE;AAiBD,KAAK,UAAU,GAAG,GAAG,CAAC;AAEtB,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,CAAC,IAAI,EAAE,mBAAmB,EAAE,KAAK,IAAI,GAC9C,UAAU,CA6VZ;AAED,OAAO,EAAE,aAAa,EAAE,CAAC;AACzB,YAAY,EAAE,mBAAmB,EAAE,CAAC"}
@@ -10,17 +10,48 @@ let nodeCounter = 0;
10
10
  function nextId() {
11
11
  return `node_${++nodeCounter}`;
12
12
  }
13
+ /**
14
+ * Recursively remove all callback registry entries for an instance and its
15
+ * descendants. Called when nodes are removed from the tree so that the
16
+ * CallbackRegistry doesn't grow unbounded.
17
+ */
18
+ function cleanupHandlers(instance) {
19
+ for (const [key, value] of Object.entries(instance.props)) {
20
+ if (key.startsWith("__handler_")) {
21
+ callbackRegistry.remove(value);
22
+ }
23
+ }
24
+ for (const child of instance.children) {
25
+ cleanupHandlers(child);
26
+ }
27
+ }
13
28
  /**
14
29
  * Sanitize props by replacing function values with handler IDs.
30
+ *
31
+ * When `existingProps` is provided (during updates), we reuse handler IDs
32
+ * for prop keys that already had a handler registered. This avoids the
33
+ * monotonically-growing callback counter producing "different" IDs on every
34
+ * React commit, which would cause the serialized tree to look different each
35
+ * time, driving an infinite commit → serialize → commit loop inside the
36
+ * worker and leaking memory at ~250 MB/s.
15
37
  */
16
- function sanitizeProps(props) {
38
+ function sanitizeProps(props, existingProps) {
17
39
  const sanitized = {};
18
40
  for (const [key, value] of Object.entries(props)) {
19
41
  if (key === "children")
20
42
  continue;
21
43
  if (typeof value === "function") {
22
- const handlerId = callbackRegistry.register(value);
23
- sanitized[`__handler_${key}`] = handlerId;
44
+ const handlerKey = `__handler_${key}`;
45
+ const existingHandlerId = existingProps?.[handlerKey];
46
+ if (existingHandlerId) {
47
+ // Reuse the existing ID but update the function it points to
48
+ callbackRegistry.update(existingHandlerId, value);
49
+ sanitized[handlerKey] = existingHandlerId;
50
+ }
51
+ else {
52
+ const handlerId = callbackRegistry.register(value);
53
+ sanitized[handlerKey] = handlerId;
54
+ }
24
55
  }
25
56
  else {
26
57
  sanitized[key] = value;
@@ -28,11 +59,49 @@ function sanitizeProps(props) {
28
59
  }
29
60
  return sanitized;
30
61
  }
62
+ /**
63
+ * Check whether a value is safe to include in serialized props.
64
+ * Rejects React elements/fibers, class instances, DOM nodes, and other
65
+ * objects that would cause deepSanitize to walk enormous object graphs.
66
+ * Only plain objects ({}) and arrays are allowed through.
67
+ */
68
+ function isPlainSerializable(value) {
69
+ if (value === null || value === undefined)
70
+ return true;
71
+ const t = typeof value;
72
+ if (t === "string" || t === "number" || t === "boolean")
73
+ return true;
74
+ if (t === "function" || t === "symbol")
75
+ return false;
76
+ if (t !== "object")
77
+ return false;
78
+ // Arrays are fine (contents checked recursively)
79
+ if (Array.isArray(value))
80
+ return true;
81
+ // React elements have $$typeof (Symbol) — reject them
82
+ if ("$$typeof" in value)
83
+ return false;
84
+ // DOM nodes
85
+ if (typeof value.nodeType === "number")
86
+ return false;
87
+ // Only allow plain objects (Object.prototype or null prototype)
88
+ const proto = Object.getPrototypeOf(value);
89
+ return proto === Object.prototype || proto === null;
90
+ }
91
+ /** Maximum nesting depth for deepSanitize to prevent runaway recursion. */
92
+ const MAX_SANITIZE_DEPTH = 10;
31
93
  /**
32
94
  * Deep-strip any values that cannot survive structured clone (postMessage).
33
- * Functions, Symbols, and circular refs are removed.
95
+ * Functions, Symbols, circular/shared refs, and non-plain objects are removed.
96
+ *
97
+ * Uses a `seen` WeakSet that is never cleared during traversal — objects are
98
+ * visited at most once across the entire graph. This prevents both infinite
99
+ * loops from circular references AND exponential blowup from shared refs
100
+ * (e.g. React fiber trees where child.return === parent).
34
101
  */
35
- function deepSanitize(value) {
102
+ function deepSanitize(value, seen = new WeakSet(), depth = 0) {
103
+ if (depth > MAX_SANITIZE_DEPTH)
104
+ return undefined;
36
105
  if (value === null || value === undefined)
37
106
  return value;
38
107
  const t = typeof value;
@@ -40,12 +109,20 @@ function deepSanitize(value) {
40
109
  return value;
41
110
  if (t === "function" || t === "symbol")
42
111
  return undefined;
43
- if (Array.isArray(value))
44
- return value.map(deepSanitize);
45
112
  if (t === "object") {
113
+ const obj = value;
114
+ if (seen.has(obj))
115
+ return undefined;
116
+ seen.add(obj);
117
+ // Reject non-plain objects (React elements, DOM nodes, class instances)
118
+ if (!isPlainSerializable(value))
119
+ return undefined;
120
+ if (Array.isArray(value)) {
121
+ return value.map((v) => deepSanitize(v, seen, depth + 1));
122
+ }
46
123
  const out = {};
47
124
  for (const [k, v] of Object.entries(value)) {
48
- const sanitized = deepSanitize(v);
125
+ const sanitized = deepSanitize(v, seen, depth + 1);
49
126
  if (sanitized !== undefined) {
50
127
  out[k] = sanitized;
51
128
  }
@@ -75,6 +152,27 @@ const DefaultEventPriority = 32;
75
152
  export function createHostConfig(onCommit) {
76
153
  // Track update priority internally (required by React 19 reconciler)
77
154
  let currentUpdatePriority = 0;
155
+ // Deduplicate ui.render: only send when the serialized tree actually changes.
156
+ // React 19's concurrent reconciler calls resetAfterCommit on every commit
157
+ // (including passive-effect-only commits where the tree hasn't changed),
158
+ // which without this guard would flood the host with identical trees,
159
+ // causing massive memory pressure from repeated postMessage structured clones.
160
+ let prevTreeCompareJson = null;
161
+ /**
162
+ * Strip __handler_* values from a serialized tree for comparison purposes.
163
+ * Handler IDs are monotonically incrementing (cb_1, cb_2, ...) so they
164
+ * change on every React commit even when the actual UI hasn't changed.
165
+ * Without stripping them, every commit produces a "different" tree,
166
+ * causing an infinite ui.render → setTree → re-render → ui.render loop
167
+ * that leaks hundreds of MB per second.
168
+ */
169
+ function treeToCompareJson(tree) {
170
+ return JSON.stringify(tree, (key, value) => {
171
+ if (key.startsWith("__handler_"))
172
+ return "__handler__";
173
+ return value;
174
+ });
175
+ }
78
176
  return {
79
177
  // -----------------------------------------------------------------------
80
178
  // Feature flags
@@ -186,6 +284,7 @@ export function createHostConfig(onCommit) {
186
284
  parent.children.push(child);
187
285
  },
188
286
  removeChild(parent, child) {
287
+ cleanupHandlers(child);
189
288
  child.parent = null;
190
289
  const idx = parent.children.indexOf(child);
191
290
  if (idx !== -1)
@@ -206,6 +305,7 @@ export function createHostConfig(onCommit) {
206
305
  container.children.push(child);
207
306
  },
208
307
  removeChildFromContainer(container, child) {
308
+ cleanupHandlers(child);
209
309
  child.parent = null;
210
310
  const idx = container.children.indexOf(child);
211
311
  if (idx !== -1)
@@ -223,17 +323,21 @@ export function createHostConfig(onCommit) {
223
323
  },
224
324
  // -----------------------------------------------------------------------
225
325
  // Updates
326
+ //
327
+ // NOTE: react-reconciler@0.32.0 (React 19) removed prepareUpdate and
328
+ // the updatePayload parameter from commitUpdate. The reconciler now
329
+ // calls commitUpdate directly with (instance, type, oldProps, newProps,
330
+ // internalHandle) — no updatePayload.
226
331
  // -----------------------------------------------------------------------
227
- prepareUpdate(_instance, _type, _oldProps, newProps) {
228
- return newProps;
229
- },
230
- commitUpdate(instance, _updatePayload, _type, _oldProps, newProps) {
231
- for (const [key, value] of Object.entries(instance.props)) {
232
- if (key.startsWith("__handler_")) {
332
+ commitUpdate(instance, _type, _oldProps, newProps) {
333
+ const oldInstanceProps = instance.props;
334
+ instance.props = sanitizeProps(newProps, oldInstanceProps);
335
+ // Clean up handler IDs that existed in old props but are no longer present
336
+ for (const [key, value] of Object.entries(oldInstanceProps)) {
337
+ if (key.startsWith("__handler_") && !(key in instance.props)) {
233
338
  callbackRegistry.remove(value);
234
339
  }
235
340
  }
236
- instance.props = sanitizeProps(newProps);
237
341
  },
238
342
  commitTextUpdate(textInstance, _oldText, newText) {
239
343
  textInstance.props = { content: newText };
@@ -252,6 +356,10 @@ export function createHostConfig(onCommit) {
252
356
  },
253
357
  resetAfterCommit(container) {
254
358
  const tree = serializeTree(container.children);
359
+ const compareJson = treeToCompareJson(tree);
360
+ if (compareJson === prevTreeCompareJson)
361
+ return; // no visible change
362
+ prevTreeCompareJson = compareJson;
255
363
  onCommit(tree);
256
364
  },
257
365
  // -----------------------------------------------------------------------
@@ -273,6 +381,9 @@ export function createHostConfig(onCommit) {
273
381
  // Container operations
274
382
  // -----------------------------------------------------------------------
275
383
  clearContainer(container) {
384
+ for (const child of container.children) {
385
+ cleanupHandlers(child);
386
+ }
276
387
  container.children = [];
277
388
  },
278
389
  preparePortalMount() {
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/renderer/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAM5D,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAEpD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,GAAG,IAAI,CAqCjE;AAED,eAAO,MAAM,MAAM;oBAEN,KAAK,CAAC,YAAY,aAChB;QAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,aAAa,CAAA;KAAE,GACzD,IAAI;CAGR,CAAC"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/renderer/render.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAW5D,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAEpD;AAqCD,wBAAgB,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,GAAG,IAAI,CAmBjE;AAED,eAAO,MAAM,MAAM;oBAEN,KAAK,CAAC,YAAY,aAChB;QAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,aAAa,CAAA;KAAE,GACzD,IAAI;CAGR,CAAC"}
@@ -1,32 +1,46 @@
1
1
  import ReactReconciler from "react-reconciler";
2
2
  import { createHostConfig } from "./reconciler";
3
3
  let bridgeInstance = null;
4
+ let reconcilerInstance = null;
5
+ let rootInstance = null;
4
6
  export function setBridge(bridge) {
5
7
  bridgeInstance = bridge;
6
8
  }
7
- export function renderExtension(element) {
8
- if (!bridgeInstance) {
9
- throw new Error("Vision SDK: Bridge not initialized. Ensure worker-entry has run before calling render.");
9
+ function ensureRoot() {
10
+ if (reconcilerInstance && rootInstance) {
11
+ return {
12
+ reconciler: reconcilerInstance,
13
+ root: rootInstance,
14
+ };
10
15
  }
11
- const bridge = bridgeInstance;
12
16
  const onCommit = (tree) => {
13
- bridge.notify("ui.render", { tree });
17
+ bridgeInstance?.notify("ui.render", { tree });
14
18
  };
15
19
  const hostConfig = createHostConfig(onCommit);
16
20
  const reconciler = ReactReconciler(hostConfig);
17
21
  const container = { children: [] };
18
- const root = reconciler.createContainer(container, 0, // ConcurrentRoot tag
22
+ const root = reconciler.createContainer(container, 1, // ConcurrentRoot (0 = LegacyRoot/sync, 1 = ConcurrentRoot/async)
19
23
  null, // hydrationCallbacks
20
24
  false, // isStrictMode
21
25
  null, // concurrentUpdatesByDefaultOverride
22
26
  "", // identifierPrefix
23
27
  (error) => console.error(error), // onRecoverableError
24
28
  null);
29
+ reconcilerInstance = reconciler;
30
+ rootInstance = root;
31
+ return { reconciler, root };
32
+ }
33
+ export function renderExtension(element) {
34
+ if (!bridgeInstance) {
35
+ throw new Error("Vision SDK: Bridge not initialized. Ensure worker-entry has run before calling render.");
36
+ }
37
+ const { reconciler, root } = ensureRoot();
25
38
  reconciler.updateContainer(element, root, null, () => {
26
- bridge.notify("ui.ready");
39
+ bridgeInstance?.notify("ui.ready");
27
40
  });
28
41
  // Clear the init timeout since render was called
29
- const clearInit = globalThis.__visionClearInitTimeout;
42
+ const clearInit = globalThis
43
+ .__visionClearInitTimeout;
30
44
  if (typeof clearInit === "function") {
31
45
  clearInit();
32
46
  }