janela 0.1.1 → 0.2.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/shim/wvshim.cc CHANGED
@@ -1,21 +1,30 @@
1
- // wvshim.cc — a C-ABI shim over webview.h shaped to scriptc's FFI format 2.
1
+ // wvshim.cc — a C-ABI shim over webview.h shaped to scriptc's FFI format 4.
2
2
  //
3
- // Three constraints drive every design decision here:
4
- // 1. scriptc has no pointer/u64 type, so webview_t can never cross the
5
- // boundary. We keep a handle table and hand out int32 indices.
6
- // 2. Callback params/returns are numeric scalars only, so the request JSON
7
- // cannot be passed as a callback argument. We stage it in a shim-side
8
- // buffer and let TS drain it byte-at-a-time via re-entrant FFI calls.
9
- // 3. Callbacks are lifetime:"call" only. webview_bind wants a retained
10
- // callback, which is not expressible. Instead the TS callback is handed
11
- // to wv_run(), which blocks for the whole app lifetime — so "call scope"
12
- // and "app lifetime" coincide, and bind dispatch happens inside it.
3
+ // One constraint still drives the design: scriptc has no pointer/u64 type, so
4
+ // webview_t can never cross the boundary. We keep a handle table and hand out
5
+ // int32 indices.
6
+ //
7
+ // What the newer formats removed:
8
+ // * format 3 — callback params may be `string`/`bytes`, so a payload crosses
9
+ // into TS as one argument instead of one FFI call per byte. Payloads going
10
+ // the other way ride `string` params on ordinary functions.
11
+ // * format 4 — callbacks may be `retained`, so the invoke and tick handlers
12
+ // are registered once and live for the app's lifetime. wv_run() is a plain
13
+ // blocking call again; it no longer has to carry a callback whose "call
14
+ // scope" was standing in for "app lifetime".
13
15
 
14
16
  #include "webview.h"
15
17
 
18
+ #include <atomic>
19
+ #include <chrono>
16
20
  #include <cstdint>
17
21
  #include <cstring>
22
+ #include <filesystem>
23
+ #include <fstream>
24
+ #include <memory>
25
+ #include <mutex>
18
26
  #include <string>
27
+ #include <thread>
19
28
  #include <vector>
20
29
 
