@alexkroman1/aai-ui 0.17.0 → 0.18.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.
Files changed (71) hide show
  1. package/README.md +6 -7
  2. package/dist/_submission-timing.d.ts +40 -0
  3. package/dist/_submission.d.ts +56 -0
  4. package/dist/_theme.d.ts +17 -0
  5. package/dist/_theme.test-d.d.ts +14 -0
  6. package/dist/{_utils-Mi-SCFA1.js → _utils-BlZBJPdj.js} +4 -5
  7. package/dist/_utils.d.ts +4 -5
  8. package/dist/agent-state.d.ts +4 -2
  9. package/dist/append-dictation.d.ts +23 -0
  10. package/dist/audio/dictation.d.ts +36 -0
  11. package/dist/audio/index.d.ts +4 -2
  12. package/dist/audio/index.js +44 -1
  13. package/dist/{chat-view-W6KcdjJY.js → chat-view-BwKpF0MN.js} +27 -17
  14. package/dist/{client-config-CzvWmw4X.js → client-config-DcMCWmkO.js} +1 -1
  15. package/dist/components/button.js +1 -1
  16. package/dist/components/chat-view.js +1 -2
  17. package/dist/components/controls.js +2 -2
  18. package/dist/components/form-fields.d.ts +24 -5
  19. package/dist/components/form-types.d.ts +32 -0
  20. package/dist/components/form.d.ts +1 -1
  21. package/dist/components/message-list.js +1 -2
  22. package/dist/components/session-state-dot.d.ts +30 -18
  23. package/dist/components/sidebar-layout.js +1 -1
  24. package/dist/components/start-screen.js +2 -2
  25. package/dist/components/tool-call-block.js +1 -2
  26. package/dist/components/tool-config-context.d.ts +21 -4
  27. package/dist/components/workflow-fields.d.ts +36 -12
  28. package/dist/components/workflow-pending-note.d.ts +30 -12
  29. package/dist/context-BF0KsI9X.js +473 -0
  30. package/dist/context.js +1 -381
  31. package/dist/default-client/assets/audio-CtBvSS3L.js +1 -0
  32. package/dist/default-client/assets/{capture-processor-Cx1AdMHL.js → capture-processor-BZXisGjZ.js} +1 -1
  33. package/dist/default-client/assets/index-BkjCv4MX.css +2 -0
  34. package/dist/default-client/assets/index-DUQoaxdq.js +127 -0
  35. package/dist/default-client/assets/{playback-processor-CCwTknSu.js → playback-processor-BHk-iXEU.js} +1 -1
  36. package/dist/default-client/index.html +2 -2
  37. package/dist/default-client.d.ts +1 -3
  38. package/dist/define-client.d.ts +30 -28
  39. package/dist/define-client.js +84 -43
  40. package/dist/dictation-transcriber.d.ts +49 -0
  41. package/dist/{eyebrow-UfmSz9yy.js → eyebrow-Bp4y1rZ9.js} +1 -1
  42. package/dist/{hooks-DN4gHYsj.js → hooks-1CoQY_Fc.js} +2 -2
  43. package/dist/hooks.js +1 -2
  44. package/dist/index.d.ts +11 -8
  45. package/dist/index.js +849 -263
  46. package/dist/internal.d.ts +1 -1
  47. package/dist/internal.js +6 -6
  48. package/dist/{message-list-BsaBkBgo.js → message-list-BBHLK05X.js} +3 -3
  49. package/dist/route-fetch.d.ts +7 -1
  50. package/dist/session/index.js +1 -1
  51. package/dist/{session-3lgPYNNO.js → session-Rud8mmU-.js} +2 -2
  52. package/dist/{tool-call-block-uXtMOUWN.js → tool-call-block-C8s6AhjS.js} +4 -4
  53. package/dist/tool-config-context-Bd-7xrfP.js +28 -0
  54. package/dist/types.d.ts +47 -2
  55. package/dist/{url-chips-oyy_Eiqh.js → url-chips-C_SHU6fq.js} +2 -2
  56. package/dist/use-dictation.d.ts +145 -0
  57. package/dist/use-route.d.ts +49 -7
  58. package/dist/use-run-key.d.ts +6 -1
  59. package/dist/{use-user-transcript-uyHhzy4d.js → use-user-transcript-lOu_uMnf.js} +1 -1
  60. package/dist/use-workflow-form.d.ts +74 -172
  61. package/dist/{use-workflow-run-DdhWmbx-.js → use-workflow-run-BTEemod0.js} +2 -2
  62. package/dist/use-workflow-runs.d.ts +8 -2
  63. package/dist/use-workflow-stream.d.ts +9 -5
  64. package/dist/workflow-def-types.d.ts +18 -2
  65. package/dist/workflow-field-kind.d.ts +59 -7
  66. package/dist/workflow-submission.d.ts +265 -0
  67. package/package.json +2 -2
  68. package/dist/default-client/assets/audio-Ct7JpSNT.js +0 -1
  69. package/dist/default-client/assets/index-BlUYCTB5.js +0 -126
  70. package/dist/default-client/assets/index-_-x5JLLa.css +0 -2
  71. package/dist/tool-config-context-Es4YUzV2.js +0 -19
