@tangle-network/blueprint-ui 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-BLXSBQU4.js +57 -0
- package/dist/chunk-BLXSBQU4.js.map +1 -0
- package/dist/detectParentOrigin-BYruoIdc.d.ts +26 -0
- package/dist/iframe/index.d.ts +113 -0
- package/dist/iframe/index.js +489 -0
- package/dist/iframe/index.js.map +1 -0
- package/dist/iframe/testing-index.d.ts +81 -0
- package/dist/iframe/testing-index.js +514 -0
- package/dist/iframe/testing-index.js.map +1 -0
- package/dist/parentBridgeProtocol-CqK9e6Fk.d.ts +136 -0
- package/dist/styles.css +3 -0
- package/dist/tangleIframeClient-D-PP-KhN.d.ts +103 -0
- package/dist/wallet/index.d.ts +10 -109
- package/dist/wallet/index.js +14 -47
- package/dist/wallet/index.js.map +1 -1
- package/package.json +11 -1
- package/src/iframe/TangleIframeProvider.tsx +171 -0
- package/src/iframe/hooks.ts +142 -0
- package/src/iframe/index.ts +72 -0
- package/src/iframe/tangleIframeClient.test.ts +229 -0
- package/src/iframe/tangleIframeClient.ts +449 -0
- package/src/iframe/testing-index.ts +15 -0
- package/src/iframe/testing.tsx +677 -0
- package/src/wallet/index.ts +8 -0
- package/src/wallet/parentBridgeProtocol.ts +85 -1
- package/src/wallet/parentBridgeProvider.ts +17 -1
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NO_WALLET_ADDRESS,
|
|
3
|
+
TANGLE_CLOUD_ORIGINS_DEFAULT,
|
|
4
|
+
TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
5
|
+
detectTangleCloudParentOrigin,
|
|
6
|
+
makeCorrelationId
|
|
7
|
+
} from "../chunk-BLXSBQU4.js";
|
|
8
|
+
|
|
9
|
+
// src/iframe/TangleIframeProvider.tsx
|
|
10
|
+
import {
|
|
11
|
+
createContext,
|
|
12
|
+
useContext,
|
|
13
|
+
useEffect,
|
|
14
|
+
useMemo,
|
|
15
|
+
useRef,
|
|
16
|
+
useState
|
|
17
|
+
} from "react";
|
|
18
|
+
|
|
19
|
+
// src/iframe/tangleIframeClient.ts
|
|
20
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
21
|
+
var NULL_WALLET = {
|
|
22
|
+
address: null,
|
|
23
|
+
chainId: null,
|
|
24
|
+
isConnected: false
|
|
25
|
+
};
|
|
26
|
+
var NULL_SERVICE = {
|
|
27
|
+
blueprintId: null,
|
|
28
|
+
serviceId: null,
|
|
29
|
+
operators: [],
|
|
30
|
+
jobs: [],
|
|
31
|
+
mode: null
|
|
32
|
+
};
|
|
33
|
+
var TangleIframeClient = class {
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.options = options;
|
|
36
|
+
}
|
|
37
|
+
options;
|
|
38
|
+
wallet = NULL_WALLET;
|
|
39
|
+
service = NULL_SERVICE;
|
|
40
|
+
handshakeAcked = false;
|
|
41
|
+
handshakeWaiters = [];
|
|
42
|
+
installed = false;
|
|
43
|
+
listeners = {
|
|
44
|
+
wallet: /* @__PURE__ */ new Set(),
|
|
45
|
+
service: /* @__PURE__ */ new Set(),
|
|
46
|
+
job: /* @__PURE__ */ new Set()
|
|
47
|
+
};
|
|
48
|
+
pendingJobs = /* @__PURE__ */ new Map();
|
|
49
|
+
/** Wire the global message listener + initial handshake. Idempotent. */
|
|
50
|
+
install() {
|
|
51
|
+
if (this.installed || typeof window === "undefined") return;
|
|
52
|
+
this.installed = true;
|
|
53
|
+
window.addEventListener("message", this.handleParentMessage);
|
|
54
|
+
this.postHandshake();
|
|
55
|
+
}
|
|
56
|
+
uninstall() {
|
|
57
|
+
if (!this.installed || typeof window === "undefined") return;
|
|
58
|
+
this.installed = false;
|
|
59
|
+
window.removeEventListener("message", this.handleParentMessage);
|
|
60
|
+
for (const [, pending] of this.pendingJobs) {
|
|
61
|
+
clearTimeout(pending.timer);
|
|
62
|
+
pending.reject(new Error("Tangle iframe client uninstalled"));
|
|
63
|
+
}
|
|
64
|
+
this.pendingJobs.clear();
|
|
65
|
+
}
|
|
66
|
+
// ── State accessors ─────────────────────────────────────────────────────
|
|
67
|
+
getWallet() {
|
|
68
|
+
return this.wallet;
|
|
69
|
+
}
|
|
70
|
+
getService() {
|
|
71
|
+
return this.service;
|
|
72
|
+
}
|
|
73
|
+
// ── Subscription API (used by React hooks) ──────────────────────────────
|
|
74
|
+
subscribe(event, listener) {
|
|
75
|
+
this.listeners[event].add(listener);
|
|
76
|
+
return () => {
|
|
77
|
+
this.listeners[event].delete(listener);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// ── Wallet operations ───────────────────────────────────────────────────
|
|
81
|
+
async signMessage(message) {
|
|
82
|
+
await this.ensureBootstrapped();
|
|
83
|
+
return this.dispatchWallet("tangle.app.signMessage", {
|
|
84
|
+
chainId: this.wallet.chainId ?? 0,
|
|
85
|
+
message
|
|
86
|
+
}).then((data) => data.signature);
|
|
87
|
+
}
|
|
88
|
+
async sendTransaction(tx) {
|
|
89
|
+
await this.ensureBootstrapped();
|
|
90
|
+
return this.dispatchWallet("tangle.app.signTransaction", {
|
|
91
|
+
chainId: this.wallet.chainId ?? 0,
|
|
92
|
+
to: tx.to,
|
|
93
|
+
data: tx.data,
|
|
94
|
+
...tx.value !== void 0 ? { value: tx.value.toString(10) } : {}
|
|
95
|
+
}).then((data) => data.txHash);
|
|
96
|
+
}
|
|
97
|
+
async switchChain(chainId) {
|
|
98
|
+
await this.ensureBootstrapped();
|
|
99
|
+
return this.dispatchWallet("tangle.app.switchChain", { chainId }).then(
|
|
100
|
+
(data) => data.chainId
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
// ── Job invocation ──────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Invoke a blueprint job. Returns a Promise that resolves on terminal
|
|
106
|
+
* status (`success` or `error`); subscribe to the `job` event for
|
|
107
|
+
* intermediate streaming chunks.
|
|
108
|
+
*
|
|
109
|
+
* Streaming opt-in: pass `stream: true` if the publisher's job emits
|
|
110
|
+
* chunks (LLM generation, video encoding). One-shot jobs (embeddings,
|
|
111
|
+
* classifications) skip the streaming machinery.
|
|
112
|
+
*/
|
|
113
|
+
async callJob(args) {
|
|
114
|
+
await this.ensureBootstrapped();
|
|
115
|
+
const correlationId = makeCorrelationId("tangle.app.callJob");
|
|
116
|
+
const timeout = this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const invocation = {
|
|
119
|
+
correlationId,
|
|
120
|
+
status: "pending",
|
|
121
|
+
chunks: []
|
|
122
|
+
};
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
this.pendingJobs.delete(correlationId);
|
|
125
|
+
reject(
|
|
126
|
+
bridgeError(4900, `Job did not respond within ${timeout}ms`)
|
|
127
|
+
);
|
|
128
|
+
}, timeout);
|
|
129
|
+
this.pendingJobs.set(correlationId, {
|
|
130
|
+
resolve,
|
|
131
|
+
reject,
|
|
132
|
+
timer,
|
|
133
|
+
invocation
|
|
134
|
+
});
|
|
135
|
+
const message = {
|
|
136
|
+
kind: "tangle.app.callJob",
|
|
137
|
+
correlationId,
|
|
138
|
+
jobIndex: args.jobIndex,
|
|
139
|
+
inputs: args.inputs,
|
|
140
|
+
...args.stream !== void 0 ? { stream: args.stream } : {}
|
|
141
|
+
};
|
|
142
|
+
this.postToParent(message);
|
|
143
|
+
this.emit("job", invocation);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// ── Internals ───────────────────────────────────────────────────────────
|
|
147
|
+
postHandshake() {
|
|
148
|
+
this.postToParent({
|
|
149
|
+
kind: "tangle.app.handshake",
|
|
150
|
+
appId: this.options.appId,
|
|
151
|
+
version: TANGLE_IFRAME_PROTOCOL_VERSION
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
postToParent(message) {
|
|
155
|
+
if (typeof window === "undefined") return;
|
|
156
|
+
try {
|
|
157
|
+
window.parent.postMessage(message, this.options.parentOrigin);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
handleParentMessage = (event) => {
|
|
162
|
+
if (event.origin !== this.options.parentOrigin) return;
|
|
163
|
+
const data = event.data;
|
|
164
|
+
if (typeof data !== "object" || data === null) return;
|
|
165
|
+
const message = data;
|
|
166
|
+
switch (message.kind) {
|
|
167
|
+
case "tangle.app.handshakeAck":
|
|
168
|
+
this.handshakeAcked = true;
|
|
169
|
+
for (const resolve of this.handshakeWaiters) resolve();
|
|
170
|
+
this.handshakeWaiters = [];
|
|
171
|
+
return;
|
|
172
|
+
case "tangle.app.readAccountResult":
|
|
173
|
+
if (message.ok) {
|
|
174
|
+
this.updateWallet({
|
|
175
|
+
address: message.data.account === NO_WALLET_ADDRESS ? null : message.data.account,
|
|
176
|
+
chainId: message.data.chainId,
|
|
177
|
+
isConnected: message.data.account !== NO_WALLET_ADDRESS
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
case "tangle.app.accountChanged":
|
|
182
|
+
this.updateWallet({
|
|
183
|
+
address: message.account,
|
|
184
|
+
chainId: this.wallet.chainId,
|
|
185
|
+
isConnected: message.account !== null
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
case "tangle.app.chainChanged":
|
|
189
|
+
this.updateWallet({
|
|
190
|
+
address: this.wallet.address,
|
|
191
|
+
chainId: message.chainId,
|
|
192
|
+
isConnected: this.wallet.isConnected
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
case "tangle.app.serviceContext":
|
|
196
|
+
this.updateService(message);
|
|
197
|
+
return;
|
|
198
|
+
case "tangle.app.jobResult":
|
|
199
|
+
this.handleJobResult(message);
|
|
200
|
+
return;
|
|
201
|
+
// Wallet-shape responses (signMessageResult etc.) are routed by
|
|
202
|
+
// dispatchWallet's promise resolver, not here.
|
|
203
|
+
default:
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
async dispatchWallet(kind, payload) {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const correlationId = makeCorrelationId(kind);
|
|
210
|
+
const timeout = this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
211
|
+
const expectedKind = {
|
|
212
|
+
"tangle.app.signMessage": "tangle.app.signMessageResult",
|
|
213
|
+
"tangle.app.signTransaction": "tangle.app.signTransactionResult",
|
|
214
|
+
"tangle.app.switchChain": "tangle.app.switchChainResult"
|
|
215
|
+
}[kind];
|
|
216
|
+
const timer = setTimeout(() => {
|
|
217
|
+
window.removeEventListener("message", listener);
|
|
218
|
+
reject(bridgeError(4900, `Parent did not respond to ${kind} in ${timeout}ms`));
|
|
219
|
+
}, timeout);
|
|
220
|
+
const listener = (event) => {
|
|
221
|
+
if (event.origin !== this.options.parentOrigin) return;
|
|
222
|
+
const data = event.data;
|
|
223
|
+
if (typeof data !== "object" || data === null) return;
|
|
224
|
+
const msg = data;
|
|
225
|
+
if (msg.kind !== expectedKind || !("correlationId" in msg) || msg.correlationId !== correlationId) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
window.removeEventListener("message", listener);
|
|
230
|
+
const env = msg;
|
|
231
|
+
if (env.ok) {
|
|
232
|
+
resolve(env.data);
|
|
233
|
+
} else {
|
|
234
|
+
reject(bridgeError(4001, env.error ?? "Parent rejected request"));
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
window.addEventListener("message", listener);
|
|
238
|
+
this.postToParent({ kind, correlationId, ...payload });
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
handleJobResult(message) {
|
|
242
|
+
const pending = this.pendingJobs.get(message.correlationId);
|
|
243
|
+
if (!pending) return;
|
|
244
|
+
const updated = {
|
|
245
|
+
correlationId: message.correlationId,
|
|
246
|
+
status: message.status,
|
|
247
|
+
chunks: message.chunk !== void 0 ? [...pending.invocation.chunks, message.chunk] : pending.invocation.chunks,
|
|
248
|
+
...message.data !== void 0 ? { data: message.data } : {},
|
|
249
|
+
...message.error !== void 0 ? { error: message.error } : {},
|
|
250
|
+
...message.progress !== void 0 ? { progress: message.progress } : {}
|
|
251
|
+
};
|
|
252
|
+
pending.invocation = updated;
|
|
253
|
+
this.emit("job", updated);
|
|
254
|
+
if (message.status === "success" || message.status === "error") {
|
|
255
|
+
clearTimeout(pending.timer);
|
|
256
|
+
this.pendingJobs.delete(message.correlationId);
|
|
257
|
+
if (message.status === "success") {
|
|
258
|
+
pending.resolve(updated);
|
|
259
|
+
} else {
|
|
260
|
+
pending.reject(bridgeError(4001, message.error ?? "Job failed"));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
updateWallet(next) {
|
|
265
|
+
if (this.wallet.address === next.address && this.wallet.chainId === next.chainId && this.wallet.isConnected === next.isConnected) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this.wallet = next;
|
|
269
|
+
this.emit("wallet", next);
|
|
270
|
+
}
|
|
271
|
+
updateService(broadcast) {
|
|
272
|
+
const next = {
|
|
273
|
+
blueprintId: broadcast.blueprintId,
|
|
274
|
+
serviceId: broadcast.serviceId,
|
|
275
|
+
operators: broadcast.operators,
|
|
276
|
+
jobs: broadcast.jobs,
|
|
277
|
+
mode: broadcast.mode
|
|
278
|
+
};
|
|
279
|
+
this.service = next;
|
|
280
|
+
this.emit("service", next);
|
|
281
|
+
}
|
|
282
|
+
emit(event, value) {
|
|
283
|
+
for (const listener of [...this.listeners[event]]) {
|
|
284
|
+
try {
|
|
285
|
+
listener(value);
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async ensureBootstrapped() {
|
|
291
|
+
if (this.handshakeAcked) return;
|
|
292
|
+
this.install();
|
|
293
|
+
await new Promise((resolve) => {
|
|
294
|
+
this.handshakeWaiters.push(resolve);
|
|
295
|
+
const retry = setInterval(() => {
|
|
296
|
+
if (this.handshakeAcked) {
|
|
297
|
+
clearInterval(retry);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.postHandshake();
|
|
301
|
+
}, 500);
|
|
302
|
+
setTimeout(() => clearInterval(retry), 1e4);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
function bridgeError(code, message) {
|
|
307
|
+
const err = new Error(message);
|
|
308
|
+
err.code = code;
|
|
309
|
+
return err;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/iframe/TangleIframeProvider.tsx
|
|
313
|
+
import { jsx } from "react/jsx-runtime";
|
|
314
|
+
var TangleIframeContext = createContext(null);
|
|
315
|
+
var NULL_WALLET2 = {
|
|
316
|
+
address: null,
|
|
317
|
+
chainId: null,
|
|
318
|
+
isConnected: false
|
|
319
|
+
};
|
|
320
|
+
var NULL_SERVICE2 = {
|
|
321
|
+
blueprintId: null,
|
|
322
|
+
serviceId: null,
|
|
323
|
+
operators: [],
|
|
324
|
+
jobs: [],
|
|
325
|
+
mode: null
|
|
326
|
+
};
|
|
327
|
+
function TangleIframeProvider({
|
|
328
|
+
appId,
|
|
329
|
+
parentOrigin: explicitOrigin,
|
|
330
|
+
extraOrigins,
|
|
331
|
+
mode: requestedMode = "auto",
|
|
332
|
+
children
|
|
333
|
+
}) {
|
|
334
|
+
const resolution = useMemo(() => {
|
|
335
|
+
if (requestedMode === "dev") {
|
|
336
|
+
return { mode: "dev", parentOrigin: null };
|
|
337
|
+
}
|
|
338
|
+
const detected = explicitOrigin ?? detectTangleCloudParentOrigin({ extraOrigins });
|
|
339
|
+
if (requestedMode === "bridge") {
|
|
340
|
+
if (!detected) {
|
|
341
|
+
console.error(
|
|
342
|
+
'[TangleIframeProvider] mode="bridge" but no trusted parent was detected. Falling back to dev mode.'
|
|
343
|
+
);
|
|
344
|
+
return { mode: "dev", parentOrigin: null };
|
|
345
|
+
}
|
|
346
|
+
return { mode: "bridge", parentOrigin: detected };
|
|
347
|
+
}
|
|
348
|
+
return detected ? { mode: "bridge", parentOrigin: detected } : { mode: "dev", parentOrigin: null };
|
|
349
|
+
}, [requestedMode, explicitOrigin, extraOrigins]);
|
|
350
|
+
const clientRef = useRef(null);
|
|
351
|
+
const [wallet, setWallet] = useState(NULL_WALLET2);
|
|
352
|
+
const [service, setService] = useState(NULL_SERVICE2);
|
|
353
|
+
const [isReady, setIsReady] = useState(false);
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
if (resolution.mode === "dev") {
|
|
356
|
+
setIsReady(true);
|
|
357
|
+
return void 0;
|
|
358
|
+
}
|
|
359
|
+
const options = {
|
|
360
|
+
parentOrigin: resolution.parentOrigin,
|
|
361
|
+
appId
|
|
362
|
+
};
|
|
363
|
+
const client = new TangleIframeClient(options);
|
|
364
|
+
clientRef.current = client;
|
|
365
|
+
const unsubWallet = client.subscribe("wallet", setWallet);
|
|
366
|
+
const unsubService = client.subscribe("service", setService);
|
|
367
|
+
client.install();
|
|
368
|
+
setIsReady(true);
|
|
369
|
+
return () => {
|
|
370
|
+
unsubWallet();
|
|
371
|
+
unsubService();
|
|
372
|
+
client.uninstall();
|
|
373
|
+
clientRef.current = null;
|
|
374
|
+
setIsReady(false);
|
|
375
|
+
};
|
|
376
|
+
}, [resolution, appId]);
|
|
377
|
+
const value = useMemo(
|
|
378
|
+
() => ({
|
|
379
|
+
client: clientRef.current,
|
|
380
|
+
wallet,
|
|
381
|
+
service,
|
|
382
|
+
mode: resolution.mode,
|
|
383
|
+
isReady
|
|
384
|
+
}),
|
|
385
|
+
[wallet, service, resolution.mode, isReady]
|
|
386
|
+
);
|
|
387
|
+
return /* @__PURE__ */ jsx(TangleIframeContext.Provider, { value, children });
|
|
388
|
+
}
|
|
389
|
+
function useTangleIframeContext() {
|
|
390
|
+
const ctx = useContext(TangleIframeContext);
|
|
391
|
+
if (!ctx) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
"useTangleIframeContext must be used inside <TangleIframeProvider>."
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return ctx;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/iframe/hooks.ts
|
|
400
|
+
import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
|
|
401
|
+
function useTangleWallet() {
|
|
402
|
+
const { client, wallet } = useTangleIframeContext();
|
|
403
|
+
const signMessage = useCallback(
|
|
404
|
+
(message) => {
|
|
405
|
+
if (!client) throw new Error("Wallet not available in dev mode.");
|
|
406
|
+
return client.signMessage(message);
|
|
407
|
+
},
|
|
408
|
+
[client]
|
|
409
|
+
);
|
|
410
|
+
const sendTransaction = useCallback(
|
|
411
|
+
(tx) => {
|
|
412
|
+
if (!client) throw new Error("Wallet not available in dev mode.");
|
|
413
|
+
return client.sendTransaction(tx);
|
|
414
|
+
},
|
|
415
|
+
[client]
|
|
416
|
+
);
|
|
417
|
+
const switchChain = useCallback(
|
|
418
|
+
(chainId) => {
|
|
419
|
+
if (!client) throw new Error("Wallet not available in dev mode.");
|
|
420
|
+
return client.switchChain(chainId);
|
|
421
|
+
},
|
|
422
|
+
[client]
|
|
423
|
+
);
|
|
424
|
+
return { ...wallet, signMessage, sendTransaction, switchChain };
|
|
425
|
+
}
|
|
426
|
+
function useTangleService() {
|
|
427
|
+
return useTangleIframeContext().service;
|
|
428
|
+
}
|
|
429
|
+
function useCallJob() {
|
|
430
|
+
const { client } = useTangleIframeContext();
|
|
431
|
+
const [invocation, setInvocation] = useState2(null);
|
|
432
|
+
const [latestId, setLatestId] = useState2(null);
|
|
433
|
+
useEffect2(() => {
|
|
434
|
+
if (!client) return void 0;
|
|
435
|
+
return client.subscribe("job", (next) => {
|
|
436
|
+
setLatestId((prevLatest) => {
|
|
437
|
+
if (prevLatest === null || prevLatest === next.correlationId) {
|
|
438
|
+
setInvocation(next);
|
|
439
|
+
return next.correlationId;
|
|
440
|
+
}
|
|
441
|
+
return prevLatest;
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}, [client]);
|
|
445
|
+
const call = useCallback(
|
|
446
|
+
async (args) => {
|
|
447
|
+
if (!client) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
"Job invocation not available in dev mode without a configured stub. See `setDevJobHandler` in the testing harness."
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
setInvocation(null);
|
|
453
|
+
const result = await client.callJob(args);
|
|
454
|
+
setLatestId(result.correlationId);
|
|
455
|
+
return result;
|
|
456
|
+
},
|
|
457
|
+
[client]
|
|
458
|
+
);
|
|
459
|
+
const reset = useCallback(() => {
|
|
460
|
+
setInvocation(null);
|
|
461
|
+
setLatestId(null);
|
|
462
|
+
}, []);
|
|
463
|
+
return useMemo2(
|
|
464
|
+
() => ({ call, invocation, reset, isPending: invocation?.status === "pending" || invocation?.status === "streaming" }),
|
|
465
|
+
[call, invocation, reset]
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
function useTangleAddress() {
|
|
469
|
+
return useTangleIframeContext().wallet.address;
|
|
470
|
+
}
|
|
471
|
+
function useTangleReady() {
|
|
472
|
+
return useTangleIframeContext().isReady;
|
|
473
|
+
}
|
|
474
|
+
function useTangleMode() {
|
|
475
|
+
return useTangleIframeContext().mode;
|
|
476
|
+
}
|
|
477
|
+
export {
|
|
478
|
+
TANGLE_CLOUD_ORIGINS_DEFAULT,
|
|
479
|
+
TangleIframeClient,
|
|
480
|
+
TangleIframeProvider,
|
|
481
|
+
useCallJob,
|
|
482
|
+
useTangleAddress,
|
|
483
|
+
useTangleIframeContext,
|
|
484
|
+
useTangleMode,
|
|
485
|
+
useTangleReady,
|
|
486
|
+
useTangleService,
|
|
487
|
+
useTangleWallet
|
|
488
|
+
};
|
|
489
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/iframe/TangleIframeProvider.tsx","../../src/iframe/tangleIframeClient.ts","../../src/iframe/hooks.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport {\n TangleIframeClient,\n type ServiceSnapshot,\n type TangleIframeClientOptions,\n type WalletSnapshot,\n} from './tangleIframeClient';\nimport {\n detectTangleCloudParentOrigin,\n TANGLE_CLOUD_ORIGINS_DEFAULT,\n} from '../wallet/detectParentOrigin';\n\ntype Props = {\n appId: string;\n /** Override the detected parent origin (e.g. dev/staging deploys). */\n parentOrigin?: string;\n /** Extra trusted origins for `detectTangleCloudParentOrigin`. */\n extraOrigins?: readonly string[];\n /**\n * Override the bootstrap behavior. When `'auto'` (default), the SDK\n * sniffs the embed context: real parent → install the bridge, top-frame\n * → drop into dev mode. `'bridge'` forces real-parent mode and throws\n * if no parent is detected. `'dev'` forces dev mode even when embedded\n * — useful for component-level tests.\n */\n mode?: 'auto' | 'bridge' | 'dev';\n children: ReactNode;\n};\n\ntype ContextValue = {\n readonly client: TangleIframeClient | null;\n readonly wallet: WalletSnapshot;\n readonly service: ServiceSnapshot;\n readonly mode: 'bridge' | 'dev';\n readonly isReady: boolean;\n};\n\nconst TangleIframeContext = createContext<ContextValue | null>(null);\n\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n};\n\n/**\n * Iframe-blueprint root provider. Wrap your app once at the entry point.\n *\n * In `auto` mode (default) the SDK detects whether the app is embedded by a\n * trusted Tangle Cloud parent. If yes → installs the postMessage bridge.\n * If no (running standalone at `localhost:5173` etc.) → enters **dev mode**\n * with an in-memory state machine that the developer can drive via the\n * exported debug controls. Dev mode keeps the hook surface identical to\n * production so component code never branches on embed-vs-not.\n *\n * Three lifecycle stages:\n *\n * 1. Mount — `client` is created, mode is decided.\n * 2. Bootstrap — handshake (bridge) or first-paint setup (dev). The\n * `isReady` flag flips to true.\n * 3. Active — wallet + service snapshots flow in via subscriptions.\n */\nexport function TangleIframeProvider({\n appId,\n parentOrigin: explicitOrigin,\n extraOrigins,\n mode: requestedMode = 'auto',\n children,\n}: Props) {\n // Resolve the effective mode once at mount. Switching modes mid-session\n // would tear down the bridge / dev state inconsistently; restart instead.\n const resolution = useMemo(() => {\n if (requestedMode === 'dev') {\n return { mode: 'dev' as const, parentOrigin: null };\n }\n const detected =\n explicitOrigin ?? detectTangleCloudParentOrigin({ extraOrigins });\n if (requestedMode === 'bridge') {\n if (!detected) {\n // eslint-disable-next-line no-console\n console.error(\n '[TangleIframeProvider] mode=\"bridge\" but no trusted parent was detected. Falling back to dev mode.',\n );\n return { mode: 'dev' as const, parentOrigin: null };\n }\n return { mode: 'bridge' as const, parentOrigin: detected };\n }\n // auto: bridge when detected, dev otherwise.\n return detected\n ? { mode: 'bridge' as const, parentOrigin: detected }\n : { mode: 'dev' as const, parentOrigin: null };\n }, [requestedMode, explicitOrigin, extraOrigins]);\n\n const clientRef = useRef<TangleIframeClient | null>(null);\n const [wallet, setWallet] = useState<WalletSnapshot>(NULL_WALLET);\n const [service, setService] = useState<ServiceSnapshot>(NULL_SERVICE);\n const [isReady, setIsReady] = useState(false);\n\n useEffect(() => {\n if (resolution.mode === 'dev') {\n // Dev mode: no bridge. The DevHarness component (or a test) seeds\n // wallet + service via `setDevWallet` / `setDevService` on the\n // returned context. Mark ready immediately so app code unblocks.\n setIsReady(true);\n return undefined;\n }\n // Bridge mode\n const options: TangleIframeClientOptions = {\n parentOrigin: resolution.parentOrigin,\n appId,\n };\n const client = new TangleIframeClient(options);\n clientRef.current = client;\n const unsubWallet = client.subscribe('wallet', setWallet);\n const unsubService = client.subscribe('service', setService);\n client.install();\n setIsReady(true);\n return () => {\n unsubWallet();\n unsubService();\n client.uninstall();\n clientRef.current = null;\n setIsReady(false);\n };\n }, [resolution, appId]);\n\n const value = useMemo<ContextValue>(\n () => ({\n client: clientRef.current,\n wallet,\n service,\n mode: resolution.mode,\n isReady,\n }),\n [wallet, service, resolution.mode, isReady],\n );\n\n return (\n <TangleIframeContext.Provider value={value}>\n {children}\n </TangleIframeContext.Provider>\n );\n}\n\nexport function useTangleIframeContext(): ContextValue {\n const ctx = useContext(TangleIframeContext);\n if (!ctx) {\n throw new Error(\n 'useTangleIframeContext must be used inside <TangleIframeProvider>.',\n );\n }\n return ctx;\n}\n\nexport { TANGLE_CLOUD_ORIGINS_DEFAULT };\n","// Thin-iframe SDK client — the framework-agnostic state machine that talks\n// to a Tangle Cloud parent dapp over postMessage. React hooks (below) are\n// thin wrappers around an instance of this class.\n//\n// Why a class, not a bag of functions: the iframe lifecycle is stateful —\n// handshake, account changes, service-context broadcasts, in-flight job\n// requests. The class owns that state once; hooks subscribe via listeners.\n// Testing the protocol shape doesn't require React.\n\nimport type { Address, Hex } from 'viem';\n\nimport {\n makeCorrelationId,\n NO_WALLET_ADDRESS,\n TANGLE_IFRAME_PROTOCOL_VERSION,\n type CallJobRequest,\n type JobInputs,\n type JobResultEvent,\n type JobResultStatus,\n type ParentMessage,\n type ServiceContextBroadcast,\n type ServiceContextJob,\n type ServiceContextOperator,\n} from '../wallet/parentBridgeProtocol';\n\nexport type WalletSnapshot = {\n readonly address: Address | null;\n readonly chainId: number | null;\n readonly isConnected: boolean;\n};\n\nexport type ServiceSnapshot = {\n readonly blueprintId: string | null;\n readonly serviceId: string | null;\n readonly operators: readonly ServiceContextOperator[];\n readonly jobs: readonly ServiceContextJob[];\n readonly mode: string | null;\n};\n\nexport type JobInvocation = {\n readonly correlationId: string;\n readonly status: JobResultStatus;\n readonly data?: unknown;\n readonly chunks: readonly unknown[];\n readonly error?: string;\n readonly progress?: { readonly percent?: number; readonly eta_ms?: number };\n};\n\nexport type ClientEventMap = {\n wallet: WalletSnapshot;\n service: ServiceSnapshot;\n job: JobInvocation;\n};\n\ntype Listener<K extends keyof ClientEventMap> = (\n value: ClientEventMap[K],\n) => void;\n\nexport type TangleIframeClientOptions = {\n /**\n * Origin of the parent dapp. The client posts every message with this\n * exact `targetOrigin` and rejects inbound messages from any other origin.\n * Pass `'*'` only in dev — production must pin to the real parent\n * (`https://cloud.tangle.tools` etc.).\n */\n parentOrigin: string;\n /**\n * Stable identifier for this iframe app. The parent surfaces it in\n * handshake logs + uses it for permission scoping.\n */\n appId: string;\n /**\n * Per-request timeout. Defaults to 60s — long enough for a user to\n * read + approve a signing prompt in the parent. Long-running jobs\n * stream progress events; the request \"completes\" only on terminal\n * status, so the timeout protects against parents that drop replies\n * entirely.\n */\n requestTimeoutMs?: number;\n};\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000;\nconst NULL_WALLET: WalletSnapshot = {\n address: null,\n chainId: null,\n isConnected: false,\n};\nconst NULL_SERVICE: ServiceSnapshot = {\n blueprintId: null,\n serviceId: null,\n operators: [],\n jobs: [],\n mode: null,\n};\n\ntype PendingJob = {\n resolve: (value: JobInvocation) => void;\n reject: (reason: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n invocation: JobInvocation;\n};\n\nexport class TangleIframeClient {\n private wallet: WalletSnapshot = NULL_WALLET;\n private service: ServiceSnapshot = NULL_SERVICE;\n private handshakeAcked = false;\n private handshakeWaiters: Array<() => void> = [];\n private installed = false;\n private listeners: {\n [K in keyof ClientEventMap]: Set<Listener<K>>;\n } = {\n wallet: new Set(),\n service: new Set(),\n job: new Set(),\n };\n private pendingJobs = new Map<string, PendingJob>();\n\n constructor(private readonly options: TangleIframeClientOptions) {}\n\n /** Wire the global message listener + initial handshake. Idempotent. */\n install(): void {\n if (this.installed || typeof window === 'undefined') return;\n this.installed = true;\n window.addEventListener('message', this.handleParentMessage);\n this.postHandshake();\n }\n\n uninstall(): void {\n if (!this.installed || typeof window === 'undefined') return;\n this.installed = false;\n window.removeEventListener('message', this.handleParentMessage);\n for (const [, pending] of this.pendingJobs) {\n clearTimeout(pending.timer);\n pending.reject(new Error('Tangle iframe client uninstalled'));\n }\n this.pendingJobs.clear();\n }\n\n // ── State accessors ─────────────────────────────────────────────────────\n\n getWallet(): WalletSnapshot {\n return this.wallet;\n }\n getService(): ServiceSnapshot {\n return this.service;\n }\n\n // ── Subscription API (used by React hooks) ──────────────────────────────\n\n subscribe<K extends keyof ClientEventMap>(\n event: K,\n listener: Listener<K>,\n ): () => void {\n this.listeners[event].add(listener);\n return () => {\n this.listeners[event].delete(listener);\n };\n }\n\n // ── Wallet operations ───────────────────────────────────────────────────\n\n async signMessage(message: string): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signMessage', {\n chainId: this.wallet.chainId ?? 0,\n message,\n }).then((data) => (data as { signature: Hex }).signature);\n }\n\n async sendTransaction(tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }): Promise<Hex> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.signTransaction', {\n chainId: this.wallet.chainId ?? 0,\n to: tx.to,\n data: tx.data,\n ...(tx.value !== undefined ? { value: tx.value.toString(10) } : {}),\n }).then((data) => (data as { txHash: Hex }).txHash);\n }\n\n async switchChain(chainId: number): Promise<number> {\n await this.ensureBootstrapped();\n return this.dispatchWallet('tangle.app.switchChain', { chainId }).then(\n (data) => (data as { chainId: number }).chainId,\n );\n }\n\n // ── Job invocation ──────────────────────────────────────────────────────\n\n /**\n * Invoke a blueprint job. Returns a Promise that resolves on terminal\n * status (`success` or `error`); subscribe to the `job` event for\n * intermediate streaming chunks.\n *\n * Streaming opt-in: pass `stream: true` if the publisher's job emits\n * chunks (LLM generation, video encoding). One-shot jobs (embeddings,\n * classifications) skip the streaming machinery.\n */\n async callJob(args: {\n jobIndex: number;\n inputs: JobInputs;\n stream?: boolean;\n }): Promise<JobInvocation> {\n await this.ensureBootstrapped();\n const correlationId = makeCorrelationId('tangle.app.callJob');\n const timeout =\n this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n return new Promise<JobInvocation>((resolve, reject) => {\n const invocation: JobInvocation = {\n correlationId,\n status: 'pending',\n chunks: [],\n };\n const timer = setTimeout(() => {\n this.pendingJobs.delete(correlationId);\n reject(\n bridgeError(4900, `Job did not respond within ${timeout}ms`),\n );\n }, timeout);\n this.pendingJobs.set(correlationId, {\n resolve,\n reject,\n timer,\n invocation,\n });\n const message: CallJobRequest = {\n kind: 'tangle.app.callJob',\n correlationId,\n jobIndex: args.jobIndex,\n inputs: args.inputs,\n ...(args.stream !== undefined ? { stream: args.stream } : {}),\n };\n this.postToParent(message);\n // Emit pending immediately so consumer UIs can show optimistic state.\n this.emit('job', invocation);\n });\n }\n\n // ── Internals ───────────────────────────────────────────────────────────\n\n private postHandshake(): void {\n this.postToParent({\n kind: 'tangle.app.handshake',\n appId: this.options.appId,\n version: TANGLE_IFRAME_PROTOCOL_VERSION,\n });\n }\n\n private postToParent(message: object): void {\n if (typeof window === 'undefined') return;\n try {\n window.parent.postMessage(message, this.options.parentOrigin);\n } catch {\n // Cross-origin / sandboxed; defensive only — postMessage shouldn't throw.\n }\n }\n\n private handleParentMessage = (event: MessageEvent): void => {\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const message = data as ParentMessage;\n switch (message.kind) {\n case 'tangle.app.handshakeAck':\n this.handshakeAcked = true;\n for (const resolve of this.handshakeWaiters) resolve();\n this.handshakeWaiters = [];\n return;\n case 'tangle.app.readAccountResult':\n if (message.ok) {\n this.updateWallet({\n address:\n message.data.account === NO_WALLET_ADDRESS\n ? null\n : message.data.account,\n chainId: message.data.chainId,\n isConnected: message.data.account !== NO_WALLET_ADDRESS,\n });\n }\n return;\n case 'tangle.app.accountChanged':\n this.updateWallet({\n address: message.account,\n chainId: this.wallet.chainId,\n isConnected: message.account !== null,\n });\n return;\n case 'tangle.app.chainChanged':\n this.updateWallet({\n address: this.wallet.address,\n chainId: message.chainId,\n isConnected: this.wallet.isConnected,\n });\n return;\n case 'tangle.app.serviceContext':\n this.updateService(message);\n return;\n case 'tangle.app.jobResult':\n this.handleJobResult(message);\n return;\n // Wallet-shape responses (signMessageResult etc.) are routed by\n // dispatchWallet's promise resolver, not here.\n default:\n return;\n }\n };\n\n private async dispatchWallet(\n kind:\n | 'tangle.app.signMessage'\n | 'tangle.app.signTransaction'\n | 'tangle.app.switchChain',\n payload: Record<string, unknown>,\n ): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const correlationId = makeCorrelationId(kind);\n const timeout =\n this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const expectedKind = (\n {\n 'tangle.app.signMessage': 'tangle.app.signMessageResult',\n 'tangle.app.signTransaction': 'tangle.app.signTransactionResult',\n 'tangle.app.switchChain': 'tangle.app.switchChainResult',\n } as const\n )[kind];\n const timer = setTimeout(() => {\n window.removeEventListener('message', listener);\n reject(bridgeError(4900, `Parent did not respond to ${kind} in ${timeout}ms`));\n }, timeout);\n const listener = (event: MessageEvent) => {\n if (event.origin !== this.options.parentOrigin) return;\n const data = event.data;\n if (typeof data !== 'object' || data === null) return;\n const msg = data as ParentMessage;\n if (\n msg.kind !== expectedKind ||\n !('correlationId' in msg) ||\n msg.correlationId !== correlationId\n ) {\n return;\n }\n clearTimeout(timer);\n window.removeEventListener('message', listener);\n // Narrow the type — expectedKind is the wallet-shape `{ok, data|error}` envelope\n const env = msg as {\n ok: boolean;\n data?: unknown;\n error?: string;\n };\n if (env.ok) {\n resolve(env.data);\n } else {\n reject(bridgeError(4001, env.error ?? 'Parent rejected request'));\n }\n };\n window.addEventListener('message', listener);\n this.postToParent({ kind, correlationId, ...payload });\n });\n }\n\n private handleJobResult(message: JobResultEvent): void {\n const pending = this.pendingJobs.get(message.correlationId);\n if (!pending) return;\n const updated: JobInvocation = {\n correlationId: message.correlationId,\n status: message.status,\n chunks:\n message.chunk !== undefined\n ? [...pending.invocation.chunks, message.chunk]\n : pending.invocation.chunks,\n ...(message.data !== undefined ? { data: message.data } : {}),\n ...(message.error !== undefined ? { error: message.error } : {}),\n ...(message.progress !== undefined ? { progress: message.progress } : {}),\n };\n pending.invocation = updated;\n this.emit('job', updated);\n if (message.status === 'success' || message.status === 'error') {\n clearTimeout(pending.timer);\n this.pendingJobs.delete(message.correlationId);\n if (message.status === 'success') {\n pending.resolve(updated);\n } else {\n pending.reject(bridgeError(4001, message.error ?? 'Job failed'));\n }\n }\n }\n\n private updateWallet(next: WalletSnapshot): void {\n if (\n this.wallet.address === next.address &&\n this.wallet.chainId === next.chainId &&\n this.wallet.isConnected === next.isConnected\n ) {\n return;\n }\n this.wallet = next;\n this.emit('wallet', next);\n }\n\n private updateService(broadcast: ServiceContextBroadcast): void {\n const next: ServiceSnapshot = {\n blueprintId: broadcast.blueprintId,\n serviceId: broadcast.serviceId,\n operators: broadcast.operators,\n jobs: broadcast.jobs,\n mode: broadcast.mode,\n };\n this.service = next;\n this.emit('service', next);\n }\n\n private emit<K extends keyof ClientEventMap>(\n event: K,\n value: ClientEventMap[K],\n ): void {\n for (const listener of [...this.listeners[event]]) {\n try {\n (listener as Listener<K>)(value);\n } catch {\n // Listener bugs shouldn't break the bridge.\n }\n }\n }\n\n private async ensureBootstrapped(): Promise<void> {\n if (this.handshakeAcked) return;\n this.install();\n await new Promise<void>((resolve) => {\n this.handshakeWaiters.push(resolve);\n const retry = setInterval(() => {\n if (this.handshakeAcked) {\n clearInterval(retry);\n return;\n }\n this.postHandshake();\n }, 500);\n setTimeout(() => clearInterval(retry), 10_000);\n });\n }\n}\n\nfunction bridgeError(code: number, message: string): Error {\n const err = new Error(message) as Error & { code?: number };\n err.code = code;\n return err;\n}\n","import { useCallback, useEffect, useMemo, useState } from 'react';\nimport type { Address, Hex } from 'viem';\n\nimport { useTangleIframeContext } from './TangleIframeProvider';\nimport type {\n JobInvocation,\n ServiceSnapshot,\n WalletSnapshot,\n} from './tangleIframeClient';\nimport type { JobInputs } from '../wallet/parentBridgeProtocol';\n\n/**\n * Read-only view of the connected wallet, plus the operations the iframe\n * can request the parent to perform.\n *\n * The iframe never holds a private key, never sees `window.ethereum`, never\n * imports wagmi. All wallet work happens upstream in the Tangle Cloud\n * dapp's wagmi config + ConnectKit modal.\n */\nexport function useTangleWallet(): WalletSnapshot & {\n signMessage: (message: string) => Promise<Hex>;\n sendTransaction: (tx: {\n to: Address;\n data: Hex;\n value?: bigint;\n }) => Promise<Hex>;\n switchChain: (chainId: number) => Promise<number>;\n} {\n const { client, wallet } = useTangleIframeContext();\n const signMessage = useCallback(\n (message: string) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.signMessage(message);\n },\n [client],\n );\n const sendTransaction = useCallback(\n (tx: { to: Address; data: Hex; value?: bigint }) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.sendTransaction(tx);\n },\n [client],\n );\n const switchChain = useCallback(\n (chainId: number) => {\n if (!client) throw new Error('Wallet not available in dev mode.');\n return client.switchChain(chainId);\n },\n [client],\n );\n return { ...wallet, signMessage, sendTransaction, switchChain };\n}\n\n/**\n * The service the iframe is currently rendering for. Broadcast by the\n * parent dapp on mount + every time the service/mode changes — the iframe\n * never queries the chain or the indexer itself.\n *\n * `serviceId === null` means the operator hasn't deployed an instance yet;\n * the iframe should render its deploy-ready / configuration surface.\n */\nexport function useTangleService(): ServiceSnapshot {\n return useTangleIframeContext().service;\n}\n\n/**\n * Invoke a blueprint job. Returns a callable + a snapshot of the most\n * recent invocation (or null if none yet).\n *\n * Streaming jobs (LLM, video, audio) opt in via `stream: true`. The hook's\n * `invocation.chunks` accumulates each streaming chunk so the UI can render\n * progressive output. For one-shot jobs (embeddings, classification), use\n * the `invocation.data` once `status === 'success'`.\n *\n * Multiple in-flight invocations are supported — each `call()` returns its\n * own correlationId. The hook tracks only the *latest* invocation in its\n * state; consumers that need all history can subscribe to the client's\n * `job` event directly.\n */\nexport function useCallJob() {\n const { client } = useTangleIframeContext();\n const [invocation, setInvocation] = useState<JobInvocation | null>(null);\n const [latestId, setLatestId] = useState<string | null>(null);\n\n useEffect(() => {\n if (!client) return undefined;\n return client.subscribe('job', (next) => {\n // Only update if this is the latest invocation, or no latest tracked.\n setLatestId((prevLatest) => {\n if (prevLatest === null || prevLatest === next.correlationId) {\n setInvocation(next);\n return next.correlationId;\n }\n return prevLatest;\n });\n });\n }, [client]);\n\n const call = useCallback(\n async (args: { jobIndex: number; inputs: JobInputs; stream?: boolean }) => {\n if (!client) {\n throw new Error(\n 'Job invocation not available in dev mode without a configured stub. See `setDevJobHandler` in the testing harness.',\n );\n }\n // Clear prior invocation state when starting a new call.\n setInvocation(null);\n const result = await client.callJob(args);\n setLatestId(result.correlationId);\n return result;\n },\n [client],\n );\n\n const reset = useCallback(() => {\n setInvocation(null);\n setLatestId(null);\n }, []);\n\n return useMemo(\n () => ({ call, invocation, reset, isPending: invocation?.status === 'pending' || invocation?.status === 'streaming' }),\n [call, invocation, reset],\n );\n}\n\n/**\n * Convenience: returns just the address when connected, or `null`. Most\n * iframe components only care about the address.\n */\nexport function useTangleAddress(): Address | null {\n return useTangleIframeContext().wallet.address;\n}\n\n/** Whether the iframe has completed its parent-handshake (or is in dev mode). */\nexport function useTangleReady(): boolean {\n return useTangleIframeContext().isReady;\n}\n\n/** Resolved mode — `'bridge'` (real parent) or `'dev'` (standalone). */\nexport function useTangleMode(): 'bridge' | 'dev' {\n return useTangleIframeContext().mode;\n}\n"],"mappings":"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","names":["NULL_WALLET","NULL_SERVICE","useEffect","useMemo","useState","useState","useEffect","useMemo"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { FC, ReactNode } from 'react';
|
|
2
|
+
import { Address } from 'viem';
|
|
3
|
+
import { W as WalletSnapshot, S as ServiceSnapshot } from '../tangleIframeClient-D-PP-KhN.js';
|
|
4
|
+
import { C as CallJobRequest, g as ServiceContextOperator, f as ServiceContextJob } from '../parentBridgeProtocol-CqK9e6Fk.js';
|
|
5
|
+
|
|
6
|
+
type MockWalletInput = Partial<{
|
|
7
|
+
address: Address | null;
|
|
8
|
+
chainId: number;
|
|
9
|
+
isConnected: boolean;
|
|
10
|
+
}>;
|
|
11
|
+
type MockServiceInput = Partial<{
|
|
12
|
+
blueprintId: string;
|
|
13
|
+
serviceId: string | null;
|
|
14
|
+
operators: readonly ServiceContextOperator[];
|
|
15
|
+
jobs: readonly ServiceContextJob[];
|
|
16
|
+
mode: string | null;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Construct a deterministic wallet snapshot for tests. Defaults:
|
|
20
|
+
* connected, vitalik.eth's address, Base Sepolia (84532).
|
|
21
|
+
*/
|
|
22
|
+
declare function mockWallet(input?: MockWalletInput): WalletSnapshot;
|
|
23
|
+
/**
|
|
24
|
+
* Construct a deterministic service snapshot for tests. Defaults: blueprint
|
|
25
|
+
* id `0`, no service deployed yet (serviceId null), single mock operator on
|
|
26
|
+
* the canonical local sidecar URL.
|
|
27
|
+
*/
|
|
28
|
+
declare function mockServiceContext(input?: MockServiceInput): ServiceSnapshot;
|
|
29
|
+
type CallJobHandler = (request: CallJobRequest) => Promise<{
|
|
30
|
+
status: 'success' | 'error';
|
|
31
|
+
data?: unknown;
|
|
32
|
+
error?: string;
|
|
33
|
+
/** Streaming chunks emitted in order before the terminal status. */
|
|
34
|
+
chunks?: readonly unknown[];
|
|
35
|
+
}>;
|
|
36
|
+
type HarnessProps = {
|
|
37
|
+
appId?: string;
|
|
38
|
+
wallet?: WalletSnapshot;
|
|
39
|
+
service?: ServiceSnapshot;
|
|
40
|
+
/** Override callJob behavior. Default: returns a static `{ ok: true }`. */
|
|
41
|
+
onCallJob?: CallJobHandler;
|
|
42
|
+
/** Surface a floating debug panel that lets the developer flip state at runtime. */
|
|
43
|
+
showDebugPanel?: boolean;
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Drop-in parent simulator for tests + storybook + standalone dev. Wraps
|
|
48
|
+
* children in a fake parent that:
|
|
49
|
+
*
|
|
50
|
+
* - Acks the iframe's handshake immediately
|
|
51
|
+
* - Broadcasts the configured wallet + service context on mount
|
|
52
|
+
* - Intercepts `callJob` requests and routes them through `onCallJob`
|
|
53
|
+
* - (Optional) Mounts a floating debug panel so the developer can
|
|
54
|
+
* mutate state at runtime: change account, switch chain, set
|
|
55
|
+
* serviceId, fire a custom job
|
|
56
|
+
*
|
|
57
|
+
* The harness runs in the same JS context as the iframe app — there's no
|
|
58
|
+
* cross-frame postMessage, just same-window event dispatch. That keeps it
|
|
59
|
+
* fully synchronous + assertable, but the messages still flow through the
|
|
60
|
+
* exact same protocol surface the production bridge uses.
|
|
61
|
+
*
|
|
62
|
+
* Usage:
|
|
63
|
+
*
|
|
64
|
+
* <TangleParentHarness wallet={mockWallet()} service={mockServiceContext()}>
|
|
65
|
+
* <TangleIframeProvider appId="my-app" mode="bridge" parentOrigin="harness://">
|
|
66
|
+
* <App />
|
|
67
|
+
* </TangleIframeProvider>
|
|
68
|
+
* </TangleParentHarness>
|
|
69
|
+
*
|
|
70
|
+
* Set `mode="bridge"` + `parentOrigin="harness://"` on the provider so it
|
|
71
|
+
* matches the harness's synthetic origin. In production, use `mode="auto"`
|
|
72
|
+
* (the default).
|
|
73
|
+
*/
|
|
74
|
+
declare const TangleParentHarness: FC<HarnessProps>;
|
|
75
|
+
/**
|
|
76
|
+
* Synthetic origin every harness instance uses. Stable across tests so the
|
|
77
|
+
* iframe SDK + the harness can pin to the same string.
|
|
78
|
+
*/
|
|
79
|
+
declare const HARNESS_ORIGIN = "harness://tangle.local";
|
|
80
|
+
|
|
81
|
+
export { type CallJobHandler, HARNESS_ORIGIN, type MockServiceInput, type MockWalletInput, TangleParentHarness, mockServiceContext, mockWallet };
|