char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport workers extension to typescriptb7bd857

main
9.5 KiB351 linesraw
1import {
2  ArrayPrototypePush as push,
3  Error,
4  ObjectCreate,
5  ObjectDefineProperty as define,
6  ReflectApply as apply,
7  ReflectConstruct as construct,
8  TypeError,
9  WeakMap,
10  WeakMapPrototypeGet as get,
11  WeakMapPrototypeSet as set,
12} from "ext:core/primordials";
13import { promiseThen as then } from "ext:core/promises";
14import { enqueueTask } from "ext:core/tasks";
15import { onRuntimeStart } from "ext:core/bootstrap";
16import { clone } from "ext:core/structured_clone";
17import {
18  op_worker_base,
19  op_worker_close,
20  op_worker_create,
21  op_worker_metadata,
22  op_worker_terminate,
23  op_worker_wait,
24} from "ext:core/ops";
25import { array, dictionary, required, usv } from "ext:web/idl.js";
26import { installInterface } from "ext:web/interface.js";
27import {
28  addListener,
29  dataCloneError,
30  dispatch,
31  ErrorEvent,
32  EventTarget,
33  preventDefault,
34  removeListener,
35  reportErrorValue,
36} from "ext:web/events.js";
37import {
38  adoptPort,
39  dispatchMessage,
40  isMessagePort,
41  messageData,
42  MessageEvent,
43  MessagePort,
44  messagePorts,
45  startPort,
46  transferList,
47} from "ext:web/messaging.js";
48import {
49  classify,
50  registerUncloneable,
51  transferCallbacks,
52} from "ext:web/serialization.js";
53import { avril } from "ext:base/main.js";
54import { preparePledge } from "ext:base/pledge.js";
55import { URL } from "ext:url/main.js";
56
57type HandlerName = "onmessage" | "onmessageerror" | "onerror";
58type HandlerState = Partial<
59  & Record<HandlerName, Function | null>
60  & Record<
61    `${HandlerName}Listener`,
62    ((event: import("ext:web/events.js").Event) => void) | null
63  >
64>;
65interface WorkerState extends HandlerState {
66  id: number;
67  port: MessagePort;
68  terminated: boolean;
69}
70type LocationState = Record<
71  | "href"
72  | "origin"
73  | "protocol"
74  | "host"
75  | "hostname"
76  | "port"
77  | "pathname"
78  | "search"
79  | "hash",
80  string
81>;
82const workers = new WeakMap<object, WorkerState>(),
83  locations = new WeakMap<object, LocationState>();
84registerUncloneable(workers);
85registerUncloneable(locations);
86const post = MessagePort.prototype.postMessage;
87const closePort = MessagePort.prototype.close;
88
89function state(value: unknown): WorkerState {
90  const result = get(workers, value);
91  if (!result) throw new TypeError("Illegal invocation");
92  return result;
93}
94
95function handler(
96  target: unknown,
97  current: HandlerState,
98  name: HandlerName,
99  type: string,
100  value: unknown,
101): void {
102  value = typeof value === "function" ? value : null;
103  current[name] = value as Function | null;
104  const key = name + "Listener" as `${HandlerName}Listener`;
105  if (!value && current[key]) {
106    removeListener(target, type, current[key]);
107    current[key] = null;
108  } else if (value && !current[key]) {
109    current[key] = (event) => {
110      if (current[name] && apply(current[name]!, target, [event]) === false) {
111        preventDefault(event);
112      }
113    };
114    addListener(target, type, current[key]);
115  }
116}
117
118class Worker extends EventTarget {
119  // @ts-expect-error Reflect.construct preserves new.target without a guest-replaceable super() call.
120  constructor(scriptURL: unknown, options: unknown = {}) {
121    required(arguments.length, 1);
122    const specifier = usv(scriptURL), init = dictionary(options);
123    const type = init.type === undefined ? "classic" : usv(init.type);
124    if (type !== "module") {
125      throw new TypeError("avril supports module workers only");
126    }
127    const name = init.name === undefined ? "" : usv(init.name);
128    const extension = dictionary(init[avril]);
129    const restrictions = extension.caps === undefined
130      ? []
131      : preparePledge(extension.caps);
132    const self = construct(EventTarget, [], new.target) as Worker;
133    const created = op_worker_create([
134      specifier,
135      op_worker_base(),
136      name,
137      restrictions,
138    ]);
139    const port = adoptPort(created[1]);
140    const current = ObjectCreate(null) as WorkerState;
141    current.id = created[0];
142    current.port = port;
143    current.terminated = false;
144    set(workers, self, current);
145    addListener(port, "message", (event) => {
146      if (!current.terminated) {
147        dispatchMessage(self, messageData(event), messagePorts(event));
148      }
149    });
150    addListener(port, "messageerror", () => {
151      if (!current.terminated) {
152        dispatch(self, new MessageEvent("messageerror", ObjectCreate(null)));
153      }
154    });
155    startPort(port);
156    then(op_worker_wait(current.id), (message) => {
157      enqueueTask(() => {
158        if (!current.terminated && message !== null) {
159          const init = ObjectCreate(null);
160          init.message = message;
161          init.cancelable = true;
162          const event = new ErrorEvent("error", init);
163          if (dispatch(self, event)) reportErrorValue(new Error(message));
164        }
165        current.terminated = true;
166        apply(closePort, port, []);
167      }, () => true);
168    });
169    return self;
170  }
171  postMessage(message: unknown, options: unknown = undefined) {
172    required(arguments.length, 1);
173    apply(post, state(this).port, [message, options]);
174  }
175  terminate() {
176    const current = state(this);
177    current.terminated = true;
178    apply(closePort, current.port, []);
179    op_worker_terminate(current.id);
180  }
181  get onmessage() {
182    return state(this).onmessage ?? null;
183  }
184  set onmessage(value: unknown) {
185    handler(this, state(this), "onmessage", "message", value);
186  }
187  get onmessageerror() {
188    return state(this).onmessageerror ?? null;
189  }
190  set onmessageerror(value: unknown) {
191    handler(this, state(this), "onmessageerror", "messageerror", value);
192  }
193  get onerror() {
194    return state(this).onerror ?? null;
195  }
196  set onerror(value: unknown) {
197    handler(this, state(this), "onerror", "error", value);
198  }
199}
200installInterface("Worker", Worker);
201
202class WorkerLocation {
203  private constructor() {
204    throw new TypeError("Illegal constructor");
205  }
206  get href() {
207    return get(locations, this)!.href;
208  }
209  get origin() {
210    return get(locations, this)!.origin;
211  }
212  get protocol() {
213    return get(locations, this)!.protocol;
214  }
215  get host() {
216    return get(locations, this)!.host;
217  }
218  get hostname() {
219    return get(locations, this)!.hostname;
220  }
221  get port() {
222    return get(locations, this)!.port;
223  }
224  get pathname() {
225    return get(locations, this)!.pathname;
226  }
227  get search() {
228    return get(locations, this)!.search;
229  }
230  get hash() {
231    return get(locations, this)!.hash;
232  }
233  toString() {
234    return get(locations, this)!.href;
235  }
236}
237
238const global = globalThis, current: HandlerState = ObjectCreate(null);
239for (const type of ["message", "messageerror"] as const) {
240  define(global, "on" + type, {
241    get: () => current["on" + type as HandlerName] ?? null,
242    set: (value) =>
243      handler(global, current, "on" + type as HandlerName, type, value),
244    enumerable: true,
245    configurable: true,
246  });
247}
248onRuntimeStart(() => {
249  const metadata = op_worker_metadata();
250  if (metadata !== null) {
251    const port = adoptPort(metadata[0]);
252    addListener(
253      port,
254      "message",
255      (event) =>
256        dispatchMessage(global, messageData(event), messagePorts(event)),
257    );
258    addListener(
259      port,
260      "messageerror",
261      () =>
262        dispatch(global, new MessageEvent("messageerror", ObjectCreate(null))),
263    );
264    startPort(port, true);
265    installInterface("WorkerLocation", WorkerLocation);
266    const location = ObjectCreate(WorkerLocation.prototype);
267    const url = new URL(metadata[1]),
268      locationState: LocationState = ObjectCreate(null);
269    for (
270      const key of [
271        "href",
272        "origin",
273        "protocol",
274        "host",
275        "hostname",
276        "port",
277        "pathname",
278        "search",
279        "hash",
280      ] as const
281    ) locationState[key] = url[key];
282    set(locations, location, locationState);
283    define(global, "location", {
284      get: () => location,
285      enumerable: true,
286      configurable: true,
287    });
288    define(global, "name", {
289      value: metadata[2],
290      enumerable: true,
291      configurable: true,
292    });
293    define(global, "postMessage", {
294      value: {
295        postMessage(message: unknown, options: unknown = undefined) {
296          required(arguments.length, 1);
297          apply(post, port, [message, options]);
298        },
299      }.postMessage,
300      writable: true,
301      enumerable: true,
302      configurable: true,
303    });
304    define(global, "close", {
305      value: function close() {
306        apply(closePort, port, []);
307        op_worker_close();
308      },
309      writable: true,
310      enumerable: true,
311      configurable: true,
312    });
313  } else {
314    define(global, "postMessage", {
315      value: {
316        postMessage(message: unknown, options: unknown = undefined) {
317          required(arguments.length, 1);
318          const transfer = transferList(options), ports = array();
319          for (let i = 0; i < transfer.length; i++) {
320            if (isMessagePort(transfer[i])) push(ports, transfer[i]);
321          }
322          const callbacks = transferCallbacks(transfer, dataCloneError);
323          let packet: { data: unknown; ports: MessagePort[] };
324          try {
325            packet = clone(
326              { __proto__: null, data: message, ports },
327              transfer,
328              classify,
329              callbacks.cloneHost,
330              dataCloneError,
331              callbacks.prepare,
332              callbacks.validate,
333              callbacks.commit,
334            ) as typeof packet;
335          } finally {
336            callbacks.clear();
337          }
338          enqueueTask(
339            () => dispatchMessage(global, packet.data, packet.ports),
340            () => true,
341          );
342        },
343      }.postMessage,
344      writable: true,
345      enumerable: true,
346      configurable: true,
347    });
348  }
349});
350
351export type { Worker, WorkerLocation };