@mostra/sdk 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +37 -2
- package/dist/bundles.json +31 -0
- package/dist/index.d.ts +374 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +484 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-CUbnK5Za.js +1010 -0
- package/dist/runtime-CUbnK5Za.js.map +1 -0
- package/dist/setup-main.d.ts +1 -0
- package/dist/setup-main.js +49 -0
- package/dist/setup-main.js.map +1 -0
- package/package.json +63 -3
- package/src/bundle/cache.ts +375 -0
- package/src/bundle/download.ts +255 -0
- package/src/bundle/extract.ts +60 -0
- package/src/bundle/setup.ts +278 -0
- package/src/bundle/tree.ts +176 -0
- package/src/events.ts +276 -0
- package/src/index.ts +67 -0
- package/src/mostra.ts +607 -0
- package/src/platform.ts +81 -0
- package/src/runtime.ts +179 -0
- package/src/setup-main.ts +49 -0
- package/src/version.ts +38 -0
package/src/events.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events as an async iterator (IN-08, D101): pages from `events/subscribe`, in cursor order. A
|
|
3
|
+
* consumer that falls behind pauses the subscription instead of buffering without bound or
|
|
4
|
+
* stalling the connection's other calls: past `highWaterMark` pages it unsubscribes, and once it
|
|
5
|
+
* has caught up it subscribes again after the last cursor it received, so nothing is missed or
|
|
6
|
+
* repeated. With a `name`, a page is acknowledged (`events/ack`) when the next one is asked for,
|
|
7
|
+
* which is when the consumer has handled it; a stream of the same name later resumes there.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
createDiagnostic,
|
|
11
|
+
type Diagnostic,
|
|
12
|
+
type EventFilter,
|
|
13
|
+
type EventRecord,
|
|
14
|
+
EventsNotifyParams,
|
|
15
|
+
eventsAckMethod,
|
|
16
|
+
eventsSubscribeMethod,
|
|
17
|
+
eventsUnsubscribeMethod,
|
|
18
|
+
type MethodDefinition,
|
|
19
|
+
RpcError,
|
|
20
|
+
} from "@mostra/protocol";
|
|
21
|
+
|
|
22
|
+
/** One page of events. */
|
|
23
|
+
export interface EventPage {
|
|
24
|
+
/** The events, in cursor order. */
|
|
25
|
+
readonly events: readonly EventRecord[];
|
|
26
|
+
/** Everything up to this cursor was examined: the cursor to resume after. */
|
|
27
|
+
readonly cursor: string;
|
|
28
|
+
/** Set when compaction removed events in this range (`MOSTRA_HOST_CURSOR_COMPACTED`). */
|
|
29
|
+
readonly compacted?: Diagnostic;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Options of a stream. */
|
|
33
|
+
export interface EventStreamOptions {
|
|
34
|
+
/** Which events: artifacts and type patterns. Default: every event. */
|
|
35
|
+
readonly filter?: EventFilter;
|
|
36
|
+
/** Start after this cursor. Default: a named stream's acknowledged cursor, else now. */
|
|
37
|
+
readonly after?: string;
|
|
38
|
+
/** A durable subscriber name: pages are acknowledged under it as they are handled. */
|
|
39
|
+
readonly name?: string;
|
|
40
|
+
/** Pages held before the subscription pauses. Default 16. */
|
|
41
|
+
readonly highWaterMark?: number;
|
|
42
|
+
/** Ends the stream. */
|
|
43
|
+
readonly signal?: AbortSignal;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What a stream needs of the session. */
|
|
47
|
+
export interface EventSession {
|
|
48
|
+
call<P, R>(method: MethodDefinition<P, R>, params: P): Promise<R>;
|
|
49
|
+
/** Registers a handler for `events/notify`; returns its removal. */
|
|
50
|
+
onNotification(handler: (params: unknown) => void): () => void;
|
|
51
|
+
/** Resolves when the session ends. */
|
|
52
|
+
readonly closed: Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Opens a stream; nothing is subscribed until the first page is asked for. */
|
|
56
|
+
export function openEventStream(
|
|
57
|
+
session: EventSession,
|
|
58
|
+
options: EventStreamOptions,
|
|
59
|
+
): AsyncIterable<EventPage> {
|
|
60
|
+
return {
|
|
61
|
+
[Symbol.asyncIterator]: () => new EventStream(session, options),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class EventStream implements AsyncIterator<EventPage> {
|
|
66
|
+
readonly #session: EventSession;
|
|
67
|
+
readonly #options: EventStreamOptions;
|
|
68
|
+
readonly #high: number;
|
|
69
|
+
readonly #queue: EventPage[] = [];
|
|
70
|
+
#subscription: string | undefined;
|
|
71
|
+
/** The cursor of the last page received, where a resumed subscription starts. */
|
|
72
|
+
#received: string | undefined;
|
|
73
|
+
/** The cursor of the last page handed out; handled once the next page is asked for. */
|
|
74
|
+
#handedOut: string | undefined;
|
|
75
|
+
/** The cursor of the last page the consumer handled, and of the last acknowledged. */
|
|
76
|
+
#handled: string | undefined;
|
|
77
|
+
#acked: string | undefined;
|
|
78
|
+
#paused = false;
|
|
79
|
+
/**
|
|
80
|
+
* Pages that arrived while a subscribe was in flight, for a subscription not known yet: the
|
|
81
|
+
* answer and its first page can reach the session in one chunk, and the page is dispatched
|
|
82
|
+
* before the code awaiting the answer resumes. Kept until the answer names its ID.
|
|
83
|
+
*/
|
|
84
|
+
#early: EventsNotifyParams[] | undefined;
|
|
85
|
+
/** The unsubscribe of a pause; its answer means no page of that subscription follows. */
|
|
86
|
+
#pausing: Promise<void> | undefined;
|
|
87
|
+
/** A subscribe whose answer is on its way (`#subscribe`). */
|
|
88
|
+
#subscribing: Promise<void> | undefined;
|
|
89
|
+
#started = false;
|
|
90
|
+
#ended: { error?: unknown } | undefined;
|
|
91
|
+
#wake: (() => void) | undefined;
|
|
92
|
+
readonly #removeHandler: () => void;
|
|
93
|
+
|
|
94
|
+
constructor(session: EventSession, options: EventStreamOptions) {
|
|
95
|
+
this.#session = session;
|
|
96
|
+
this.#options = options;
|
|
97
|
+
this.#high = Math.max(1, options.highWaterMark ?? 16);
|
|
98
|
+
this.#removeHandler = session.onNotification((params) => this.#receive(params));
|
|
99
|
+
void session.closed.then(() =>
|
|
100
|
+
this.#end(
|
|
101
|
+
new RpcError(
|
|
102
|
+
createDiagnostic("MOSTRA_HOST_CONNECTION_CLOSED", {
|
|
103
|
+
message: "The Mostra instance exited while its events were being read.",
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
options.signal?.addEventListener("abort", () => void this.return(), { once: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#receive(params: unknown): void {
|
|
112
|
+
const parsed = EventsNotifyParams.safeParse(params);
|
|
113
|
+
if (!parsed.success) return;
|
|
114
|
+
if (parsed.data.subscriptionId !== this.#subscription) {
|
|
115
|
+
this.#early?.push(parsed.data);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.#accept(parsed.data);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#accept({ events, cursor, compacted }: EventsNotifyParams): void {
|
|
122
|
+
this.#queue.push({ events, cursor, ...(compacted ? { compacted } : {}) });
|
|
123
|
+
this.#received = cursor;
|
|
124
|
+
if (this.#queue.length >= this.#high && !this.#paused) this.#pausing = this.#pause();
|
|
125
|
+
this.#wake?.();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Subscribes after `after`. While the answer is on its way the call is `#subscribing`, so
|
|
130
|
+
* `return()` can wait for it and end the subscription it names (GPT review finding 5).
|
|
131
|
+
*/
|
|
132
|
+
#subscribe(after: string | undefined): Promise<void> {
|
|
133
|
+
const opening = this.#open(after);
|
|
134
|
+
this.#subscribing = opening;
|
|
135
|
+
return opening.finally(() => {
|
|
136
|
+
if (this.#subscribing === opening) this.#subscribing = undefined;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async #open(after: string | undefined): Promise<void> {
|
|
141
|
+
this.#early = [];
|
|
142
|
+
let answer: { subscriptionId: string; cursor: string };
|
|
143
|
+
let early: EventsNotifyParams[];
|
|
144
|
+
try {
|
|
145
|
+
answer = await this.#session.call(eventsSubscribeMethod, {
|
|
146
|
+
...(this.#options.filter ? { filter: this.#options.filter } : {}),
|
|
147
|
+
...(after !== undefined ? { after } : {}),
|
|
148
|
+
...(this.#options.name ? { name: this.#options.name } : {}),
|
|
149
|
+
});
|
|
150
|
+
} finally {
|
|
151
|
+
early = this.#early;
|
|
152
|
+
this.#early = undefined;
|
|
153
|
+
}
|
|
154
|
+
// Synchronous from here: no page can be dispatched before the ID is known.
|
|
155
|
+
this.#subscription = answer.subscriptionId;
|
|
156
|
+
this.#received ??= answer.cursor;
|
|
157
|
+
// The pages of this subscription that came before its answer was handled, in order.
|
|
158
|
+
for (const page of early) if (page.subscriptionId === answer.subscriptionId) this.#accept(page);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async #pause(): Promise<void> {
|
|
162
|
+
this.#paused = true;
|
|
163
|
+
const id = this.#subscription;
|
|
164
|
+
if (id === undefined) return;
|
|
165
|
+
// What the consumer handled is acknowledged while the subscription still exists: the ack goes
|
|
166
|
+
// out before the unsubscribe on the connection, which keeps their order.
|
|
167
|
+
const acked = this.#sendAck(id);
|
|
168
|
+
// Pages already on their way still arrive under this ID and are kept. The answer comes after
|
|
169
|
+
// them on the connection, so once it is here the subscription is over.
|
|
170
|
+
await this.#session
|
|
171
|
+
.call(eventsUnsubscribeMethod, { subscriptionId: id })
|
|
172
|
+
.catch(() => undefined);
|
|
173
|
+
// A pause runs from a page's arrival, where nobody awaits it, so it never rejects (Opus review
|
|
174
|
+
// finding 7): an ack the instance refused (or a connection gone) ends the stream with that
|
|
175
|
+
// error, which the consumer's next() then throws.
|
|
176
|
+
await acked.catch((error: unknown) => this.#end(error));
|
|
177
|
+
if (this.#subscription === id) this.#subscription = undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async #resume(): Promise<void> {
|
|
181
|
+
await this.#pausing;
|
|
182
|
+
this.#pausing = undefined;
|
|
183
|
+
this.#paused = false;
|
|
184
|
+
await this.#subscribe(this.#received);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Acknowledges what the consumer handled under subscription `id`, if a named stream has handled
|
|
189
|
+
* more than it acknowledged. The line is written when this is called (the session writes each
|
|
190
|
+
* call at once), so it is ordered before anything sent after it.
|
|
191
|
+
*/
|
|
192
|
+
#sendAck(id: string): Promise<void> {
|
|
193
|
+
const cursor = this.#handled;
|
|
194
|
+
if (!this.#options.name || cursor === undefined || cursor === this.#acked)
|
|
195
|
+
return Promise.resolve();
|
|
196
|
+
this.#acked = cursor;
|
|
197
|
+
return this.#session
|
|
198
|
+
.call(eventsAckMethod, { subscriptionId: id, cursor })
|
|
199
|
+
.then(() => undefined);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#end(error?: unknown): void {
|
|
203
|
+
if (this.#ended) return;
|
|
204
|
+
this.#ended = error === undefined ? {} : { error };
|
|
205
|
+
this.#removeHandler();
|
|
206
|
+
this.#wake?.();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async next(): Promise<IteratorResult<EventPage>> {
|
|
210
|
+
if (!this.#started) {
|
|
211
|
+
this.#started = true;
|
|
212
|
+
await this.#subscribe(this.#options.after);
|
|
213
|
+
}
|
|
214
|
+
// Asking for the next page means the consumer handled the one before it.
|
|
215
|
+
if (this.#handedOut !== undefined) this.#handled = this.#handedOut;
|
|
216
|
+
if (this.#paused && !this.#ended && this.#queue.length <= this.#high / 2) await this.#resume();
|
|
217
|
+
// Never under a subscription being paused: its unsubscribe may already be on its way, and
|
|
218
|
+
// the pause acknowledges what was handled before it.
|
|
219
|
+
if (!this.#paused && this.#subscription !== undefined) await this.#sendAck(this.#subscription);
|
|
220
|
+
for (;;) {
|
|
221
|
+
const page = this.#queue.shift();
|
|
222
|
+
if (page !== undefined) {
|
|
223
|
+
this.#handedOut = page.cursor;
|
|
224
|
+
return { done: false, value: page };
|
|
225
|
+
}
|
|
226
|
+
if (this.#ended) {
|
|
227
|
+
if (this.#ended.error !== undefined) throw this.#ended.error;
|
|
228
|
+
return { done: true, value: undefined };
|
|
229
|
+
}
|
|
230
|
+
if (this.#paused) {
|
|
231
|
+
await this.#resume();
|
|
232
|
+
await this.#sendAck(this.#subscription as string);
|
|
233
|
+
}
|
|
234
|
+
await new Promise<void>((done) => {
|
|
235
|
+
this.#wake = done;
|
|
236
|
+
});
|
|
237
|
+
this.#wake = undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Ends the stream: the subscription ends, and a named stream acknowledges every page the
|
|
243
|
+
* consumer handled, never the last one handed out (the loop may have stopped inside it).
|
|
244
|
+
*/
|
|
245
|
+
async return(): Promise<IteratorResult<EventPage>> {
|
|
246
|
+
this.#end();
|
|
247
|
+
this.#queue.length = 0;
|
|
248
|
+
await this.#pausing;
|
|
249
|
+
// A subscribe on its way names a subscription that exists on the instance once it is
|
|
250
|
+
// answered: wait for it, so it is ended below like any other.
|
|
251
|
+
await this.#subscribing?.catch(() => undefined);
|
|
252
|
+
const id = this.#subscription;
|
|
253
|
+
if (id !== undefined) {
|
|
254
|
+
this.#subscription = undefined;
|
|
255
|
+
const acked = this.#sendAck(id);
|
|
256
|
+
await this.#session
|
|
257
|
+
.call(eventsUnsubscribeMethod, { subscriptionId: id })
|
|
258
|
+
.catch(() => undefined);
|
|
259
|
+
await acked.catch(() => undefined);
|
|
260
|
+
} else if (this.#options.name && this.#handled !== undefined && this.#handled !== this.#acked) {
|
|
261
|
+
// Paused when it ended, with pages handled since: a subscription of the name, just long
|
|
262
|
+
// enough to acknowledge them (its pages are ignored; the handler is gone).
|
|
263
|
+
await this.#subscribe(this.#received).catch(() => undefined);
|
|
264
|
+
const again = this.#subscription;
|
|
265
|
+
if (again !== undefined) {
|
|
266
|
+
this.#subscription = undefined;
|
|
267
|
+
const acked = this.#sendAck(again);
|
|
268
|
+
await this.#session
|
|
269
|
+
.call(eventsUnsubscribeMethod, { subscriptionId: again })
|
|
270
|
+
.catch(() => undefined);
|
|
271
|
+
await acked.catch(() => undefined);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { done: true, value: undefined };
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@mostra/sdk`: Mostra for JavaScript hosts (HO-04). `createMostra({ home, ports })` spawns the
|
|
3
|
+
* host's own instance on a verified runtime bundle and returns a handle for the calls a host
|
|
4
|
+
* makes: publish, verify, wait, results, state, live values, links and events, plus any other
|
|
5
|
+
* host protocol method through `call`.
|
|
6
|
+
*
|
|
7
|
+
* The bundle functions (`resolveRuntime`, `setupBundle` and the rest) are what the CLI and
|
|
8
|
+
* `create-mostra` use to install and check the runtime; a host rarely needs them directly.
|
|
9
|
+
*
|
|
10
|
+
* Embedded use never sends telemetry (D119): this package contains no code that could.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
bundleDir,
|
|
17
|
+
cacheRoot,
|
|
18
|
+
type InstallOptions,
|
|
19
|
+
type InstallProgress,
|
|
20
|
+
installPinned,
|
|
21
|
+
} from "./bundle/cache.ts";
|
|
22
|
+
export { type DownloadProgress, redactUrl } from "./bundle/download.ts";
|
|
23
|
+
export {
|
|
24
|
+
type Bundle,
|
|
25
|
+
browsersDir,
|
|
26
|
+
checkBrowser,
|
|
27
|
+
checkFiles,
|
|
28
|
+
type Environment,
|
|
29
|
+
offline,
|
|
30
|
+
readBundle,
|
|
31
|
+
readSetup,
|
|
32
|
+
type SetupOptions,
|
|
33
|
+
type SetupStep,
|
|
34
|
+
setupBundle,
|
|
35
|
+
setupExtras,
|
|
36
|
+
} from "./bundle/setup.ts";
|
|
37
|
+
export {
|
|
38
|
+
checkTree,
|
|
39
|
+
describeTree,
|
|
40
|
+
hashFile,
|
|
41
|
+
linkStaysInside,
|
|
42
|
+
safeRelative,
|
|
43
|
+
type TreeProblems,
|
|
44
|
+
} from "./bundle/tree.ts";
|
|
45
|
+
export type { EventPage, EventStreamOptions } from "./events.ts";
|
|
46
|
+
export {
|
|
47
|
+
type ArtifactInput,
|
|
48
|
+
artifactRef,
|
|
49
|
+
type CallOptions,
|
|
50
|
+
createMostra,
|
|
51
|
+
type HomeSettings,
|
|
52
|
+
type InstanceExit,
|
|
53
|
+
type Mostra,
|
|
54
|
+
type MostraOptions,
|
|
55
|
+
type Ports,
|
|
56
|
+
SDK_CLIENT_NAME,
|
|
57
|
+
} from "./mostra.ts";
|
|
58
|
+
export { bundlePlatform, currentPlatform, type PlatformFacts } from "./platform.ts";
|
|
59
|
+
export {
|
|
60
|
+
enclosingBundle,
|
|
61
|
+
type Runtime,
|
|
62
|
+
type RuntimeOptions,
|
|
63
|
+
readPins,
|
|
64
|
+
resolveRuntime,
|
|
65
|
+
runtimeOf,
|
|
66
|
+
} from "./runtime.ts";
|
|
67
|
+
export { sdkVersion } from "./version.ts";
|