21
30
  namespace {
@@ -24,20 +33,37 @@ struct Bind {
24
33
  std::string name;
25
34
  };
26
35
 
36
+ // An invoke whose webview_return was postponed by wv_defer(): the page's
37
+ // promise stays unsettled until TS calls wv_resolve() on a later turn.
38
+ struct Pending {
39
+ bool used = false;
40
+ std::string call_id;
41
+ };
42
+
27
43
  struct App {
28
44
  webview_t w = nullptr;
29
45
  bool used = false;
30
46
  std::vector<Bind> binds;
31
47
 
32
- // Callback supplied by TS for the duration of wv_run().
33
- int32_t (*cb)(uint32_t, uint32_t, void *) = nullptr;
34
- void *cb_ctx = nullptr;
48
+ // Retained TS handlers, registered once and valid until the app exits. The
49
+ // request rides in as a (ptr, len) string param.
50
+ int32_t (*on_invoke)(const uint8_t *, size_t, void *) = nullptr;
51
+ void *on_invoke_ctx = nullptr;
52
+ void (*on_tick)(void *) = nullptr;
53
+ void *on_tick_ctx = nullptr;
35
54
 
36
55
  // Staging for the in-flight request.
37
56
  std::string req; // JSON args array from JS
38
57
  std::string cur_id; // webview's call id, needed by webview_return
39
- std::string reply; // response body assembled by TS
58
+ std::string reply; // response body handed over by TS in one call
40
59
  uint32_t seq = 0;
60
+
61
+ // ---- async support ----
62
+ std::vector<Pending> pending; // deferred invokes, addressed by index
63
+ bool deferred = false; // set by wv_defer() during the current call
64
+ std::thread ticker; // pure-C++ thread; never touches TS itself
65
+ std::atomic<bool> ticking{false};
66
+ std::atomic<int32_t> tick_ms{16};
41
67
  };
42
68
 
43
69
  // Fixed-size table: handles are indices, never pointers.
@@ -53,26 +79,164 @@ std::string to_str(const uint8_t *p, size_t n) {
53
79
  return std::string(reinterpret_cast<const char *>(p), n);
54
80
  }
55
81
 
56
- // The single C trampoline registered with webview_bind for every binding.
57
- // `arg` encodes the app index and bind index.
82
+ // ---- file I/O jobs ---------------------------------------------------------
83
+ //
84
+ // The whole point of this subsystem is that the blocking syscall happens HERE,
85
+ // on a worker thread, and never on the UI thread. A worker touches only its
86
+ // own job and never calls into TS — scriptc's runtime is not thread-safe, so
87
+ // results cross back on the UI thread, drained by the tick loop.
88
+
89
+ const int32_t FS_PENDING = 0;
90
+ const int32_t FS_OK = 1;
91
+ const int32_t FS_ERROR = 2;
92
+
93
+ struct FsJob {
94
+ // Written by the worker before `status` flips; read by the UI thread only
95
+ // after it observes a terminal status. The release/acquire pair on `status`
96
+ // is what publishes `data`, so no lock is needed for the payload itself.
97
+ std::atomic<int32_t> status{FS_PENDING};
98
+ std::string data; // file contents on success, the error message on failure
99
+ std::thread worker;
100
+ bool used = false;
101
+ };
102
+
103
+ // Jobs are addressed by index and held behind unique_ptr so the vector may
104
+ // grow without invalidating a worker's pointer to its own job.
105
+ std::mutex g_fs_mu;
106
+ std::vector<std::unique_ptr<FsJob>> g_fs_jobs;
107
+
108
+ FsJob *fs_job_at(int32_t id) {
109
+ std::lock_guard<std::mutex> lock(g_fs_mu);
110
+ if (id < 0 || static_cast<size_t>(id) >= g_fs_jobs.size()) return nullptr;
111
+ FsJob *j = g_fs_jobs[id].get();
112
+ return j->used ? j : nullptr;
113
+ }
114
+
115
+ // Reuses a finished slot when one is free, so a long-running app that reads
116
+ // many files does not grow the table without bound.
117
+ int32_t fs_new_job() {
118
+ std::lock_guard<std::mutex> lock(g_fs_mu);
119
+ for (size_t i = 0; i < g_fs_jobs.size(); i++) {
120
+ if (g_fs_jobs[i]->used) continue;
121
+ if (g_fs_jobs[i]->worker.joinable()) g_fs_jobs[i]->worker.join();
122
+ g_fs_jobs[i]->status.store(FS_PENDING);
123
+ g_fs_jobs[i]->data.clear();
124
+ g_fs_jobs[i]->used = true;
125
+ return static_cast<int32_t>(i);
126
+ }
127
+ g_fs_jobs.push_back(std::unique_ptr<FsJob>(new FsJob()));
128
+ g_fs_jobs.back()->used = true;
129
+ return static_cast<int32_t>(g_fs_jobs.size() - 1);
130
+ }
131
+
132
+ // Node-shaped messages: janela apps already surface node:fs errors from
133
+ // synchronous handlers, so the async path should read the same.
134
+ std::string fs_error_message(const std::string &path, const char *op) {
135
+ std::error_code ec;
136
+ auto st = std::filesystem::status(path, ec);
137
+ if (st.type() == std::filesystem::file_type::not_found) {
138
+ return "ENOENT: no such file or directory, " + std::string(op) + " '" +
139
+ path + "'";
140
+ }
141
+ if (st.type() == std::filesystem::file_type::directory) {
142
+ return "EISDIR: illegal operation on a directory, " + std::string(op) +
143
+ " '" + path + "'";
144
+ }
145
+ return "EIO: failed to " + std::string(op) + " '" + path + "'";
146
+ }
147
+
148
+ void fs_finish(FsJob *j, int32_t status, std::string payload) {
149
+ j->data = std::move(payload);
150
+ j->status.store(status, std::memory_order_release);
151
+ }
152
+
153
+ void fs_read_worker(FsJob *j, std::string path) {
154
+ std::error_code ec;
155
+ if (std::filesystem::is_directory(path, ec)) {
156
+ fs_finish(j, FS_ERROR, fs_error_message(path, "read"));
157
+ return;
158
+ }
159
+ std::ifstream in(path, std::ios::binary);
160
+ if (!in) {
161
+ fs_finish(j, FS_ERROR, fs_error_message(path, "open"));
162
+ return;
163
+ }
164
+ std::string buf((std::istreambuf_iterator<char>(in)),
165
+ std::istreambuf_iterator<char>());
166
+ if (in.bad()) {
167
+ fs_finish(j, FS_ERROR, fs_error_message(path, "read"));
168
+ return;
169
+ }
170
+ fs_finish(j, FS_OK, std::move(buf));
171
+ }
172
+
173
+ void fs_write_worker(FsJob *j, std::string path, std::string data) {
174
+ std::ofstream out(path, std::ios::binary | std::ios::trunc);
175
+ if (!out) {
176
+ fs_finish(j, FS_ERROR, fs_error_message(path, "open"));
177
+ return;
178
+ }
179
+ out.write(data.data(), static_cast<std::streamsize>(data.size()));
180
+ out.flush();
181
+ if (!out) {
182
+ fs_finish(j, FS_ERROR, fs_error_message(path, "write"));
183
+ return;
184
+ }
185
+ fs_finish(j, FS_OK, std::string());
186
+ }
187
+
188
+ // Join every worker. Called at shutdown so no thread outlives the process's
189
+ // orderly exit (and so nothing writes into a job after main returns).
190
+ void fs_join_all() {
191
+ std::lock_guard<std::mutex> lock(g_fs_mu);
192
+ for (size_t i = 0; i < g_fs_jobs.size(); i++) {
193
+ if (g_fs_jobs[i]->worker.joinable()) g_fs_jobs[i]->worker.join();
194
+ }
195
+ }
196
+
197
+ // The single C trampoline registered with webview_bind. `arg` is the app
198
+ // index; every binding routes to the one retained invoke handler.
58
199
  void trampoline(const char *id, const char *req, void *arg) {
59
- uintptr_t packed = reinterpret_cast<uintptr_t>(arg);
60
- App *a = &g_apps[packed >> 32];
61
- uint32_t bind_index = static_cast<uint32_t>(packed & 0xffffffffu);
200
+ App *a = &g_apps[reinterpret_cast<uintptr_t>(arg)];
62
201
 
63
- if (!a->cb) return; // no TS handler installed
202
+ if (!a->on_invoke) return; // no TS handler installed
64
203
 
65
204
  a->req = req ? req : "";
66
205
  a->cur_id = id ? id : "";
67
206
  a->reply.clear();
207
+ a->deferred = false;
68
208
  a->seq++;
69
209
 
70
- // Re-entrancy: this call lands back in TS, which will call wv_req_byte()
71
- // etc. back into this shim before returning.
72
- int32_t status = a->cb(bind_index, a->seq, a->cb_ctx);
210
+ // Re-entrancy: this call lands back in TS, which calls wv_reply() back into
211
+ // this shim before returning.
212
+ int32_t status = a->on_invoke(
213
+ reinterpret_cast<const uint8_t *>(a->req.data()), a->req.size(),
214
+ a->on_invoke_ctx);
215
+
216
+ // An async handler called wv_defer(): the call id now lives in the pending
217
+ // table and wv_resolve() will answer the page later. Returning now would
218
+ // settle the promise with a stale value.
219
+ if (a->deferred) {
220
+ a->deferred = false;
221
+ a->cur_id.clear();
222
+ return;
223
+ }
73
224
 
74
225
  webview_return(a->w, a->cur_id.c_str(), status,
75
226
  a->reply.empty() ? "null" : a->reply.c_str());
227
+ // wv_defer() treats a non-empty cur_id as "an invoke is in flight". Clearing
228
+ // it here means a defer from anywhere else — a tick, say — fails with -1
229
+ // instead of stealing this already-answered call's id.
230
+ a->cur_id.clear();
231
+ }
232
+
233
+ // Runs on the UI thread (posted by the ticker via webview_dispatch), so the
234
+ // TS it calls stays single-threaded — scriptc's runtime is NOT thread-safe.
235
+ void tick_on_ui_thread(webview_t, void *arg) {
236
+ App *a = &g_apps[reinterpret_cast<uintptr_t>(arg)];
237
+ if (!a->used || !a->on_tick) return; // app quit between dispatch and delivery
238
+ a->seq++;
239
+ a->on_tick(a->on_tick_ctx);
76
240
  }
77
241
 
78
242
  } // namespace
