@independo/capacitor-inderun 1.0.1-dev.1 → 1.1.0-dev.10

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 (33) hide show
  1. package/Package.swift +19 -2
  2. package/README.md +190 -13
  3. package/android/build.gradle.kts +12 -6
  4. package/android/settings.gradle +7 -0
  5. package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunCapacitorPlugin.kt +154 -0
  6. package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunSerializer.kt +203 -1
  7. package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunStreamRegistry.kt +118 -0
  8. package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerCapabilitiesTest.kt +253 -0
  9. package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerStreamTest.kt +226 -0
  10. package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerTest.kt +79 -0
  11. package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunStreamRegistryTest.kt +163 -0
  12. package/dist/definitions.d.ts +175 -1
  13. package/dist/definitions.d.ts.map +1 -1
  14. package/dist/errors.d.ts +19 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +36 -0
  17. package/dist/index.d.ts +17 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +44 -13
  20. package/dist/streaming.d.ts +17 -0
  21. package/dist/streaming.d.ts.map +1 -0
  22. package/dist/streaming.js +303 -0
  23. package/dist/web.d.ts +18 -2
  24. package/dist/web.d.ts.map +1 -1
  25. package/dist/web.js +136 -7
  26. package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorBridge.swift +181 -0
  27. package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorPlugin.swift +93 -3
  28. package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorStreamRegistry.swift +121 -0
  29. package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorBridgeTests.swift +234 -0
  30. package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorStreamCodecTests.swift +178 -0
  31. package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorStreamPumpTests.swift +215 -0
  32. package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorStreamRegistryTests.swift +154 -0
  33. package/package.json +21 -9
