@tokamakdev/plugin 0.1.0-beta.126 → 0.1.0-beta.128
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 +3 -2
- package/package.json +1 -2
- package/src/index.ts +14 -10
- package/src/resources.ts +3 -27
- package/src/bindings.ts +0 -246
package/README.md
CHANGED
|
@@ -24,8 +24,9 @@ export function alarms(ctx: TokamakContext) { return new Alarms(ctx); }
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
`call` uses the standard JSON endpoint codec. `callBytes` sends and receives
|
|
27
|
-
opaque bytes. `stream` exposes JSON values as a cancellable byte stream
|
|
28
|
-
`decodeValues<T>` consumes it.
|
|
27
|
+
opaque bytes. `stream` exposes JSON values as a cancellable byte stream,
|
|
28
|
+
subscribing once a promised argument resolves; `decodeValues<T>` consumes it.
|
|
29
|
+
Errors, including that promise's rejection, terminate a stream. A slow consumer
|
|
29
30
|
exceeding its bounded buffer receives `QuotaExceededError`.
|
|
30
31
|
|
|
31
32
|
Declare the factory in `tokamak-plugin.json` alongside native build metadata:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokamakdev/plugin",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.128",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/mantty/tokamak.git",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.ts",
|
|
12
12
|
"./streams": "./src/streams.ts",
|
|
13
|
-
"./bindings": "./src/bindings.ts",
|
|
14
13
|
"./events": "./src/events.ts",
|
|
15
14
|
"./worker": "./src/worker.ts",
|
|
16
15
|
"./testing": "./src/testing.ts",
|
package/src/index.ts
CHANGED
|
@@ -55,17 +55,21 @@ export abstract class Plugin {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* A stream of newline-delimited JSON values, consumed with decodeValues. `
|
|
59
|
-
*
|
|
58
|
+
* A stream of newline-delimited JSON values, consumed with decodeValues. A promised `value`
|
|
59
|
+
* subscribes once it resolves, and its rejection ends the stream. `overflow` says what happens
|
|
60
|
+
* to values a slow reader has not taken: ending the stream, or dropping the oldest.
|
|
60
61
|
*/
|
|
61
62
|
protected stream<T>(method: string, value: unknown = null, overflow: Overflow = "error"): JsonStream<T> {
|
|
62
|
-
return byteStream((next, error, close) =>
|
|
63
|
-
|
|
64
|
-
next(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
return byteStream((next, error, close) => {
|
|
64
|
+
const listen = (argument: unknown) => this.ctx[nativeConnection].listen(this.pluginId, method, encodeJson(argument), {
|
|
65
|
+
next(payload, done) {
|
|
66
|
+
next(jsonLine(payload));
|
|
67
|
+
if (done) close();
|
|
68
|
+
},
|
|
69
|
+
error,
|
|
70
|
+
overflow,
|
|
71
|
+
});
|
|
72
|
+
return value instanceof Promise ? value.then(listen) : listen(value);
|
|
73
|
+
}, overflow);
|
|
70
74
|
}
|
|
71
75
|
}
|
package/src/resources.ts
CHANGED
|
@@ -6,9 +6,6 @@ const collected = typeof FinalizationRegistry === "function"
|
|
|
6
6
|
? new FinalizationRegistry<() => void>((release) => { release(); })
|
|
7
7
|
: undefined;
|
|
8
8
|
|
|
9
|
-
/** The invocation that owns `reference`. */
|
|
10
|
-
export let ownerOf: (reference: NativeReference) => TokamakContext;
|
|
11
|
-
|
|
12
9
|
/**
|
|
13
10
|
* A native object that stays in native memory. The receiving invocation owns it until `close`,
|
|
14
11
|
* the invocation's end, or garbage collection where the runtime has `FinalizationRegistry`.
|
|
@@ -18,10 +15,6 @@ export class NativeReference {
|
|
|
18
15
|
readonly #id: string;
|
|
19
16
|
#release: (() => void) | undefined;
|
|
20
17
|
|
|
21
|
-
static {
|
|
22
|
-
ownerOf = (reference) => reference.#ctx;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
18
|
private constructor(ctx: TokamakContext, id: string, release: () => void) {
|
|
26
19
|
this.#ctx = ctx;
|
|
27
20
|
this.#id = id;
|
|
@@ -31,15 +24,9 @@ export class NativeReference {
|
|
|
31
24
|
|
|
32
25
|
/**
|
|
33
26
|
* Retains the object that the native subscription `method` of `plugin` holds open: its first
|
|
34
|
-
* value is the object's ID
|
|
27
|
+
* value is the object's ID.
|
|
35
28
|
*/
|
|
36
|
-
static async acquire(
|
|
37
|
-
ctx: TokamakContext,
|
|
38
|
-
plugin: string,
|
|
39
|
-
method: string,
|
|
40
|
-
payload: Uint8Array,
|
|
41
|
-
next: (value: Uint8Array) => void = () => {},
|
|
42
|
-
): Promise<NativeReference> {
|
|
29
|
+
static async acquire(ctx: TokamakContext, plugin: string, method: string, payload: Uint8Array): Promise<NativeReference> {
|
|
43
30
|
let release = () => {};
|
|
44
31
|
try {
|
|
45
32
|
// The subscription's listeners never hold the reference, so garbage collection can release it.
|
|
@@ -47,10 +34,7 @@ export class NativeReference {
|
|
|
47
34
|
let settled = false;
|
|
48
35
|
release = ctx[nativeConnection].listen(plugin, method, payload, {
|
|
49
36
|
next(value, done) {
|
|
50
|
-
if (settled)
|
|
51
|
-
if (!done) next(value);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
37
|
+
if (settled) return;
|
|
54
38
|
settled = true;
|
|
55
39
|
if (done) reject(new DOMException("The native object was not retained", "InvalidStateError"));
|
|
56
40
|
else resolve(value);
|
|
@@ -70,14 +54,6 @@ export class NativeReference {
|
|
|
70
54
|
}
|
|
71
55
|
}
|
|
72
56
|
|
|
73
|
-
/** The object retained as `id` in the scope of the invocation `ctx`, which ends with it. */
|
|
74
|
-
static scoped(ctx: TokamakContext, id: string): NativeReference {
|
|
75
|
-
return new NativeReference(ctx, id, () => {
|
|
76
|
-
// A failed release changes nothing: the invocation's end has released the object.
|
|
77
|
-
ctx[nativeConnection].call("tokamak", "resource.release", encodeJson(id)).catch(() => undefined);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
57
|
/** Copies the current contents of a native byte buffer. Other native objects reject with `TypeError`. */
|
|
82
58
|
async bytes(): Promise<Uint8Array> {
|
|
83
59
|
return this.#ctx[nativeConnection].call("tokamak", "resource.read", encodeJson(this.idFor(this.#ctx)));
|
package/src/bindings.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { nativeConnection, type TokamakContext } from "./context.js";
|
|
2
|
-
import { decodeJson, encodeJson } from "./codec.js";
|
|
3
|
-
import { nativeError, type NativeFailure } from "./native-protocol.mjs";
|
|
4
|
-
import { NativeReference, ownerOf } from "./resources.js";
|
|
5
|
-
import { byteStream } from "./streams.js";
|
|
6
|
-
|
|
7
|
-
export type { TokamakContext } from "./context.js";
|
|
8
|
-
export { NativeReference } from "./resources.js";
|
|
9
|
-
|
|
10
|
-
/** The reference `object` views: pass it to another class's constructor or to a plugin's native endpoint. */
|
|
11
|
-
export let referenceOf: (object: NativeObject) => NativeReference;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A native object that a generated binding returned. Its class mirrors the native class; every
|
|
15
|
-
* member is asynchronous. The invocation that received it owns it until `release`, the
|
|
16
|
-
* invocation's end, or garbage collection.
|
|
17
|
-
*/
|
|
18
|
-
export class NativeObject {
|
|
19
|
-
readonly #reference: NativeReference;
|
|
20
|
-
|
|
21
|
-
static {
|
|
22
|
-
referenceOf = (object) => object.#reference;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Views the object `reference` holds as this class. The native side checks the type at each call. */
|
|
26
|
-
constructor(reference: NativeReference) {
|
|
27
|
-
this.#reference = reference;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Releases `object` for every view of it. Later calls on it reject with `InvalidStateError`. */
|
|
32
|
-
export function release(object: NativeObject): void {
|
|
33
|
-
referenceOf(object).close();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type NativeClass = new (reference: NativeReference) => NativeObject;
|
|
37
|
-
|
|
38
|
-
/** The classes of a binding module, by native name. */
|
|
39
|
-
export type Classes = Readonly<Record<string, NativeClass>>;
|
|
40
|
-
|
|
41
|
-
/** The calls of one generated binding module, whose native plugin is `plugin`. */
|
|
42
|
-
export class Bindings {
|
|
43
|
-
readonly #plugin: string;
|
|
44
|
-
readonly #classes: () => Classes;
|
|
45
|
-
|
|
46
|
-
/** `classes` returns the module's classes once they are defined. */
|
|
47
|
-
constructor(plugin: string, classes: () => Classes) {
|
|
48
|
-
this.#plugin = plugin;
|
|
49
|
-
this.#classes = classes;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Calls `member` of `target`, the object or, for a static member, the invocation's context.
|
|
54
|
-
* Aborting `signal` cancels an asynchronous member's task.
|
|
55
|
-
*/
|
|
56
|
-
async call<T>(target: TokamakContext | NativeObject, member: string, args: readonly unknown[], signal?: AbortSignal): Promise<T> {
|
|
57
|
-
const ctx = contextOf(target);
|
|
58
|
-
const reply = await ctx[nativeConnection].call(this.#plugin, member, await request(ctx, target, args), { signal });
|
|
59
|
-
return this.#decode(ctx, reply) as T;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The key of the first of `overloads` whose parameters take `args`: each parameter lists the
|
|
64
|
-
* kinds of value it takes, a class or a `typeof` name, `"array"`, `"bytes"`, `"date"`, `"object"`
|
|
65
|
-
* (a plain object), `"null"` or `"any"`.
|
|
66
|
-
*/
|
|
67
|
-
overload(args: readonly unknown[], overloads: readonly (readonly [string, readonly (readonly Kind[])[]])[]): string {
|
|
68
|
-
const found = overloads.find(([, params]) =>
|
|
69
|
-
params.length === args.length && params.every((kinds, index) => kinds.some((kind) => takes(kind, args[index]))));
|
|
70
|
-
if (!found) throw new TypeError("No overload takes these arguments");
|
|
71
|
-
return found[0];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The elements of the native sequence `member` of `target` returns. Cancelling the stream ends it. */
|
|
75
|
-
stream<T>(target: TokamakContext | NativeObject, member: string, args: readonly unknown[]): ReadableStream<T> {
|
|
76
|
-
const ctx = contextOf(target);
|
|
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
|
-
}));
|
|
82
|
-
return elements.pipeThrough(new TransformStream<Uint8Array, T>({
|
|
83
|
-
transform: (element, controller) => { controller.enqueue(this.#decode(ctx, element) as T); },
|
|
84
|
-
}));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* A native object implementing `protocol` whose `methods`, where `handlers` has them, call
|
|
89
|
-
* `handlers` in order. Its platform defaults remain for the methods `handlers` lacks.
|
|
90
|
-
*/
|
|
91
|
-
async implement<T extends NativeObject>(
|
|
92
|
-
ctx: TokamakContext,
|
|
93
|
-
Protocol: new (reference: NativeReference) => T,
|
|
94
|
-
protocol: string,
|
|
95
|
-
methods: readonly string[],
|
|
96
|
-
handlers: object,
|
|
97
|
-
): Promise<T> {
|
|
98
|
-
const handled = methods.filter((method) => typeof Reflect.get(handlers, method) === "function");
|
|
99
|
-
const payload = encodeJson({ scope: await scopeOf(ctx), methods: handled });
|
|
100
|
-
const reference = await NativeReference.acquire(ctx, this.#plugin, protocol, payload, (value) => {
|
|
101
|
-
const { method, arguments: args } = decodeJson<{ method: string; arguments: Wire[] }>(value);
|
|
102
|
-
const classes = this.#classes();
|
|
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); });
|
|
107
|
-
});
|
|
108
|
-
return new Protocol(reference);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
#decode(ctx: TokamakContext, payload: Uint8Array): unknown {
|
|
112
|
-
return fromWire(decodeJson<Wire>(payload), ctx, this.#classes());
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** A kind of value an overload's parameter takes. */
|
|
117
|
-
type Kind = string | (abstract new (reference: NativeReference) => NativeObject);
|
|
118
|
-
|
|
119
|
-
/** The kinds named by strings other than `typeof` results. */
|
|
120
|
-
const named = new Map<string, (value: unknown) => boolean>([
|
|
121
|
-
["any", () => true],
|
|
122
|
-
["null", (value) => value === null || value === undefined],
|
|
123
|
-
["array", (value) => Array.isArray(value)],
|
|
124
|
-
["bytes", (value) => value instanceof Uint8Array],
|
|
125
|
-
["date", (value) => value instanceof Date],
|
|
126
|
-
["object", (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof NativeObject)],
|
|
127
|
-
]);
|
|
128
|
-
|
|
129
|
-
function takes(kind: Kind, value: unknown): boolean {
|
|
130
|
-
if (typeof kind === "function") return value instanceof kind;
|
|
131
|
-
const takes = named.get(kind);
|
|
132
|
-
return takes ? takes(value) : typeof value === kind;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** The Android application `Context`. */
|
|
136
|
-
export function applicationContext(ctx: TokamakContext): Promise<NativeReference> {
|
|
137
|
-
return root(ctx, "context");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** The Android activity the app shows. Rejects with `NeedsUIError` while the app cannot show UI. */
|
|
141
|
-
export function foregroundActivity(ctx: TokamakContext): Promise<NativeReference> {
|
|
142
|
-
return root(ctx, "activity");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* The Apple object a native screen is presented from: the top `UIViewController` on iOS, the
|
|
147
|
-
* `NSWindow` on macOS. Rejects with `NeedsUIError` while the app cannot show UI.
|
|
148
|
-
*/
|
|
149
|
-
export function presenter(ctx: TokamakContext): Promise<NativeReference> {
|
|
150
|
-
return root(ctx, "presenter");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function root(ctx: TokamakContext, name: string): Promise<NativeReference> {
|
|
154
|
-
const payload = encodeJson({ scope: await scopeOf(ctx), name });
|
|
155
|
-
const id = decodeJson<string>(await ctx[nativeConnection].call("tokamak", "resource.root", payload));
|
|
156
|
-
return NativeReference.scoped(ctx, id);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const scopes = new WeakMap<TokamakContext, Promise<string>>();
|
|
160
|
-
|
|
161
|
-
/** The scope that keeps the objects the invocation `ctx` receives until it ends. */
|
|
162
|
-
function scopeOf(ctx: TokamakContext): Promise<string> {
|
|
163
|
-
let scope = scopes.get(ctx);
|
|
164
|
-
if (!scope) {
|
|
165
|
-
// The subscription stays open for the invocation, whose end releases the scope's objects.
|
|
166
|
-
scope = new Promise((resolve, reject) => {
|
|
167
|
-
ctx[nativeConnection].listen("tokamak", "resource.scope", encodeJson(null), {
|
|
168
|
-
next: (value) => { resolve(decodeJson<string>(value)); },
|
|
169
|
-
error: reject,
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
scopes.set(ctx, scope);
|
|
173
|
-
}
|
|
174
|
-
return scope;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function contextOf(target: TokamakContext | NativeObject): TokamakContext {
|
|
178
|
-
return target instanceof NativeObject ? ownerOf(referenceOf(target)) : target;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function request(ctx: TokamakContext, target: TokamakContext | NativeObject, args: readonly unknown[]): Promise<Uint8Array> {
|
|
182
|
-
return encodeJson({
|
|
183
|
-
scope: await scopeOf(ctx),
|
|
184
|
-
target: target instanceof NativeObject ? toWire(target, ctx) : undefined,
|
|
185
|
-
arguments: args.map((arg) => toWire(arg, ctx)),
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Binding values as JSON. Objects tagged `#` carry what JSON lacks: native objects, dates, bytes,
|
|
191
|
-
* errors, non-finite numbers, and records whose own keys include `#`.
|
|
192
|
-
*/
|
|
193
|
-
type Wire = null | boolean | number | string | Wire[] | { [key: string]: Wire };
|
|
194
|
-
|
|
195
|
-
function toWire(value: unknown, ctx: TokamakContext): Wire {
|
|
196
|
-
if (value === null || value === undefined) return null;
|
|
197
|
-
if (typeof value === "number" && !Number.isFinite(value)) return { "#": "number", value: String(value) };
|
|
198
|
-
if (typeof value !== "object") return value as Wire;
|
|
199
|
-
return objectToWire(value, ctx);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function objectToWire(value: object, ctx: TokamakContext): Wire {
|
|
203
|
-
const reference = value instanceof NativeObject ? referenceOf(value) : value;
|
|
204
|
-
if (reference instanceof NativeReference) return { "#": "object", id: reference.idFor(ctx) };
|
|
205
|
-
if (value instanceof Date) return { "#": "date", value: value.getTime() };
|
|
206
|
-
if (value instanceof Uint8Array) return { "#": "bytes", value: toBase64(value) };
|
|
207
|
-
if (Array.isArray(value)) return value.map((item) => toWire(item, ctx));
|
|
208
|
-
const fields = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, toWire(item, ctx)]));
|
|
209
|
-
return "#" in fields ? { "#": "record", value: fields } : fields;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
type Tagged = Record<string, Wire>;
|
|
213
|
-
|
|
214
|
-
/** The decoder of each tag. */
|
|
215
|
-
const tags = new Map<string, (value: Tagged, ctx: TokamakContext, classes: Classes) => unknown>([
|
|
216
|
-
["object", (value, ctx, classes) => {
|
|
217
|
-
const Class = (value.types as string[]).map((type) => classes[type]).find(Boolean) ?? NativeObject;
|
|
218
|
-
return new Class(NativeReference.scoped(ctx, value.id as string));
|
|
219
|
-
}],
|
|
220
|
-
["number", (value) => Number(value.value)],
|
|
221
|
-
["date", (value) => new Date(value.value as number)],
|
|
222
|
-
["bytes", (value) => fromBase64(value.value as string)],
|
|
223
|
-
["error", (value) => nativeError(value as unknown as NativeFailure)],
|
|
224
|
-
["record", (value, ctx, classes) => fields(value.value as Tagged, ctx, classes)],
|
|
225
|
-
]);
|
|
226
|
-
|
|
227
|
-
function fromWire(value: Wire, ctx: TokamakContext, classes: Classes): unknown {
|
|
228
|
-
if (Array.isArray(value)) return value.map((item) => fromWire(item, ctx, classes));
|
|
229
|
-
if (value === null || typeof value !== "object") return value;
|
|
230
|
-
const decode = typeof value["#"] === "string" ? tags.get(value["#"]) : undefined;
|
|
231
|
-
return decode ? decode(value, ctx, classes) : fields(value, ctx, classes);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function fields(value: Record<string, Wire>, ctx: TokamakContext, classes: Classes): Record<string, unknown> {
|
|
235
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, fromWire(item, ctx, classes)]));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function toBase64(bytes: Uint8Array): string {
|
|
239
|
-
let binary = "";
|
|
240
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
241
|
-
return btoa(binary);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function fromBase64(text: string): Uint8Array {
|
|
245
|
-
return Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
|
|
246
|
-
}
|