janela 0.5.0 → 0.7.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/runtime/types.ts CHANGED
@@ -73,64 +73,6 @@ export interface WindowConfig {
73
73
  height: number;
74
74
  }
75
75
 
76
- export interface JanelaApp {
77
- handle: number;
78
- names: string[];
79
- handlers: CommandHandler[];
80
- /** Register a named command, callable from the page as janela.invoke(name, args). */
81
- command: (name: string, h: CommandHandler) => void;
82
- /** Register a command that answers later; see AsyncCommandHandler. */
83
- commandAsync: (name: string, h: AsyncCommandHandler) => void;
84
- /** Run fn on the next turn of the host loop — the way to slice long work. */
85
- defer: (fn: () => void) => void;
86
- /** Run fn after at least ms. The host loop's timer; scriptc's setTimeout
87
- * cannot fire while the window is open (its loop is parked inside run()). */
88
- sleep: (ms: number, fn: () => void) => void;
89
- /**
90
- * Read a file without blocking the window. The syscall runs on a shim
91
- * worker thread; the callback lands on the UI thread on a later turn.
92
- * Prefer this over node:fs readFileSync inside a command — that one blocks
93
- * the loop, and with it the whole window.
94
- */
95
- readFileAsync: (path: string, cb: FsCallback) => void;
96
- /** Write a file without blocking the window; cb(null) on success. */
97
- writeFileAsync: (
98
- path: string,
99
- data: string,
100
- cb: (err: string | null) => void,
101
- ) => void;
102
- /**
103
- * Show the native "open" dialog. `cb` gets the chosen paths, or null if the
104
- * user cancelled. The modal runs on a later turn of the UI thread, so
105
- * calling this from inside a command does not block that command's reply —
106
- * pair it with commandAsync when the page is waiting for the result.
107
- */
108
- openFileDialog: (
109
- options: OpenDialogOptions,
110
- cb: (paths: string[] | null, err?: string) => void,
111
- ) => void;
112
- /** Show the native "save" dialog; cb gets the path, or null on cancel. */
113
- saveFileDialog: (
114
- options: SaveDialogOptions,
115
- cb: (path: string | null, err?: string) => void,
116
- ) => void;
117
- /** Change the window title at any time, not just at startup. */
118
- setTitle: (title: string) => void;
119
- /**
120
- * Resize the window. `hint` is webview's sizing hint: 0 none, 1 minimum,
121
- * 2 maximum, 3 fixed.
122
- */
123
- setSize: (width: number, height: number, hint?: number) => void;
124
- /** Enter or leave fullscreen. */
125
- setFullscreen: (on: boolean) => void;
126
- /** Fire an event into the page; the payload is delivered as a value. */
127
- emit: (event: string, payload: unknown) => void;
128
- /** Close the window and make run() return. */
129
- quit: () => void;
130
- /** Show the page and block until the window closes. Returns the run status. */
131
- run: (html: string) => number;
132
- }
133
-
134
76
  // ---------------------------------------------------------------------------
135
77
  // Typed IPC contract
136
78
  // ---------------------------------------------------------------------------
