@av-pi-studio/web-client 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/web/assets/{BinaryFallbackViewer-CtnuJYpf.js → BinaryFallbackViewer-DA3AdHMQ.js} +2 -2
  3. package/dist/web/assets/{BinaryFallbackViewer-CtnuJYpf.js.map → BinaryFallbackViewer-DA3AdHMQ.js.map} +1 -1
  4. package/dist/web/assets/{ChatPanel-C6lq2gvF.js → ChatPanel-BPn7-j3E.js} +2 -2
  5. package/dist/web/assets/{ChatPanel-C6lq2gvF.js.map → ChatPanel-BPn7-j3E.js.map} +1 -1
  6. package/dist/web/assets/{CodeView-DaLKvul7.js → CodeView-D1Mr31Vt.js} +2 -2
  7. package/dist/web/assets/{CodeView-DaLKvul7.js.map → CodeView-D1Mr31Vt.js.map} +1 -1
  8. package/dist/web/assets/{FilePanel-CLVso0Ss.js → FilePanel-rWWLYKHq.js} +3 -3
  9. package/dist/web/assets/{FilePanel-CLVso0Ss.js.map → FilePanel-rWWLYKHq.js.map} +1 -1
  10. package/dist/web/assets/{ImageViewer-CX3twfDw.js → ImageViewer-DOWlnLVh.js} +2 -2
  11. package/dist/web/assets/{ImageViewer-CX3twfDw.js.map → ImageViewer-DOWlnLVh.js.map} +1 -1
  12. package/dist/web/assets/{MarkdownFileViewer-CJAMxC3S.js → MarkdownFileViewer-BWNEncBa.js} +2 -2
  13. package/dist/web/assets/{MarkdownFileViewer-CJAMxC3S.js.map → MarkdownFileViewer-BWNEncBa.js.map} +1 -1
  14. package/dist/web/assets/{TerminalPanel-CIRCZ-fc.js → TerminalPanel-BqceIOgb.js} +2 -2
  15. package/dist/web/assets/{TerminalPanel-CIRCZ-fc.js.map → TerminalPanel-BqceIOgb.js.map} +1 -1
  16. package/dist/web/assets/{TextViewer-BGB_hlbZ.js → TextViewer-ZRCymlC7.js} +2 -2
  17. package/dist/web/assets/{TextViewer-BGB_hlbZ.js.map → TextViewer-ZRCymlC7.js.map} +1 -1
  18. package/dist/web/assets/{VideoViewer-CVKaj7vE.js → VideoViewer-Bk2r-nwy.js} +2 -2
  19. package/dist/web/assets/{VideoViewer-CVKaj7vE.js.map → VideoViewer-Bk2r-nwy.js.map} +1 -1
  20. package/dist/web/assets/index-B2U-99PA.css +1 -0
  21. package/dist/web/assets/{index-D9OEyo5k.js → index-DnLvR47L.js} +3 -3
  22. package/dist/web/assets/index-DnLvR47L.js.map +1 -0
  23. package/dist/web/assets/{use-file-download-ypnyMrxl.js → use-file-download-DCDpzMZs.js} +2 -2
  24. package/dist/web/assets/{use-file-download-ypnyMrxl.js.map → use-file-download-DCDpzMZs.js.map} +1 -1
  25. package/dist/web/index.html +2 -2
  26. package/package.json +3 -3
  27. package/dist/web/assets/index-D9OEyo5k.js.map +0 -1
  28. package/dist/web/assets/index-DrGTLQo0.css +0 -1