@@ -0,0 +1,19 @@
1
+ import { type IndeRunError } from "@independo/inderun-contracts";
2
+ /**
3
+ * Unwraps whatever the Capacitor boundary hands back into the canonical
4
+ * `IndeRunError` the native side rejected with.
5
+ *
6
+ * Capacitor wraps a native rejection's structured data under `error.data`, so a
7
+ * thrown value is either already an `IndeRunError` (the web implementation throws
8
+ * one directly) or carries one there. Anything else is passed through untouched
9
+ * rather than guessed at.
10
+ */
11
+ export declare function normalizePluginError(error: unknown): unknown;
12
+ /**
13
+ * Mints an `IndeRunError` for a failure of the bridge itself — event loss across
14
+ * the JS boundary, a malformed handle — as opposed to one reported by a provider
15
+ * or the engine. `Internal` is the correct class: nothing about the request or the
16
+ * provider is at fault, the transport is.
17
+ */
18
+ export declare function createBridgeError(message: string, runId?: string): IndeRunError;
19
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEvF;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAe5D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,YAAY,CAO/E"}
package/dist/errors.js ADDED
@@ -0,0 +1,36 @@
1
+ import { validateIndeRunError } from "@independo/inderun-contracts";
2
+ /**
3
+ * Unwraps whatever the Capacitor boundary hands back into the canonical
4
+ * `IndeRunError` the native side rejected with.
5
+ *
6
+ * Capacitor wraps a native rejection's structured data under `error.data`, so a
7
+ * thrown value is either already an `IndeRunError` (the web implementation throws
8
+ * one directly) or carries one there. Anything else is passed through untouched
9
+ * rather than guessed at.
10
+ */
11
+ export function normalizePluginError(error) {
12
+ if (validateIndeRunError(error)) {
13
+ return error;
14
+ }
15
+ if (typeof error === "object" &&
16
+ error !== null &&
17
+ "data" in error &&
18
+ validateIndeRunError(error.data)) {
19
+ return error.data;
20
+ }
21
+ return error;
22
+ }
23
+ /**
24
+ * Mints an `IndeRunError` for a failure of the bridge itself — event loss across
25
+ * the JS boundary, a malformed handle — as opposed to one reported by a provider
26
+ * or the engine. `Internal` is the correct class: nothing about the request or the
27
+ * provider is at fault, the transport is.
28
+ */
29
+ export function createBridgeError(message, runId) {
30
+ return {
31
+ schemaVersion: "1.0",
32
+ errorClass: "Internal",
33
+ message,
34
+ ...(runId !== undefined ? { runId } : {})
35
+ };
36
+ }
package/dist/index.d.ts CHANGED
@@ -1,10 +1,23 @@
1
- import { type TaskRequest, type TaskResult } from "@independo/inderun-contracts";
2
- import type { ConfigureOptions, IndeRunCapacitorPlugin as IndeRunCapacitorPluginContract, OpenAIProviderBootstrapOptions } from "./definitions.js";
3
- export declare const IndeRunCapacitor: IndeRunCapacitorPluginContract;
1
+ import type { TaskRequest, TaskResult } from "@independo/inderun-contracts";
2
+ import type { CancelStreamOptions, CheckCapabilitiesResult, ConfigureOptions, IndeRunCapacitorPlugin as IndeRunCapacitorPluginContract, OnnxProviderBootstrapOptions, OpenAIProviderBootstrapOptions, ProviderCapabilitySnapshot, ProviderDescriptor, ProviderDynamicCapabilities, StartStreamOptions, StreamErrorNotification, StreamEventNotification, StreamRun, SystemModelProviderBootstrapOptions } from "./definitions.js";
3
+ /**
4
+ * The plugin contract plus the ergonomic Mode 2 entry point. `stream()` is not a
5
+ * plugin method — it is `startStream`, the event listeners, and `cancelStream`
6
+ * reassembled into the same shape the platform SDKs return from `stream()`.
7
+ */
8
+ export interface IndeRunCapacitorApi extends Omit<IndeRunCapacitorPluginContract, "checkCapabilities"> {
9
+ stream(request: TaskRequest): Promise<StreamRun>;
10
+ /** The plugin's `{ providers }` envelope, unwrapped to match the platform SDKs. */
11
+ checkCapabilities(): Promise<ProviderCapabilitySnapshot[]>;
12
+ }
13
+ export declare const IndeRunCapacitor: IndeRunCapacitorApi;
4
14
  export interface IndeRunCapacitorInstance {
5
15
  run(request: TaskRequest): Promise<TaskResult>;
16
+ stream(request: TaskRequest): Promise<StreamRun>;
17
+ checkCapabilities(): Promise<ProviderCapabilitySnapshot[]>;
6
18
  }
7
19
  export declare function createIndeRunCapacitor(options?: ConfigureOptions): IndeRunCapacitorInstance;
8
- export type { ConfigureOptions, IndeRunCapacitorPluginContract as IndeRunCapacitorPlugin, OpenAIProviderBootstrapOptions, TaskRequest, TaskResult };
20
+ export { STREAM_ERROR_NAME, STREAM_EVENT_NAME } from "./streaming.js";
21
+ export type { CancelStreamOptions, CheckCapabilitiesResult, ConfigureOptions, IndeRunCapacitorPluginContract as IndeRunCapacitorPlugin, OnnxProviderBootstrapOptions, OpenAIProviderBootstrapOptions, ProviderCapabilitySnapshot, ProviderDescriptor, ProviderDynamicCapabilities, StartStreamOptions, StreamErrorNotification, StreamEventNotification, StreamRun, SystemModelProviderBootstrapOptions, TaskRequest, TaskResult };
9
22
  export type * from "@independo/inderun-contracts";
