rooklang 0.3.1 → 0.3.2

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.
Files changed (93) hide show
  1. package/README.md +17 -4
  2. package/README.ru.md +17 -4
  3. package/dist/ast/expressions.d.ts +16 -1
  4. package/dist/ast/markup.d.ts +6 -1
  5. package/dist/browser.d.ts +5 -0
  6. package/dist/browser.js +1 -0
  7. package/dist/checker/base.js +4 -4
  8. package/dist/checker/builtins.js +4 -35
  9. package/dist/checker/bytes.d.ts +2 -2
  10. package/dist/checker/bytes.js +2 -2
  11. package/dist/checker/channels.d.ts +3 -0
  12. package/dist/checker/channels.js +39 -1
  13. package/dist/checker/expressions.d.ts +2 -0
  14. package/dist/checker/expressions.js +4 -0
  15. package/dist/checker/helpers.js +2 -0
  16. package/dist/checker/queues.d.ts +6 -0
  17. package/dist/checker/queues.js +24 -0
  18. package/dist/checker/resolution.js +6 -2
  19. package/dist/checker/transfer.js +4 -1
  20. package/dist/checker/types/printing.js +7 -0
  21. package/dist/checker/types/queue.d.ts +6 -0
  22. package/dist/checker/types/queue.js +42 -0
  23. package/dist/checker/types/relations.js +12 -1
  24. package/dist/checker/types.d.ts +1 -0
  25. package/dist/checker/types.js +1 -0
  26. package/dist/checker/untyped-globals.d.ts +1 -0
  27. package/dist/checker/untyped-globals.js +47 -0
  28. package/dist/codegen/attributes.js +0 -1
  29. package/dist/codegen/calls.d.ts +9 -0
  30. package/dist/codegen/calls.js +41 -1
  31. package/dist/codegen/component-values.js +0 -1
  32. package/dist/codegen/components.js +0 -1
  33. package/dist/codegen/control-flow.js +0 -3
  34. package/dist/codegen/control-statements.js +0 -4
  35. package/dist/codegen/creation.d.ts +6 -8
  36. package/dist/codegen/creation.js +1 -10
  37. package/dist/codegen/early-returns.js +0 -1
  38. package/dist/codegen/elements.js +0 -3
  39. package/dist/codegen/emitter.d.ts +3 -5
  40. package/dist/codegen/emitter.js +0 -2
  41. package/dist/codegen/expressions.d.ts +4 -1
  42. package/dist/codegen/expressions.js +9 -13
  43. package/dist/codegen/functions.js +0 -2
  44. package/dist/codegen/js.d.ts +0 -7
  45. package/dist/codegen/js.js +33 -59
  46. package/dist/codegen/parallel.js +2 -4
  47. package/dist/codegen/refs.js +0 -1
  48. package/dist/codegen/runtime.d.ts +15 -0
  49. package/dist/codegen/runtime.js +99 -0
  50. package/dist/codegen/simple-statements.js +3 -1
  51. package/dist/codegen/styles.d.ts +1 -1
  52. package/dist/codegen/styles.js +2 -2
  53. package/dist/codegen/web-components.js +0 -1
  54. package/dist/compile.d.ts +7 -0
  55. package/dist/compile.js +1 -0
  56. package/dist/completion.js +2 -1
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +1 -0
  59. package/dist/loader.js +8 -0
  60. package/dist/parser/base.d.ts +3 -1
  61. package/dist/parser/expressions.js +9 -2
  62. package/dist/parser/literals.d.ts +7 -0
  63. package/dist/parser/literals.js +45 -1
  64. package/dist/parser/simple-statements.js +3 -0
  65. package/dist/parser/types.d.ts +1 -1
  66. package/dist/parser/types.js +10 -3
  67. package/dist/runtime/sources.d.ts +2 -0
  68. package/dist/runtime/sources.js +14 -0
  69. package/dist/ts-types.js +9 -1
  70. package/dist/vite.js +7 -1
  71. package/dist/walk-types.js +1 -0
  72. package/dist/walk.js +9 -0
  73. package/package.json +6 -4
  74. package/src/runtime/chan.js +277 -0
  75. package/src/runtime/core.js +23 -0
  76. package/src/runtime/go.js +334 -0
  77. package/src/runtime/index.js +13 -0
  78. package/src/runtime/markup.js +336 -0
  79. package/src/runtime/queue.js +118 -0
  80. package/src/runtime/select.js +170 -0
  81. package/src/runtime/streams.js +61 -0
  82. package/src/runtime/threads.js +240 -0
  83. package/src/runtime/timers.js +27 -0
  84. package/dist/codegen/chan-helpers.d.ts +0 -2
  85. package/dist/codegen/chan-helpers.js +0 -253
  86. package/dist/codegen/go-helpers.d.ts +0 -9
  87. package/dist/codegen/go-helpers.js +0 -307
  88. package/dist/codegen/helpers.d.ts +0 -10
  89. package/dist/codegen/helpers.js +0 -356
  90. package/dist/codegen/select-helpers.d.ts +0 -2
  91. package/dist/codegen/select-helpers.js +0 -135
  92. package/dist/codegen/thread-helpers.d.ts +0 -2
  93. package/dist/codegen/thread-helpers.js +0 -198
