janela 0.6.0 → 0.8.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 +90 -22
- package/api/index.d.ts +30 -6
- package/bin/janela.mjs +9 -7
- package/package.json +1 -1
- package/runtime/janela.ts +36 -6
- package/runtime/types.ts +81 -11
- package/templates/react/files/src-host/main.ts +15 -18
- package/templates/solid/files/src-host/main.ts +15 -18
- package/templates/svelte/files/src-host/main.ts +15 -18
- package/templates/vue/files/src-host/main.ts +15 -18
package/README.md
CHANGED
|
@@ -138,22 +138,21 @@ framework templates' default.
|
|
|
138
138
|
|
|
139
139
|
```ts
|
|
140
140
|
// src-host/main.ts
|
|
141
|
-
import {
|
|
141
|
+
import type { JanelaApp } from "janela/host";
|
|
142
142
|
|
|
143
143
|
export type AppCommands = {
|
|
144
|
-
add:
|
|
145
|
-
greet:
|
|
146
|
-
wait:
|
|
144
|
+
add: (args: { a: number; b: number }) => number;
|
|
145
|
+
greet: (args: { name: string }) => string;
|
|
146
|
+
wait: (args: { ms: number }) => string;
|
|
147
|
+
quit: () => void; // a command that takes nothing
|
|
147
148
|
};
|
|
148
149
|
export type AppEvents = { added: number };
|
|
149
150
|
|
|
150
|
-
|
|
151
|
-
export
|
|
152
|
-
|
|
153
|
-
export type App = { commands: typeof commands; events: typeof events };
|
|
151
|
+
/** The app, carrying its contract. This is what the page imports. */
|
|
152
|
+
export type App = JanelaApp<AppCommands, AppEvents>;
|
|
154
153
|
|
|
155
154
|
// Typing the app with the contract is what makes the methods below checked.
|
|
156
|
-
export function setup(app:
|
|
155
|
+
export function setup(app: App): void {
|
|
157
156
|
app.command("add", (args) => { // args inferred: { a: number; b: number }
|
|
158
157
|
app.emit("added", args.a + args.b); // event name and payload checked
|
|
159
158
|
return args.a + args.b; // return type checked against the contract
|
|
@@ -197,11 +196,23 @@ Two things worth knowing:
|
|
|
197
196
|
own declarations rather than from an assertion you write by hand, which is
|
|
198
197
|
only possible because both sides are TypeScript.
|
|
199
198
|
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
A command that takes nothing is declared `() => void`, and its handler
|
|
200
|
+
returns `null` — every command answers the page's promise with a value, so
|
|
201
|
+
`void` is normalised to `null`. The page calls it as
|
|
202
|
+
`client.invoke("quit", null)`.
|
|
203
|
+
|
|
204
|
+
An **event payload is a single value** of any JSON-shaped type. For an event
|
|
205
|
+
carrying several things, prefer an object (`{ done: number; total: number }`):
|
|
206
|
+
adding a field later does not break existing listeners, and the names read
|
|
207
|
+
better at the call site. A tuple works too, but note that only the payload
|
|
208
|
+
*value* may be a tuple — the varargs spelling `app.emit("progress", 3, 10)`
|
|
209
|
+
does not compile (`SC2011: values of type '[done: number, total: number]'
|
|
210
|
+
have no static representation`, which would require `--dynamic` and ~620 KB
|
|
211
|
+
of embedded engine).
|
|
202
212
|
|
|
203
|
-
The
|
|
204
|
-
|
|
213
|
+
The `{ args; result }` record form of 0.5.x–0.7.x is still accepted, and the
|
|
214
|
+
untyped `invoke` / `listen` still work unchanged; the contract is additive,
|
|
215
|
+
and the `vanilla` template still uses the global.
|
|
205
216
|
|
|
206
217
|
## Async commands
|
|
207
218
|
|
|
@@ -312,6 +323,61 @@ nested modal loop would otherwise re-enter the host loop underneath a live TS
|
|
|
312
323
|
frame; [docs/native-shell.md](../../docs/native-shell.md) has the details, the
|
|
313
324
|
per-platform table, and the Windows GUI-subsystem note.
|
|
314
325
|
|
|
326
|
+
## Migrating from 0.7.x
|
|
327
|
+
|
|
328
|
+
Commands are declared as the functions they are. The old `{ args; result }`
|
|
329
|
+
record form still compiles, so this is optional — but the function form is
|
|
330
|
+
shorter, and a command that takes nothing is finally natural to write.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
// 0.7.x
|
|
334
|
+
export type AppCommands = {
|
|
335
|
+
add: { args: { a: number; b: number }; result: number };
|
|
336
|
+
quit: { args: null; result: null };
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// 0.8.0
|
|
340
|
+
export type AppCommands = {
|
|
341
|
+
add: (args: { a: number; b: number }) => number;
|
|
342
|
+
quit: () => void;
|
|
343
|
+
};
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Nothing else changes: `export type App = JanelaApp<AppCommands, AppEvents>`,
|
|
347
|
+
`setup(app: App)`, `app.command(...)` and the page's `createClient<App>()` are
|
|
348
|
+
all as they were. A handler for a `() => void` command returns `null`.
|
|
349
|
+
|
|
350
|
+
## Migrating from 0.6.x
|
|
351
|
+
|
|
352
|
+
The contract lives entirely in the types now, so the runtime tokens are gone.
|
|
353
|
+
Name the app instead of wrapping its two tables:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
// before (0.6.x)
|
|
357
|
+
import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
|
|
358
|
+
|
|
359
|
+
export const commands = defineCommands<AppCommands>();
|
|
360
|
+
export const events = defineEvents<AppEvents>();
|
|
361
|
+
export type App = { commands: typeof commands; events: typeof events };
|
|
362
|
+
|
|
363
|
+
export function setup(app: JanelaApp<AppCommands, AppEvents>): void { … }
|
|
364
|
+
|
|
365
|
+
// after (0.7.x)
|
|
366
|
+
import type { JanelaApp } from "janela/host";
|
|
367
|
+
|
|
368
|
+
export type App = JanelaApp<AppCommands, AppEvents>;
|
|
369
|
+
|
|
370
|
+
export function setup(app: App): void { … }
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
`AppCommands` and `AppEvents` are unchanged, and so is every page: the
|
|
374
|
+
frontend still writes `createClient<App>()` and `client.invoke(...)`, because
|
|
375
|
+
`createClient` reads the contract off either shape.
|
|
376
|
+
|
|
377
|
+
`defineCommands` and `defineEvents` still exist and still work — they are
|
|
378
|
+
`@deprecated` no-ops that only ever carried types — so a 0.6.x project keeps
|
|
379
|
+
compiling and running untouched.
|
|
380
|
+
|
|
315
381
|
## Migrating from 0.5.x
|
|
316
382
|
|
|
317
383
|
The contract now rides on the app itself, so the standalone registrars are no
|
|
@@ -333,14 +399,13 @@ export function setup(app: JanelaApp<AppCommands, AppEvents>): void {
|
|
|
333
399
|
}
|
|
334
400
|
```
|
|
335
401
|
|
|
336
|
-
Declare each contract as a named type
|
|
337
|
-
|
|
402
|
+
Declare each contract as a named type and hand both to the app (0.7.x drops
|
|
403
|
+
the `defineCommands` / `defineEvents` tokens entirely — see above):
|
|
338
404
|
|
|
339
405
|
```ts
|
|
340
406
|
export type AppCommands = { add: { args: { a: number; b: number }; result: number } };
|
|
341
407
|
export type AppEvents = { added: number };
|
|
342
|
-
export
|
|
343
|
-
export const events = defineEvents<AppEvents>();
|
|
408
|
+
export type App = JanelaApp<AppCommands, AppEvents>;
|
|
344
409
|
```
|
|
345
410
|
|
|
346
411
|
`on`, `onAsync` and `emit` still work — they are `@deprecated` one-line
|
|
@@ -358,9 +423,11 @@ Two things are new:
|
|
|
358
423
|
|
|
359
424
|
- `listen()` (and the injected `janela.listen`) now **return a disposer**.
|
|
360
425
|
Previously they returned nothing, so existing code is unaffected.
|
|
361
|
-
- The **typed contract** —
|
|
426
|
+
- The **typed contract** — a contract-typed app on the host,
|
|
362
427
|
`createClient<App>()` on the page. The framework templates now scaffold with
|
|
363
|
-
it. See [The typed contract](#the-typed-contract).
|
|
428
|
+
it. See [The typed contract](#the-typed-contract). (0.4.x shipped this with
|
|
429
|
+
`defineCommands` / `defineEvents` tokens; 0.7.x replaced them with the `App`
|
|
430
|
+
type alias below, and the tokens are deprecated but still work.)
|
|
364
431
|
|
|
365
432
|
To adopt it in an existing app, declare what the host already exposes and swap
|
|
366
433
|
the registrations:
|
|
@@ -373,10 +440,11 @@ app.command("add", (args) => {
|
|
|
373
440
|
});
|
|
374
441
|
|
|
375
442
|
// after
|
|
376
|
-
export
|
|
443
|
+
export type AppCommands = {
|
|
377
444
|
add: { args: { a: number; b: number }; result: number };
|
|
378
|
-
}
|
|
379
|
-
export type
|
|
445
|
+
};
|
|
446
|
+
export type AppEvents = { added: number };
|
|
447
|
+
export type App = JanelaApp<AppCommands, AppEvents>;
|
|
380
448
|
|
|
381
449
|
app.command("add", (args) => args.a + args.b); // args inferred, no cast
|
|
382
450
|
```
|
package/api/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* parse.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { JanelaAppImpl } from "../runtime/janela";
|
|
9
10
|
import type { CommandShapes, Commands, Events } from "../runtime/types";
|
|
10
11
|
|
|
11
12
|
/** Removes a subscription created by `listen` / `client.on`. */
|
|
@@ -50,17 +51,40 @@ export declare function listen<T = unknown>(
|
|
|
50
51
|
// Typed contract
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Anything shaped like a host contract module's exported `App` type.
|
|
56
|
+
*
|
|
57
|
+
* @deprecated The `{ commands, events }` wrapper of 0.5.x/0.6.x. Export
|
|
58
|
+
* `type App = JanelaApp<AppCommands, AppEvents>` instead.
|
|
59
|
+
*/
|
|
54
60
|
export interface Contract {
|
|
55
61
|
commands: Commands<CommandShapes>;
|
|
56
62
|
events: Events<unknown>;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The command table declared by a contract.
|
|
67
|
+
*
|
|
68
|
+
* Reads the contract off the app type — what a host's `App` is from 0.7.0 —
|
|
69
|
+
* and falls back to the 0.5.x/0.6.x `{ commands, events }` wrapper, so a
|
|
70
|
+
* project written against either shape keeps checking.
|
|
71
|
+
*
|
|
72
|
+
* The inference targets the class rather than the `JanelaApp` alias: the
|
|
73
|
+
* alias applies `Norm<C>`, which cannot be inferred backwards, and the
|
|
74
|
+
* normalised table is what indexing wants in any case.
|
|
75
|
+
*/
|
|
76
|
+
export type CommandsOf<A> = A extends JanelaAppImpl<infer M, infer _E>
|
|
77
|
+
? M
|
|
78
|
+
: A extends { commands: Commands<infer M> }
|
|
79
|
+
? M
|
|
80
|
+
: never;
|
|
61
81
|
|
|
62
|
-
/** The event table declared by a contract. */
|
|
63
|
-
export type EventsOf<A> = A extends
|
|
82
|
+
/** The event table declared by a contract; see CommandsOf for the two shapes. */
|
|
83
|
+
export type EventsOf<A> = A extends JanelaAppImpl<infer _M, infer E>
|
|
84
|
+
? E
|
|
85
|
+
: A extends { events: Events<infer E> }
|
|
86
|
+
? E
|
|
87
|
+
: never;
|
|
64
88
|
|
|
65
89
|
/**
|
|
66
90
|
* A client bound to a host contract: command names, argument shapes, result
|
|
@@ -89,7 +113,7 @@ export interface JanelaClient<A> {
|
|
|
89
113
|
* Build a client checked against a host's contract.
|
|
90
114
|
*
|
|
91
115
|
* ```ts
|
|
92
|
-
* import type { App } from "../src-host/main";
|
|
116
|
+
* import type { App } from "../src-host/main"; // App = JanelaApp<Cmds, Evts>
|
|
93
117
|
* const client = createClient<App>();
|
|
94
118
|
* const sum = await client.invoke("add", { a: 2, b: 40 }); // number
|
|
95
119
|
* const off = client.on("added", (v) => console.log(v)); // v: number
|
package/bin/janela.mjs
CHANGED
|
@@ -516,17 +516,19 @@ function build(root, { devUrl = null, gui = true } = {}) {
|
|
|
516
516
|
join(buildDir, "entry.ts"),
|
|
517
517
|
`// Generated by janela — do not edit.\n` +
|
|
518
518
|
`import { createApp } from "./janela";\n` +
|
|
519
|
-
`import type { CommandShapes,
|
|
519
|
+
`import type { CommandShapes, JanelaAppImpl } from "./janela";\n` +
|
|
520
520
|
`import { WINDOW } from "./config";\n` +
|
|
521
521
|
`import { INDEX_HTML } from "./frontend";\n` +
|
|
522
522
|
`import { setup } from "./main";\n\n` +
|
|
523
523
|
`// The app's type parameters are read back off setup()'s own signature,\n` +
|
|
524
|
-
`// so a contract-typed setup(app:
|
|
525
|
-
`//
|
|
526
|
-
`//
|
|
527
|
-
`//
|
|
528
|
-
|
|
529
|
-
|
|
524
|
+
`// so a contract-typed setup(app: App) and a plain setup(app: JanelaApp)\n` +
|
|
525
|
+
`// each get an app instantiated to match. scriptc monomorphises generic\n` +
|
|
526
|
+
`// classes, so the right instantiation must be CONSTRUCTED here - no cast\n` +
|
|
527
|
+
`// can bridge two of them. The inference reads the CLASS, not the\n` +
|
|
528
|
+
`// JanelaApp alias: the alias applies Norm<C>, which cannot be reversed,\n` +
|
|
529
|
+
`// and what is wanted here is the normalised table anyway.\n` +
|
|
530
|
+
`type CmdsOf<F> = F extends (app: JanelaAppImpl<infer C, infer _E>) => void ? C : CommandShapes;\n` +
|
|
531
|
+
`type EvtsOf<F> = F extends (app: JanelaAppImpl<infer _C, infer E>) => void ? E : Record<string, unknown>;\n\n` +
|
|
530
532
|
`const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
|
|
531
533
|
`setup(app);\n` +
|
|
532
534
|
`const rc = app.run(INDEX_HTML) + 0;\n` +
|
package/package.json
CHANGED
package/runtime/janela.ts
CHANGED
|
@@ -83,11 +83,16 @@ const BOOTSTRAP =
|
|
|
83
83
|
// user's editor can see them). Re-exported here because the compiled build
|
|
84
84
|
// resolves them through this module — see the specifier rewrite in the CLI.
|
|
85
85
|
export type {
|
|
86
|
+
ArgsOf,
|
|
86
87
|
AsyncCommandHandler,
|
|
87
88
|
CommandHandler,
|
|
88
89
|
CommandShape,
|
|
89
90
|
CommandShapes,
|
|
91
|
+
CommandSpec,
|
|
92
|
+
CommandSpecs,
|
|
90
93
|
Commands,
|
|
94
|
+
Norm,
|
|
95
|
+
ResultOf,
|
|
91
96
|
DialogFilter,
|
|
92
97
|
Events,
|
|
93
98
|
FsCallback,
|
|
@@ -105,7 +110,9 @@ import type {
|
|
|
105
110
|
AsyncCommandHandler,
|
|
106
111
|
CommandHandler,
|
|
107
112
|
CommandShapes,
|
|
113
|
+
CommandSpecs,
|
|
108
114
|
Commands,
|
|
115
|
+
Norm,
|
|
109
116
|
DialogFilter,
|
|
110
117
|
Events,
|
|
111
118
|
FsCallback,
|
|
@@ -156,7 +163,7 @@ const TICK_DRAIN_MS = 4;
|
|
|
156
163
|
* interface (being signature-only) never is. A class receiver works even as a
|
|
157
164
|
* plain function parameter, which is what `setup(app)` is.
|
|
158
165
|
*/
|
|
159
|
-
export class
|
|
166
|
+
export class JanelaAppImpl<
|
|
160
167
|
C extends CommandShapes = CommandShapes,
|
|
161
168
|
E = Record<string, unknown>,
|
|
162
169
|
> {
|
|
@@ -571,11 +578,34 @@ export class JanelaApp<
|
|
|
571
578
|
}
|
|
572
579
|
}
|
|
573
580
|
|
|
581
|
+
/**
|
|
582
|
+
* A running janela app, typed by the contract it serves.
|
|
583
|
+
*
|
|
584
|
+
* This is an alias rather than the class itself so that a contract may be
|
|
585
|
+
* written as plain function types: `Norm` converts it to the record form the
|
|
586
|
+
* class indexes, at a point where the table is still concrete. Writing the
|
|
587
|
+
* record form directly keeps working — `Norm` is idempotent.
|
|
588
|
+
*
|
|
589
|
+
* ```ts
|
|
590
|
+
* export type AppCommands = { add: (args: { a: number; b: number }) => number };
|
|
591
|
+
* export type AppEvents = { added: number };
|
|
592
|
+
* export type App = JanelaApp<AppCommands, AppEvents>;
|
|
593
|
+
*
|
|
594
|
+
* export function setup(app: App): void {
|
|
595
|
+
* app.command("add", (args) => args.a + args.b); // args inferred, result checked
|
|
596
|
+
* }
|
|
597
|
+
* ```
|
|
598
|
+
*/
|
|
599
|
+
export type JanelaApp<
|
|
600
|
+
C extends CommandSpecs = CommandShapes,
|
|
601
|
+
E = Record<string, unknown>,
|
|
602
|
+
> = JanelaAppImpl<Norm<C>, E>;
|
|
603
|
+
|
|
574
604
|
export function createApp<
|
|
575
605
|
C extends CommandShapes = CommandShapes,
|
|
576
606
|
E = Record<string, unknown>,
|
|
577
|
-
>(cfg: WindowConfig):
|
|
578
|
-
return new
|
|
607
|
+
>(cfg: WindowConfig): JanelaAppImpl<C, E> {
|
|
608
|
+
return new JanelaAppImpl<C, E>(cfg);
|
|
579
609
|
}
|
|
580
610
|
|
|
581
611
|
// ---------------------------------------------------------------------------
|
|
@@ -586,7 +616,7 @@ export function createApp<
|
|
|
586
616
|
|
|
587
617
|
/** @deprecated Use `app.command(name, handler)` on a contract-typed app. */
|
|
588
618
|
export function on<M extends CommandShapes, K extends keyof M & string>(
|
|
589
|
-
app:
|
|
619
|
+
app: JanelaAppImpl,
|
|
590
620
|
_commands: Commands<M>,
|
|
591
621
|
name: K,
|
|
592
622
|
handler: (args: M[K]["args"]) => M[K]["result"],
|
|
@@ -596,7 +626,7 @@ export function on<M extends CommandShapes, K extends keyof M & string>(
|
|
|
596
626
|
|
|
597
627
|
/** @deprecated Use `app.commandAsync(name, handler)` on a contract-typed app. */
|
|
598
628
|
export function onAsync<M extends CommandShapes, K extends keyof M & string>(
|
|
599
|
-
app:
|
|
629
|
+
app: JanelaAppImpl,
|
|
600
630
|
_commands: Commands<M>,
|
|
601
631
|
name: K,
|
|
602
632
|
handler: (
|
|
@@ -615,7 +645,7 @@ export function onAsync<M extends CommandShapes, K extends keyof M & string>(
|
|
|
615
645
|
|
|
616
646
|
/** @deprecated Use `app.emit(event, payload)` on a contract-typed app. */
|
|
617
647
|
export function emit<E, K extends keyof E & string>(
|
|
618
|
-
app:
|
|
648
|
+
app: JanelaAppImpl,
|
|
619
649
|
_events: Events<E>,
|
|
620
650
|
name: K,
|
|
621
651
|
payload: E[K],
|
package/runtime/types.ts
CHANGED
|
@@ -85,21 +85,83 @@ export interface WindowConfig {
|
|
|
85
85
|
// Payloads still cross as JSON, so these types are compile-time only. Nothing
|
|
86
86
|
// validates a malformed payload at runtime.
|
|
87
87
|
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// compile in a host build, while `on(app, commands, ...)` does.
|
|
88
|
+
// The contract is carried by the app's own type — `JanelaApp<Commands, Events>`
|
|
89
|
+
// — so the tables below are types and nothing more. The `Commands`/`Events`
|
|
90
|
+
// tokens and their `define*` constructors are the 0.5.x/0.6.x shape, kept so
|
|
91
|
+
// projects written against it still compile.
|
|
93
92
|
|
|
94
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* One command's argument and result types, in normalised form.
|
|
95
|
+
*
|
|
96
|
+
* This is what the app class works with internally. A contract is *written*
|
|
97
|
+
* as plain function types — see CommandSpec — and normalised to this by
|
|
98
|
+
* `Norm` before it reaches the class.
|
|
99
|
+
*/
|
|
95
100
|
export interface CommandShape {
|
|
96
101
|
args: unknown;
|
|
97
102
|
result: unknown;
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
/** A
|
|
105
|
+
/** A normalised command table: name → shape. */
|
|
101
106
|
export type CommandShapes = Record<string, CommandShape>;
|
|
102
107
|
|
|
108
|
+
/**
|
|
109
|
+
* How a command may be declared in a contract: as a plain function type
|
|
110
|
+
* (preferred), or as the `{ args; result }` record of 0.5.x–0.7.x.
|
|
111
|
+
*
|
|
112
|
+
* ```ts
|
|
113
|
+
* type AppCommands = {
|
|
114
|
+
* add: (args: { a: number; b: number }) => number;
|
|
115
|
+
* quit: () => void; // no arguments
|
|
116
|
+
* legacy: { args: { name: string }; result: string }; // still accepted
|
|
117
|
+
* };
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export type CommandSpec = ((...args: never[]) => unknown) | CommandShape;
|
|
121
|
+
|
|
122
|
+
/** A contract's command table as written: name → spec. */
|
|
123
|
+
export type CommandSpecs = Record<string, CommandSpec>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The argument type of a declared command. A function's single parameter, or
|
|
127
|
+
* a record's `args`. A command declared with no parameters takes `null` — the
|
|
128
|
+
* page's `invoke(name)` sends null, and nothing is lost.
|
|
129
|
+
*/
|
|
130
|
+
export type ArgsOf<F> = F extends (...a: infer P) => unknown
|
|
131
|
+
? P extends [infer A]
|
|
132
|
+
? A
|
|
133
|
+
: null
|
|
134
|
+
: F extends { args: infer A }
|
|
135
|
+
? A
|
|
136
|
+
: null;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The result type of a declared command. `void` is normalised to `null`:
|
|
140
|
+
* every command answers the page's promise with a value, and scriptc has no
|
|
141
|
+
* conversion from a void value to the `unknown` the handler table holds.
|
|
142
|
+
*/
|
|
143
|
+
export type ResultOf<F> = F extends (...a: never[]) => infer R
|
|
144
|
+
? [R] extends [void]
|
|
145
|
+
? null
|
|
146
|
+
: R
|
|
147
|
+
: F extends { result: infer R }
|
|
148
|
+
? R
|
|
149
|
+
: never;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Normalise a written contract to the record form the app class indexes.
|
|
153
|
+
*
|
|
154
|
+
* This runs where `C` is still concrete — in the `JanelaApp<C, E>` alias, one
|
|
155
|
+
* step before the class — on purpose. scriptc cannot compile a *value* whose
|
|
156
|
+
* type is an unresolved conditional or a mapped type indexed by a type
|
|
157
|
+
* parameter (`SC2001: values of type 'ArgsOf<C[K]>' cannot be compiled yet`),
|
|
158
|
+
* so the class body only ever sees plain indexed access on a record.
|
|
159
|
+
* Idempotent: normalising a record-form table returns it unchanged.
|
|
160
|
+
*/
|
|
161
|
+
export type Norm<C> = {
|
|
162
|
+
[K in keyof C]: { args: ArgsOf<C[K]>; result: ResultOf<C[K]> };
|
|
163
|
+
};
|
|
164
|
+
|
|
103
165
|
/**
|
|
104
166
|
* A declared command contract. Carries `M` at the type level only — the value
|
|
105
167
|
* is empty, and exists so that inference has something to read at a call site.
|
|
@@ -116,17 +178,25 @@ export interface Events<E> {
|
|
|
116
178
|
/**
|
|
117
179
|
* Declare the commands a host exposes.
|
|
118
180
|
*
|
|
181
|
+
* @deprecated The contract needs no runtime token. Declare the tables as
|
|
182
|
+
* types and name the app itself:
|
|
183
|
+
*
|
|
119
184
|
* ```ts
|
|
120
|
-
* export
|
|
121
|
-
*
|
|
122
|
-
*
|
|
185
|
+
* export type AppCommands = { add: (args: { a: number; b: number }) => number };
|
|
186
|
+
* export type AppEvents = { added: number };
|
|
187
|
+
* export type App = JanelaApp<AppCommands, AppEvents>;
|
|
188
|
+
* export function setup(app: App): void { … }
|
|
123
189
|
* ```
|
|
124
190
|
*/
|
|
125
191
|
export function defineCommands<M extends CommandShapes>(): Commands<M> {
|
|
126
192
|
return {};
|
|
127
193
|
}
|
|
128
194
|
|
|
129
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Declare the events a host emits: `defineEvents<{ added: number }>()`.
|
|
197
|
+
*
|
|
198
|
+
* @deprecated Pass the event table to `JanelaApp` instead — see defineCommands.
|
|
199
|
+
*/
|
|
130
200
|
export function defineEvents<E>(): Events<E> {
|
|
131
201
|
return {};
|
|
132
202
|
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
// src-host/main.ts — your app's backend, compiled to native code by scriptc.
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// The two tables below are the single declaration of what this app exposes,
|
|
4
|
+
// and `App` names an app that carries them. The frontend imports `App` with
|
|
5
|
+
// `import type`, so the page is checked against these exact types — command
|
|
6
|
+
// names, argument shapes, results and event payloads — with no code
|
|
7
|
+
// generation and nothing to keep in sync.
|
|
7
8
|
//
|
|
8
9
|
// Two gotchas inherited from scriptc:
|
|
9
10
|
// - never use a bare FFI-backed call as a complete variable initializer;
|
|
10
11
|
// wrap it in any expression (`+ 0`);
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
// to compile.
|
|
12
|
+
// - a command that returns nothing is declared `() => void` and its handler
|
|
13
|
+
// returns `null`; every command answers the page's promise with a value.
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import type { JanelaApp } from "janela/host";
|
|
16
16
|
|
|
17
17
|
/** Every command this app answers. Declared once; the page checks against it. */
|
|
18
18
|
export type AppCommands = {
|
|
19
|
-
add:
|
|
20
|
-
greet:
|
|
21
|
-
log:
|
|
22
|
-
wait:
|
|
23
|
-
quit:
|
|
19
|
+
add: (args: { a: number; b: number }) => number;
|
|
20
|
+
greet: (args: { name: string }) => string;
|
|
21
|
+
log: (args: string) => void;
|
|
22
|
+
wait: (args: { ms: number }) => string;
|
|
23
|
+
quit: () => void;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
/** Every event this app emits, and what each one carries. */
|
|
@@ -28,16 +28,13 @@ export type AppEvents = {
|
|
|
28
28
|
added: number;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
export const commands = defineCommands<AppCommands>();
|
|
32
|
-
export const events = defineEvents<AppEvents>();
|
|
33
|
-
|
|
34
31
|
/** The contract the page imports with `import type { App } from "../src-host/main"`. */
|
|
35
|
-
export type App =
|
|
32
|
+
export type App = JanelaApp<AppCommands, AppEvents>;
|
|
36
33
|
|
|
37
34
|
// Typing the app with the contract is what makes `app.command` checked: the
|
|
38
35
|
// name must be one of the declared ones, `args` is inferred from it, and the
|
|
39
36
|
// return value has to match. Same for `app.emit`.
|
|
40
|
-
export function setup(app:
|
|
37
|
+
export function setup(app: App): void {
|
|
41
38
|
app.command("add", (args) => {
|
|
42
39
|
const sum = args.a + args.b;
|
|
43
40
|
app.emit("added", sum);
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
// src-host/main.ts — your app's backend, compiled to native code by scriptc.
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// The two tables below are the single declaration of what this app exposes,
|
|
4
|
+
// and `App` names an app that carries them. The frontend imports `App` with
|
|
5
|
+
// `import type`, so the page is checked against these exact types — command
|
|
6
|
+
// names, argument shapes, results and event payloads — with no code
|
|
7
|
+
// generation and nothing to keep in sync.
|
|
7
8
|
//
|
|
8
9
|
// Two gotchas inherited from scriptc:
|
|
9
10
|
// - never use a bare FFI-backed call as a complete variable initializer;
|
|
10
11
|
// wrap it in any expression (`+ 0`);
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
// to compile.
|
|
12
|
+
// - a command that returns nothing is declared `() => void` and its handler
|
|
13
|
+
// returns `null`; every command answers the page's promise with a value.
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import type { JanelaApp } from "janela/host";
|
|
16
16
|
|
|
17
17
|
/** Every command this app answers. Declared once; the page checks against it. */
|
|
18
18
|
export type AppCommands = {
|
|
19
|
-
add:
|
|
20
|
-
greet:
|
|
21
|
-
log:
|
|
22
|
-
wait:
|
|
23
|
-
quit:
|
|
19
|
+
add: (args: { a: number; b: number }) => number;
|
|
20
|
+
greet: (args: { name: string }) => string;
|
|
21
|
+
log: (args: string) => void;
|
|
22
|
+
wait: (args: { ms: number }) => string;
|
|
23
|
+
quit: () => void;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
/** Every event this app emits, and what each one carries. */
|
|
@@ -28,16 +28,13 @@ export type AppEvents = {
|
|
|
28
28
|
added: number;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
export const commands = defineCommands<AppCommands>();
|
|
32
|
-
export const events = defineEvents<AppEvents>();
|
|
33
|
-
|
|
34
31
|
/** The contract the page imports with `import type { App } from "../src-host/main"`. */
|
|
35
|
-
export type App =
|
|
32
|
+
export type App = JanelaApp<AppCommands, AppEvents>;
|
|
36
33
|
|
|
37
34
|
// Typing the app with the contract is what makes `app.command` checked: the
|
|
38
35
|
// name must be one of the declared ones, `args` is inferred from it, and the
|
|
39
36
|
// return value has to match. Same for `app.emit`.
|
|
40
|
-
export function setup(app:
|
|
37
|
+
export function setup(app: App): void {
|
|
41
38
|
app.command("add", (args) => {
|
|
42
39
|
const sum = args.a + args.b;
|
|
43
40
|
app.emit("added", sum);
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
// src-host/main.ts — your app's backend, compiled to native code by scriptc.
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// The two tables below are the single declaration of what this app exposes,
|
|
4
|
+
// and `App` names an app that carries them. The frontend imports `App` with
|
|
5
|
+
// `import type`, so the page is checked against these exact types — command
|
|
6
|
+
// names, argument shapes, results and event payloads — with no code
|
|
7
|
+
// generation and nothing to keep in sync.
|
|
7
8
|
//
|
|
8
9
|
// Two gotchas inherited from scriptc:
|
|
9
10
|
// - never use a bare FFI-backed call as a complete variable initializer;
|
|
10
11
|
// wrap it in any expression (`+ 0`);
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
// to compile.
|
|
12
|
+
// - a command that returns nothing is declared `() => void` and its handler
|
|
13
|
+
// returns `null`; every command answers the page's promise with a value.
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import type { JanelaApp } from "janela/host";
|
|
16
16
|
|
|
17
17
|
/** Every command this app answers. Declared once; the page checks against it. */
|
|
18
18
|
export type AppCommands = {
|
|
19
|
-
add:
|
|
20
|
-
greet:
|
|
21
|
-
log:
|
|
22
|
-
wait:
|
|
23
|
-
quit:
|
|
19
|
+
add: (args: { a: number; b: number }) => number;
|
|
20
|
+
greet: (args: { name: string }) => string;
|
|
21
|
+
log: (args: string) => void;
|
|
22
|
+
wait: (args: { ms: number }) => string;
|
|
23
|
+
quit: () => void;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
/** Every event this app emits, and what each one carries. */
|
|
@@ -28,16 +28,13 @@ export type AppEvents = {
|
|
|
28
28
|
added: number;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
export const commands = defineCommands<AppCommands>();
|
|
32
|
-
export const events = defineEvents<AppEvents>();
|
|
33
|
-
|
|
34
31
|
/** The contract the page imports with `import type { App } from "../src-host/main"`. */
|
|
35
|
-
export type App =
|
|
32
|
+
export type App = JanelaApp<AppCommands, AppEvents>;
|
|
36
33
|
|
|
37
34
|
// Typing the app with the contract is what makes `app.command` checked: the
|
|
38
35
|
// name must be one of the declared ones, `args` is inferred from it, and the
|
|
39
36
|
// return value has to match. Same for `app.emit`.
|
|
40
|
-
export function setup(app:
|
|
37
|
+
export function setup(app: App): void {
|
|
41
38
|
app.command("add", (args) => {
|
|
42
39
|
const sum = args.a + args.b;
|
|
43
40
|
app.emit("added", sum);
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
// src-host/main.ts — your app's backend, compiled to native code by scriptc.
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// The two tables below are the single declaration of what this app exposes,
|
|
4
|
+
// and `App` names an app that carries them. The frontend imports `App` with
|
|
5
|
+
// `import type`, so the page is checked against these exact types — command
|
|
6
|
+
// names, argument shapes, results and event payloads — with no code
|
|
7
|
+
// generation and nothing to keep in sync.
|
|
7
8
|
//
|
|
8
9
|
// Two gotchas inherited from scriptc:
|
|
9
10
|
// - never use a bare FFI-backed call as a complete variable initializer;
|
|
10
11
|
// wrap it in any expression (`+ 0`);
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
// to compile.
|
|
12
|
+
// - a command that returns nothing is declared `() => void` and its handler
|
|
13
|
+
// returns `null`; every command answers the page's promise with a value.
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import type { JanelaApp } from "janela/host";
|
|
16
16
|
|
|
17
17
|
/** Every command this app answers. Declared once; the page checks against it. */
|
|
18
18
|
export type AppCommands = {
|
|
19
|
-
add:
|
|
20
|
-
greet:
|
|
21
|
-
log:
|
|
22
|
-
wait:
|
|
23
|
-
quit:
|
|
19
|
+
add: (args: { a: number; b: number }) => number;
|
|
20
|
+
greet: (args: { name: string }) => string;
|
|
21
|
+
log: (args: string) => void;
|
|
22
|
+
wait: (args: { ms: number }) => string;
|
|
23
|
+
quit: () => void;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
/** Every event this app emits, and what each one carries. */
|
|
@@ -28,16 +28,13 @@ export type AppEvents = {
|
|
|
28
28
|
added: number;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
export const commands = defineCommands<AppCommands>();
|
|
32
|
-
export const events = defineEvents<AppEvents>();
|
|
33
|
-
|
|
34
31
|
/** The contract the page imports with `import type { App } from "../src-host/main"`. */
|
|
35
|
-
export type App =
|
|
32
|
+
export type App = JanelaApp<AppCommands, AppEvents>;
|
|
36
33
|
|
|
37
34
|
// Typing the app with the contract is what makes `app.command` checked: the
|
|
38
35
|
// name must be one of the declared ones, `args` is inferred from it, and the
|
|
39
36
|
// return value has to match. Same for `app.emit`.
|
|
40
|
-
export function setup(app:
|
|
37
|
+
export function setup(app: App): void {
|
|
41
38
|
app.command("add", (args) => {
|
|
42
39
|
const sum = args.a + args.b;
|
|
43
40
|
app.emit("added", sum);
|