@@ -1,2 +1,2 @@
1
- var m=Object.defineProperty;var p=(n,e,t)=>e in n?m(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var l=(n,e,t)=>p(n,typeof e!="symbol"?e+"":e,t);import{r as u}from"./vendor-react-BoIQBqDW.js";import{u as h}from"./vendor-query-CAh-Aqig.js";import{a as d,r as w}from"./index-D9OEyo5k.js";import"./vendor--os7hw7d.js";function y(n){const e=n.reduce((i,s)=>i+s.length,0),t=new Uint8Array(e);let o=0;for(const i of n)t.set(i,o),o+=i.length;return t}class g{constructor(e){l(this,"pending",new Map);l(this,"nextStream",1);l(this,"detach",null);this.daemon=e}start(){this.detach||(this.detach=this.daemon.onFileTransferFrame(e=>this.dispatch(e)))}stop(){var e;(e=this.detach)==null||e.call(this),this.detach=null;for(const[t,o]of this.pending)o.reject(new Error("file transfer router stopped")),this.pending.delete(t)}async download(e){const t=await this.daemon.request("file_download_token_request",{path:e});if(!t.ok||!t.token)throw new Error(t.error??"failed to issue download token");const o=this.nextStream++,i=new Promise((a,r)=>{this.pending.set(o,{chunks:[],meta:{},resolve:a,reject:r})}),s=await this.daemon.request("file_download_request",{token:t.token,stream:o});if(!s.ok)throw this.pending.delete(o),new Error(s.error??"download request failed");return i}dispatch(e){const t=this.pending.get(e.stream);if(t)switch(e.opcode){case"Begin":t.meta.fileName=e.meta.fileName,t.meta.mimeType=e.meta.mimeType;return;case"Chunk":t.chunks.push(e.data);return;case"End":this.pending.delete(e.stream),e.ok?t.resolve({bytes:y(t.chunks),...t.meta}):t.reject(new Error("download failed"));return;case"Error":this.pending.delete(e.stream),t.reject(new Error(e.message));return}}}const f=new WeakMap;function k(n){let e=f.get(n);return e||(e=new g(n),e.start(),f.set(n,e)),e}function q(n,e=!0){var a;const t=d(r=>r.client),o=d(r=>r.daemon),i=h({queryKey:w.fileDownload(n),queryFn:async()=>{if(!t||!o)throw new Error("not connected");const r=await k(o).download(n),c=new Blob([Uint8Array.from(r.bytes)],{type:r.mimeType||"application/octet-stream"});return{objectUrl:URL.createObjectURL(c),mimeType:r.mimeType,fileName:r.fileName}},enabled:!!t&&!!o&&!!n&&e,staleTime:1/0,gcTime:0}),s=u.useRef(null);return u.useEffect(()=>{var c;const r=((c=i.data)==null?void 0:c.objectUrl)??null;r&&s.current&&s.current!==r&&URL.revokeObjectURL(s.current),s.current=r},[(a=i.data)==null?void 0:a.objectUrl]),u.useEffect(()=>()=>{s.current&&URL.revokeObjectURL(s.current)},[]),i}export{q as u};
2
- //# sourceMappingURL=use-file-download-ypnyMrxl.js.map
1
+ var m=Object.defineProperty;var p=(n,e,t)=>e in n?m(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var l=(n,e,t)=>p(n,typeof e!="symbol"?e+"":e,t);import{r as u}from"./vendor-react-BoIQBqDW.js";import{u as h}from"./vendor-query-CAh-Aqig.js";import{a as d,r as w}from"./index-DnLvR47L.js";import"./vendor--os7hw7d.js";function y(n){const e=n.reduce((i,s)=>i+s.length,0),t=new Uint8Array(e);let o=0;for(const i of n)t.set(i,o),o+=i.length;return t}class g{constructor(e){l(this,"pending",new Map);l(this,"nextStream",1);l(this,"detach",null);this.daemon=e}start(){this.detach||(this.detach=this.daemon.onFileTransferFrame(e=>this.dispatch(e)))}stop(){var e;(e=this.detach)==null||e.call(this),this.detach=null;for(const[t,o]of this.pending)o.reject(new Error("file transfer router stopped")),this.pending.delete(t)}async download(e){const t=await this.daemon.request("file_download_token_request",{path:e});if(!t.ok||!t.token)throw new Error(t.error??"failed to issue download token");const o=this.nextStream++,i=new Promise((a,r)=>{this.pending.set(o,{chunks:[],meta:{},resolve:a,reject:r})}),s=await this.daemon.request("file_download_request",{token:t.token,stream:o});if(!s.ok)throw this.pending.delete(o),new Error(s.error??"download request failed");return i}dispatch(e){const t=this.pending.get(e.stream);if(t)switch(e.opcode){case"Begin":t.meta.fileName=e.meta.fileName,t.meta.mimeType=e.meta.mimeType;return;case"Chunk":t.chunks.push(e.data);return;case"End":this.pending.delete(e.stream),e.ok?t.resolve({bytes:y(t.chunks),...t.meta}):t.reject(new Error("download failed"));return;case"Error":this.pending.delete(e.stream),t.reject(new Error(e.message));return}}}const f=new WeakMap;function k(n){let e=f.get(n);return e||(e=new g(n),e.start(),f.set(n,e)),e}function q(n,e=!0){var a;const t=d(r=>r.client),o=d(r=>r.daemon),i=h({queryKey:w.fileDownload(n),queryFn:async()=>{if(!t||!o)throw new Error("not connected");const r=await k(o).download(n),c=new Blob([Uint8Array.from(r.bytes)],{type:r.mimeType||"application/octet-stream"});return{objectUrl:URL.createObjectURL(c),mimeType:r.mimeType,fileName:r.fileName}},enabled:!!t&&!!o&&!!n&&e,staleTime:1/0,gcTime:0}),s=u.useRef(null);return u.useEffect(()=>{var c;const r=((c=i.data)==null?void 0:c.objectUrl)??null;r&&s.current&&s.current!==r&&URL.revokeObjectURL(s.current),s.current=r},[(a=i.data)==null?void 0:a.objectUrl]),u.useEffect(()=>()=>{s.current&&URL.revokeObjectURL(s.current)},[]),i}export{q as u};
2
+ //# sourceMappingURL=use-file-download-DCDpzMZs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-file-download-ypnyMrxl.js","sources":["../../../../client/src/file-transfer-client.ts","../../../src/hooks/use-file-download.ts"],"sourcesContent":["import { type FileTransferFrame } from \"@av-pi-studio/protocol\";\n\nimport type { DaemonClient } from \"./daemon-client.js\";\n\n/**\n * Client-side demux + assembly for binary file downloads (features/file-explorer-transfer.md\n * § Binary transfer frames). Mirrors `TerminalStreamRouter`'s inbound-frame-routing shape, but a\n * download is a one-shot request/response rather than a persistent per-slot subscription: request\n * a token, request the download, await the assembled bytes.\n */\n\nexport interface DownloadedFile {\n bytes: Uint8Array;\n fileName?: string;\n mimeType?: string;\n}\n\ninterface PendingDownload {\n chunks: Uint8Array[];\n meta: { fileName?: string; mimeType?: string };\n resolve: (file: DownloadedFile) => void;\n reject: (error: Error) => void;\n}\n\ninterface TokenResponse {\n ok: boolean;\n token?: string;\n error?: string;\n}\n\ninterface DownloadRequestResponse {\n ok: boolean;\n error?: string;\n}\n\nfunction concat(chunks: Uint8Array[]): Uint8Array {\n const total = chunks.reduce((sum, c) => sum + c.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nexport class FileTransferClient {\n private readonly pending = new Map<number, PendingDownload>();\n private nextStream = 1;\n private detach: (() => void) | null = null;\n\n constructor(private readonly daemon: DaemonClient) {}\n\n /** Begin routing inbound file-transfer frames. Idempotent. */\n start(): void {\n if (this.detach) return;\n this.detach = this.daemon.onFileTransferFrame((frame) => this.dispatch(frame));\n }\n\n /** Stop routing inbound frames. Any downloads still pending are rejected. */\n stop(): void {\n this.detach?.();\n this.detach = null;\n for (const [stream, pending] of this.pending) {\n pending.reject(new Error(\"file transfer router stopped\"));\n this.pending.delete(stream);\n }\n }\n\n /**\n * Download the file at `path`: requests a single-use token, then requests the chunked\n * transfer, assembling `Begin → Chunk* → End` frames into one buffer.\n */\n async download(path: string): Promise<DownloadedFile> {\n const tokenResponse = await this.daemon.request<TokenResponse>(\n \"file_download_token_request\",\n { path },\n );\n if (!tokenResponse.ok || !tokenResponse.token) {\n throw new Error(tokenResponse.error ?? \"failed to issue download token\");\n }\n\n const stream = this.nextStream++;\n const result = new Promise<DownloadedFile>((resolve, reject) => {\n this.pending.set(stream, { chunks: [], meta: {}, resolve, reject });\n });\n\n const requestResponse = await this.daemon.request<DownloadRequestResponse>(\n \"file_download_request\",\n { token: tokenResponse.token, stream },\n );\n if (!requestResponse.ok) {\n this.pending.delete(stream);\n throw new Error(requestResponse.error ?? \"download request failed\");\n }\n\n return result;\n }\n\n private dispatch(frame: FileTransferFrame): void {\n const pending = this.pending.get(frame.stream);\n if (!pending) return; // not a download stream we're tracking (e.g. an upload) — ignore\n switch (frame.opcode) {\n case \"Begin\":\n pending.meta.fileName = frame.meta.fileName;\n pending.meta.mimeType = frame.meta.mimeType;\n return;\n case \"Chunk\":\n pending.chunks.push(frame.data);\n return;\n case \"End\":\n this.pending.delete(frame.stream);\n if (frame.ok) {\n pending.resolve({ bytes: concat(pending.chunks), ...pending.meta });\n } else {\n pending.reject(new Error(\"download failed\"));\n }\n return;\n case \"Error\":\n this.pending.delete(frame.stream);\n pending.reject(new Error(frame.message));\n return;\n }\n }\n}\n","/**\n * `use-file-download` — TanStack Query wrapper over the binary file-transfer download path\n * (`file_download_token_request` → `file_download_request` → chunked `Begin`/`Chunk`/`End`\n * binary frames), for viewers that need raw bytes rather than the UTF-8 preview\n * `file_read_request` returns (images, video — POC_TO_APP_PLAN_UI.md §4.5 follow-up: modular\n * file preview). Resolves to an object URL, revoked once superseded by a new one or once this\n * consumer unmounts.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { DaemonClient } from \"@av-pi-studio/client\";\nimport { FileTransferClient } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\n\n// One FileTransferClient per daemon connection — mirrors `TerminalPanel.tsx`'s `routerFor`\n// (multiple viewers share the one frame demuxer over the shared socket).\nconst transferByDaemon = new WeakMap<DaemonClient, FileTransferClient>();\n\nfunction transferFor(daemon: DaemonClient): FileTransferClient {\n let transfer = transferByDaemon.get(daemon);\n if (!transfer) {\n transfer = new FileTransferClient(daemon);\n transfer.start();\n transferByDaemon.set(daemon, transfer);\n }\n return transfer;\n}\n\nexport interface FileDownload {\n objectUrl: string;\n mimeType?: string;\n fileName?: string;\n}\n\nexport function useFileDownload(path: string, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n const daemon = useConnectionStore((s) => s.daemon);\n\n const query = useQuery({\n queryKey: rpcKeys.fileDownload(path),\n queryFn: async (): Promise<FileDownload> => {\n if (!client || !daemon) throw new Error(\"not connected\");\n const file = await transferFor(daemon).download(path);\n // `Blob`'s DOM typings require an `ArrayBuffer`-backed view; the assembled `Uint8Array`'s\n // `ArrayBufferLike` backing (a decode-time implementation detail) doesn't satisfy that on\n // its own, so copy into a plain `ArrayBuffer`-backed `Uint8Array` first.\n const blob = new Blob([Uint8Array.from(file.bytes)], {\n type: file.mimeType || \"application/octet-stream\",\n });\n return { objectUrl: URL.createObjectURL(blob), mimeType: file.mimeType, fileName: file.fileName };\n },\n enabled: Boolean(client) && Boolean(daemon) && Boolean(path) && enabled,\n // Object URLs are process-local, non-serializable browser resources — never persist them or\n // silently refetch behind this hook's back (that would leak the previous blob's URL).\n staleTime: Infinity,\n gcTime: 0,\n });\n\n // Revoke the previous object URL once a new one supersedes it, and the current one on unmount\n // — TanStack Query's cache has no notion that this value owns a browser resource to release.\n const urlRef = useRef<string | null>(null);\n useEffect(() => {\n const nextUrl = query.data?.objectUrl ?? null;\n if (nextUrl && urlRef.current && urlRef.current !== nextUrl) {\n URL.revokeObjectURL(urlRef.current);\n }\n urlRef.current = nextUrl;\n }, [query.data?.objectUrl]);\n\n useEffect(() => {\n return () => {\n if (urlRef.current) URL.revokeObjectURL(urlRef.current);\n };\n }, []);\n\n return query;\n}\n"],"names":["concat","chunks","total","sum","c","out","offset","chunk","FileTransferClient","daemon","__publicField","frame","_a","stream","pending","path","tokenResponse","result","resolve","reject","requestResponse","transferByDaemon","transferFor","transfer","useFileDownload","enabled","client","useConnectionStore","s","query","useQuery","rpcKeys","file","blob","urlRef","useRef","useEffect","nextUrl"],"mappings":"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"}
1
+ {"version":3,"file":"use-file-download-DCDpzMZs.js","sources":["../../../../client/src/file-transfer-client.ts","../../../src/hooks/use-file-download.ts"],"sourcesContent":["import { type FileTransferFrame } from \"@av-pi-studio/protocol\";\n\nimport type { DaemonClient } from \"./daemon-client.js\";\n\n/**\n * Client-side demux + assembly for binary file downloads (features/file-explorer-transfer.md\n * § Binary transfer frames). Mirrors `TerminalStreamRouter`'s inbound-frame-routing shape, but a\n * download is a one-shot request/response rather than a persistent per-slot subscription: request\n * a token, request the download, await the assembled bytes.\n */\n\nexport interface DownloadedFile {\n bytes: Uint8Array;\n fileName?: string;\n mimeType?: string;\n}\n\ninterface PendingDownload {\n chunks: Uint8Array[];\n meta: { fileName?: string; mimeType?: string };\n resolve: (file: DownloadedFile) => void;\n reject: (error: Error) => void;\n}\n\ninterface TokenResponse {\n ok: boolean;\n token?: string;\n error?: string;\n}\n\ninterface DownloadRequestResponse {\n ok: boolean;\n error?: string;\n}\n\nfunction concat(chunks: Uint8Array[]): Uint8Array {\n const total = chunks.reduce((sum, c) => sum + c.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nexport class FileTransferClient {\n private readonly pending = new Map<number, PendingDownload>();\n private nextStream = 1;\n private detach: (() => void) | null = null;\n\n constructor(private readonly daemon: DaemonClient) {}\n\n /** Begin routing inbound file-transfer frames. Idempotent. */\n start(): void {\n if (this.detach) return;\n this.detach = this.daemon.onFileTransferFrame((frame) => this.dispatch(frame));\n }\n\n /** Stop routing inbound frames. Any downloads still pending are rejected. */\n stop(): void {\n this.detach?.();\n this.detach = null;\n for (const [stream, pending] of this.pending) {\n pending.reject(new Error(\"file transfer router stopped\"));\n this.pending.delete(stream);\n }\n }\n\n /**\n * Download the file at `path`: requests a single-use token, then requests the chunked\n * transfer, assembling `Begin → Chunk* → End` frames into one buffer.\n */\n async download(path: string): Promise<DownloadedFile> {\n const tokenResponse = await this.daemon.request<TokenResponse>(\n \"file_download_token_request\",\n { path },\n );\n if (!tokenResponse.ok || !tokenResponse.token) {\n throw new Error(tokenResponse.error ?? \"failed to issue download token\");\n }\n\n const stream = this.nextStream++;\n const result = new Promise<DownloadedFile>((resolve, reject) => {\n this.pending.set(stream, { chunks: [], meta: {}, resolve, reject });\n });\n\n const requestResponse = await this.daemon.request<DownloadRequestResponse>(\n \"file_download_request\",\n { token: tokenResponse.token, stream },\n );\n if (!requestResponse.ok) {\n this.pending.delete(stream);\n throw new Error(requestResponse.error ?? \"download request failed\");\n }\n\n return result;\n }\n\n private dispatch(frame: FileTransferFrame): void {\n const pending = this.pending.get(frame.stream);\n if (!pending) return; // not a download stream we're tracking (e.g. an upload) — ignore\n switch (frame.opcode) {\n case \"Begin\":\n pending.meta.fileName = frame.meta.fileName;\n pending.meta.mimeType = frame.meta.mimeType;\n return;\n case \"Chunk\":\n pending.chunks.push(frame.data);\n return;\n case \"End\":\n this.pending.delete(frame.stream);\n if (frame.ok) {\n pending.resolve({ bytes: concat(pending.chunks), ...pending.meta });\n } else {\n pending.reject(new Error(\"download failed\"));\n }\n return;\n case \"Error\":\n this.pending.delete(frame.stream);\n pending.reject(new Error(frame.message));\n return;\n }\n }\n}\n","/**\n * `use-file-download` — TanStack Query wrapper over the binary file-transfer download path\n * (`file_download_token_request` → `file_download_request` → chunked `Begin`/`Chunk`/`End`\n * binary frames), for viewers that need raw bytes rather than the UTF-8 preview\n * `file_read_request` returns (images, video — POC_TO_APP_PLAN_UI.md §4.5 follow-up: modular\n * file preview). Resolves to an object URL, revoked once superseded by a new one or once this\n * consumer unmounts.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { DaemonClient } from \"@av-pi-studio/client\";\nimport { FileTransferClient } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\n\n// One FileTransferClient per daemon connection — mirrors `TerminalPanel.tsx`'s `routerFor`\n// (multiple viewers share the one frame demuxer over the shared socket).\nconst transferByDaemon = new WeakMap<DaemonClient, FileTransferClient>();\n\nfunction transferFor(daemon: DaemonClient): FileTransferClient {\n let transfer = transferByDaemon.get(daemon);\n if (!transfer) {\n transfer = new FileTransferClient(daemon);\n transfer.start();\n transferByDaemon.set(daemon, transfer);\n }\n return transfer;\n}\n\nexport interface FileDownload {\n objectUrl: string;\n mimeType?: string;\n fileName?: string;\n}\n\nexport function useFileDownload(path: string, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n const daemon = useConnectionStore((s) => s.daemon);\n\n const query = useQuery({\n queryKey: rpcKeys.fileDownload(path),\n queryFn: async (): Promise<FileDownload> => {\n if (!client || !daemon) throw new Error(\"not connected\");\n const file = await transferFor(daemon).download(path);\n // `Blob`'s DOM typings require an `ArrayBuffer`-backed view; the assembled `Uint8Array`'s\n // `ArrayBufferLike` backing (a decode-time implementation detail) doesn't satisfy that on\n // its own, so copy into a plain `ArrayBuffer`-backed `Uint8Array` first.\n const blob = new Blob([Uint8Array.from(file.bytes)], {\n type: file.mimeType || \"application/octet-stream\",\n });\n return { objectUrl: URL.createObjectURL(blob), mimeType: file.mimeType, fileName: file.fileName };\n },\n enabled: Boolean(client) && Boolean(daemon) && Boolean(path) && enabled,\n // Object URLs are process-local, non-serializable browser resources — never persist them or\n // silently refetch behind this hook's back (that would leak the previous blob's URL).\n staleTime: Infinity,\n gcTime: 0,\n });\n\n // Revoke the previous object URL once a new one supersedes it, and the current one on unmount\n // — TanStack Query's cache has no notion that this value owns a browser resource to release.\n const urlRef = useRef<string | null>(null);\n useEffect(() => {\n const nextUrl = query.data?.objectUrl ?? null;\n if (nextUrl && urlRef.current && urlRef.current !== nextUrl) {\n URL.revokeObjectURL(urlRef.current);\n }\n urlRef.current = nextUrl;\n }, [query.data?.objectUrl]);\n\n useEffect(() => {\n return () => {\n if (urlRef.current) URL.revokeObjectURL(urlRef.current);\n };\n }, []);\n\n return query;\n}\n"],"names":["concat","chunks","total","sum","c","out","offset","chunk","FileTransferClient","daemon","__publicField","frame","_a","stream","pending","path","tokenResponse","result","resolve","reject","requestResponse","transferByDaemon","transferFor","transfer","useFileDownload","enabled","client","useConnectionStore","s","query","useQuery","rpcKeys","file","blob","urlRef","useRef","useEffect","nextUrl"],"mappings":"8UAmCA,SAASA,EAAOC,EAAkC,CAChD,MAAMC,EAAQD,EAAO,OAAO,CAACE,EAAKC,IAAMD,EAAMC,EAAE,OAAQ,CAAC,EACnDC,EAAM,IAAI,WAAWH,CAAK,EAChC,IAAII,EAAS,EACb,UAAWC,KAASN,EAClBI,EAAI,IAAIE,EAAOD,CAAM,EACrBA,GAAUC,EAAM,OAElB,OAAOF,CACT,CAEO,MAAMG,CAAmB,CAK9B,YAA6BC,EAAsB,CAJlCC,EAAA,mBAAc,KACvBA,EAAA,kBAAa,GACbA,EAAA,cAA8B,MAET,KAAA,OAAAD,CAAuB,CAGpD,OAAc,CACR,KAAK,SACT,KAAK,OAAS,KAAK,OAAO,oBAAqBE,GAAU,KAAK,SAASA,CAAK,CAAC,EAC/E,CAGA,MAAa,QACXC,EAAA,KAAK,SAAL,MAAAA,EAAA,WACA,KAAK,OAAS,KACd,SAAW,CAACC,EAAQC,CAAO,IAAK,KAAK,QACnCA,EAAQ,OAAO,IAAI,MAAM,8BAA8B,CAAC,EACxD,KAAK,QAAQ,OAAOD,CAAM,CAE9B,CAMA,MAAM,SAASE,EAAuC,CACpD,MAAMC,EAAgB,MAAM,KAAK,OAAO,QACtC,8BACA,CAAE,KAAAD,CAAA,CAAK,EAET,GAAI,CAACC,EAAc,IAAM,CAACA,EAAc,MACtC,MAAM,IAAI,MAAMA,EAAc,OAAS,gCAAgC,EAGzE,MAAMH,EAAS,KAAK,aACdI,EAAS,IAAI,QAAwB,CAACC,EAASC,IAAW,CAC9D,KAAK,QAAQ,IAAIN,EAAQ,CAAE,OAAQ,CAAA,EAAI,KAAM,CAAA,EAAI,QAAAK,EAAS,OAAAC,CAAA,CAAQ,CACpE,CAAC,EAEKC,EAAkB,MAAM,KAAK,OAAO,QACxC,wBACA,CAAE,MAAOJ,EAAc,MAAO,OAAAH,CAAA,CAAO,EAEvC,GAAI,CAACO,EAAgB,GACnB,WAAK,QAAQ,OAAOP,CAAM,EACpB,IAAI,MAAMO,EAAgB,OAAS,yBAAyB,EAGpE,OAAOH,CACT,CAEQ,SAASN,EAAgC,CAC/C,MAAMG,EAAU,KAAK,QAAQ,IAAIH,EAAM,MAAM,EAC7C,GAAKG,EACL,OAAQH,EAAM,OAAA,CACZ,IAAK,QACHG,EAAQ,KAAK,SAAWH,EAAM,KAAK,SACnCG,EAAQ,KAAK,SAAWH,EAAM,KAAK,SACnC,OACF,IAAK,QACHG,EAAQ,OAAO,KAAKH,EAAM,IAAI,EAC9B,OACF,IAAK,MACH,KAAK,QAAQ,OAAOA,EAAM,MAAM,EAC5BA,EAAM,GACRG,EAAQ,QAAQ,CAAE,MAAOd,EAAOc,EAAQ,MAAM,EAAG,GAAGA,EAAQ,KAAM,EAElEA,EAAQ,OAAO,IAAI,MAAM,iBAAiB,CAAC,EAE7C,OACF,IAAK,QACH,KAAK,QAAQ,OAAOH,EAAM,MAAM,EAChCG,EAAQ,OAAO,IAAI,MAAMH,EAAM,OAAO,CAAC,EACvC,MAAA,CAEN,CACF,CC1GA,MAAMU,MAAuB,QAE7B,SAASC,EAAYb,EAA0C,CAC7D,IAAIc,EAAWF,EAAiB,IAAIZ,CAAM,EAC1C,OAAKc,IACHA,EAAW,IAAIf,EAAmBC,CAAM,EACxCc,EAAS,MAAA,EACTF,EAAiB,IAAIZ,EAAQc,CAAQ,GAEhCA,CACT,CAQO,SAASC,EAAgBT,EAAcU,EAAU,GAAM,OAC5D,MAAMC,EAASC,EAAoBC,GAAMA,EAAE,MAAM,EAC3CnB,EAASkB,EAAoBC,GAAMA,EAAE,MAAM,EAE3CC,EAAQC,EAAS,CACrB,SAAUC,EAAQ,aAAahB,CAAI,EACnC,QAAS,SAAmC,CAC1C,GAAI,CAACW,GAAU,CAACjB,EAAQ,MAAM,IAAI,MAAM,eAAe,EACvD,MAAMuB,EAAO,MAAMV,EAAYb,CAAM,EAAE,SAASM,CAAI,EAI9CkB,EAAO,IAAI,KAAK,CAAC,WAAW,KAAKD,EAAK,KAAK,CAAC,EAAG,CACnD,KAAMA,EAAK,UAAY,0BAAA,CACxB,EACD,MAAO,CAAE,UAAW,IAAI,gBAAgBC,CAAI,EAAG,SAAUD,EAAK,SAAU,SAAUA,EAAK,QAAA,CACzF,EACA,QAAS,EAAQN,GAAW,EAAQjB,GAAW,EAAQM,GAASU,EAGhE,UAAW,IACX,OAAQ,CAAA,CACT,EAIKS,EAASC,EAAAA,OAAsB,IAAI,EACzCC,OAAAA,EAAAA,UAAU,IAAM,OACd,MAAMC,IAAUzB,EAAAiB,EAAM,OAAN,YAAAjB,EAAY,YAAa,KACrCyB,GAAWH,EAAO,SAAWA,EAAO,UAAYG,GAClD,IAAI,gBAAgBH,EAAO,OAAO,EAEpCA,EAAO,QAAUG,CACnB,EAAG,EAACzB,EAAAiB,EAAM,OAAN,YAAAjB,EAAY,SAAS,CAAC,EAE1BwB,EAAAA,UAAU,IACD,IAAM,CACPF,EAAO,SAAS,IAAI,gBAAgBA,EAAO,OAAO,CACxD,EACC,CAAA,CAAE,EAEEL,CACT"}
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Pi-Studio</title>
7
- <script type="module" crossorigin src="/assets/index-D9OEyo5k.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-DnLvR47L.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BoIQBqDW.js">
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-query-CAh-Aqig.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-markdown-B8HSIhaA.js">
@@ -13,7 +13,7 @@
13
13
  <link rel="modulepreload" crossorigin href="/assets/vendor-icons-DUloQbkI.js">
14
14
  <link rel="modulepreload" crossorigin href="/assets/vendor-overlays-UwLNGk04.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/vendor-dnd-D9kA7FgH.js">
16
- <link rel="stylesheet" crossorigin href="/assets/index-DrGTLQo0.css">
16
+ <link rel="stylesheet" crossorigin href="/assets/index-B2U-99PA.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@av-pi-studio/web-client",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -14,8 +14,8 @@
14
14
  "clean": "rm -rf dist *.tsbuildinfo"
15
15
  },
16
16
  "dependencies": {
17
- "@av-pi-studio/client": "^0.0.12",
18
- "@av-pi-studio/protocol": "^0.0.12",
17
+ "@av-pi-studio/client": "^0.0.13",
18
+ "@av-pi-studio/protocol": "^0.0.13",
19
19
  "@codemirror/language": "^6.12.4",
20
20
  "@codemirror/language-data": "^6.5.2",
21
21
  "@codemirror/state": "^6.7.1",