@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.
- package/Package.swift +19 -2
- package/README.md +190 -13
- package/android/build.gradle.kts +12 -6
- package/android/settings.gradle +7 -0
- package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunCapacitorPlugin.kt +154 -0
- package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunSerializer.kt +203 -1
- package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunStreamRegistry.kt +118 -0
- package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerCapabilitiesTest.kt +253 -0
- package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerStreamTest.kt +226 -0
- package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerTest.kt +79 -0
- package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunStreamRegistryTest.kt +163 -0
- package/dist/definitions.d.ts +175 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +36 -0
- package/dist/index.d.ts +17 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -13
- package/dist/streaming.d.ts +17 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +303 -0
- package/dist/web.d.ts +18 -2
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +136 -7
- package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorBridge.swift +181 -0
- package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorPlugin.swift +93 -3
- package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorStreamRegistry.swift +121 -0
- package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorBridgeTests.swift +234 -0
- package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorStreamCodecTests.swift +178 -0
- package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorStreamPumpTests.swift +215 -0
- package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorStreamRegistryTests.swift +154 -0
- package/package.json +21 -9
package/dist/errors.d.ts
ADDED
|
@@ -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 {
|
|
2
|
-
import type { ConfigureOptions, IndeRunCapacitorPlugin as IndeRunCapacitorPluginContract, OpenAIProviderBootstrapOptions } from "./definitions.js";
|
|
3
|
-
|
|
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
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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;
|
|
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"}
|