@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.
- package/dist/bridge/worker-bridge.d.ts +6 -0
- package/dist/bridge/worker-bridge.d.ts.map +1 -1
- package/dist/bridge/worker-bridge.js +48 -7
- package/dist/hooks/useMutation.d.ts +18 -0
- package/dist/hooks/useMutation.d.ts.map +1 -0
- package/dist/hooks/useMutation.js +50 -0
- package/dist/hooks/useQuery.d.ts +16 -0
- package/dist/hooks/useQuery.d.ts.map +1 -0
- package/dist/hooks/useQuery.js +62 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/renderer/callback-registry.d.ts +1 -0
- package/dist/renderer/callback-registry.d.ts.map +1 -1
- package/dist/renderer/callback-registry.js +3 -0
- package/dist/renderer/reconciler.d.ts.map +1 -1
- package/dist/renderer/reconciler.js +126 -15
- package/dist/renderer/render.d.ts.map +1 -1
- package/dist/renderer/render.js +22 -8
- package/dist/server/declarative.d.ts +159 -0
- package/dist/server/declarative.d.ts.map +1 -0
- package/dist/server/declarative.js +139 -0
- package/dist/server/functions.d.ts +8 -0
- package/dist/server/functions.d.ts.map +1 -1
- package/dist/server/functions.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/schema.d.ts +1 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/worker-runtime.js +224 -30
- package/package.json +2 -2
|
@@ -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;
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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";
|
|
@@ -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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconciler.d.ts","sourceRoot":"","sources":["../../src/renderer/reconciler.ts"],"names":[],"mappings":"
|
|
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
|
|
23
|
-
|
|
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,
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
for (const [key, value] of Object.entries(
|
|
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":"
|
|
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"}
|
package/dist/renderer/render.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
if (
|
|
9
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
39
|
+
bridgeInstance?.notify("ui.ready");
|
|
27
40
|
});
|
|
28
41
|
// Clear the init timeout since render was called
|
|
29
|
-
const clearInit = globalThis
|
|
42
|
+
const clearInit = globalThis
|
|
43
|
+
.__visionClearInitTimeout;
|
|
30
44
|
if (typeof clearInit === "function") {
|
|
31
45
|
clearInit();
|
|
32
46
|
}
|