@@ -0,0 +1,265 @@
1
+ /**
2
+ * What a workflow SUBMISSION is, as a type: the object `useWorkflowSubmit` and
3
+ * `useWorkflowStream` both hand back. Moved out of `use-workflow-form.ts` at
4
+ * the source-length cap, on the seam `form-types.ts` is cut on — the two hooks
5
+ * and the form statechart all name these, and a type living in one of them
6
+ * would make the others import a hook to say what they return.
7
+ */
8
+ import type { UploadProgress, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
9
+ import type { FormValues } from "./components/form-types.ts";
10
+ import type { WorkflowRun } from "./workflow-client.ts";
11
+ import type { SubmitInputOf } from "./workflow-def-types.ts";
12
+ /**
13
+ * What {@link WorkflowSubmission.upload} reports while the bytes are going.
14
+ *
15
+ * The SDK's per-request {@link UploadProgress} plus WHICH file it describes,
16
+ * because a form is allowed more than one and a bar over "the upload" would
17
+ * restart at zero partway through with nothing to say why.
18
+ *
19
+ * @sealed
20
+ * @public
21
+ */
22
+ export type UploadStatus = UploadProgress & {
23
+ /** The file being sent, by the name the picker gave it. */
24
+ name: string;
25
+ /** Which file of the submission this is, counting from 1. */
26
+ index: number;
27
+ /** How many files this submission sends in total. */
28
+ count: number;
29
+ /**
30
+ * Whether the person has parked this upload.
31
+ *
32
+ * A paused upload is not a stopped one: the windows already stored stay stored,
33
+ * `loaded` holds where it got to, and resuming sends what is missing rather than
34
+ * the file. So a bar rendering this reads "Paused at 62%", never "62% and
35
+ * frozen" — which is what a page could otherwise only guess from a number that
36
+ * stopped moving, the same ambiguity `complete` exists to remove on the run side.
37
+ */
38
+ paused: boolean;
39
+ };
40
+ /**
41
+ * The two marks a submission's wall clock is read from — press to settle,
42
+ * across the upload, the run, and the gap between them, which no server-side
43
+ * number can give: `output.elapsedMs` is the RUN's own clock and begins after
44
+ * the upload. Only the browser holds both ends.
45
+ *
46
+ * `performance.now()` marks rather than elapsed durations, and that is the
47
+ * load-bearing part: an elapsed number is stale the instant it is computed, so
48
+ * a page holding one has to re-render to keep it true. A mark lets the one
49
+ * section that prints a running clock re-render on its own interval and read
50
+ * `performance.now() - submittedAt` at render. Monotonic, so a clock
51
+ * correction cannot make a run look instant, or negative.
52
+ *
53
+ * @sealed
54
+ * @public
55
+ */
56
+ export type WorkflowSubmissionTiming = {
57
+ /** When `submit()` was called on this mount, or `undefined` before the first. */
58
+ submittedAt?: number | undefined;
59
+ /**
60
+ * When the submission settled — the run reached a terminal status, or the
61
+ * submit itself failed — measured at that moment rather than at the next
62
+ * render. `undefined` while it is in flight, and for a run this page did
63
+ * not start (there was no press to time from).
64
+ */
65
+ settledAt?: number | undefined;
66
+ };
67
+ /**
68
+ * Who can find the run again — the lifetime of the key it was recorded under.
69
+ *
70
+ * - `"tab"` — `useWorkflowSubmit`'s own `sessionStorage` key: a reload or a
71
+ * same-tab navigation finds the run; a new tab does not.
72
+ * - `"browser"` — a `localStorage` key (`key: { storage: "local" }`, or
73
+ * `useRunKey({ storage: "local" })`), or a key the page supplied itself (an
74
+ * account's id): any tab on this browser finds it, so closing the tab loses
75
+ * nothing.
76
+ * - `"none"` — nothing will find it: `recover: false`, or `useWorkflowStream`,
77
+ * whose run is reading the bytes THIS page is sending and is ended, not
78
+ * orphaned, by a reload.
79
+ *
80
+ * `<WorkflowPendingNote>` reads it to say the right sentence; the raw fact is
81
+ * published so a page with its own note can too.
82
+ *
83
+ * @public
84
+ */
85
+ export type WorkflowSubmissionScope = "tab" | "browser" | "none";
86
+ /**
87
+ * What {@link useWorkflowSubmit} returns.
88
+ *
89
+ * @see {@link WorkflowStreamSubmission} — an ALIAS of this type, returned by
90
+ * `useWorkflowStream`, which is a drop-in sibling. Exactly two fields MEAN
91
+ * something different there, and both differences follow from WHEN the run is
92
+ * created: there, `submit()` resolves when the UPLOAD finishes rather than when
93
+ * the run is accepted, and `run` is non-`undefined` from before the bytes are
94
+ * in, so a page can render `<WorkflowProgress>` beside the upload bar instead
95
+ * of after it. Here the run does not exist until the last byte lands.
96
+ *
97
+ * @sealed
98
+ */
99
+ export type WorkflowSubmission<R = unknown, I = unknown> = {
100
+ /**
101
+ * Start a run with this input. Resolves once the run EXISTS — progress
102
+ * arrives through `run` — so a `<Form>`'s handler can await it to know the
103
+ * submission was accepted.
104
+ */
105
+ submit: (input: I) => Promise<void>;
106
+ /**
107
+ * Start a run from a `<Form>`'s values, which are UNVALIDATED.
108
+ *
109
+ * The same function as {@link WorkflowSubmission.submit}, with the type the
110
+ * form path can honestly offer. `FormValues` is `Record<string, unknown>`
111
+ * scraped off the DOM at submit time — a `<TextField name="limit">`
112
+ * contributes a string whatever the schema says — so the shape is not known
113
+ * here and the SERVER is what checks it against the workflow's schema.
114
+ *
115
+ * Two doors rather than one loose one: `submit` takes the workflow's own
116
+ * input type, so a hand-built object is checked at compile time and
117
+ * `submit({ ur1: 42 })` is an error; widening it to accept `FormValues` would
118
+ * have made every object satisfy it and given the typing back. Reaching for
119
+ * this one is the author saying "these came from a form", which is a fact
120
+ * about the values and not a cast.
121
+ */
122
+ submitForm: (values: FormValues) => Promise<void>;
123
+ /** Clear the run and any error, putting the form back to its initial state. */
124
+ reset: () => void;
125
+ /**
126
+ * End the current run's `sleep()` early — "file it now" — resolving how many
127
+ * pending sleeps were interrupted.
128
+ *
129
+ * Bound to the run this submission is following, which is the point: it is
130
+ * the only reason a page holding one of these hooks needed an `api` of its
131
+ * own. `0` is an answer rather than a failure (the run had already moved past
132
+ * its wait, or there is no run yet), so nothing here has to be guarded.
133
+ */
134
+ wake: () => Promise<number>;
135
+ /**
136
+ * Stop the current run, resolving whether this call is what ended it.
137
+ *
138
+ * `false` for a run that had already finished, and for no run at all — the
139
+ * SDK's contract, because two tabs pressing Stop is ordinary. Distinct from
140
+ * `reset()`, which puts the FORM back and leaves the run running.
141
+ */
142
+ cancel: () => Promise<boolean>;
143
+ /** The run, once started, followed to completion. */
144
+ run: WorkflowRun<R> | undefined;
145
+ /**
146
+ * True from `submit()` on this mount until `reset()` — did THIS page start
147
+ * the run it is showing?
148
+ *
149
+ * A page needs it to say the right sentence and cannot derive it: a run
150
+ * ADOPTED by the mount-time lookup after a reload looks exactly like one this
151
+ * page started. Six templates kept a `useState(false)` next to this hook, set
152
+ * it in their `onSubmit` and mirrored it in their `onClear` — shadow state
153
+ * for a fact only this hook can know, since it is the thing that decides
154
+ * between `submit()` and the recovery lookup. One of the six grew a fourth
155
+ * branch and had to move the whole note into its own module with its own
156
+ * spec, which is what a seam missing one layer down looks like.
157
+ *
158
+ * The RAW fact rather than a derived "recovered", deliberately: with `run`
159
+ * these are three states, not two, and the third is the one a page most needs
160
+ * to explain. `startedHere` is "you pressed the button"; `!startedHere &&
161
+ * !run` is the mount-time lookup still going; `!startedHere && run` is a run
162
+ * this browser started earlier, now in front of somebody who did not press
163
+ * anything. A boolean meaning only the last of those cannot express the
164
+ * middle one.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * declare const submission: import("@alexkroman1/aai-ui").WorkflowSubmission;
169
+ * const note = submission.startedHere
170
+ * ? "You can close this tab or reload it — this page will find the run again."
171
+ * : submission.run === undefined
172
+ * ? "Looking for a run this tab started earlier…"
173
+ * : "Still working on the run this tab started earlier. Reloading is safe.";
174
+ * ```
175
+ */
176
+ startedHere: boolean;
177
+ /**
178
+ * True from `submit()` until the run reaches a terminal status.
179
+ *
180
+ * The WORK, not the request: a run outlives its `POST`, and a submit button
181
+ * that re-enabled on the response would invite a second submission of work
182
+ * already in flight.
183
+ */
184
+ pending: boolean;
185
+ /**
186
+ * How far the submission's files have got, while any are still going.
187
+ *
188
+ * Undefined before the first byte and again from the moment the last one
189
+ * lands, so a page can render `{upload && <UploadProgressBar upload={upload} />}`
190
+ * and the bar exists exactly for as long as there is an upload to describe. A
191
+ * form with no files never sets it at all.
192
+ *
193
+ * The wait it covers is the one `run` cannot: a run does not EXIST until its
194
+ * input is stored, so `pending` is true and there is nothing to poll — which
195
+ * for a 200 MB recording is minutes of a page that looks stuck.
196
+ */
197
+ upload: UploadStatus | undefined;
198
+ /**
199
+ * Park the upload where it is, stopping the bytes in flight.
200
+ *
201
+ * The windows already stored stay stored, so `resumeUpload()` sends what is
202
+ * missing rather than the file — which is the difference between a pause a
203
+ * person will actually use on a 200 MB recording and a cancel dressed up as one.
204
+ *
205
+ * `submit()`'s promise stays unresolved across a pause, because the submission
206
+ * genuinely has not finished: the run does not exist until the last byte lands,
207
+ * so resolving here would tell a `<Form>` the work was accepted when nothing has
208
+ * been started. A no-op when there is no upload in flight.
209
+ */
210
+ pauseUpload: () => void;
211
+ /** Continue a paused upload, sending only the windows the store does not have. */
212
+ resumeUpload: () => void;
213
+ /** The submit's own failure (a rejected input), or the watch's. */
214
+ error: string | undefined;
215
+ /**
216
+ * Press-to-settle marks for the submission THIS mount made — see
217
+ * {@link WorkflowSubmissionTiming}. Both `undefined` before the first
218
+ * `submit()` and again after `reset()`; a run adopted by the mount-time
219
+ * lookup has no press to time from and sets neither.
220
+ */
221
+ timing: WorkflowSubmissionTiming;
222
+ /**
223
+ * Who can find the run again after this page is gone — see
224
+ * {@link WorkflowSubmissionScope}. Derived from the hook's options (`key`,
225
+ * `recover`), so a page never restates it; `<WorkflowPendingNote>` reads it.
226
+ */
227
+ scope: WorkflowSubmissionScope;
228
+ };
229
+ /**
230
+ * The submission `useWorkflowSubmit<D>` / `useWorkflowStream<D>` hands back for
231
+ * the workflow def `D`, named so a component can take one as a prop.
232
+ *
233
+ * The hook's own return is inferred, so a page that renders everything in one
234
+ * function never needs this. A page that splits does, and there was no way to
235
+ * write it from the outside without three names — `WorkflowSubmission`,
236
+ * `WorkflowOutputOf` and `SubmitInputOf` — composed by hand, which is what a
237
+ * `ReturnType<typeof useWorkflowSubmit>` cannot say because the hook is
238
+ * generic. `podcast-digest-workflow`'s `client.tsx` composed exactly that.
239
+ *
240
+ * @example
241
+ * ```tsx
242
+ * // file: agent.ts
243
+ * import { agent, workflow } from "@alexkroman1/aai";
244
+ * import { z } from "zod";
245
+ *
246
+ * export const dailyDigest = workflow({
247
+ * input: z.object({ hour: z.number() }),
248
+ * output: z.object({ posted: z.string() }),
249
+ * run: async ({ hour }) => ({ posted: `daily at ${hour}:00` }),
250
+ * });
251
+ *
252
+ * export default agent({ name: "Digest", workflows: { dailyDigest } });
253
+ *
254
+ * // file: client.tsx
255
+ * import type { WorkflowSubmissionOf } from "@alexkroman1/aai-ui";
256
+ * import type { dailyDigest } from "./agent.ts";
257
+ *
258
+ * function ScheduleControls({ submission }: { submission: WorkflowSubmissionOf<typeof dailyDigest> }) {
259
+ * return submission.run?.status === "completed" ? <p>{submission.run.output.posted}</p> : null;
260
+ * }
261
+ * ```
262
+ *
263
+ * @public
264
+ */
265
+ export type WorkflowSubmissionOf<D> = WorkflowSubmission<WorkflowOutputOf<D>, SubmitInputOf<D>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-ui",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,7 +46,7 @@
46
46
  "use-stick-to-bottom": "^1.1.6",
47
47
  "use-sync-external-store": "^1.7.0",
48
48
  "xstate": "^5.33.2",
49
- "@alexkroman1/aai": "0.17.0"
49
+ "@alexkroman1/aai": "0.18.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tailwindcss/vite": "^4.3.3",
@@ -1 +0,0 @@
1
- import{a as e,i as t,t as n}from"./index-BlUYCTB5.js";var r={echoCancellation:!0,noiseSuppression:!1,autoGainControl:!1,voiceIsolation:!1};function i(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}function a(e){let t=Error(`Audio ${e} worklet crashed`);return console.error(`[aai-ui]`,t.message),t}function o(e){e.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{})}function s(e,t,n){let r=new AudioWorkletNode(e,`capture-processor`,{channelCount:1,channelCountMode:`explicit`}),i=null;return r.port.onmessage=e=>{let r=e.data;r.event===`chunk`&&r.buffer?t(r.buffer):r.event===`silent`?n?.():r.event===`stopped`&&(i?.(),i=null)},{node:r,start(){r.port.postMessage({event:`start`})},stop(){let{promise:e,resolve:t}=Promise.withResolvers(),n=setTimeout(t,250);return i=()=>{clearTimeout(n),t()},r.port.postMessage({event:`stop`}),e}}}async function c(e){let{sampleRate:t,captureWorkletSrc:n,maxSeconds:a}=e,c=new AudioContext({sampleRate:t,latencyHint:`interactive`}),l=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...r}}),u;try{[u]=await Promise.all([l,c.resume(),c.audioWorklet.addModule(n)]),i(c.sampleRate,t,`capture`)}catch(e){throw o(l),await c.close().catch(()=>{}),e}let d=Math.round(t*a)*2,f=[],p=0,m=!1,h=null,g=c.createMediaStreamSource(u),_=s(c,e=>{if(h){h.onChunk(e);return}for(f.push(e),p+=e.byteLength;p>d&&f.length>1;)p-=f.shift()?.byteLength??0},()=>{h?h.onSilent?.():m=!0});g.connect(_.node),_.start();let v=()=>{let e=f;return f=[],p=0,e},y=!1,b=async()=>{y||(y=!0,g.disconnect(),_.node.disconnect(),await c.close().catch(e=>{console.warn(`AudioContext close failed:`,e)}))};return{sampleRate:t,stream:u,ctx:c,handOff(e){h=e;let t=v();return t.length>0&&e.onBuffered(t),m&&e.onSilent?.(),{mic:g,capture:_}},async detach(){return await _.stop(),await b(),{chunks:v(),sampleRate:t}},async close(){y||await _.stop();for(let e of u.getTracks())e.stop();await b()}}}function l(e){let t=new Int16Array(e.length);for(let n=0;n<e.length;n++){let r=Math.max(-1,Math.min(1,e[n]??0));t[n]=r<0?r*32768:r*32767}return t.buffer}async function u(e,t,r){let i=e.reduce((e,t)=>e+t.byteLength/2,0);if(i===0)return[];if(t===r)return e;try{let a=Math.round(i*r/t),o=new OfflineAudioContext(1,a,r),s=o.createBuffer(1,i,t),c=s.getChannelData(0),u=0;for(let t of e)for(let e of new Int16Array(t))c[u++]=e/32768;let d=o.createBufferSource();d.buffer=s,d.connect(o.destination),d.start();let f=(await o.startRendering()).getChannelData(0),p=Math.max(1,Math.round(r*n)),m=[];for(let e=0;e<f.length;e+=p)m.push(l(f.subarray(e,e+p)));return m}catch(e){return console.warn(`[aai-ui] pre-connect audio dropped: resample failed`,e),[]}}async function d(e,t,n){let r=e.createMediaStreamSource(t),i=s(e,n.onMicData,n.onMicSilent);if(r.connect(i.node),n.buffered){let{chunks:t,sampleRate:r}=n.buffered,i=await u(t,r,e.sampleRate);i.length>0&&n.onBuffered(i)}return i.start(),{mic:r,capture:i}}async function f(n){let{sttSampleRate:s,ttsSampleRate:c,captureWorkletSrc:l,playbackWorkletSrc:u,onMicData:f,onError:p,onPlaybackStats:m,onPlaybackProgress:h,onMicSilent:g,preConnect:_}=n,v=n.onPreConnectAudio??(e=>{for(let t of e)f(t)}),y=_?.sampleRate===s?_:null,b=_&&!y?await _.detach():null,x=new AudioContext({sampleRate:c,latencyHint:`playback`}),S=!y&&s===c,C=y?.ctx??(S?x:new AudioContext({sampleRate:s,latencyHint:`interactive`}));async function w(){let e=S?[x]:[x,C];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let T=_?Promise.resolve(_.stream):navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...r}}),E;try{[E]=await Promise.all([T,x.resume(),C.resume(),y?void 0:C.audioWorklet.addModule(l),x.audioWorklet.addModule(u)]),i(C.sampleRate,s,`capture`),i(x.sampleRate,c,`playback`)}catch(e){throw o(T),await w(),e}let{mic:D,capture:O}=y?y.handOff({onBuffered:v,onChunk:f,onSilent:g}):await d(C,E,{onMicData:f,onMicSilent:g,buffered:b,onBuffered:v});O.node.onprocessorerror=()=>p?.(a(`capture`));let k=null,A=0,j=null,M=new AbortController;function N(){let e=j;j=null,e?.settle()}function P(e){e.stats&&e.stats.concealedSamples>0&&m?.(e.stats),e.reason!==`interrupt`&&(j===null||e.turn===j.turn)&&N()}function F(){if(k)return k;let e=new AudioWorkletNode(x,`playback-processor`);return e.connect(x.destination),e.port.onmessage=e=>{e.data.event===`stop`?P(e.data):e.data.event===`progress`&&h?.(e.data.bufferedMs)},e.onprocessorerror=()=>{let e=a(`playback`);N(),p?.(e)},k=e,e}let I={enqueue(e){M.signal.aborted||e.byteLength!==0&&F().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){if(!k)return Promise.resolve();let n=++A;if(k.port.postMessage({event:`done`,turn:n}),x.state!==`running`)return N(),Promise.resolve();let{promise:r,resolve:i}=Promise.withResolvers();N();let a=()=>{clearInterval(o),clearTimeout(s),j?.settle===a&&(j=null),i()},o=setInterval(()=>{x.state!==`running`&&a()},e),s=setTimeout(a,t);return j={turn:n,settle:a},r},flush(){k&&(N(),k.port.postMessage({event:`interrupt`}))},async close(){if(!M.signal.aborted){M.abort(),await O.stop(),D.disconnect(),O.node.disconnect(),k&&k.disconnect();for(let e of E.getTracks())e.stop();await w()}},async[Symbol.asyncDispose](){await I.close()}};return I}export{f as createVoiceIO,c as openPreConnectCapture};