@tokamakdev/plugin 0.1.0-beta.116 → 0.1.0-beta.118

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/README.md CHANGED
@@ -115,8 +115,8 @@ host.emit("ring", event: ["id": alarm.id], timeout: 10) { result in
115
115
 
116
116
  ```kotlin
117
117
  // Blocks, so call it off the main thread.
118
- val reply = host.emit("ring", JSONObject().put("id", alarm.id).toString(), 10_000)
119
- // `reply` is the reply's JSON, "null" when no listener replies.
118
+ val reply = host.emit("ring", JSONObject().put("id", alarm.id), 10_000)
119
+ // `reply` is the reply, parsed from JSON, or null when no listener replies.
120
120
  ```
121
121
 
122
122
  `timeout` includes starting the runtime when the app is not running. `emit`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamakdev/plugin",
3
- "version": "0.1.0-beta.116",
3
+ "version": "0.1.0-beta.118",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/mantty/tokamak.git",
package/src/bindings.ts CHANGED
@@ -74,21 +74,11 @@ export class Bindings {
74
74
  /** The elements of the native sequence `member` of `target` returns. Cancelling the stream ends it. */
75
75
  stream<T>(target: TokamakContext | NativeObject, member: string, args: readonly unknown[]): ReadableStream<T> {
76
76
  const ctx = contextOf(target);
77
- const elements = byteStream((next, error, close) => {
78
- let stop: (() => void) | undefined;
79
- let stopped = false;
80
- request(ctx, target, args).then((payload) => {
81
- if (stopped) return;
82
- stop = ctx[nativeConnection].listen(this.#plugin, member, payload, {
83
- next: (value, done) => { if (done) close(); else next(value); },
84
- error,
85
- });
86
- }, error);
87
- return () => {
88
- stopped = true;
89
- stop?.();
90
- };
91
- });
77
+ const elements = byteStream(async (next, error, close) =>
78
+ ctx[nativeConnection].listen(this.#plugin, member, await request(ctx, target, args), {
79
+ next: (value, done) => { if (done) close(); else next(value); },
80
+ error,
81
+ }));
92
82
  return elements.pipeThrough(new TransformStream<Uint8Array, T>({
93
83
  transform: (element, controller) => { controller.enqueue(this.#decode(ctx, element) as T); },
94
84
  }));
@@ -110,14 +100,10 @@ export class Bindings {
110
100
  const reference = await NativeReference.acquire(ctx, this.#plugin, protocol, payload, (value) => {
111
101
  const { method, arguments: args } = decodeJson<{ method: string; arguments: Wire[] }>(value);
112
102
  const classes = this.#classes();
113
- try {
114
- const handler = Reflect.get(handlers, method) as (...values: unknown[]) => unknown;
115
- Promise.resolve(handler.apply(handlers, args.map((arg) => fromWire(arg, ctx, classes)))).catch((error: unknown) => {
116
- reportError(error);
117
- });
118
- } catch (error) {
119
- reportError(error);
120
- }
103
+ const handler = Reflect.get(handlers, method) as (...values: unknown[]) => unknown;
104
+ // The constructor runs the handler now, and rejects with what it throws.
105
+ new Promise((resolve) => { resolve(handler.apply(handlers, args.map((arg) => fromWire(arg, ctx, classes)))); })
106
+ .catch((error: unknown) => { reportError(error); });
121
107
  });
122
108
  return new Protocol(reference);
123
109
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { capabilitiesOf, nativeConnection, type CallOptions, type NativeConnection, type Overflow, type TokamakContext } from "./context.js";
2
- import { byteStream, type JsonStream } from "./streams.js";
1
+ import { capabilitiesOf, nativeConnection, type CallOptions, type Overflow, type TokamakContext } from "./context.js";
2
+ import { byteStream, jsonLine, type JsonStream } from "./streams.js";
3
3
  import { decodeJson, encodeJson } from "./codec.js";
4
4
  import { NativeReference } from "./resources.js";
5
5
 
@@ -24,12 +24,8 @@ export async function lifecycleStage(ctx: TokamakContext): Promise<LifecycleStag
24
24
 
25
25
  /** Native endpoint calls owned by the receiving invocation. */
26
26
  export abstract class Plugin {
27
- private readonly connection: NativeConnection;
28
-
29
- protected constructor(private readonly ctx: TokamakContext, private readonly pluginId: string) {
30
- const connection = Reflect.get(ctx, nativeConnection) as NativeConnection | undefined;
31
- if (!connection) throw new TypeError("A Tokamak invocation context is required");
32
- this.connection = connection;
27
+ protected constructor(protected readonly ctx: TokamakContext, private readonly pluginId: string) {
28
+ if (!Reflect.has(ctx, nativeConnection)) throw new TypeError("A Tokamak invocation context is required");
33
29
  }
34
30
 
35
31
  protected async call<T>(method: string, value: unknown = null, options?: CallOptions): Promise<T> {
@@ -45,7 +41,7 @@ export abstract class Plugin {
45
41
  }
46
42
 
47
43
  protected callBytes(method: string, payload: Uint8Array, options?: CallOptions): Promise<Uint8Array> {
48
- return this.connection.call(this.pluginId, method, payload, options);
44
+ return this.ctx[nativeConnection].call(this.pluginId, method, payload, options);
49
45
  }
50
46
 
51
47
  /** Retains the native object that the subscription `method` holds open, without copying it. */
@@ -63,12 +59,9 @@ export abstract class Plugin {
63
59
  * happens to values a slow reader has not taken: ending the stream, or dropping the oldest.
64
60
  */
65
61
  protected stream<T>(method: string, value: unknown = null, overflow: Overflow = "error"): JsonStream<T> {
66
- return byteStream((next, error, close) => this.connection.listen(this.pluginId, method, encodeJson(value), {
62
+ return byteStream((next, error, close) => this.ctx[nativeConnection].listen(this.pluginId, method, encodeJson(value), {
67
63
  next(payload, done) {
68
- const line = new Uint8Array(payload.byteLength + 1);
69
- line.set(payload);
70
- line[payload.byteLength] = 10;
71
- next(line);
64
+ next(jsonLine(payload));
72
65
  if (done) close();
73
66
  },
74
67
  error,
package/src/streams.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { encodeJson } from "./codec.js";
2
2
  import type { Overflow } from "./context.js";
3
3
 
4
- type Source<T> = (next: (value: T) => void, error: (error: unknown) => void, close: () => void) => () => void;
4
+ /** Starts a push source, returning or resolving to the function that stops it. */
5
+ type Source<T> = (next: (value: T) => void, error: (error: unknown) => void, close: () => void) => (() => void) | Promise<() => void>;
5
6
 
6
7
  /** A byte stream of newline-delimited JSON values of type `T`, which `decodeValues` reads. */
7
8
  export type JsonStream<T> = ReadableStream<Uint8Array> & {
@@ -43,9 +44,9 @@ export function byteStream(subscribe: Source<Uint8Array>, overflow: Overflow = "
43
44
  wake();
44
45
  };
45
46
  return new ReadableStream<Uint8Array>({
46
- start() {
47
+ async start() {
47
48
  try {
48
- stop = subscribe(push, finish, () => { finish(); });
49
+ stop = await subscribe(push, finish, () => { finish(); });
49
50
  if (end) stop();
50
51
  } catch (error) { finish(error); }
51
52
  },
@@ -66,15 +67,19 @@ export function byteStream(subscribe: Source<Uint8Array>, overflow: Overflow = "
66
67
  export function valueStream<T>(subscribe: Source<T>, overflow: Overflow = "error"): JsonStream<T> {
67
68
  return byteStream((next, error, close) => subscribe((value) => {
68
69
  try {
69
- const payload = encodeJson(value);
70
- const line = new Uint8Array(payload.byteLength + 1);
71
- line.set(payload);
72
- line[payload.byteLength] = 10;
73
- next(line);
70
+ next(jsonLine(encodeJson(value)));
74
71
  } catch (failure) { error(failure); }
75
72
  }, error, close), overflow);
76
73
  }
77
74
 
75
+ /** `payload`, one encoded JSON value, as a line of a newline-delimited stream. */
76
+ export function jsonLine(payload: Uint8Array): Uint8Array {
77
+ const line = new Uint8Array(payload.byteLength + 1);
78
+ line.set(payload);
79
+ line[payload.byteLength] = 10;
80
+ return line;
81
+ }
82
+
78
83
  /** Decodes JSON values; ending iteration cancels the source. */
79
84
  export async function* decodeValues<T>(source: JsonStream<T> | Promise<JsonStream<T>>, signal: AbortSignal = new AbortController().signal): AsyncGenerator<T, void, unknown> {
80
85
  const reader = (await source).getReader();
package/src/testing.ts CHANGED
@@ -96,9 +96,7 @@ export class FakeNativeTransport {
96
96
 
97
97
  /** Replies to the pending request `id`. A reply is the last unless `done` is false. */
98
98
  deliver(id: number, { value, error, done = true }: Answer): void {
99
- const reply = this.pending.get(id);
100
- if (done) this.pending.delete(id);
101
- reply?.(encodeJson(value), error && nativeError(error), done);
99
+ this.reply(id, encodeJson(value), error && nativeError(error), done);
102
100
  }
103
101
 
104
102
  private cancel(id: number): void {
@@ -111,15 +109,24 @@ export class FakeNativeTransport {
111
109
  }
112
110
 
113
111
  respond(reply: { value?: unknown; error?: NativeFailure }, done = true): void {
114
- this.respondBytes(encodeJson(reply.value), done, reply.error && nativeError(reply.error));
112
+ this.deliver(this.last(), { ...reply, done });
115
113
  }
116
114
 
117
115
  respondBytes(payload: Uint8Array, done = true, error?: DOMException): void {
116
+ this.reply(this.last(), payload.slice(), error, done);
117
+ }
118
+
119
+ /** The ID of the last request sent. */
120
+ private last(): number {
118
121
  const id = this.sent.at(-1)?.id;
119
122
  if (id === undefined) throw new Error("No native request is pending");
123
+ return id;
124
+ }
125
+
126
+ private reply(id: number, payload: Uint8Array, error: DOMException | undefined, done: boolean): void {
120
127
  const reply = this.pending.get(id);
121
128
  if (done) this.pending.delete(id);
122
- reply?.(payload.slice(), error, done);
129
+ reply?.(payload, error, done);
123
130
  }
124
131
  }
125
132