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/README.md +156 -14
- package/bin/janela.mjs +148 -13
- package/package.json +3 -3
- package/runtime/janela.ts +248 -101
- package/shim/wvshim.cc +359 -46
- package/templates/index.html +24 -0
- package/templates/main.ts +36 -16
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
|
|
1
|
+
// wvshim.cc — a C-ABI shim over webview.h shaped to scriptc's FFI format 4.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
-
//
|
|
33
|
-
|
|
34
|
-
void *
|
|
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
|
|
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
|
-
//
|
|
57
|
-
//
|
|
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
|
-
|
|
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->
|
|
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
|
|
71
|
-
//
|
|
72
|
-
int32_t status = a->
|
|
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
|
-
|
|
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
|
-
|
|
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 *>(
|
|
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
|
-
//
|
|
146
|
-
|
|
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
|
-
|
|
327
|
+
a->reply.assign(reinterpret_cast<const char *>(p), n);
|
|
328
|
+
return 0;
|
|
150
329
|
}
|
|
151
330
|
|
|
152
|
-
|
|
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 ||
|
|
155
|
-
|
|
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
|
-
//
|
|
159
|
-
|
|
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
|
-
|
|
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->
|
|
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
|
|
174
|
-
int32_t wv_run(int32_t h
|
|
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
|
-
|
|
182
|
-
a->
|
|
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;
|
package/templates/index.html
CHANGED
|
@@ -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
|
|
5
|
-
//
|
|
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
|
|
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", (
|
|
15
|
-
const a =
|
|
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",
|
|
19
|
-
return
|
|
18
|
+
app.emit("added", sum);
|
|
19
|
+
return sum;
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
app.command("greet", (
|
|
23
|
-
const a =
|
|
24
|
-
return
|
|
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", (
|
|
28
|
-
|
|
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
|
-
|
|
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
|
}
|