@@ -84,7 +248,21 @@ int32_t wv_create(int32_t debug) {
84
248
  if (g_apps[i].used) continue;
85
249
  webview_t w = webview_create(debug, nullptr);
86
250
  if (!w) return -1;
87
- g_apps[i] = App{};
251
+ // Field-wise reset: App holds a thread and atomics, so it is not
252
+ // copy-assignable from a temporary.
253
+ g_apps[i].binds.clear();
254
+ g_apps[i].on_invoke = nullptr;
255
+ g_apps[i].on_invoke_ctx = nullptr;
256
+ g_apps[i].on_tick = nullptr;
257
+ g_apps[i].on_tick_ctx = nullptr;
258
+ g_apps[i].req.clear();
259
+ g_apps[i].cur_id.clear();
260
+ g_apps[i].reply.clear();
261
+ g_apps[i].seq = 0;
262
+ g_apps[i].pending.clear();
263
+ g_apps[i].deferred = false;
264
+ g_apps[i].ticking.store(false);
265
+ g_apps[i].tick_ms.store(16);
88
266
  g_apps[i].w = w;
89
267
  g_apps[i].used = true;
90
268
  return i;
@@ -133,53 +311,186 @@ int32_t wv_eval(int32_t h, const uint8_t *p, size_t n) {
133
311
  int32_t wv_bind(int32_t h, const uint8_t *p, size_t n) {
134
312
  App *a = app_at(h);
135
313
  if (!a) return -1;
136
- uint32_t idx = static_cast<uint32_t>(a->binds.size());
314
+ size_t idx = a->binds.size();
137
315
  a->binds.push_back(Bind{to_str(p, n)});
138
- uintptr_t packed = (static_cast<uintptr_t>(h) << 32) | idx;
139
316
  int rc = webview_bind(a->w, a->binds[idx].name.c_str(), trampoline,
140
- reinterpret_cast<void *>(packed));
317
+ reinterpret_cast<void *>(static_cast<uintptr_t>(h)));
141
318
  if (rc != WEBVIEW_ERROR_OK) return -1;
142
319
  return static_cast<int32_t>(idx);
143
320
  }
144
321
 
145
- // ---- request payload, drained byte-at-a-time from inside the TS callback ----
146
- int32_t wv_req_len(int32_t h) {
322
+ // Stage the response body for the call being handled (or, after wv_defer, for
323
+ // the pending call that wv_resolve will answer). One call, whole payload.
324
+ int32_t wv_reply(int32_t h, const uint8_t *p, size_t n) {
147
325
  App *a = app_at(h);
148
326
  if (!a) return -1;
149
- return static_cast<int32_t>(a->req.size());
327
+ a->reply.assign(reinterpret_cast<const char *>(p), n);
328
+ return 0;
150
329
  }
151
330
 
152
- int32_t wv_req_byte(int32_t h, int32_t i) {
331
+ // ---- async: deferred returns + a UI-thread pump -----------------------------
332
+ //
333
+ // scriptc's own event loop does not run while the program sits inside an FFI
334
+ // call, and wv_run() is one such call for the app's whole life — so setTimeout
335
+ // and promise continuations in TS never fire while the window is open. These
336
+ // four functions supply the missing loop: TS may postpone an invoke's answer
337
+ // (wv_defer), answer it later (wv_resolve), and get called back periodically
338
+ // on the UI thread to make progress (wv_tick_start / wv_tick_stop).
339
+
340
+ // Postpone the answer to the invoke being handled right now. Returns a
341
+ // pending id to hand back to wv_resolve(), or -1 outside a bind callback.
342
+ int32_t wv_defer(int32_t h) {
153
343
  App *a = app_at(h);
154
- if (!a || i < 0 || static_cast<size_t>(i) >= a->req.size()) return -1;
155
- return static_cast<uint8_t>(a->req[i]);
344
+ if (!a || a->cur_id.empty()) return -1;
345
+ for (size_t i = 0; i < a->pending.size(); i++) {
346
+ if (!a->pending[i].used) {
347
+ a->pending[i].used = true;
348
+ a->pending[i].call_id = a->cur_id;
349
+ a->deferred = true;
350
+ return static_cast<int32_t>(i);
351
+ }
352
+ }
353
+ a->pending.push_back(Pending{true, a->cur_id});
354
+ a->deferred = true;
355
+ return static_cast<int32_t>(a->pending.size() - 1);
156
356
  }
157
357
 
158
- // ---- reply payload, pushed byte-at-a-time from inside the TS callback ----
159
- int32_t wv_reply_reset(int32_t h) {
358
+ // Answer a deferred invoke with whatever TS has staged via wv_reply_push().
359
+ // status 0 resolves the page's promise, non-zero rejects it.
360
+ int32_t wv_resolve(int32_t h, int32_t id, int32_t status) {
160
361
  App *a = app_at(h);
161
- if (!a) return -1;
362
+ if (!a || id < 0 || static_cast<size_t>(id) >= a->pending.size()) return -1;
363
+ Pending &p = a->pending[id];
364
+ if (!p.used) return -1;
365
+ webview_return(a->w, p.call_id.c_str(), status,
366
+ a->reply.empty() ? "null" : a->reply.c_str());
367
+ p.used = false;
368
+ p.call_id.clear();
162
369
  a->reply.clear();
163
370
  return 0;
164
371
  }
165
372
 
166
- int32_t wv_reply_push(int32_t h, int32_t byte) {
373
+ // Start pumping the retained tick handler every interval_ms. The thread
374
+ // itself only sleeps and posts; all TS execution happens on the UI thread.
375
+ int32_t wv_tick_start(int32_t h, int32_t interval_ms) {
376
+ App *a = app_at(h);
377
+ if (!a) return -1;
378
+ a->tick_ms = interval_ms > 0 ? interval_ms : 16;
379
+ if (a->ticking.exchange(true)) return 0; // already running
380
+ uintptr_t idx = static_cast<uintptr_t>(h);
381
+ a->ticker = std::thread([a, idx]() {
382
+ while (a->ticking.load()) {
383
+ std::this_thread::sleep_for(
384
+ std::chrono::milliseconds(a->tick_ms.load()));
385
+ if (!a->ticking.load()) break;
386
+ webview_dispatch(a->w, tick_on_ui_thread, reinterpret_cast<void *>(idx));
387
+ }
388
+ });
389
+ return 0;
390
+ }
391
+
392
+ int32_t wv_tick_stop(int32_t h) {
393
+ App *a = app_at(h);
394
+ if (!a) return -1;
395
+ if (!a->ticking.exchange(false)) return 0;
396
+ if (a->ticker.joinable()) a->ticker.join();
397
+ return 0;
398
+ }
399
+
400
+ // ---- async file I/O ---------------------------------------------------------
401
+ //
402
+ // wv_fs_read/wv_fs_write start a worker thread and return immediately with a
403
+ // job id. TS polls wv_fs_status() from its tick loop and drains the payload
404
+ // with wv_fs_byte() once the job is terminal. On failure the payload is the
405
+ // error message, so success and failure share one drain path.
406
+
407
+ int32_t wv_fs_read(int32_t h, const uint8_t *p, size_t n) {
408
+ if (!app_at(h)) return -1;
409
+ int32_t id = fs_new_job();
410
+ FsJob *j = fs_job_at(id);
411
+ if (!j) return -1;
412
+ j->worker = std::thread(fs_read_worker, j, to_str(p, n));
413
+ return id;
414
+ }
415
+
416
+ int32_t wv_fs_write(int32_t h, const uint8_t *p, size_t n, const uint8_t *dp,
417
+ size_t dn) {
418
+ if (!app_at(h)) return -1;
419
+ int32_t id = fs_new_job();
420
+ FsJob *j = fs_job_at(id);
421
+ if (!j) return -1;
422
+ j->worker = std::thread(fs_write_worker, j, to_str(p, n), to_str(dp, dn));
423
+ return id;
424
+ }
425
+
426
+ // 0 = still running, 1 = done, 2 = failed, -1 = no such job.
427
+ int32_t wv_fs_status(int32_t h, int32_t id) {
428
+ if (!app_at(h)) return -1;
429
+ FsJob *j = fs_job_at(id);
430
+ if (!j) return -1;
431
+ return j->status.load(std::memory_order_acquire);
432
+ }
433
+
434
+ // Hand a finished job's payload to TS in one call. The callback is
435
+ // lifetime:"call", so it runs synchronously here — on the UI thread, the only
436
+ // thread allowed to touch the scriptc runtime. The worker is already done by
437
+ // then (the caller has observed a terminal status), so `data` is stable.
438
+ int32_t wv_fs_take(int32_t h, int32_t id,
439
+ void (*sink)(const uint8_t *, size_t, void *), void *ctx) {
440
+ if (!app_at(h)) return -1;
441
+ FsJob *j = fs_job_at(id);
442
+ if (!j || !sink) return -1;
443
+ if (j->status.load(std::memory_order_acquire) == FS_PENDING) return -1;
444
+ sink(reinterpret_cast<const uint8_t *>(j->data.data()), j->data.size(), ctx);
445
+ return 0;
446
+ }
447
+
448
+ // Release the slot for reuse. Refuses while the worker is still running, so a
449
+ // job's buffer can never be recycled out from under its own thread.
450
+ int32_t wv_fs_free(int32_t h, int32_t id) {
451
+ if (!app_at(h)) return -1;
452
+ FsJob *j = fs_job_at(id);
453
+ if (!j) return -1;
454
+ if (j->status.load(std::memory_order_acquire) == FS_PENDING) return -1;
455
+ std::lock_guard<std::mutex> lock(g_fs_mu);
456
+ if (j->worker.joinable()) j->worker.join();
457
+ j->data.clear();
458
+ j->data.shrink_to_fit();
459
+ j->used = false;
460
+ return 0;
461
+ }
462
+
463
+ // Register the retained handler for page invokes. Valid until the app exits.
464
+ int32_t wv_on_invoke(int32_t h,
465
+ int32_t (*cb)(const uint8_t *, size_t, void *),
466
+ void *ctx) {
467
+ App *a = app_at(h);
468
+ if (!a) return -1;
469
+ a->on_invoke = cb;
470
+ a->on_invoke_ctx = ctx;
471
+ return 0;
472
+ }
473
+
474
+ // Register the retained handler the ticker pumps on the UI thread.
475
+ int32_t wv_on_tick(int32_t h, void (*cb)(void *), void *ctx) {
167
476
  App *a = app_at(h);
168
477
  if (!a) return -1;
169
- a->reply.push_back(static_cast<char>(byte & 0xff));
478
+ a->on_tick = cb;
479
+ a->on_tick_ctx = ctx;
170
480
  return 0;
171
481
  }
172
482
 
173
- // Blocks for the app's lifetime; dispatches bind calls into `cb`.
174
- int32_t wv_run(int32_t h, int32_t (*cb)(uint32_t, uint32_t, void *),
175
- void *cb_ctx) {
483
+ // Blocks for the app's lifetime, dispatching into the retained handlers.
484
+ int32_t wv_run(int32_t h) {
176
485
  App *a = app_at(h);
177
486
  if (!a) return -1;
178
- a->cb = cb;
179
- a->cb_ctx = cb_ctx;
180
487
  int rc = webview_run(a->w);
181
- a->cb = nullptr; // callback must not outlive the call, per lifetime:"call"
182
- a->cb_ctx = nullptr;
488
+ // Nothing may call into TS once run() has returned.
489
+ a->ticking.store(false);
490
+ if (a->ticker.joinable()) a->ticker.join();
491
+ fs_join_all(); // nor may an in-flight read outlive the app
492
+ a->on_invoke = nullptr;
493
+ a->on_tick = nullptr;
183
494
  return rc;
184
495
  }
185
496
 
@@ -192,6 +503,8 @@ int32_t wv_terminate(int32_t h) {
192
503
  int32_t wv_destroy(int32_t h) {
193
504
  App *a = app_at(h);
194
505
  if (!a) return -1;
506
+ a->ticking.store(false);
507
+ if (a->ticker.joinable()) a->ticker.join();
195
508
  webview_destroy(a->w);
196
509
  a->used = false;
197
510
  a->w = nullptr;
@@ -14,8 +14,13 @@
14
14
  <input id="a" type="number" value="2" /> +
15
15
  <input id="b" type="number" value="40" />
16
16
  <button id="add">add</button>
17
+ <button id="wait">wait 2s (async)</button>
17
18
  <button id="quit">Quit</button>
18
19
  </p>
20
+ <p>
21
+ <input id="path" type="text" value="janela.conf.json" size="32" />
22
+ <button id="read">read file (async)</button>
23
+ </p>
19
24
  <pre id="out">booting…</pre>
20
25
  <ul id="events"></ul>
21
26
 
@@ -41,6 +46,25 @@
41
46
  const sum = await janela.invoke("add", { a, b });
42
47
  out.textContent = `add(${a}, ${b}) -> ${sum}`;
43
48
  };
49
+ // The window stays responsive while this is pending: the spinner keeps
50
+ // animating and "add" still works.
51
+ document.getElementById("wait").onclick = async (e) => {
52
+ e.target.disabled = true;
53
+ out.textContent = "waiting… (try 'add' — it still answers)";
54
+ out.textContent = await janela.invoke("wait", { ms: 2000 });
55
+ e.target.disabled = false;
56
+ };
57
+
58
+ // Reads run on a worker thread in the shim, so even a large file
59
+ // does not freeze the window while it loads.
60
+ document.getElementById("read").onclick = async () => {
61
+ const path = document.getElementById("path").value;
62
+ const r = await janela.invoke("readFile", { path });
63
+ out.textContent = r.ok
64
+ ? `${path}: ${r.length} chars\n\n` + r.text.slice(0, 400)
65
+ : "error: " + r.error;
66
+ };
67
+
44
68
  document.getElementById("quit").onclick = () => janela.invoke("quit");
45
69
  };
46
70
  </script>
package/templates/main.ts CHANGED
@@ -1,37 +1,57 @@
1
1
  // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
2
  //
3
3
  // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
- // Handlers receive the args as JSON text and return JSON text (what the
5
- // frontend promise resolves with).
4
+ // Handlers take the arguments as a value and return a value — the runtime owns
5
+ // JSON at the boundary, so there is no parsing or stringifying to do here.
6
6
  //
7
- // Gotcha inherited from scriptc 0.0.32: never use a bare FFI-backed call as a
7
+ // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
8
  // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
9
  // TypeScript like everything in this file is unaffected.
10
10
 
11
11
  import type { JanelaApp } from "./janela";
12
12
 
13
13
  export function setup(app: JanelaApp): void {
14
- app.command("add", (argsJson) => {
15
- const a = JSON.parse(argsJson) as { a: number; b: number };
14
+ app.command("add", (args) => {
15
+ const a = args as { a: number; b: number };
16
16
  const sum = a.a + a.b;
17
17
  // Backend→frontend event, just to show the channel exists.
18
- app.emit("added", JSON.stringify(sum));
19
- return JSON.stringify(sum);
18
+ app.emit("added", sum);
19
+ return sum;
20
20
  });
21
21
 
22
- app.command("greet", (argsJson) => {
23
- const a = JSON.parse(argsJson) as { name: string };
24
- return JSON.stringify("Hello, " + a.name + " — from the native TS binary");
22
+ app.command("greet", (args) => {
23
+ const a = args as { name: string };
24
+ return "Hello, " + a.name + " — from the native TS binary";
25
25
  });
26
26
 
27
- app.command("log", (argsJson) => {
28
- const a = JSON.parse(argsJson) as string;
29
- console.log("[host] page says:", a);
30
- return "null";
27
+ app.command("log", (args) => {
28
+ console.log("[host] page says:", args as string);
31
29
  });
32
30
 
33
- app.command("quit", (_argsJson) => {
31
+ // An async command: answers later, without freezing the window. The page
32
+ // still just does `await janela.invoke("wait", { ms: 1000 })`.
33
+ app.commandAsync("wait", (args, resolve) => {
34
+ const a = args as { ms: number };
35
+ app.sleep(a.ms, () => {
36
+ resolve("waited " + a.ms + "ms without blocking the UI");
37
+ });
38
+ });
39
+
40
+ // File I/O without freezing the window: the read happens on a worker thread
41
+ // in the shim. Use this instead of node:fs readFileSync, which blocks the
42
+ // host loop — and with it the whole UI — until the syscall returns.
43
+ app.commandAsync("readFile", (args, resolve) => {
44
+ const a = args as { path: string };
45
+ app.readFileAsync(a.path, (err, text) => {
46
+ if (err !== null) {
47
+ resolve({ ok: false, error: err });
48
+ return;
49
+ }
50
+ resolve({ ok: true, length: text.length, text: text });
51
+ });
52
+ });
53
+
54
+ app.command("quit", (_args) => {
34
55
  app.quit();
35
- return "null";
36
56
  });
37
57
  }