@bytecodealliance/preview2-shim 0.14.2 → 0.15.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.
@@ -3,6 +3,9 @@ import { createSyncFn } from "../synckit/index.js";
3
3
  import {
4
4
  CALL_MASK,
5
5
  CALL_TYPE_MASK,
6
+ FILE,
7
+ HTTP_SERVER_INCOMING_HANDLER,
8
+ HTTP,
6
9
  INPUT_STREAM_BLOCKING_READ,
7
10
  INPUT_STREAM_BLOCKING_SKIP,
8
11
  INPUT_STREAM_DISPOSE,
@@ -22,50 +25,62 @@ import {
22
25
  OUTPUT_STREAM_WRITE,
23
26
  POLL_POLL_LIST,
24
27
  POLL_POLLABLE_BLOCK,
28
+ POLL_POLLABLE_DISPOSE,
25
29
  POLL_POLLABLE_READY,
26
- HTTP_SERVER_INCOMING_HANDLER,
27
- callTypeMap,
28
- callMap
30
+ SOCKET_TCP,
31
+ STDERR,
32
+ STDIN,
33
+ STDOUT,
34
+ reverseMap,
29
35
  } from "./calls.js";
30
- import { STDERR } from "./calls.js";
31
-
32
- const DEBUG = false;
36
+ import { _rawDebug, exit, stderr, stdout, env } from "node:process";
33
37
 
34
38
  const workerPath = fileURLToPath(
35
39
  new URL("./worker-thread.js", import.meta.url)
36
40
  );
37
41
 
38
42
  const httpIncomingHandlers = new Map();
39
- export function registerIncomingHttpHandler (id, handler) {
43
+ export function registerIncomingHttpHandler(id, handler) {
40
44
  httpIncomingHandlers.set(id, handler);
41
45
  }
42
46
 
43
47
  const instanceId = Math.round(Math.random() * 1000).toString();
48
+ const DEBUG_DEFAULT = false;
49
+ const DEBUG =
50
+ env.PREVIEW2_SHIM_DEBUG === "0"
51
+ ? false
52
+ : env.PREVIEW2_SHIM_DEBUG === "1"
53
+ ? true
54
+ : DEBUG_DEFAULT;
44
55
 
45
56
  /**
46
57
  * @type {(call: number, id: number | null, payload: any) -> any}
47
58
  */
48
- export let ioCall = createSyncFn(workerPath, (type, id, payload) => {
59
+ export let ioCall = createSyncFn(workerPath, DEBUG, (type, id, payload) => {
49
60
  // 'callbacks' from the worker
50
61
  // ONLY happens for an http server incoming handler, and NOTHING else (not even sockets, since accept is sync!)
51
62
  if (type !== HTTP_SERVER_INCOMING_HANDLER)
52
- throw new Error('Internal error: only incoming handler callback is permitted');
63
+ throw new Error(
64
+ "Internal error: only incoming handler callback is permitted"
65
+ );
53
66
  const handler = httpIncomingHandlers.get(id);
54
67
  if (!handler)
55
- throw new Error(`Internal error: no incoming handler registered for server ${id}`);
68
+ throw new Error(
69
+ `Internal error: no incoming handler registered for server ${id}`
70
+ );
56
71
  handler(payload);
57
72
  });
58
73
  if (DEBUG) {
59
74
  const _ioCall = ioCall;
60
75
  ioCall = function ioCall(num, id, payload) {
61
- if (typeof id !== 'number' && id !== null)
62
- throw new Error('id must be a number or null');
76
+ if (typeof id !== "number" && id !== null)
77
+ throw new Error("id must be a number or null");
63
78
  let ret;
64
79
  try {
65
- console.error(
80
+ _rawDebug(
66
81
  instanceId,
67
- callMap[(num & CALL_MASK)],
68
- callTypeMap[num & CALL_TYPE_MASK],
82
+ reverseMap[num & CALL_MASK],
83
+ reverseMap[num & CALL_TYPE_MASK],
69
84
  id,
70
85
  payload
71
86
  );
@@ -75,16 +90,53 @@ if (DEBUG) {
75
90
  ret = e;
76
91
  throw ret;
77
92
  } finally {
78
- console.error(instanceId, "->", ret);
93
+ _rawDebug(instanceId, "->", ret);
79
94
  }
80
95
  };
81
96
  }
82
97
 
83
98
  const symbolDispose = Symbol.dispose || Symbol.for("dispose");
84
99
 
100
+ const finalizationRegistry = new FinalizationRegistry(
101
+ (dispose) => void dispose()
102
+ );
103
+
104
+ const dummySymbol = Symbol();
105
+
106
+ /**
107
+ *
108
+ * @param {any} resource
109
+ * @param {any} parentResource
110
+ * @param {number} id
111
+ * @param {(number) => void} disposeFn
112
+ */
113
+ export function registerDispose(resource, parentResource, id, disposeFn) {
114
+ // While strictly speaking all components should handle their disposal,
115
+ // this acts as a last-resort to catch all missed drops through the JS GC.
116
+ // Mainly for two cases - (1) components which are long lived, that get shut
117
+ // down and (2) users that interface with low-level WASI APIs directly in JS
118
+ // for various reasons may end up leaning on JS GC inadvertantly.
119
+ function finalizer() {
120
+ // This has no functional purpose other than to pin a strong reference
121
+ // from the child resource's finalizer to the parent resource, to ensure
122
+ // that we can never finalize a parent resource before a child resource.
123
+ // This makes the generational JS GC become piecewise over child resource
124
+ // graphs (generational at each resource hierarchy level at least).
125
+ if (parentResource?.[dummySymbol]) return;
126
+ disposeFn(id);
127
+ }
128
+ finalizationRegistry.register(resource, finalizer, finalizer);
129
+ return finalizer;
130
+ }
131
+
132
+ export function earlyDispose(finalizer) {
133
+ finalizationRegistry.unregister(finalizer);
134
+ finalizer();
135
+ }
136
+
85
137
  const _Error = Error;
86
138
  const IoError = class Error extends _Error {
87
- constructor (payload) {
139
+ constructor(payload) {
88
140
  super(payload);
89
141
  this.payload = payload;
90
142
  }
@@ -104,16 +156,14 @@ function streamIoErrorCall(call, id, payload) {
104
156
  }
105
157
  // any invalid error is a trap
106
158
  console.trace(e);
107
- process.exit(1);
159
+ exit(1);
108
160
  }
109
161
  }
110
162
 
111
163
  class InputStream {
112
164
  #id;
113
165
  #streamType;
114
- get _id() {
115
- return this.#id;
116
- }
166
+ #finalizer;
117
167
  read(len) {
118
168
  return streamIoErrorCall(
119
169
  INPUT_STREAM_READ | this.#streamType,
@@ -144,24 +194,65 @@ class InputStream {
144
194
  }
145
195
  subscribe() {
146
196
  return pollableCreate(
147
- ioCall(INPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id)
197
+ ioCall(INPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id),
198
+ this
148
199
  );
149
200
  }
150
- [symbolDispose]() {
151
- ioCall(INPUT_STREAM_DISPOSE | this.#streamType, this.#id);
152
- }
153
201
  static _id(stream) {
154
202
  return stream.#id;
155
203
  }
156
204
  /**
157
- * @param {InputStreamType} streamType
205
+ * @param {FILE | SOCKET_TCP | STDIN | HTTP} streamType
158
206
  */
159
207
  static _create(streamType, id) {
160
208
  const stream = new InputStream();
161
209
  stream.#id = id;
162
210
  stream.#streamType = streamType;
211
+ let disposeFn;
212
+ switch (streamType) {
213
+ case FILE:
214
+ disposeFn = fileInputStreamDispose;
215
+ break;
216
+ case SOCKET_TCP:
217
+ disposeFn = socketTcpInputStreamDispose;
218
+ break;
219
+ case STDIN:
220
+ disposeFn = stdinInputStreamDispose;
221
+ break;
222
+ case HTTP:
223
+ disposeFn = httpInputStreamDispose;
224
+ break;
225
+ default:
226
+ throw new Error(
227
+ "wasi-io trap: Dispose function not created for stream type " +
228
+ reverseMap[streamType]
229
+ );
230
+ }
231
+ stream.#finalizer = registerDispose(stream, null, id, disposeFn);
163
232
  return stream;
164
233
  }
234
+ [symbolDispose]() {
235
+ if (this.#finalizer) {
236
+ earlyDispose(this.#finalizer);
237
+ this.#finalizer = null;
238
+ }
239
+ }
240
+ }
241
+
242
+ function fileInputStreamDispose(id) {
243
+ ioCall(INPUT_STREAM_DISPOSE | FILE, id, null);
244
+ }
245
+
246
+ function socketTcpInputStreamDispose(id) {
247
+ ioCall(INPUT_STREAM_DISPOSE | SOCKET_TCP, id, null);
248
+ }
249
+
250
+ function stdinInputStreamDispose(id) {
251
+ ioCall(INPUT_STREAM_DISPOSE | STDIN, id, null);
252
+ }
253
+
254
+ function httpInputStreamDispose(id) {
255
+ ioCall(INPUT_STREAM_DISPOSE | HTTP, id, null);
165
256
  }
166
257
 
167
258
  export const inputStreamCreate = InputStream._create;
@@ -173,9 +264,7 @@ delete InputStream._id;
173
264
  class OutputStream {
174
265
  #id;
175
266
  #streamType;
176
- get _id() {
177
- return this.#id;
178
- }
267
+ #finalizer;
179
268
  checkWrite(len) {
180
269
  return streamIoErrorCall(
181
270
  OUTPUT_STREAM_CHECK_WRITE | this.#streamType,
@@ -193,8 +282,7 @@ class OutputStream {
193
282
  }
194
283
  blockingWriteAndFlush(buf) {
195
284
  if (this.#streamType <= STDERR) {
196
- const stream =
197
- this.#streamType === STDERR ? process.stderr : process.stdout;
285
+ const stream = this.#streamType === STDERR ? stderr : stdout;
198
286
  return void stream.write(buf);
199
287
  }
200
288
  return streamIoErrorCall(
@@ -245,9 +333,6 @@ class OutputStream {
245
333
  ioCall(OUTPUT_STREAM_SUBSCRIBE | this.#streamType, this.#id)
246
334
  );
247
335
  }
248
- [symbolDispose]() {
249
- ioCall(OUTPUT_STREAM_DISPOSE | this.#streamType, this.#id);
250
- }
251
336
 
252
337
  static _id(outputStream) {
253
338
  return outputStream.#id;
@@ -260,8 +345,54 @@ class OutputStream {
260
345
  const stream = new OutputStream();
261
346
  stream.#id = id;
262
347
  stream.#streamType = streamType;
348
+ let disposeFn;
349
+ switch (streamType) {
350
+ case STDOUT:
351
+ disposeFn = stdoutOutputStreamDispose;
352
+ break;
353
+ case STDERR:
354
+ disposeFn = stderrOutputStreamDispose;
355
+ break;
356
+ case SOCKET_TCP:
357
+ disposeFn = socketTcpOutputStreamDispose;
358
+ break;
359
+ case FILE:
360
+ disposeFn = fileOutputStreamDispose;
361
+ break;
362
+ case HTTP:
363
+ return stream;
364
+ default:
365
+ throw new Error(
366
+ "wasi-io trap: Dispose function not created for stream type " +
367
+ reverseMap[streamType]
368
+ );
369
+ }
370
+ stream.#finalizer = registerDispose(stream, null, id, disposeFn);
263
371
  return stream;
264
372
  }
373
+
374
+ [symbolDispose]() {
375
+ if (this.#finalizer) {
376
+ earlyDispose(this.#finalizer);
377
+ this.#finalizer = null;
378
+ }
379
+ }
380
+ }
381
+
382
+ function stdoutOutputStreamDispose(id) {
383
+ ioCall(OUTPUT_STREAM_DISPOSE | STDOUT, id);
384
+ }
385
+
386
+ function stderrOutputStreamDispose(id) {
387
+ ioCall(OUTPUT_STREAM_DISPOSE | STDERR, id);
388
+ }
389
+
390
+ function socketTcpOutputStreamDispose(id) {
391
+ ioCall(OUTPUT_STREAM_DISPOSE | SOCKET_TCP, id);
392
+ }
393
+
394
+ function fileOutputStreamDispose(id) {
395
+ ioCall(OUTPUT_STREAM_DISPOSE | FILE, id);
265
396
  }
266
397
 
267
398
  export const outputStreamCreate = OutputStream._create;
@@ -274,27 +405,42 @@ export const error = { Error: IoError };
274
405
 
275
406
  export const streams = { InputStream, OutputStream };
276
407
 
408
+ function pollableDispose(id) {
409
+ ioCall(POLL_POLLABLE_DISPOSE, id);
410
+ }
411
+
277
412
  class Pollable {
278
413
  #id;
279
- get _id() {
280
- return this.#id;
281
- }
414
+ #finalizer;
282
415
  ready() {
283
416
  if (this.#id === 0) return true;
284
417
  return ioCall(POLL_POLLABLE_READY, this.#id);
285
418
  }
286
419
  block() {
287
- if (this.#id === 0) return;
288
- ioCall(POLL_POLLABLE_BLOCK, this.#id);
420
+ if (this.#id !== 0) {
421
+ ioCall(POLL_POLLABLE_BLOCK, this.#id);
422
+ }
289
423
  }
290
424
  static _getId(pollable) {
291
425
  return pollable.#id;
292
426
  }
293
- static _create(id) {
427
+ static _create(id, parent) {
294
428
  const pollable = new Pollable();
295
429
  pollable.#id = id;
430
+ pollable.#finalizer = registerDispose(
431
+ pollable,
432
+ parent,
433
+ id,
434
+ pollableDispose
435
+ );
296
436
  return pollable;
297
437
  }
438
+ [symbolDispose]() {
439
+ if (this.#finalizer) {
440
+ earlyDispose(this.#finalizer);
441
+ this.#finalizer = null;
442
+ }
443
+ }
298
444
  }
299
445
 
300
446
  export const pollableCreate = Pollable._create;
@@ -310,10 +456,6 @@ export const poll = {
310
456
  },
311
457
  };
312
458
 
313
- export function resolvedPoll() {
314
- return pollableCreate(0);
315
- }
316
-
317
459
  export function createPoll(call, id, initPayload) {
318
460
  return pollableCreate(ioCall(call, id, initPayload));
319
461
  }