@@ -143,11 +85,10 @@ export interface JanelaApp {
143
85
  // Payloads still cross as JSON, so these types are compile-time only. Nothing
144
86
  // validates a malformed payload at runtime.
145
87
  //
146
- // SHAPE NOTE: the registrars below are standalone generic FUNCTIONS taking the
147
- // contract as a value, rather than methods on a returned registrar object.
148
- // That is not a style choice — scriptc cannot dispatch a generic method
149
- // through an interface-typed receiver (SC1090), so `commands.on(...)` does not
150
- // 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.
151
92
 
152
93
  /** One command's argument and result types. */
153
94
  export interface CommandShape {
@@ -174,78 +115,25 @@ export interface Events<E> {
174
115
  /**
175
116
  * Declare the commands a host exposes.
176
117
  *
118
+ * @deprecated The contract needs no runtime token. Declare the tables as
119
+ * types and name the app itself:
120
+ *
177
121
  * ```ts
178
- * export const commands = defineCommands<{
179
- * add: { args: { a: number; b: number }; result: number };
180
- * }>();
122
+ * export type AppCommands = { add: { args: { a: number; b: number }; result: number } };
123
+ * export type AppEvents = { added: number };
124
+ * export type App = JanelaApp<AppCommands, AppEvents>;
125
+ * export function setup(app: App): void { … }
181
126
  * ```
182
127
  */
183
128
  export function defineCommands<M extends CommandShapes>(): Commands<M> {
184
129
  return {};
185
130
  }
186
131
 
187
- /** Declare the events a host emits: `defineEvents<{ added: number }>()`. */
188
- export function defineEvents<E>(): Events<E> {
189
- return {};
190
- }
191
-
192
- /**
193
- * Register a command against the contract. `args` is inferred from the
194
- * contract, and the return type is checked against it, so the handler is
195
- * written once with no casts.
196
- *
197
- * ```ts
198
- * on(app, commands, "add", (args) => args.a + args.b);
199
- * ```
200
- */
201
- export function on<M extends CommandShapes, K extends keyof M & string>(
202
- app: JanelaApp,
203
- _commands: Commands<M>,
204
- name: K,
205
- handler: (args: M[K]["args"]) => M[K]["result"],
206
- ): void {
207
- app.command(name, (args: unknown) => handler(args as M[K]["args"]));
208
- }
209
-
210
132
  /**
211
- * Register a command that answers on a later turn. `resolve` takes the
212
- * contract's result type; see AsyncCommandHandler for the timing rules.
213
- */
214
- export function onAsync<M extends CommandShapes, K extends keyof M & string>(
215
- app: JanelaApp,
216
- _commands: Commands<M>,
217
- name: K,
218
- handler: (
219
- args: M[K]["args"],
220
- resolve: (value: M[K]["result"]) => void,
221
- reject: (reason: unknown) => void,
222
- ) => void,
223
- ): void {
224
- app.commandAsync(
225
- name,
226
- (args: unknown, resolve: (value: unknown) => void, reject: (reason: unknown) => void) => {
227
- handler(
228
- args as M[K]["args"],
229
- (value: M[K]["result"]) => resolve(value),
230
- reject,
231
- );
232
- },
233
- );
234
- }
235
-
236
- /**
237
- * Emit a declared event. The name must exist in the contract and the payload
238
- * must match its type.
133
+ * Declare the events a host emits: `defineEvents<{ added: number }>()`.
239
134
  *
240
- * ```ts
241
- * emit(app, events, "added", 42);
242
- * ```
135
+ * @deprecated Pass the event table to `JanelaApp` instead — see defineCommands.
243
136
  */
244
- export function emit<E, K extends keyof E & string>(
245
- app: JanelaApp,
246
- _events: Events<E>,
247
- name: K,
248
- payload: E[K],
249
- ): void {
250
- app.emit(name, payload);
137
+ export function defineEvents<E>(): Events<E> {
138
+ return {};
251
139
  }
@@ -1,9 +1,10 @@
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;
@@ -12,56 +13,52 @@
12
13
  // an `undefined` argument lowers to a zero-parameter function and fails
13
14
  // to compile.
14
15
 
15
- import {
16
- defineCommands,
17
- defineEvents,
18
- emit,
19
- on,
20
- onAsync,
21
- type JanelaApp,
22
- } from "janela/host";
16
+ import type { JanelaApp } from "janela/host";
23
17
 
24
- export const commands = defineCommands<{
18
+ /** Every command this app answers. Declared once; the page checks against it. */
19
+ export type AppCommands = {
25
20
  add: { args: { a: number; b: number }; result: number };
26
21
  greet: { args: { name: string }; result: string };
27
22
  log: { args: string; result: null };
28
23
  wait: { args: { ms: number }; result: string };
29
24
  quit: { args: null; result: null };
30
- }>();
25
+ };
31
26
 
32
- export const events = defineEvents<{
27
+ /** Every event this app emits, and what each one carries. */
28
+ export type AppEvents = {
33
29
  added: number;
34
- }>();
30
+ };
35
31
 
36
32
  /** The contract the page imports with `import type { App } from "../src-host/main"`. */
37
- export type App = { commands: typeof commands; events: typeof events };
33
+ export type App = JanelaApp<AppCommands, AppEvents>;
38
34
 
39
- export function setup(app: JanelaApp): void {
40
- // `args` is inferred from the contract, and the return type is checked
41
- // against it — no casts, and no way to drift from what the page expects.
42
- on(app, commands, "add", (args) => {
35
+ // Typing the app with the contract is what makes `app.command` checked: the
36
+ // name must be one of the declared ones, `args` is inferred from it, and the
37
+ // return value has to match. Same for `app.emit`.
38
+ export function setup(app: App): void {
39
+ app.command("add", (args) => {
43
40
  const sum = args.a + args.b;
44
- emit(app, events, "added", sum);
41
+ app.emit("added", sum);
45
42
  return sum;
46
43
  });
47
44
 
48
- on(app, commands, "greet", (args) => {
45
+ app.command("greet", (args) => {
49
46
  return "Hello, " + args.name + " — from the native TS binary";
50
47
  });
51
48
 
52
- on(app, commands, "log", (args) => {
49
+ app.command("log", (args) => {
53
50
  console.log("[host] page says:", args);
54
51
  return null;
55
52
  });
56
53
 
57
54
  // An async command: answers later, without freezing the window.
58
- onAsync(app, commands, "wait", (args, resolve) => {
55
+ app.commandAsync("wait", (args, resolve) => {
59
56
  app.sleep(args.ms, () => {
60
57
  resolve("waited " + args.ms + "ms without blocking the UI");
61
58
  });
62
59
  });
63
60
 
64
- on(app, commands, "quit", (_args) => {
61
+ app.command("quit", (_args) => {
65
62
  app.quit();
66
63
  return null;
67
64
  });
@@ -1,9 +1,10 @@
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;
@@ -12,56 +13,52 @@
12
13
  // an `undefined` argument lowers to a zero-parameter function and fails
13
14
  // to compile.
14
15
 
15
- import {
16
- defineCommands,
17
- defineEvents,
18
- emit,
19
- on,
20
- onAsync,
21
- type JanelaApp,
22
- } from "janela/host";
16
+ import type { JanelaApp } from "janela/host";
23
17
 
24
- export const commands = defineCommands<{
18
+ /** Every command this app answers. Declared once; the page checks against it. */
19
+ export type AppCommands = {
25
20
  add: { args: { a: number; b: number }; result: number };
26
21
  greet: { args: { name: string }; result: string };
27
22
  log: { args: string; result: null };
28
23
  wait: { args: { ms: number }; result: string };
29
24
  quit: { args: null; result: null };
30
- }>();
25
+ };
31
26
 
32
- export const events = defineEvents<{
27
+ /** Every event this app emits, and what each one carries. */
28
+ export type AppEvents = {
33
29
  added: number;
34
- }>();
30
+ };
35
31
 
36
32
  /** The contract the page imports with `import type { App } from "../src-host/main"`. */
37
- export type App = { commands: typeof commands; events: typeof events };
33
+ export type App = JanelaApp<AppCommands, AppEvents>;
38
34
 
39
- export function setup(app: JanelaApp): void {
40
- // `args` is inferred from the contract, and the return type is checked
41
- // against it — no casts, and no way to drift from what the page expects.
42
- on(app, commands, "add", (args) => {
35
+ // Typing the app with the contract is what makes `app.command` checked: the
36
+ // name must be one of the declared ones, `args` is inferred from it, and the
37
+ // return value has to match. Same for `app.emit`.
38
+ export function setup(app: App): void {
39
+ app.command("add", (args) => {
43
40
  const sum = args.a + args.b;
44
- emit(app, events, "added", sum);
41
+ app.emit("added", sum);
45
42
  return sum;
46
43
  });
47
44
 
48
- on(app, commands, "greet", (args) => {
45
+ app.command("greet", (args) => {
49
46
  return "Hello, " + args.name + " — from the native TS binary";
50
47
  });
51
48
 
52
- on(app, commands, "log", (args) => {
49
+ app.command("log", (args) => {
53
50
  console.log("[host] page says:", args);
54
51
  return null;
55
52
  });
56
53
 
57
54
  // An async command: answers later, without freezing the window.
58
- onAsync(app, commands, "wait", (args, resolve) => {
55
+ app.commandAsync("wait", (args, resolve) => {
59
56
  app.sleep(args.ms, () => {
60
57
  resolve("waited " + args.ms + "ms without blocking the UI");
61
58
  });
62
59
  });
63
60
 
64
- on(app, commands, "quit", (_args) => {
61
+ app.command("quit", (_args) => {
65
62
  app.quit();
66
63
  return null;
67
64
  });
@@ -1,9 +1,10 @@
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;
@@ -12,56 +13,52 @@
12
13
  // an `undefined` argument lowers to a zero-parameter function and fails
13
14
  // to compile.
14
15
 
15
- import {
16
- defineCommands,
17
- defineEvents,
18
- emit,
19
- on,
20
- onAsync,
21
- type JanelaApp,
22
- } from "janela/host";
16
+ import type { JanelaApp } from "janela/host";
23
17
 
24
- export const commands = defineCommands<{
18
+ /** Every command this app answers. Declared once; the page checks against it. */
19
+ export type AppCommands = {
25
20
  add: { args: { a: number; b: number }; result: number };
26
21
  greet: { args: { name: string }; result: string };
27
22
  log: { args: string; result: null };
28
23
  wait: { args: { ms: number }; result: string };
29
24
  quit: { args: null; result: null };
30
- }>();
25
+ };
31
26
 
32
- export const events = defineEvents<{
27
+ /** Every event this app emits, and what each one carries. */
28
+ export type AppEvents = {
33
29
  added: number;
34
- }>();
30
+ };
35
31
 
36
32
  /** The contract the page imports with `import type { App } from "../src-host/main"`. */
37
- export type App = { commands: typeof commands; events: typeof events };
33
+ export type App = JanelaApp<AppCommands, AppEvents>;
38
34
 
39
- export function setup(app: JanelaApp): void {
40
- // `args` is inferred from the contract, and the return type is checked
41
- // against it — no casts, and no way to drift from what the page expects.
42
- on(app, commands, "add", (args) => {
35
+ // Typing the app with the contract is what makes `app.command` checked: the
36
+ // name must be one of the declared ones, `args` is inferred from it, and the
37
+ // return value has to match. Same for `app.emit`.
38
+ export function setup(app: App): void {
39
+ app.command("add", (args) => {
43
40
  const sum = args.a + args.b;
44
- emit(app, events, "added", sum);
41
+ app.emit("added", sum);
45
42
  return sum;
46
43
  });
47
44
 
48
- on(app, commands, "greet", (args) => {
45
+ app.command("greet", (args) => {
49
46
  return "Hello, " + args.name + " — from the native TS binary";
50
47
  });
51
48
 
52
- on(app, commands, "log", (args) => {
49
+ app.command("log", (args) => {
53
50
  console.log("[host] page says:", args);
54
51
  return null;
55
52
  });
56
53
 
57
54
  // An async command: answers later, without freezing the window.
58
- onAsync(app, commands, "wait", (args, resolve) => {
55
+ app.commandAsync("wait", (args, resolve) => {
59
56
  app.sleep(args.ms, () => {
60
57
  resolve("waited " + args.ms + "ms without blocking the UI");
61
58
  });
62
59
  });
63
60
 
64
- on(app, commands, "quit", (_args) => {
61
+ app.command("quit", (_args) => {
65
62
  app.quit();
66
63
  return null;
67
64
  });
@@ -1,9 +1,10 @@
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;
@@ -12,56 +13,52 @@
12
13
  // an `undefined` argument lowers to a zero-parameter function and fails
13
14
  // to compile.
14
15
 
15
- import {
16
- defineCommands,
17
- defineEvents,
18
- emit,
19
- on,
20
- onAsync,
21
- type JanelaApp,
22
- } from "janela/host";
16
+ import type { JanelaApp } from "janela/host";
23
17
 
24
- export const commands = defineCommands<{
18
+ /** Every command this app answers. Declared once; the page checks against it. */
19
+ export type AppCommands = {
25
20
  add: { args: { a: number; b: number }; result: number };
26
21
  greet: { args: { name: string }; result: string };
27
22
  log: { args: string; result: null };
28
23
  wait: { args: { ms: number }; result: string };
29
24
  quit: { args: null; result: null };
30
- }>();
25
+ };
31
26
 
32
- export const events = defineEvents<{
27
+ /** Every event this app emits, and what each one carries. */
28
+ export type AppEvents = {
33
29
  added: number;
34
- }>();
30
+ };
35
31
 
36
32
  /** The contract the page imports with `import type { App } from "../src-host/main"`. */
37
- export type App = { commands: typeof commands; events: typeof events };
33
+ export type App = JanelaApp<AppCommands, AppEvents>;
38
34
 
39
- export function setup(app: JanelaApp): void {
40
- // `args` is inferred from the contract, and the return type is checked
41
- // against it — no casts, and no way to drift from what the page expects.
42
- on(app, commands, "add", (args) => {
35
+ // Typing the app with the contract is what makes `app.command` checked: the
36
+ // name must be one of the declared ones, `args` is inferred from it, and the
37
+ // return value has to match. Same for `app.emit`.
38
+ export function setup(app: App): void {
39
+ app.command("add", (args) => {
43
40
  const sum = args.a + args.b;
44
- emit(app, events, "added", sum);
41
+ app.emit("added", sum);
45
42
  return sum;
46
43
  });
47
44
 
48
- on(app, commands, "greet", (args) => {
45
+ app.command("greet", (args) => {
49
46
  return "Hello, " + args.name + " — from the native TS binary";
50
47
  });
51
48
 
52
- on(app, commands, "log", (args) => {
49
+ app.command("log", (args) => {
53
50
  console.log("[host] page says:", args);
54
51
  return null;
55
52
  });
56
53
 
57
54
  // An async command: answers later, without freezing the window.
58
- onAsync(app, commands, "wait", (args, resolve) => {
55
+ app.commandAsync("wait", (args, resolve) => {
59
56
  app.sleep(args.ms, () => {
60
57
  resolve("waited " + args.ms + "ms without blocking the UI");
61
58
  });
62
59
  });
63
60
 
64
- on(app, commands, "quit", (_args) => {
61
+ app.command("quit", (_args) => {
65
62
  app.quit();
66
63
  return null;
67
64
  });