janela 0.8.0 → 0.10.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/README.md +23 -4
- package/bin/janela.mjs +266 -18
- package/package.json +4 -3
- package/runtime/ios.ts +483 -0
- package/runtime/janela.ts +88 -82
- package/shim/ios/app.mm +386 -0
- package/shim/wvshim.cc +166 -59
- package/templates/janela.conf.json +5 -0
- package/templates/react/files/janela.conf.json +5 -0
- package/templates/solid/files/janela.conf.json +5 -0
- package/templates/svelte/files/janela.conf.json +5 -0
- package/templates/vue/files/janela.conf.json +5 -0
package/runtime/ios.ts
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
// janela's iOS runtime lane.
|
|
2
|
+
//
|
|
3
|
+
// Same public surface as runtime/janela.ts — `app.command`, `app.emit`, the
|
|
4
|
+
// typed contract — so a project's src-host/main.ts compiles unchanged on both.
|
|
5
|
+
// The CLI picks a lane by copying one of these two files in as `./janela`.
|
|
6
|
+
//
|
|
7
|
+
// What differs is who is in charge. On desktop, TypeScript owns `main` and
|
|
8
|
+
// calls a blocking wvRun(); scriptc's event loop is parked for the app's life,
|
|
9
|
+
// so this runtime carries a ticker, a timer queue and a deferred-job pool to
|
|
10
|
+
// get work done anyway. On iOS none of that can exist:
|
|
11
|
+
//
|
|
12
|
+
// - scriptc builds iOS as a LIBRARY (it refuses executables for the target),
|
|
13
|
+
// and library mode requires an async-free module graph — SC4005 rejects a
|
|
14
|
+
// build whose graph reaches setTimeout, promises or threads. There is no
|
|
15
|
+
// event loop linked into the artifact at all.
|
|
16
|
+
// - UIKit owns the run loop and calls us. Each handleInvoke() runs to
|
|
17
|
+
// completion and returns, so nothing needs pumping.
|
|
18
|
+
//
|
|
19
|
+
// The result is much smaller: a command registry and a dispatch function.
|
|
20
|
+
// Everything that needed the loop is not available on iOS *yet* — it reports
|
|
21
|
+
// when called rather than failing silently, and every such path goes through
|
|
22
|
+
// one guard so that restoring parity is a single edit. See the stubs below.
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
AsyncCommandHandler,
|
|
26
|
+
CommandHandler,
|
|
27
|
+
CommandShapes,
|
|
28
|
+
CommandSpecs,
|
|
29
|
+
DialogFilter,
|
|
30
|
+
FsCallback,
|
|
31
|
+
Norm,
|
|
32
|
+
OpenDialogOptions,
|
|
33
|
+
SaveDialogOptions,
|
|
34
|
+
WindowConfig,
|
|
35
|
+
} from "./types";
|
|
36
|
+
|
|
37
|
+
// The host-callback channel declared in the generated library profile. The
|
|
38
|
+
// shell registers it before jl_init(); calling a channel the host never
|
|
39
|
+
// registered is a defined trap (SC4025), not undefined behaviour.
|
|
40
|
+
declare function janelaEmit(event: string, payloadJson: string): void;
|
|
41
|
+
declare function hostSchedule(id: number, ms: number): void;
|
|
42
|
+
declare function hostSettle(pendingId: number, envelopeJson: string): void;
|
|
43
|
+
declare function hostReadFile(jobId: number, path: string): void;
|
|
44
|
+
declare function hostWriteFile(jobId: number, path: string, data: string): void;
|
|
45
|
+
|
|
46
|
+
// One job table serves reads and writes; these sentinels say which callback of
|
|
47
|
+
// the pair is the real one. Identity comparison is the whole trick, so they
|
|
48
|
+
// must be module-level singletons rather than fresh closures.
|
|
49
|
+
const nullRead: FsCallback = (_e: string | null, _t: string) => {};
|
|
50
|
+
const nullWrite: (err: string | null) => void = (_e: string | null) => {};
|
|
51
|
+
|
|
52
|
+
function encode(value: unknown): string {
|
|
53
|
+
if (value === undefined) return "null";
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// The one place iOS says "not yet"
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
//
|
|
61
|
+
// Everything below funnels through `pending()`. Nothing else in this file
|
|
62
|
+
// decides what is or is not available, so the day a capability lands on iOS
|
|
63
|
+
// its stub becomes a real implementation and this comment shrinks.
|
|
64
|
+
//
|
|
65
|
+
// The scheduling family — commandAsync, defer, sleep — and file I/O now work
|
|
66
|
+
// exactly as they do on desktop: TS never holds a timer or a file handle, it
|
|
67
|
+
// parks work with the shell under an id and is re-entered when the shell has
|
|
68
|
+
// an answer. A library build still links no event loop (SC4005); that is why
|
|
69
|
+
// the design routes through the shell rather than a limitation of iOS.
|
|
70
|
+
//
|
|
71
|
+
// What remains behind pending(): the file dialogs (iOS wants a document
|
|
72
|
+
// picker, which is its own delegate lifecycle) and window control, which is
|
|
73
|
+
// permanently meaningless on a phone rather than unfinished.
|
|
74
|
+
//
|
|
75
|
+
// FAILING LOUDLY WITHOUT KILLING THE APP: an uncaught throw in library mode
|
|
76
|
+
// reaches the panic sink and then ABORTS the process (SC4013). So a stub must
|
|
77
|
+
// never throw from setup() — registering an async command at startup would
|
|
78
|
+
// kill the app before its first frame. Stubs that hand back a callback report
|
|
79
|
+
// through it; fire-and-forget stubs log; and commandAsync registers a command
|
|
80
|
+
// that throws only when the page calls it, where dispatch()'s try/catch turns
|
|
81
|
+
// it into a rejected promise.
|
|
82
|
+
|
|
83
|
+
/** The single message every not-yet-on-iOS path reports. */
|
|
84
|
+
function pending(api: string, why: string): string {
|
|
85
|
+
return (
|
|
86
|
+
"janela: app." + api + " is not available on iOS yet — " + why + ". " +
|
|
87
|
+
"Parity is planned; see docs/ios.md."
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The scheduling family's guard — commandAsync, defer and sleep.
|
|
93
|
+
*
|
|
94
|
+
* These three are absent for one shared reason, and will return for one
|
|
95
|
+
* shared reason. Replacing this function with a real implementation (timers
|
|
96
|
+
* scheduled by the shell, the library re-entered when they fire) is the whole
|
|
97
|
+
* of that change on this side.
|
|
98
|
+
*/
|
|
99
|
+
/**
|
|
100
|
+
* A running janela app on iOS, typed by the contract it serves.
|
|
101
|
+
*
|
|
102
|
+
* Deliberately the same class name as the desktop lane: the generated entry
|
|
103
|
+
* and a project's main.ts are compiled against whichever file the CLI copied
|
|
104
|
+
* in, so both must present the same type.
|
|
105
|
+
*/
|
|
106
|
+
export class JanelaAppImpl<
|
|
107
|
+
C extends CommandShapes = CommandShapes,
|
|
108
|
+
E = Record<string, unknown>,
|
|
109
|
+
> {
|
|
110
|
+
names: string[] = [];
|
|
111
|
+
handlers: CommandHandler[] = [];
|
|
112
|
+
html = "";
|
|
113
|
+
|
|
114
|
+
// ---- scheduling ----------------------------------------------------------
|
|
115
|
+
// Identical in shape to the desktop lane, and for a stronger reason: an iOS
|
|
116
|
+
// library links no event loop at all (SC4005), so TS could not hold a timer
|
|
117
|
+
// even if it wanted to. It parks a continuation under an id, asks the shell
|
|
118
|
+
// to schedule it, and the shell re-enters onTimer(id) on the main queue when
|
|
119
|
+
// it comes due. Nothing here polls.
|
|
120
|
+
contIds: number[] = [];
|
|
121
|
+
contFns: (() => void)[] = [];
|
|
122
|
+
nextCont = 1;
|
|
123
|
+
|
|
124
|
+
// An invoke whose answer is not ready when dispatch() returns. The shell
|
|
125
|
+
// holds the page's reply under this id and settles it when hostSettle()
|
|
126
|
+
// arrives. Mirrors the desktop shim's wv_defer/wv_resolve pair.
|
|
127
|
+
pendingIds: number[] = [];
|
|
128
|
+
nextPending = 1;
|
|
129
|
+
deferred = -1; // set by an async command during its own dispatch()
|
|
130
|
+
|
|
131
|
+
// In-flight file jobs: the shell does the blocking I/O on its own queue and
|
|
132
|
+
// re-enters onFsDone() with the result.
|
|
133
|
+
jobIds: number[] = [];
|
|
134
|
+
jobCbs: FsCallback[] = [];
|
|
135
|
+
jobWriteCbs: ((err: string | null) => void)[] = [];
|
|
136
|
+
nextJob = 1;
|
|
137
|
+
|
|
138
|
+
// Kept so the shared WindowConfig shape compiles; iOS has no window to size.
|
|
139
|
+
constructor(_cfg: WindowConfig) {}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Register a named command, callable from the page as janela.invoke(name, args).
|
|
143
|
+
*
|
|
144
|
+
* With a contract (`JanelaApp<App>`) the name must be one the contract
|
|
145
|
+
* declares, `args` is inferred from it, and the return value is checked
|
|
146
|
+
* against it. Without one, `args` is `unknown` and any name is accepted.
|
|
147
|
+
*/
|
|
148
|
+
command<K extends keyof C & string>(
|
|
149
|
+
name: K,
|
|
150
|
+
handler: (args: C[K]["args"]) => C[K]["result"],
|
|
151
|
+
): void {
|
|
152
|
+
this.names.push(name);
|
|
153
|
+
// The cast is on the VALUE, inside a contextually-typed closure: casting
|
|
154
|
+
// the function itself to another signature and calling through it fails
|
|
155
|
+
// at runtime.
|
|
156
|
+
this.handlers.push((args: unknown) => handler(args as C[K]["args"]));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Fire an event into the page; the payload is delivered as a value. Under a
|
|
161
|
+
* contract, the name must be declared and the payload must match its type.
|
|
162
|
+
*
|
|
163
|
+
* Reaches the page through the host-callback channel: the shell evaluates
|
|
164
|
+
* `window.__wvEmit(...)` on the main queue.
|
|
165
|
+
*/
|
|
166
|
+
emit<K extends keyof E & string>(event: K, payload: E[K]): void {
|
|
167
|
+
janelaEmit(event, encode(payload));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Answer one page invoke. Called by the generated entry, which the shell
|
|
172
|
+
* calls through the library's C ABI.
|
|
173
|
+
*
|
|
174
|
+
* A command that throws is caught here and reported as a rejection rather
|
|
175
|
+
* than taking the app down with it — on iOS an uncaught throw would abort
|
|
176
|
+
* the process.
|
|
177
|
+
*/
|
|
178
|
+
dispatch(cmd: string, argsJson: string): string {
|
|
179
|
+
try {
|
|
180
|
+
const args = JSON.parse(argsJson) as unknown;
|
|
181
|
+
for (let i = 0; i < this.names.length; i++) {
|
|
182
|
+
if (this.names[i] === cmd) {
|
|
183
|
+
this.deferred = -1;
|
|
184
|
+
const value = this.handlers[i](args);
|
|
185
|
+
// An async command parked its answer during the call above. Tell the
|
|
186
|
+
// shell to hold the page's reply under that id instead of settling
|
|
187
|
+
// now; hostSettle() answers it later.
|
|
188
|
+
if (this.deferred >= 0) {
|
|
189
|
+
const held = this.deferred;
|
|
190
|
+
this.deferred = -1;
|
|
191
|
+
return encode({ pending: held });
|
|
192
|
+
}
|
|
193
|
+
return encode({ ok: true, value });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return encode({ ok: false, error: "unknown command: " + cmd });
|
|
197
|
+
} catch (e) {
|
|
198
|
+
return encode({ ok: false, error: (e as Error).message });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The document the shell should load. Set by the generated entry. */
|
|
203
|
+
setHtml(html: string): void {
|
|
204
|
+
this.html = html;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
indexHtml(): string {
|
|
208
|
+
return this.html;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// -------------------------------------------------------------------------
|
|
212
|
+
// Not yet on iOS
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
// Present so that a project written for desktop still COMPILES for iOS —
|
|
215
|
+
// the typed contract and main.ts are shared source. Each reports clearly at
|
|
216
|
+
// the point of use instead of doing nothing quietly, and each routes through
|
|
217
|
+
// pending() above so there is one place to change.
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* An async command: answer later, without freezing the window.
|
|
221
|
+
*
|
|
222
|
+
* The handler runs during dispatch() but need not produce a value. It parks
|
|
223
|
+
* a pending id; dispatch() returns that id to the shell, which holds the
|
|
224
|
+
* page's promise until resolve/reject settles it. Same contract as desktop.
|
|
225
|
+
*/
|
|
226
|
+
commandAsync<K extends keyof C & string>(
|
|
227
|
+
name: K,
|
|
228
|
+
handler: (
|
|
229
|
+
args: C[K]["args"],
|
|
230
|
+
resolve: (value: C[K]["result"]) => void,
|
|
231
|
+
reject: (reason: unknown) => void,
|
|
232
|
+
) => void,
|
|
233
|
+
): void {
|
|
234
|
+
this.names.push(name);
|
|
235
|
+
this.handlers.push((args: unknown) => {
|
|
236
|
+
const id = this.nextPending;
|
|
237
|
+
this.nextPending = id + 1;
|
|
238
|
+
this.pendingIds.push(id);
|
|
239
|
+
// dispatch() reads this immediately after the handler returns.
|
|
240
|
+
this.deferred = id;
|
|
241
|
+
handler(
|
|
242
|
+
args as C[K]["args"],
|
|
243
|
+
(value: C[K]["result"]) => {
|
|
244
|
+
this.settle(id, encode({ ok: true, value: value }));
|
|
245
|
+
},
|
|
246
|
+
(reason: unknown) => {
|
|
247
|
+
this.settle(id, encode({ ok: false, error: String(reason) }));
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
return null;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Settle a held reply once, ignoring a second resolve/reject. */
|
|
255
|
+
settle(id: number, envelope: string): void {
|
|
256
|
+
for (let i = 0; i < this.pendingIds.length; i++) {
|
|
257
|
+
if (this.pendingIds[i] === id) {
|
|
258
|
+
this.pendingIds.splice(i, 1);
|
|
259
|
+
hostSettle(id, envelope);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Park `fn` with the shell and ask to be called back in `ms`.
|
|
267
|
+
*
|
|
268
|
+
* The id is the whole protocol: TS keeps the closure, the shell keeps the
|
|
269
|
+
* clock. Identical to the desktop lane.
|
|
270
|
+
*/
|
|
271
|
+
schedule(ms: number, fn: () => void): void {
|
|
272
|
+
const id = this.nextCont;
|
|
273
|
+
this.nextCont = id + 1;
|
|
274
|
+
this.contIds.push(id);
|
|
275
|
+
this.contFns.push(fn);
|
|
276
|
+
const delay = ms > 0 ? ms : 0;
|
|
277
|
+
hostSchedule(id, delay);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Run fn on the next turn of the host loop — no timer involved. */
|
|
281
|
+
defer(fn: () => void): void {
|
|
282
|
+
this.schedule(0, fn);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Run fn after roughly `ms`, without blocking the window meanwhile. */
|
|
286
|
+
sleep(ms: number, fn: () => void): void {
|
|
287
|
+
this.schedule(ms, fn);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Called by the shell on the main queue when a continuation comes due.
|
|
292
|
+
*
|
|
293
|
+
* This always lands at the top of a fresh turn with no TS frame beneath it,
|
|
294
|
+
* because the shell posts through its own queue rather than calling back
|
|
295
|
+
* from inside a channel handler (upstream #263: a breach silently appears
|
|
296
|
+
* to work, so it has to hold by construction).
|
|
297
|
+
*/
|
|
298
|
+
onTimer(id: number): void {
|
|
299
|
+
for (let i = 0; i < this.contIds.length; i++) {
|
|
300
|
+
if (this.contIds[i] === id) {
|
|
301
|
+
const fn = this.contFns[i];
|
|
302
|
+
// Unregister BEFORE running: a continuation that schedules another one
|
|
303
|
+
// must not disturb the entry being removed.
|
|
304
|
+
this.contIds.splice(i, 1);
|
|
305
|
+
this.contFns.splice(i, 1);
|
|
306
|
+
fn();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Read a file without blocking the UI; the shell does the I/O off-queue. */
|
|
313
|
+
readFileAsync(path: string, cb: FsCallback): void {
|
|
314
|
+
const id = this.nextJob;
|
|
315
|
+
this.nextJob = id + 1;
|
|
316
|
+
this.jobIds.push(id);
|
|
317
|
+
this.jobCbs.push(cb);
|
|
318
|
+
this.jobWriteCbs.push(nullWrite);
|
|
319
|
+
hostReadFile(id, path);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Write a file without blocking the UI; the shell does the I/O off-queue. */
|
|
323
|
+
writeFileAsync(path: string, data: string, cb: (err: string | null) => void): void {
|
|
324
|
+
const id = this.nextJob;
|
|
325
|
+
this.nextJob = id + 1;
|
|
326
|
+
this.jobIds.push(id);
|
|
327
|
+
this.jobCbs.push(nullRead);
|
|
328
|
+
this.jobWriteCbs.push(cb);
|
|
329
|
+
hostWriteFile(id, path, data);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Called by the shell on the main queue when a file job finishes. `payload`
|
|
334
|
+
* carries the contents on a read, or the error message when `ok` is false.
|
|
335
|
+
*/
|
|
336
|
+
onFsDone(id: number, ok: boolean, payload: string): void {
|
|
337
|
+
for (let i = 0; i < this.jobIds.length; i++) {
|
|
338
|
+
if (this.jobIds[i] === id) {
|
|
339
|
+
const readCb = this.jobCbs[i];
|
|
340
|
+
const writeCb = this.jobWriteCbs[i];
|
|
341
|
+
this.jobIds.splice(i, 1);
|
|
342
|
+
this.jobCbs.splice(i, 1);
|
|
343
|
+
this.jobWriteCbs.splice(i, 1);
|
|
344
|
+
if (readCb !== nullRead) {
|
|
345
|
+
if (ok) readCb(null, payload);
|
|
346
|
+
else readCb(payload, "");
|
|
347
|
+
} else {
|
|
348
|
+
writeCb(ok ? null : payload);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** @remarks Not on iOS yet; reports through the callback. */
|
|
356
|
+
openFileDialog(
|
|
357
|
+
_options: OpenDialogOptions,
|
|
358
|
+
cb: (paths: string[] | null, err?: string) => void,
|
|
359
|
+
): void {
|
|
360
|
+
cb(null, pending("openFileDialog", "iOS needs a document picker"));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** @remarks Not on iOS yet; reports through the callback. */
|
|
364
|
+
saveFileDialog(
|
|
365
|
+
_options: SaveDialogOptions,
|
|
366
|
+
cb: (path: string | null, err?: string) => void,
|
|
367
|
+
): void {
|
|
368
|
+
cb(null, pending("saveFileDialog", "iOS needs a document picker"));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** @remarks No-op on iOS: an app has no window title to set. */
|
|
372
|
+
setTitle(_title: string): void {
|
|
373
|
+
console.error(pending("setTitle", "an iOS app has no window title"));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** @remarks No-op on iOS: an app fills the screen. */
|
|
377
|
+
setSize(_width: number, _height: number, _hint?: number): void {
|
|
378
|
+
console.error(pending("setSize", "an iOS app fills the screen"));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** @remarks No-op on iOS: an app is always fullscreen. */
|
|
382
|
+
setFullscreen(_on: boolean): void {
|
|
383
|
+
console.error(pending("setFullscreen", "an iOS app is always fullscreen"));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** @remarks No-op on iOS: apps are dismissed by the user, not by code. */
|
|
387
|
+
quit(): void {
|
|
388
|
+
console.error(pending("quit", "iOS apps are dismissed by the user"));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Present for source compatibility with the desktop lane; UIKit owns the run
|
|
393
|
+
* loop here, so the shell shows the page rather than this.
|
|
394
|
+
*/
|
|
395
|
+
run(html: string): number {
|
|
396
|
+
this.setHtml(html);
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// The public host types live in ./types (shipped as `janela/host` too, so a
|
|
402
|
+
// user's editor can see them). Re-exported here because the compiled build
|
|
403
|
+
// resolves them through this module — see the specifier rewrite in the CLI.
|
|
404
|
+
export type {
|
|
405
|
+
ArgsOf,
|
|
406
|
+
AsyncCommandHandler,
|
|
407
|
+
CommandHandler,
|
|
408
|
+
CommandShape,
|
|
409
|
+
CommandShapes,
|
|
410
|
+
CommandSpec,
|
|
411
|
+
CommandSpecs,
|
|
412
|
+
Commands,
|
|
413
|
+
Norm,
|
|
414
|
+
ResultOf,
|
|
415
|
+
DialogFilter,
|
|
416
|
+
Events,
|
|
417
|
+
FsCallback,
|
|
418
|
+
OpenDialogOptions,
|
|
419
|
+
SaveDialogOptions,
|
|
420
|
+
WindowConfig,
|
|
421
|
+
} from "./types";
|
|
422
|
+
|
|
423
|
+
export { defineCommands, defineEvents } from "./types";
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* A running janela app, typed by the contract it serves. See the desktop lane
|
|
427
|
+
* for the full explanation; the alias exists so a contract may be written as
|
|
428
|
+
* plain function types.
|
|
429
|
+
*/
|
|
430
|
+
export type JanelaApp<
|
|
431
|
+
C extends CommandSpecs = CommandShapes,
|
|
432
|
+
E = Record<string, unknown>,
|
|
433
|
+
> = JanelaAppImpl<Norm<C>, E>;
|
|
434
|
+
|
|
435
|
+
export function createApp<
|
|
436
|
+
C extends CommandShapes = CommandShapes,
|
|
437
|
+
E = Record<string, unknown>,
|
|
438
|
+
>(cfg: WindowConfig): JanelaAppImpl<C, E> {
|
|
439
|
+
return new JanelaAppImpl<C, E>(cfg);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// Deprecated standalone registrars (0.5.x)
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
/** @deprecated Use `app.command(name, handler)` on a contract-typed app. */
|
|
447
|
+
export function on<M extends CommandShapes, K extends keyof M & string>(
|
|
448
|
+
app: JanelaAppImpl,
|
|
449
|
+
_commands: unknown,
|
|
450
|
+
name: K,
|
|
451
|
+
handler: (args: M[K]["args"]) => M[K]["result"],
|
|
452
|
+
): void {
|
|
453
|
+
app.command(name, (args: unknown) => handler(args as M[K]["args"]));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** @deprecated Use `app.commandAsync(name, handler)` on a contract-typed app. */
|
|
457
|
+
export function onAsync<M extends CommandShapes, K extends keyof M & string>(
|
|
458
|
+
app: JanelaAppImpl,
|
|
459
|
+
_commands: unknown,
|
|
460
|
+
name: K,
|
|
461
|
+
handler: (
|
|
462
|
+
args: M[K]["args"],
|
|
463
|
+
resolve: (value: M[K]["result"]) => void,
|
|
464
|
+
reject: (reason: unknown) => void,
|
|
465
|
+
) => void,
|
|
466
|
+
): void {
|
|
467
|
+
app.commandAsync(
|
|
468
|
+
name,
|
|
469
|
+
(args: unknown, resolve: (v: unknown) => void, reject: (r: unknown) => void) => {
|
|
470
|
+
handler(args as M[K]["args"], (value: M[K]["result"]) => resolve(value), reject);
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** @deprecated Use `app.emit(event, payload)` on a contract-typed app. */
|
|
476
|
+
export function emit<E, K extends keyof E & string>(
|
|
477
|
+
app: JanelaAppImpl,
|
|
478
|
+
_events: unknown,
|
|
479
|
+
name: K,
|
|
480
|
+
payload: E[K],
|
|
481
|
+
): void {
|
|
482
|
+
app.emit(name, payload as unknown);
|
|
483
|
+
}
|