10
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,IAAI,8BAA8B,EACxD,8BAA8B,EAC/B,MAAM,kBAAkB,CAAC;AAuB1B,eAAO,MAAM,gBAAgB,EAAE,8BAgB9B,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAChD;AAED,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,wBAAwB,CAiB3F;AAED,YAAY,EACV,gBAAgB,EAChB,8BAA8B,IAAI,sBAAsB,EACxD,8BAA8B,EAC9B,WAAW,EACX,UAAU,EACX,CAAC;AAEF,mBAAmB,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE7F,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,sBAAsB,IAAI,8BAA8B,EACxD,4BAA4B,EAC5B,8BAA8B,EAC9B,0BAA0B,EAC1B,kBAAkB,EAClB,2BAA2B,EAC3B,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,SAAS,EACT,mCAAmC,EACpC,MAAM,kBAAkB,CAAC;AAQ1B;;;;GAIG;AACH,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,8BAA8B,EAAE,mBAAmB,CAAC;IACpG,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACjD,mFAAmF;IACnF,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAAC;CAC5D;AAED,eAAO,MAAM,gBAAgB,EAAE,mBAqD9B,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACjD,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAAC;CAC5D;AAED,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,wBAAwB,CA4B3F;AAED,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEtE,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,8BAA8B,IAAI,sBAAsB,EACxD,4BAA4B,EAC5B,8BAA8B,EAC9B,0BAA0B,EAC1B,kBAAkB,EAClB,2BAA2B,EAC3B,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,SAAS,EACT,mCAAmC,EACnC,WAAW,EACX,UAAU,EACX,CAAC;AAEF,mBAAmB,8BAA8B,CAAC"}
package/dist/index.js CHANGED
@@ -1,20 +1,9 @@
1
- import { validateIndeRunError } from "@independo/inderun-contracts";
2
1
  import { registerPlugin } from "@capacitor/core";
2
+ import { normalizePluginError } from "./errors.js";
3
+ import { startCapacitorStream } from "./streaming.js";
3
4
  const IndeRunCapacitorNative = registerPlugin("IndeRunCapacitor", {
4
5
  web: () => import("./web.js").then((module) => new module.IndeRunWeb())
5
6
  });
