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 CHANGED
@@ -138,22 +138,21 @@ framework templates' default.
138
138
 
139
139
  ```ts
140
140
  // src-host/main.ts
141
- import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
141
+ import type { JanelaApp } from "janela/host";
142
142
 
143
143
  export type AppCommands = {
144
- add: { args: { a: number; b: number }; result: number };
145
- greet: { args: { name: string }; result: string };
146
- wait: { args: { ms: number }; result: string };
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
- export const commands = defineCommands<AppCommands>();
151
- export const events = defineEvents<AppEvents>();
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: JanelaApp<AppCommands, AppEvents>): void {
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
- Write `args: null` for a command that takes nothing `args: undefined`
201
- lowers to a zero-parameter function and fails to compile.
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 untyped `invoke` / `listen` still work unchanged; the contract is
204
- additive, and the `vanilla` template still uses the global.
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 so the same one feeds `defineCommands`
337
- and the `setup` signature:
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 const commands = defineCommands<AppCommands>();
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** — `defineCommands` / `defineEvents` on the host,
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 const commands = defineCommands<{
443
+ export type AppCommands = {
377
444
  add: { args: { a: number; b: number }; result: number };
378
- }>();
379
- export type App = { commands: typeof commands; events: typeof events };
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
- /** Anything shaped like a host contract module's exported `App` type. */
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
- /** The command table declared by a contract. */
60
- export type CommandsOf<A> = A extends { commands: Commands<infer M> } ? M : never;
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 { events: Events<infer E> } ? E : never;
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, JanelaApp } from "./janela";\n` +
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: JanelaApp<App>) and a plain\n` +
525
- `// setup(app: JanelaApp) each get an app instantiated to match. scriptc\n` +
526
- `// monomorphises generic classes, so the right instantiation must be\n` +
527
- `// CONSTRUCTED here - no cast can bridge JanelaApp<A> and JanelaApp<B>.\n` +
528
- `type CmdsOf<F> = F extends (app: JanelaApp<infer C, infer _E>) => void ? C : CommandShapes;\n` +
529
- `type EvtsOf<F> = F extends (app: JanelaApp<infer _C, infer E>) => void ? E : Record<string, unknown>;\n\n` +
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Desktop apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
5
5
  "type": "module",
6
6
  "bin": {
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 JanelaApp<
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): JanelaApp<C, E> {
578
- return new JanelaApp<C, E>(cfg);
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: JanelaApp,
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: JanelaApp,
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: JanelaApp,
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
- // SHAPE NOTE: the registrars below are standalone generic FUNCTIONS taking the
89
- // contract as a value, rather than methods on a returned registrar object.
90
- // That is not a style choice scriptc cannot dispatch a generic method
91
- // through an interface-typed receiver (SC1090), so `commands.on(...)` does not
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
- /** One command's argument and result types. */
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 contract's command table: name → shape. */
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 const commands = defineCommands<{
121
- * add: { args: { a: number; b: number }; result: number };
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
- /** Declare the events a host emits: `defineEvents<{ added: number }>()`. */
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 contract below is the single declaration of what this app exposes. The
4
- // frontend imports `App` with `import type`, so the page is checked against
5
- // these exact types command names, argument shapes, results and event
6
- // payloads with no code generation and nothing to keep in sync.
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
- // - write `args: null` for a command that takes nothing, not `undefined`
12
- // an `undefined` argument lowers to a zero-parameter function and fails
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 { defineCommands, defineEvents, type JanelaApp } from "janela/host";
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: { args: { a: number; b: number }; result: number };
20
- greet: { args: { name: string }; result: string };
21
- log: { args: string; result: null };
22
- wait: { args: { ms: number }; result: string };
23
- quit: { args: null; result: null };
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 = { commands: typeof commands; events: typeof events };
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: JanelaApp<AppCommands, AppEvents>): void {
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 contract below is the single declaration of what this app exposes. The
4
- // frontend imports `App` with `import type`, so the page is checked against
5
- // these exact types command names, argument shapes, results and event
6
- // payloads with no code generation and nothing to keep in sync.
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
- // - write `args: null` for a command that takes nothing, not `undefined`
12
- // an `undefined` argument lowers to a zero-parameter function and fails
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 { defineCommands, defineEvents, type JanelaApp } from "janela/host";
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: { args: { a: number; b: number }; result: number };
20
- greet: { args: { name: string }; result: string };
21
- log: { args: string; result: null };
22
- wait: { args: { ms: number }; result: string };
23
- quit: { args: null; result: null };
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 = { commands: typeof commands; events: typeof events };
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: JanelaApp<AppCommands, AppEvents>): void {
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 contract below is the single declaration of what this app exposes. The
4
- // frontend imports `App` with `import type`, so the page is checked against
5
- // these exact types command names, argument shapes, results and event
6
- // payloads with no code generation and nothing to keep in sync.
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
- // - write `args: null` for a command that takes nothing, not `undefined`
12
- // an `undefined` argument lowers to a zero-parameter function and fails
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 { defineCommands, defineEvents, type JanelaApp } from "janela/host";
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: { args: { a: number; b: number }; result: number };
20
- greet: { args: { name: string }; result: string };
21
- log: { args: string; result: null };
22
- wait: { args: { ms: number }; result: string };
23
- quit: { args: null; result: null };
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 = { commands: typeof commands; events: typeof events };
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: JanelaApp<AppCommands, AppEvents>): void {
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 contract below is the single declaration of what this app exposes. The
4
- // frontend imports `App` with `import type`, so the page is checked against
5
- // these exact types command names, argument shapes, results and event
6
- // payloads with no code generation and nothing to keep in sync.
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
- // - write `args: null` for a command that takes nothing, not `undefined`
12
- // an `undefined` argument lowers to a zero-parameter function and fails
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 { defineCommands, defineEvents, type JanelaApp } from "janela/host";
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: { args: { a: number; b: number }; result: number };
20
- greet: { args: { name: string }; result: string };
21
- log: { args: string; result: null };
22
- wait: { args: { ms: number }; result: string };
23
- quit: { args: null; result: null };
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 = { commands: typeof commands; events: typeof events };
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: JanelaApp<AppCommands, AppEvents>): void {
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);