package/dist/walk.js CHANGED
@@ -103,6 +103,14 @@ export function forEachChild(node, visit) {
103
103
  visit(node.element);
104
104
  optional(node.size);
105
105
  break;
106
+ case 'StreamChanExpression':
107
+ visit(node.stream);
108
+ optional(node.size);
109
+ break;
110
+ case 'QueueExpression':
111
+ visit(node.element);
112
+ optional(node.items);
113
+ break;
106
114
  case 'MakeExpression':
107
115
  visit(node.type);
108
116
  visit(node.length);
@@ -256,6 +264,7 @@ export function forEachChild(node, visit) {
256
264
  case 'TypeParameter':
257
265
  case 'ArrayType':
258
266
  case 'ChanType':
267
+ case 'QueueType':
259
268
  case 'NullableType':
260
269
  case 'FuncType':
261
270
  case 'ComponentType':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rooklang",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "A language with JavaScript semantics, Go-style syntax, goroutines on real threads and static types that compiles to readable JavaScript",
5
5
  "keywords": [
6
6
  "rooklang",
@@ -47,10 +47,12 @@
47
47
  "types": "./dist/browser.d.ts",
48
48
  "default": "./dist/browser.js"
49
49
  },
50
+ "./runtime": "./src/runtime/index.js",
50
51
  "./package.json": "./package.json"
51
52
  },
52
53
  "files": [
53
- "dist"
54
+ "dist",
55
+ "src/runtime/*.js"
54
56
  ],
55
57
  "scripts": {
56
58
  "rook": "node src/cli.ts",
@@ -59,7 +61,7 @@
59
61
  "site": "npm run site:build && node src/cli.ts run site/server.rk",
60
62
  "site:images": "node scripts/og-image.ts",
61
63
  "vscode:build": "node scripts/build-vscode.ts",
62
- "vscode:package": "npm run vscode:build && cd editors/vscode && npx --yes @vscode/vsce@4 package --no-dependencies --allow-missing-repository --skip-license --out rooklang.vsix",
64
+ "vscode:package": "npm run vscode:build && cd editors/vscode && npx --yes @vscode/vsce@4 package --no-dependencies --baseContentUrl https://github.com/QWERN-9876r/RookLang/blob/HEAD/editors/vscode --baseImagesUrl https://github.com/QWERN-9876r/RookLang/raw/HEAD/editors/vscode --out rooklang.vsix",
63
65
  "vscode:install": "npm run vscode:package && code --install-extension editors/vscode/rooklang.vsix --force",
64
66
  "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
65
67
  "prepack": "npm run build",
@@ -68,7 +70,7 @@
68
70
  "test": "vitest run",
69
71
  "test:watch": "vitest",
70
72
  "lint": "eslint .",
71
- "format": "prettier --write . && eslint --fix --fix-type layout .",
73
+ "format": "prettier --write . && eslint --fix --fix-type layout . && node scripts/runtime-sources.ts",
72
74
  "format:check": "prettier --check .",
73
75
  "check": "npm run typecheck && npm run lint && npm run format:check && npm test",
74
76
  "prepare": "husky"
@@ -0,0 +1,277 @@
1
+ // Channels: sending, receiving and closing, here or in the thread that made the channel
2
+ // (threads.js), and the queues of a channel. Receiving from a closed channel gives
3
+ // the zero value of its type, as in Go.
4
+
5
+ import { $$Queue } from './queue.js';
6
+ import { $$commit, $$lock, $$select, $$try, $$unlock, $$unwait } from './select.js';
7
+ import { $$adopt, $$ask, $$ownerOf, $$post, $$registry, $$stopped } from './threads.js';
8
+
9
+ $$registry().handle ??= { message: $$message, pump: $$pump };
10
+
11
+ export function $$chan(size, zero) {
12
+ const registry = $$registry();
13
+ const id = `${registry.key}:${registry.channels++}`;
14
+ const channel = { $$chan: id, size, zero };
15
+
16
+ registry.states.set(id, {
17
+ size,
18
+ zero,
19
+ buffer: new $$Queue(),
20
+ receivers: new $$Queue(),
21
+ senders: new $$Queue(),
22
+ closed: false,
23
+ holders: 1,
24
+ busy: false,
25
+ });
26
+ $$adopt(channel);
27
+
28
+ return channel;
29
+ }
30
+
31
+ // A local operation waits in the queue of its channel; one of another thread asks the thread
32
+ // that made the channel.
33
+ export function $$send(channel, value) {
34
+ const id = channel.$$chan;
35
+ const owner = $$ownerOf(id);
36
+
37
+ if (owner !== $$registry().key) return $$ask(owner, { op: 'send', channel: id, value });
38
+
39
+ return new Promise((resolve, reject) =>
40
+ $$wait(id, 'senders', { value, done: resolve, fail: reject }),
41
+ );
42
+ }
43
+
44
+ export function $$receive(channel, withOk = false) {
45
+ const id = channel.$$chan;
46
+ const owner = $$ownerOf(id);
47
+ const received =
48
+ owner !== $$registry().key
49
+ ? $$ask(owner, { op: 'receive', channel: id })
50
+ : new Promise((resolve, reject) => $$wait(id, 'receivers', { done: resolve, fail: reject }));
51
+
52
+ return withOk ? received : received.then(([value]) => value);
53
+ }
54
+
55
+ // Closing a channel of another thread fails this one if it was closed already, as a panic would.
56
+ export function $$close(channel) {
57
+ const id = channel.$$chan;
58
+ const owner = $$ownerOf(id);
59
+
60
+ if (owner === $$registry().key) return $$closeHere(id);
61
+ $$ask(owner, { op: 'close', channel: id }).catch((error) => {
62
+ setTimeout(() => {
63
+ throw error;
64
+ });
65
+ });
66
+ }
67
+
68
+ export function $$closeHere(id) {
69
+ const state = $$registry().states.get(id);
70
+
71
+ if (state.closed) throw new Error('close of a closed channel');
72
+ state.closed = true;
73
+ $$pump(id);
74
+ }
75
+
76
+ // What a closed channel gives: a value of its own for each receiver, if it is an array or a queue.
77
+ export function $$zero({ zero }) {
78
+ return Array.isArray(zero) ? [] : zero instanceof $$Queue ? new $$Queue() : zero;
79
+ }
80
+
81
+ export async function* $$range(channel) {
82
+ while (true) {
83
+ const [value, ok] = await $$receive(channel, true);
84
+
85
+ if (!ok) return;
86
+ yield value;
87
+ }
88
+ }
89
+
90
+ export function $$wait(id, queue, entry) {
91
+ const state = $$registry().states.get(id);
92
+
93
+ if (!state) return entry.fail?.($$stopped());
94
+ state[queue].push(entry);
95
+ $$pump(id);
96
+ }
97
+
98
+ // The operations of other threads on the channels of this one.
99
+ export function $$message(from, data) {
100
+ const answer = (value) => $$post(from, { answer: data.ask, value });
101
+ const fail = (error) => $$post(from, { answer: data.ask, error });
102
+ const select = { key: from, sid: data.sid };
103
+
104
+ switch (data.op) {
105
+ case 'send':
106
+ return $$wait(data.channel, 'senders', { from, value: data.value, done: answer, fail });
107
+
108
+ case 'receive':
109
+ return $$wait(data.channel, 'receivers', { from, done: answer, fail });
110
+
111
+ case 'close':
112
+ try {
113
+ $$closeHere(data.channel);
114
+
115
+ return answer();
116
+ } catch (error) {
117
+ return fail(error);
118
+ }
119
+
120
+ case 'try':
121
+ try {
122
+ return answer($$try(data.channel, data.send, data.value));
123
+ } catch (error) {
124
+ return fail(error);
125
+ }
126
+
127
+ case 'select':
128
+ return $$select(data.cases, data.hasDefault).then(answer, fail);
129
+
130
+ case 'wait':
131
+ return $$wait(data.channel, data.send ? 'senders' : 'receivers', {
132
+ select,
133
+ case: data.case,
134
+ value: data.value,
135
+ });
136
+
137
+ case 'cancel':
138
+ return $$unwait(select, data.channels);
139
+
140
+ case 'lock':
141
+ return answer($$lock(data.sid));
142
+
143
+ case 'unlock':
144
+ return $$unlock(data.sid);
145
+
146
+ case 'commit':
147
+ return $$commit(data.sid, data.case, data.result, data.error);
148
+ }
149
+ }
150
+
151
+ // Matches what waits in a channel: a receiver with a value of the buffer, a sender or the zero value
152
+ // after close; a sender with room in the buffer. A case of a select goes on only if its select
153
+ // has not gone on with another case, which the thread of the select tells.
154
+ export function $$pump(id) {
155
+ const state = $$registry().states.get(id);
156
+
157
+ while (state && !state.busy) {
158
+ const receiver = state.receivers.at(0);
159
+ const sender = state.senders.at(0);
160
+
161
+ if (receiver && (state.buffer.length > 0 || state.closed)) {
162
+ if (!$$pair(id, state, receiver, null)) return;
163
+ } else if (receiver && sender) {
164
+ if (!$$pair(id, state, receiver, sender)) return;
165
+ } else if (sender && (state.closed || state.buffer.length < state.size)) {
166
+ if (!$$pair(id, state, null, sender)) return;
167
+ } else {
168
+ return;
169
+ }
170
+ }
171
+ }
172
+
173
+ // Whether the pump can go on at once: a pair whose selects are all in this thread is decided here.
174
+ export function $$pair(id, state, receiver, sender) {
175
+ const entries = [receiver, sender].filter((entry) => entry !== null);
176
+ const key = $$registry().key;
177
+
178
+ if (entries.some((entry) => entry.select && entry.select.key !== key)) {
179
+ $$pairLater(id, state, receiver, sender);
180
+
181
+ return false;
182
+ }
183
+
184
+ const locks = entries.map((entry) => (entry.select ? $$lock(entry.select.sid) : 'locked'));
185
+
186
+ if (locks.every((lock) => lock === 'locked')) {
187
+ $$complete(state, receiver, sender);
188
+
189
+ return true;
190
+ }
191
+
192
+ entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));
193
+
194
+ return $$settle(id, state, entries, locks);
195
+ }
196
+
197
+ // A pair with a select of another thread: the channel waits for the answers of that thread.
198
+ export async function $$pairLater(id, state, receiver, sender) {
199
+ state.busy = true;
200
+
201
+ const entries = [receiver, sender].filter((entry) => entry !== null);
202
+ const locks = [];
203
+
204
+ for (const { select } of entries) {
205
+ const lock = !select
206
+ ? 'locked'
207
+ : select.key === $$registry().key
208
+ ? $$lock(select.sid)
209
+ : await $$ask(select.key, { op: 'lock', sid: select.sid }).catch(() => 'done');
210
+
211
+ locks.push(lock);
212
+ if (lock !== 'locked') break;
213
+ }
214
+
215
+ state.busy = false;
216
+ if (locks.length === entries.length && locks.every((lock) => lock === 'locked')) {
217
+ $$complete(state, receiver, sender);
218
+ } else {
219
+ entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));
220
+ if (!$$settle(id, state, entries, locks)) return;
221
+ }
222
+
223
+ $$pump(id);
224
+ }
225
+
226
+ // After a pair that could not go on: a select that has gone on leaves the queue; one that another
227
+ // channel is deciding is tried again a little later.
228
+ export function $$settle(id, state, entries, locks) {
229
+ entries.forEach((entry, i) => {
230
+ if (locks[i] === 'done') $$remove(state, entry);
231
+ });
232
+ if (locks.includes('done')) return true;
233
+ setTimeout(() => $$pump(id), 1 + Math.random() * 4);
234
+
235
+ return false;
236
+ }
237
+
238
+ export function $$remove(state, entry) {
239
+ state.receivers.$$delete(entry);
240
+ state.senders.$$delete(entry);
241
+ }
242
+
243
+ // Takes the pair off the queues and passes the value.
244
+ export function $$complete(state, receiver, sender) {
245
+ if (receiver) state.receivers.$$delete(receiver);
246
+ if (sender) state.senders.$$delete(sender);
247
+ if (receiver) {
248
+ const result =
249
+ state.buffer.length > 0
250
+ ? [state.buffer.shift(), true]
251
+ : sender
252
+ ? [sender.value, true]
253
+ : [$$zero(state), false];
254
+
255
+ $$finish(receiver, result);
256
+ if (sender) $$finish(sender);
257
+ } else if (state.closed) {
258
+ $$finish(sender, undefined, new Error('send on a closed channel'));
259
+ } else {
260
+ state.buffer.push(sender.value);
261
+ $$finish(sender);
262
+ }
263
+ }
264
+
265
+ export function $$finish(entry, result, error) {
266
+ const { select } = entry;
267
+
268
+ if (!select) return error ? entry.fail(error) : entry.done(result);
269
+ if (select.key === $$registry().key) return $$commit(select.sid, entry.case, result, error);
270
+ $$post(select.key, { op: 'commit', sid: select.sid, case: entry.case, result, error });
271
+ }
272
+
273
+ export function $$unlockAt(select) {
274
+ if (!select) return;
275
+ if (select.key === $$registry().key) $$unlock(select.sid);
276
+ else $$post(select.key, { op: 'unlock', sid: select.sid });
277
+ }
@@ -0,0 +1,23 @@
1
+ // What the code of any module may need: `for` over a value of unknown type, and `defer`.
2
+
3
+ // `for v in x` over an untyped value: a Map gives its values, as a typed one does.
4
+ export function $$values(iterable) {
5
+ if (typeof iterable === 'function') return iterable();
6
+
7
+ return iterable instanceof Map ? iterable.values() : iterable;
8
+ }
9
+
10
+ // Runs deferred calls in reverse order; every call runs even if an earlier one throws.
11
+ export function $$runDeferred(deferred) {
12
+ let failure;
13
+
14
+ for (let i = deferred.length - 1; i >= 0; i--) {
15
+ try {
16
+ deferred[i]();
17
+ } catch (error) {
18
+ failure = { error };
19
+ }
20
+ }
21
+
22
+ if (failure) throw failure.error;
23
+ }
@@ -0,0 +1,334 @@
1
+ // `go`: the pool of workers of the program, and the side of the worker. They work
2
+ // with Web Workers and with the worker threads of Node, whose API differs: `on("message")` instead
3
+ // of `onmessage`, and `unref()`, so that idle workers do not keep the program running.
4
+
5
+ import {
6
+ $$adopt,
7
+ $$connect,
8
+ $$copy,
9
+ $$gone,
10
+ $$incoming,
11
+ $$post,
12
+ $$registry,
13
+ $$transfer,
14
+ } from './threads.js';
15
+
16
+ // `go` in a module: a call in a worker of the pool. `start` starts a worker of the module, with its
17
+ // worker module: it is $$startWorker of the module, whose URL a bundler needs to see there.
18
+ export function $$goWorker(start, name, args) {
19
+ return (globalThis[Symbol.for('rook.goroutines')] ??= $$pool()).call(
20
+ start,
21
+ name,
22
+ $$copy(args, true),
23
+ );
24
+ }
25
+
26
+ // The workers of the program, shared by its modules: up to one per processor. Each runs the worker
27
+ // module of one module, `start`. Calls wait in one queue and go to a worker of their module that is
28
+ // free: one whose thread is not running code, though its earlier calls may wait in `await`. A
29
+ // worker that is still busy keeps its calls for later; an idle worker of another module makes room
30
+ // for a new one, unless it keeps channels.
31
+ export function $$pool() {
32
+ const size = globalThis.navigator?.hardwareConcurrency ?? 4;
33
+ const registry = $$registry();
34
+ const workers = [];
35
+ const queue = [];
36
+ const calls = new Map();
37
+ let ids = 0;
38
+ // A worker without calls lets the program end, unless something here waits for it.
39
+ const unref = (worker) => {
40
+ if (worker.calls === 0 && !(registry.pins.get(worker.key) > 0)) worker.thread.unref?.();
41
+ };
42
+
43
+ registry.idle = (key) => {
44
+ const worker = workers.find((each) => each.key === key);
45
+
46
+ if (worker) unref(worker);
47
+ };
48
+
49
+ const send = (worker, call) => {
50
+ worker.busy = true;
51
+ worker.calls++;
52
+ worker.thread.ref?.();
53
+ call.worker = worker;
54
+ calls.set(call.id, call);
55
+ $$post(worker.key, { call: call.id, name: call.name, args: call.args });
56
+ call.args = null;
57
+ };
58
+ const dispatch = () => {
59
+ const starting = new Map();
60
+
61
+ for (const worker of workers) {
62
+ if (!worker.key) starting.set(worker.start, (starting.get(worker.start) ?? 0) + 1);
63
+ }
64
+
65
+ for (let i = 0; i < queue.length; i++) {
66
+ const call = queue[i];
67
+ let free = null;
68
+
69
+ for (const worker of workers) {
70
+ if (worker.start !== call.start || !worker.key || worker.busy) continue;
71
+ if (!free || worker.calls < free.calls) free = worker;
72
+ }
73
+
74
+ // A free worker without calls; else a new worker; else one whose calls wait in await.
75
+ if (free?.calls === 0 || (free && workers.length >= size)) {
76
+ queue.splice(i--, 1);
77
+ send(free, call);
78
+ } else if (starting.get(call.start) > 0) {
79
+ starting.set(call.start, starting.get(call.start) - 1);
80
+ } else if (workers.length < size || retire(call.start)) {
81
+ add(call.start);
82
+ } else {
83
+ // It waits for a busy worker of its module, which may have let go of the program when
84
+ // its last call returned: the program runs on until that worker is free and takes it.
85
+ for (const worker of workers)
86
+ if (worker.start === call.start && worker.key) worker.thread.ref?.();
87
+ }
88
+ }
89
+ };
90
+ const retire = (start) => {
91
+ const idle = workers.find(
92
+ (worker) =>
93
+ worker.key && worker.start !== start && !worker.busy && worker.calls === 0 && !worker.owns,
94
+ );
95
+
96
+ if (!idle) return false;
97
+ idle.thread.terminate();
98
+ remove(idle, null);
99
+
100
+ return true;
101
+ };
102
+ // A worker that stopped: its calls fail, and the threads that talked to it forget it.
103
+ const remove = (worker, error) => {
104
+ workers.splice(workers.indexOf(worker), 1);
105
+ for (const [id, call] of calls) {
106
+ if (call.worker !== worker) continue;
107
+ calls.delete(id);
108
+ call.reject(error);
109
+ }
110
+
111
+ if (worker.key) {
112
+ $$gone(worker.key);
113
+ for (const other of workers) {
114
+ if (other.key) other.thread.postMessage({ gone: worker.key });
115
+ }
116
+ }
117
+ };
118
+ const fail = (worker, error) => {
119
+ remove(worker, error);
120
+ dispatch();
121
+ };
122
+ const receive = (worker, data) => {
123
+ if ('op' in data || 'answer' in data) return $$incoming(worker.key, data);
124
+ if ('hello' in data) {
125
+ worker.key = data.hello;
126
+ $$connect(worker.key, worker.thread, false);
127
+ worker.thread.postMessage({ init: registry.key });
128
+ // Workers talk to each other through ports of their own, also while this thread is busy.
129
+ for (const other of workers) {
130
+ if (other === worker || !other.key) continue;
131
+
132
+ const { port1, port2 } = new MessageChannel();
133
+
134
+ worker.thread.postMessage({ port: other.key, through: port1 }, [port1]);
135
+ other.thread.postMessage({ port: worker.key, through: port2 }, [port2]);
136
+ }
137
+
138
+ worker.busy = false;
139
+ unref(worker);
140
+ } else if ('free' in data) {
141
+ worker.busy = false;
142
+ worker.owns = data.owns;
143
+ } else if ('returned' in data) {
144
+ $$adopt(data);
145
+
146
+ const call = calls.get(data.returned);
147
+
148
+ calls.delete(data.returned);
149
+ worker.owns = data.owns;
150
+ worker.calls--;
151
+ unref(worker);
152
+ if ('error' in data) call?.reject(data.error);
153
+ else call?.resolve(data.value);
154
+ }
155
+
156
+ dispatch();
157
+ // A worker that a waiting call held on to, now free and without a call, lets go again.
158
+ if ('free' in data) unref(worker);
159
+ };
160
+ const add = (start) => {
161
+ const worker = { start, key: null, thread: null, busy: true, calls: 0, owns: false };
162
+
163
+ workers.push(worker);
164
+ start().then(
165
+ (thread) => {
166
+ worker.thread = thread;
167
+ if (thread.on) {
168
+ thread.on('message', (data) => receive(worker, data));
169
+ thread.on('error', (error) => fail(worker, error));
170
+ } else {
171
+ thread.onmessage = (event) => receive(worker, event.data);
172
+ thread.onerror = (event) => fail(worker, new Error(event.message ?? 'a worker failed'));
173
+ }
174
+ },
175
+ (error) => {
176
+ remove(worker, error);
177
+ for (const call of queue.filter((each) => each.start === start)) {
178
+ queue.splice(queue.indexOf(call), 1);
179
+ call.reject(error);
180
+ }
181
+ },
182
+ );
183
+ };
184
+ const tasks = new WeakMap();
185
+
186
+ return {
187
+ call: (start, name, args) => {
188
+ const call = { id: ids++, start, name, args, resolve: null, reject: null, worker: null };
189
+ const task = new Promise((resolve, reject) => Object.assign(call, { resolve, reject }));
190
+
191
+ tasks.set(task, call);
192
+ queue.push(call);
193
+ dispatch();
194
+
195
+ return task;
196
+ },
197
+ // A call that waits leaves the queue; a worker cannot stop a call, so the worker stops, and
198
+ // the other calls in it fail too.
199
+ cancel: (task) => {
200
+ const call = tasks.get(task);
201
+
202
+ tasks.delete(task);
203
+ if (!call || (!queue.includes(call) && !calls.has(call.id))) return;
204
+ task.catch(() => {});
205
+ if (queue.includes(call)) {
206
+ queue.splice(queue.indexOf(call), 1);
207
+
208
+ return call.reject(new Error('goroutine cancelled'));
209
+ }
210
+
211
+ calls.delete(call.id);
212
+ call.reject(new Error('goroutine cancelled'));
213
+ call.worker.thread.terminate();
214
+ remove(
215
+ call.worker,
216
+ new Error('the worker of the goroutine stopped: another goroutine in it was cancelled'),
217
+ );
218
+ dispatch();
219
+ },
220
+ };
221
+ }
222
+
223
+ // `go` inside a goroutine: the function runs in the same worker, with copies of the arguments. A
224
+ // name is one of the `functions` of the worker module, a function literal is itself.
225
+ export function $$goHere(functions, fn, args) {
226
+ const copies = $$copy(args);
227
+ const run = typeof fn === 'function' ? fn : functions[fn];
228
+ let cancel;
229
+ const cancelled = new Promise((_, reject) => (cancel = reject));
230
+ const task = Promise.race([cancelled, Promise.resolve().then(() => cancel && run(...copies))]);
231
+
232
+ $$registry().cancels.set(task, () => {
233
+ task.catch(() => {});
234
+ cancel(new Error('goroutine cancelled'));
235
+ cancel = null;
236
+ });
237
+
238
+ return task;
239
+ }
240
+
241
+ // `go for`: the array is split into parts, one per processor, each part runs as a goroutine of `go`
242
+ // of the module, and
243
+ // the loop waits for all of them. A typed array on a SharedArrayBuffer is shared with the workers,
244
+ // which write into it; other parts are copies, moved to the workers and back into the array.
245
+ export async function $$goFor(go, name, items, ...captured) {
246
+ const count = Math.min(globalThis.navigator?.hardwareConcurrency ?? 4, items.length);
247
+ const shared =
248
+ typeof SharedArrayBuffer === 'function' && items.buffer instanceof SharedArrayBuffer;
249
+ const parts = [];
250
+
251
+ for (let k = 0; k < count; k++) {
252
+ const from = Math.floor((items.length * k) / count);
253
+ const to = Math.floor((items.length * (k + 1)) / count);
254
+ const part = shared ? items.subarray(from, to) : items.slice(from, to);
255
+
256
+ if (ArrayBuffer.isView(part) && !shared) $$transfer(part);
257
+ parts.push(
258
+ go(name, [part, from, ...captured]).then((written) => {
259
+ if (written === undefined || shared) return;
260
+ if (ArrayBuffer.isView(items)) items.set(written, from);
261
+ else for (let i = 0; i < written.length; i++) items[from + i] = written[i];
262
+ }),
263
+ );
264
+ }
265
+
266
+ await Promise.all(parts);
267
+ }
268
+
269
+ // `make([]T, n, shared)`: the memory of a typed array that the workers of `go for` write into,
270
+ // without copies. A page has SharedArrayBuffer only with cross-origin isolation.
271
+ export function $$shared(bytes) {
272
+ if (typeof SharedArrayBuffer !== 'function') {
273
+ throw new Error(
274
+ 'make(..., shared) needs SharedArrayBuffer, which a page has only with cross-origin isolation: the headers Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp',
275
+ );
276
+ }
277
+
278
+ return new SharedArrayBuffer(bytes);
279
+ }
280
+
281
+ // The worker: runs the calls that the pool sends and sends back their results or errors. Once the
282
+ // thread is free again, it says so, and the pool may send another call.
283
+ export async function $$serve(functions) {
284
+ const threads = 'node:worker_threads';
285
+ const port =
286
+ typeof postMessage === 'function'
287
+ ? globalThis
288
+ : (await import(/* @vite-ignore */ threads)).parentPort;
289
+ const registry = $$registry();
290
+
291
+ // The port to the main thread keeps the worker running.
292
+ registry.idle = (key) => key !== registry.main && registry.ports.get(key)?.unref?.();
293
+ port.postMessage({ hello: registry.key });
294
+
295
+ const free = () => port.postMessage({ free: true, owns: registry.states.size > 0 });
296
+ const receive = async (data) => {
297
+ if ('op' in data || 'answer' in data) return $$incoming(registry.main, data);
298
+ if ('init' in data) {
299
+ registry.main = data.init;
300
+
301
+ return $$connect(data.init, port, false);
302
+ }
303
+
304
+ if ('port' in data) return $$connect(data.port, data.through, true);
305
+ if ('gone' in data) return $$gone(data.gone);
306
+ setTimeout(free);
307
+ $$adopt(data.args);
308
+
309
+ const { main } = registry;
310
+
311
+ try {
312
+ const value = await functions[data.name](...data.args);
313
+
314
+ $$post(main, { returned: data.call, value, owns: registry.states.size > 0 });
315
+ } catch (error) {
316
+ try {
317
+ $$post(main, { returned: data.call, error, owns: registry.states.size > 0 });
318
+ } catch {
319
+ $$post(main, { returned: data.call, error: new Error(String(error)), owns: true });
320
+ }
321
+ }
322
+ };
323
+
324
+ if (port.on) port.on('message', receive);
325
+ else port.onmessage = (event) => receive(event.data);
326
+ }
327
+
328
+ // `cancel(task)`: stops a goroutine of `go`, whose Promise fails with "goroutine cancelled". A call
329
+ // in a worker stops with its worker; inside a goroutine, `go` runs in the same worker, so its
330
+ // Promise fails but code that has started goes on.
331
+ export function $$cancel(task) {
332
+ globalThis[Symbol.for('rook.goroutines')]?.cancel(task);
333
+ $$registry().cancels.get(task)?.();
334
+ }