6
- function normalizePluginError(error) {
7
- if (validateIndeRunError(error)) {
8
- return error;
9
- }
10
- if (typeof error === "object" &&
11
- error !== null &&
12
- "data" in error &&
13
- validateIndeRunError(error.data)) {
14
- return error.data;
15
- }
16
- return error;
17
- }
18
7
  export const IndeRunCapacitor = {
19
8
  async configure(options) {
20
9
  try {
@@ -31,6 +20,38 @@ export const IndeRunCapacitor = {
31
20
  catch (error) {
32
21
  throw normalizePluginError(error);
33
22
  }
23
+ },
24
+ async checkCapabilities() {
25
+ try {
26
+ const result = await IndeRunCapacitorNative.checkCapabilities();
27
+ return result.providers;
28
+ }
29
+ catch (error) {
30
+ throw normalizePluginError(error);
31
+ }
32
+ },
33
+ async startStream(options) {
34
+ try {
35
+ return await IndeRunCapacitorNative.startStream(options);
36
+ }
37
+ catch (error) {
38
+ throw normalizePluginError(error);
39
+ }
40
+ },
41
+ async cancelStream(options) {
42
+ try {
43
+ await IndeRunCapacitorNative.cancelStream(options);
44
+ }
45
+ catch (error) {
46
+ throw normalizePluginError(error);
47
+ }
48
+ },
49
+ addListener: IndeRunCapacitorNative.addListener.bind(IndeRunCapacitorNative),
50
+ async removeAllListeners() {
51
+ await IndeRunCapacitorNative.removeAllListeners();
52
+ },
53
+ stream(request) {
54
+ return startCapacitorStream(IndeRunCapacitorNative, request);
34
55
  }
35
56
  };
36
57
  export function createIndeRunCapacitor(options) {
@@ -46,6 +67,16 @@ export function createIndeRunCapacitor(options) {
46
67
  async run(request) {
47
68
  await ensureConfigured();
48
69
  return IndeRunCapacitor.run(request);
70
+ },
71
+ async stream(request) {
72
+ await ensureConfigured();
73
+ return IndeRunCapacitor.stream(request);
74
+ },
75
+ // Configures first: without a registry there are no providers to report on.
76
+ async checkCapabilities() {
77
+ await ensureConfigured();
78
+ return IndeRunCapacitor.checkCapabilities();
49
79
  }
50
80
  };
51
81
  }
82
+ export { STREAM_ERROR_NAME, STREAM_EVENT_NAME } from "./streaming.js";
@@ -0,0 +1,17 @@
1
+ import { type TaskRequest } from "@independo/inderun-contracts";
2
+ import type { IndeRunCapacitorPlugin, StreamRun } from "./definitions.js";
3
+ /**
4
+ * Listener event names. These are **public contract**: an app may attach its own
5
+ * listener, and native code emits exactly these. Renaming one is a breaking change.
6
+ */
7
+ export declare const STREAM_EVENT_NAME = "indeRunStreamEvent";
8
+ export declare const STREAM_ERROR_NAME = "indeRunStreamError";
9
+ /**
10
+ * Starts a Mode 2 run over the bridge and reassembles it into the same `StreamRun`
11
+ * shape `@independo/inderun-web` returns directly.
12
+ *
13
+ * The listeners are registered *before* `startStream` is called, so no event can be
14
+ * emitted for a run whose sink does not exist yet.
15
+ */
16
+ export declare function startCapacitorStream(plugin: IndeRunCapacitorPlugin, request: TaskRequest): Promise<StreamRun>;
17
+ //# sourceMappingURL=streaming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EACV,sBAAsB,EAGtB,SAAS,EACV,MAAM,kBAAkB,CAAC;AAG1B;;;GAGG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AACtD,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AA6QtD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,sBAAsB,EAC9B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,SAAS,CAAC,CAwDpB"}
@@ -0,0 +1,303 @@
1
+ import { validateStreamRunHandle } from "@independo/inderun-contracts";
2
+ import { createBridgeError, normalizePluginError } from "./errors.js";
3
+ /**
4
+ * Listener event names. These are **public contract**: an app may attach its own
5
+ * listener, and native code emits exactly these. Renaming one is a breaking change.
6
+ */
7
+ export const STREAM_EVENT_NAME = "indeRunStreamEvent";
8
+ export const STREAM_ERROR_NAME = "indeRunStreamError";
9
+ /**
10
+ * How long to wait, after the terminal event has arrived, for a still-missing
11
+ * earlier event before declaring the run's event sequence irrecoverable.
12
+ *
13
+ * Deliberately a module constant rather than a public option: any value here is a
14
+ * behavioural policy, and behaviour belongs in the engines, not in the bridge.
15
+ */
16
+ const REORDER_GRACE_MS = 250;
17
+ function createStreamId() {
18
+ const globalCrypto = globalThis.crypto;
19
+ if (typeof globalCrypto?.randomUUID === "function") {
20
+ return globalCrypto.randomUUID();
21
+ }
22
+ return `stream_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 11)}`;
23
+ }
24
+ /**
25
+ * Reassembles one run's canonical event sequence on the JS side of the bridge.
26
+ *
27
+ * The bridge hop is not order-preserving, which is why `StreamEvent.sequence` is
28
+ * the contract's ordering authority rather than arrival order. This sink buffers
29
+ * out-of-order arrivals and yields strictly by `sequence`, so a consumer sees
30
+ * exactly what the engine's Event Gate admitted, in the order it admitted it.
31
+ *
32
+ * It owns no run semantics: it never synthesizes a terminal event, never retries,
33
+ * never decides an outcome. A gap that never closes becomes a transport error, not
34
+ * an invented ending. Everything about *what* a run does stays in the engines.
35
+ */
36
+ class StreamSink {
37
+ nextSequence = 0;
38
+ pending = new Map();
39
+ ready = [];
40
+ terminalSequence = null;
41
+ runId = null;
42
+ failure = null;
43
+ done = false;
44
+ wake = null;
45
+ graceTimer = null;
46
+ bindRunId(runId) {
47
+ this.runId = runId;
48
+ }
49
+ /**
50
+ * True once the run has ended — the terminal event has been admitted, or the
51
+ * transport has failed — regardless of whether the consumer has drained it yet.
52
+ */
53
+ isFinished() {
54
+ return this.done || this.failure !== null;
55
+ }
56
+ accept(event) {
57
+ if (this.isFinished()) {
58
+ return;
59
+ }
60
+ // Defence in depth on top of streamId routing: an event for another run is
61
+ // never this run's business.
62
+ if (this.runId !== null && event.runId !== this.runId) {
63
+ return;
64
+ }
65
+ // Already delivered (a duplicate), or arriving after the terminal that closed
66
+ // the run. The Event Gate guarantees the terminal is the highest sequence.
67
+ if (event.sequence < this.nextSequence) {
68
+ return;
69
+ }
70
+ if (this.terminalSequence !== null && event.sequence > this.terminalSequence) {
71
+ return;
72
+ }
73
+ this.pending.set(event.sequence, event);
74
+ if (event.type === "terminal") {
75
+ this.terminalSequence = event.sequence;
76
+ }
77
+ this.drain();
78
+ }
79
+ fail(error) {
80
+ if (this.isFinished()) {
81
+ return;
82
+ }
83
+ this.clearGrace();
84
+ this.failure = error;
85
+ this.signal();
86
+ }
87
+ async *iterate() {
88
+ for (;;) {
89
+ while (this.ready.length > 0) {
90
+ yield this.ready.shift();
91
+ }
92
+ if (this.failure !== null) {
93
+ throw this.failure;
94
+ }
95
+ if (this.done) {
96
+ return;
97
+ }
98
+ await new Promise((resolve) => {
99
+ this.wake = resolve;
100
+ });
101
+ }
102
+ }
103
+ dispose() {
104
+ this.clearGrace();
105
+ }
106
+ drain() {
107
+ let moved = false;
108
+ for (;;) {
109
+ const next = this.pending.get(this.nextSequence);
110
+ if (next === undefined) {
111
+ break;
112
+ }
113
+ this.pending.delete(this.nextSequence);
114
+ this.ready.push(next);
115
+ this.nextSequence += 1;
116
+ moved = true;
117
+ }
118
+ if (this.terminalSequence !== null) {
119
+ if (this.nextSequence > this.terminalSequence) {
120
+ // The whole 0..terminal range has been handed over, in order.
121
+ this.clearGrace();
122
+ this.done = true;
123
+ }
124
+ else {
125
+ this.startGrace();
126
+ }
127
+ }
128
+ if (moved || this.done) {
129
+ this.signal();
130
+ }
131
+ }
132
+ startGrace() {
133
+ if (this.graceTimer !== null) {
134
+ return;
135
+ }
136
+ this.graceTimer = setTimeout(() => {
137
+ this.graceTimer = null;
138
+ this.onGraceExpired();
139
+ }, REORDER_GRACE_MS);
140
+ }
141
+ clearGrace() {
142
+ if (this.graceTimer !== null) {
143
+ clearTimeout(this.graceTimer);
144
+ this.graceTimer = null;
145
+ }
146
+ }
147
+ /**
148
+ * The terminal arrived but an earlier event never did.
149
+ *
150
+ * Hand over the content that did arrive, in order, then fail. The terminal itself
151
+ * is deliberately withheld: delivering it would assert an ending — a `finalText`,
152
+ * a clean `completed` — that the consumer's own event sequence does not support.
153
+ * A transport that lost events reports that, rather than papering over it.
154
+ */
155
+ onGraceExpired() {
156
+ if (this.isFinished()) {
157
+ return;
158
+ }
159
+ const terminalSequence = this.terminalSequence;
160
+ let lost = 0;
161
+ for (let sequence = this.nextSequence; sequence <= terminalSequence; sequence += 1) {
162
+ if (!this.pending.has(sequence)) {
163
+ lost += 1;
164
+ }
165
+ }
166
+ for (const [sequence, event] of [...this.pending.entries()].sort(([a], [b]) => a - b)) {
167
+ if (sequence !== terminalSequence) {
168
+ this.ready.push(event);
169
+ }
170
+ }
171
+ this.pending.clear();
172
+ this.failure = createBridgeError(`Capacitor bridge lost ${lost} stream event(s) for run ${this.runId ?? "<unknown>"}.`, this.runId ?? undefined);
173
+ this.signal();
174
+ }
175
+ signal() {
176
+ const wake = this.wake;
177
+ this.wake = null;
178
+ wake?.();
179
+ }
180
+ }
181
+ /**
182
+ * Owns the two plugin listeners and fans notifications out to the sink for each
183
+ * live run.
184
+ *
185
+ * One registration serves every concurrent run: listeners are attached on the
186
+ * first bind and removed once the last run releases, so a consumer that streams
187
+ * repeatedly does not accumulate listeners.
188
+ */
189
+ class StreamDispatcher {
190
+ sinks = new Map();
191
+ handles = null;
192
+ registering = null;
193
+ async bind(plugin, streamId, sink) {
194
+ this.sinks.set(streamId, sink);
195
+ try {
196
+ await this.ensureListeners(plugin);
197
+ }
198
+ catch (error) {
199
+ this.sinks.delete(streamId);
200
+ throw error;
201
+ }
202
+ }
203
+ release(streamId) {
204
+ const sink = this.sinks.get(streamId);
205
+ if (sink === undefined) {
206
+ return;
207
+ }
208
+ sink.dispose();
209
+ this.sinks.delete(streamId);
210
+ if (this.sinks.size === 0) {
211
+ void this.removeListeners();
212
+ }
213
+ }
214
+ ensureListeners(plugin) {
215
+ this.registering ??= (async () => {
216
+ const [events, errors] = await Promise.all([
217
+ plugin.addListener(STREAM_EVENT_NAME, this.handleEvent),
218
+ plugin.addListener(STREAM_ERROR_NAME, this.handleError)
219
+ ]);
220
+ this.handles = [events, errors];
221
+ })().catch((error) => {
222
+ this.registering = null;
223
+ throw error;
224
+ });
225
+ return this.registering;
226
+ }
227
+ async removeListeners() {
228
+ const handles = this.handles;
229
+ this.handles = null;
230
+ this.registering = null;
231
+ if (handles === null) {
232
+ return;
233
+ }
234
+ await Promise.all(handles.map((handle) => handle.remove()));
235
+ }
236
+ // Arrow properties: they are handed to addListener and must keep their binding.
237
+ handleEvent = (notification) => {
238
+ // An unknown streamId is a retained notification replayed for a run that has
239
+ // already finished. Dropping it is correct, not an error.
240
+ this.sinks.get(notification.streamId)?.accept(notification.event);
241
+ };
242
+ handleError = (notification) => {
243
+ this.sinks.get(notification.streamId)?.fail(notification.error);
244
+ };
245
+ }
246
+ const dispatcher = new StreamDispatcher();
247
+ /**
248
+ * Starts a Mode 2 run over the bridge and reassembles it into the same `StreamRun`
249
+ * shape `@independo/inderun-web` returns directly.
250
+ *
251
+ * The listeners are registered *before* `startStream` is called, so no event can be
252
+ * emitted for a run whose sink does not exist yet.
253
+ */
254
+ export async function startCapacitorStream(plugin, request) {
255
+ const streamId = createStreamId();
256
+ const sink = new StreamSink();
257
+ await dispatcher.bind(plugin, streamId, sink);
258
+ let handle;
259
+ try {
260
+ handle = await plugin.startStream({ streamId, request });
261
+ }
262
+ catch (error) {
263
+ dispatcher.release(streamId);
264
+ throw normalizePluginError(error);
265
+ }
266
+ if (!validateStreamRunHandle(handle)) {
267
+ dispatcher.release(streamId);
268
+ throw createBridgeError("Capacitor bridge received a malformed StreamRunHandle from the native plugin.");
269
+ }
270
+ sink.bindRunId(handle.runId);
271
+ let cancelled = false;
272
+ const requestCancel = (reason) => {
273
+ // Cancelling a run that has already ended is a no-op by contract, and is
274
+ // answered locally so the bridge is not crossed for nothing.
275
+ if (cancelled || sink.isFinished()) {
276
+ return;
277
+ }
278
+ cancelled = true;
279
+ void plugin
280
+ .cancelStream({ streamId, ...(reason !== undefined ? { reason } : {}) })
281
+ .catch((error) => sink.fail(normalizePluginError(error)));
282
+ };
283
+ const generator = (async function* consume() {
284
+ try {
285
+ yield* sink.iterate();
286
+ }
287
+ finally {
288
+ // Covers the consumer abandoning the loop with `break`: tell native to stop
289
+ // producing, and let go of the listeners.
290
+ requestCancel();
291
+ dispatcher.release(streamId);
292
+ }
293
+ })();
294
+ return {
295
+ handle,
296
+ events: {
297
+ // Single-use, matching the engines: the run is driven once, not restarted per
298
+ // consumer, so a second iteration observes completion rather than a replay.
299
+ [Symbol.asyncIterator]: () => generator
300
+ },
301
+ cancel: requestCancel
302
+ };
303
+ }
package/dist/web.d.ts CHANGED
@@ -1,10 +1,26 @@
1
- import type { TaskResult } from "@independo/inderun-contracts";
1
+ import type { StreamRunHandle, TaskResult } from "@independo/inderun-contracts";
2
2
  import { WebPlugin } from "@capacitor/core";
3
- import type { ConfigureOptions, IndeRunCapacitorPlugin } from "./definitions.js";
3
+ import type { CancelStreamOptions, CheckCapabilitiesResult, ConfigureOptions, IndeRunCapacitorPlugin, StartStreamOptions } from "./definitions.js";
4
4
  import type { TaskRequest } from "@independo/inderun-contracts";
5
5
  export declare class IndeRunWeb extends WebPlugin implements IndeRunCapacitorPlugin {
6
6
  private engine;
7
+ private readonly activeStreams;
8
+ private readonly startingStreams;
9
+ private readonly pendingCancels;
7
10
  configure(options?: ConfigureOptions): Promise<void>;
8
11
  run(request: TaskRequest): Promise<TaskResult>;
12
+ checkCapabilities(): Promise<CheckCapabilitiesResult>;
13
+ /**
14
+ * The web path deliberately round-trips through `notifyListeners` rather than
15
+ * handing the engine's `StreamRun` straight back.
16
+ *
17
+ * Short-circuiting would require the facade to branch on the platform and would
18
+ * leave two reassembly paths whose equivalence is only a convention — while this
19
+ * way there is exactly one, exercised on every platform. It also equalizes the
20
+ * loss of generator backpressure that the native paths have regardless.
21
+ */
22
+ startStream(options: StartStreamOptions): Promise<StreamRunHandle>;
23
+ cancelStream(options: CancelStreamOptions): Promise<void>;
24
+ private pump;
9
25
  }
10
26
  //# sourceMappingURL=web.d.ts.map
package/dist/web.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AACjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAGhE,qBAAa,UAAW,SAAQ,SAAU,YAAW,sBAAsB;IACzE,OAAO,CAAC,MAAM,CAAwB;IAEhC,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BzD;IAEK,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAYnD;CACF"}
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EAEnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAIhE,qBAAa,UAAW,SAAQ,SAAU,YAAW,sBAAsB;IACzE,OAAO,CAAC,MAAM,CAAwB;IAItC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgC;IAC9D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyC;IAElE,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCzD;IAEK,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAYnD;IAEK,iBAAiB,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAe1D;IAED;;;;;;;;OAQG;IACG,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CA8BvE;IAEK,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB9D;YAEa,IAAI;CAenB"}