rooklang 0.3.3 → 0.3.4

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 (170) hide show
  1. package/README.md +32 -7
  2. package/README.ru.md +32 -7
  3. package/dist/ast/declarations.d.ts +20 -1
  4. package/dist/ast/expressions.d.ts +31 -1
  5. package/dist/ast/markup.d.ts +6 -1
  6. package/dist/ast/statements.d.ts +25 -1
  7. package/dist/auto-import.d.ts +11 -0
  8. package/dist/auto-import.js +81 -0
  9. package/dist/breaks.d.ts +6 -0
  10. package/dist/breaks.js +36 -0
  11. package/dist/build.d.ts +7 -0
  12. package/dist/build.js +23 -2
  13. package/dist/checker/array-literals.d.ts +6 -0
  14. package/dist/checker/array-literals.js +59 -0
  15. package/dist/checker/assignments.d.ts +2 -0
  16. package/dist/checker/assignments.js +14 -0
  17. package/dist/checker/base.js +6 -6
  18. package/dist/checker/builtins.js +15 -5
  19. package/dist/checker/bytes.d.ts +2 -2
  20. package/dist/checker/bytes.js +2 -2
  21. package/dist/checker/calls.js +3 -0
  22. package/dist/checker/channel-objects.d.ts +3 -0
  23. package/dist/checker/channel-objects.js +40 -0
  24. package/dist/checker/channels.d.ts +1 -0
  25. package/dist/checker/channels.js +16 -2
  26. package/dist/checker/checker.d.ts +1 -1
  27. package/dist/checker/checker.js +19 -0
  28. package/dist/checker/class-members.d.ts +22 -0
  29. package/dist/checker/class-members.js +105 -0
  30. package/dist/checker/classes.d.ts +5 -4
  31. package/dist/checker/classes.js +41 -12
  32. package/dist/checker/closing.d.ts +21 -0
  33. package/dist/checker/closing.js +164 -0
  34. package/dist/checker/completion.d.ts +2 -2
  35. package/dist/checker/completion.js +2 -2
  36. package/dist/checker/context.d.ts +13 -0
  37. package/dist/checker/control-flow.d.ts +5 -0
  38. package/dist/checker/control-flow.js +17 -0
  39. package/dist/checker/declarations.js +1 -0
  40. package/dist/checker/decode.d.ts +8 -0
  41. package/dist/checker/decode.js +32 -0
  42. package/dist/checker/expressions.d.ts +5 -1
  43. package/dist/checker/expressions.js +13 -34
  44. package/dist/checker/flow.js +8 -4
  45. package/dist/checker/helpers.d.ts +1 -1
  46. package/dist/checker/helpers.js +9 -2
  47. package/dist/checker/iterables.d.ts +3 -1
  48. package/dist/checker/iterables.js +13 -0
  49. package/dist/checker/iterators.d.ts +4 -1
  50. package/dist/checker/iterators.js +12 -4
  51. package/dist/checker/json.d.ts +5 -0
  52. package/dist/checker/json.js +92 -0
  53. package/dist/checker/loops.js +23 -3
  54. package/dist/checker/members.js +7 -0
  55. package/dist/checker/operators.d.ts +2 -2
  56. package/dist/checker/operators.js +7 -5
  57. package/dist/checker/queues.d.ts +2 -2
  58. package/dist/checker/queues.js +2 -2
  59. package/dist/checker/resolution.js +5 -3
  60. package/dist/checker/signals.d.ts +8 -0
  61. package/dist/checker/signals.js +48 -0
  62. package/dist/checker/topics.d.ts +6 -0
  63. package/dist/checker/topics.js +17 -0
  64. package/dist/checker/transfer.d.ts +3 -1
  65. package/dist/checker/transfer.js +25 -5
  66. package/dist/checker/try.d.ts +6 -0
  67. package/dist/checker/try.js +19 -0
  68. package/dist/checker/types/constructors.d.ts +1 -1
  69. package/dist/checker/types/constructors.js +2 -2
  70. package/dist/checker/types/generics.js +1 -1
  71. package/dist/checker/types/iterables.d.ts +13 -0
  72. package/dist/checker/types/iterables.js +52 -0
  73. package/dist/checker/types/model.d.ts +25 -0
  74. package/dist/checker/types/params.js +22 -3
  75. package/dist/checker/types/printing.js +4 -3
  76. package/dist/checker/types/relations.js +19 -25
  77. package/dist/checker/types/topic.d.ts +6 -0
  78. package/dist/checker/types/topic.js +23 -0
  79. package/dist/checker/types.d.ts +2 -0
  80. package/dist/checker/types.js +2 -0
  81. package/dist/cli.js +18 -36
  82. package/dist/codegen/calls.d.ts +23 -0
  83. package/dist/codegen/calls.js +88 -7
  84. package/dist/codegen/control-statements.d.ts +10 -2
  85. package/dist/codegen/control-statements.js +56 -19
  86. package/dist/codegen/decode.d.ts +14 -0
  87. package/dist/codegen/decode.js +93 -0
  88. package/dist/codegen/emitter.d.ts +10 -41
  89. package/dist/codegen/emitter.js +9 -4
  90. package/dist/codegen/expressions.d.ts +9 -0
  91. package/dist/codegen/expressions.js +15 -24
  92. package/dist/codegen/js.d.ts +2 -2
  93. package/dist/codegen/js.js +5 -2
  94. package/dist/codegen/options.d.ts +40 -0
  95. package/dist/codegen/options.js +1 -0
  96. package/dist/codegen/runtime.d.ts +7 -3
  97. package/dist/codegen/runtime.js +27 -13
  98. package/dist/codegen/simple-statements.d.ts +10 -4
  99. package/dist/codegen/simple-statements.js +40 -15
  100. package/dist/codegen/statements.js +64 -4
  101. package/dist/codegen/try.d.ts +5 -0
  102. package/dist/codegen/try.js +40 -0
  103. package/dist/compile.d.ts +9 -0
  104. package/dist/compile.js +19 -6
  105. package/dist/completion.d.ts +4 -2
  106. package/dist/completion.js +10 -5
  107. package/dist/declarations.js +32 -6
  108. package/dist/decorator-copy.js +5 -2
  109. package/dist/goroutines.d.ts +1 -1
  110. package/dist/goroutines.js +5 -4
  111. package/dist/index.d.ts +6 -2
  112. package/dist/index.js +14 -5
  113. package/dist/lexer/base.d.ts +2 -0
  114. package/dist/lexer/base.js +6 -1
  115. package/dist/lexer/lexer.js +3 -0
  116. package/dist/lexer/literals.d.ts +5 -0
  117. package/dist/lexer/literals.js +51 -1
  118. package/dist/lexer/markup.js +3 -4
  119. package/dist/lexer/tables.js +2 -0
  120. package/dist/lexer/token.d.ts +4 -3
  121. package/dist/lexer/token.js +0 -2
  122. package/dist/loader.js +30 -8
  123. package/dist/parser/base.d.ts +4 -0
  124. package/dist/parser/base.js +2 -1
  125. package/dist/parser/classes.d.ts +2 -0
  126. package/dist/parser/classes.js +88 -23
  127. package/dist/parser/control-flow.d.ts +3 -1
  128. package/dist/parser/control-flow.js +21 -24
  129. package/dist/parser/declarations.d.ts +7 -4
  130. package/dist/parser/declarations.js +45 -6
  131. package/dist/parser/expressions.js +27 -3
  132. package/dist/parser/jumps.d.ts +11 -0
  133. package/dist/parser/jumps.js +77 -0
  134. package/dist/parser/literals.d.ts +9 -0
  135. package/dist/parser/literals.js +45 -6
  136. package/dist/parser/markup.js +1 -1
  137. package/dist/parser/simple-statements.d.ts +1 -1
  138. package/dist/parser/simple-statements.js +16 -9
  139. package/dist/parser/statements.d.ts +0 -3
  140. package/dist/parser/statements.js +22 -25
  141. package/dist/parser/syntax.d.ts +11 -1
  142. package/dist/parser/syntax.js +16 -2
  143. package/dist/parser/types.js +8 -3
  144. package/dist/project-modules.d.ts +12 -0
  145. package/dist/project-modules.js +106 -0
  146. package/dist/run-file.d.ts +6 -0
  147. package/dist/run-file.js +111 -0
  148. package/dist/runtime/sources.js +11 -5
  149. package/dist/source-map.d.ts +17 -0
  150. package/dist/source-map.js +75 -0
  151. package/dist/ts-types.js +4 -1
  152. package/dist/typescript/convert-objects.d.ts +2 -1
  153. package/dist/typescript/convert-objects.js +18 -9
  154. package/dist/vite.js +12 -4
  155. package/dist/walk-types.js +1 -0
  156. package/dist/walk.d.ts +0 -2
  157. package/dist/walk.js +11 -28
  158. package/package.json +1 -1
  159. package/src/runtime/chan.js +15 -5
  160. package/src/runtime/channel.js +28 -0
  161. package/src/runtime/classes.js +32 -0
  162. package/src/runtime/core.js +10 -1
  163. package/src/runtime/decode.js +190 -0
  164. package/src/runtime/go.js +3 -0
  165. package/src/runtime/index.js +6 -0
  166. package/src/runtime/select.js +14 -9
  167. package/src/runtime/signals.js +59 -0
  168. package/src/runtime/threads.js +32 -5
  169. package/src/runtime/topic.js +118 -0
  170. package/src/runtime/try.js +45 -0
@@ -0,0 +1,106 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { markupFor } from "./config.js";
4
+ import { parse } from "./parser/parser.js";
5
+ /** Directories that hold no sources of the package. */
6
+ const SKIPPED = new Set(['node_modules', 'dist', 'build', 'out', 'coverage']);
7
+ /** A bound on the files read, for a file opened in a directory as big as the home directory. */
8
+ const MAX_FILES = 5000;
9
+ const parsed = new Map();
10
+ /** The names exported by the other `.rk` modules of the package of `filename`. */
11
+ export function projectExports(filename) {
12
+ const self = resolve(filename);
13
+ return rookFiles(packageRoot(self))
14
+ .filter((path) => path !== self)
15
+ .flatMap(exportsOf);
16
+ }
17
+ /** The directory of the nearest package.json above the file, or that of the file without one. */
18
+ function packageRoot(filename) {
19
+ for (let dir = dirname(filename);; dir = dirname(dir)) {
20
+ if (exists(join(dir, 'package.json')))
21
+ return dir;
22
+ if (dirname(dir) === dir)
23
+ return dirname(filename);
24
+ }
25
+ }
26
+ function exists(path) {
27
+ try {
28
+ statSync(path);
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ function rookFiles(root) {
36
+ const files = [];
37
+ const visit = (dir) => {
38
+ let entries;
39
+ try {
40
+ entries = readdirSync(dir, { withFileTypes: true });
41
+ }
42
+ catch {
43
+ return;
44
+ }
45
+ for (const entry of entries) {
46
+ if (files.length >= MAX_FILES)
47
+ return;
48
+ if (entry.name.startsWith('.'))
49
+ continue;
50
+ const path = join(dir, entry.name);
51
+ if (entry.isDirectory() && !SKIPPED.has(entry.name))
52
+ visit(path);
53
+ else if (entry.isFile() && entry.name.endsWith('.rk'))
54
+ files.push(path);
55
+ }
56
+ };
57
+ visit(root);
58
+ return files;
59
+ }
60
+ function exportsOf(path) {
61
+ let version;
62
+ try {
63
+ const stats = statSync(path);
64
+ version = `${stats.mtimeMs}:${stats.size}`;
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ const cached = parsed.get(path);
70
+ if (cached?.version === version)
71
+ return cached.names;
72
+ let names;
73
+ try {
74
+ const { program } = parse(readFileSync(path, 'utf8'), { markup: markupFor(path) });
75
+ names = program.body.flatMap((statement) => exported(statement, path));
76
+ }
77
+ catch {
78
+ names = [];
79
+ }
80
+ parsed.set(path, { version, names });
81
+ return names;
82
+ }
83
+ function exported(statement, path) {
84
+ const name = (node, kind, spaces) => ({ name: node.name, kind, spaces, path });
85
+ if (!('exported' in statement) || !statement.exported)
86
+ return [];
87
+ switch (statement.kind) {
88
+ case 'FuncDeclaration':
89
+ return [name(statement.name, 'function', ['value'])];
90
+ case 'ClassDeclaration':
91
+ return [name(statement.name, 'class', ['value', 'type'])];
92
+ case 'ComponentDeclaration':
93
+ return [name(statement.name, 'component', ['value', 'tag'])];
94
+ case 'DecoratorDeclaration':
95
+ return [name(statement.name, 'function', ['decorator'])];
96
+ case 'InterfaceDeclaration':
97
+ case 'TypeAliasDeclaration':
98
+ return [name(statement.name, 'type', ['type'])];
99
+ case 'VariableDeclaration': {
100
+ const kind = statement.keyword === 'const' ? 'constant' : 'variable';
101
+ return statement.names.map((each) => name(each, kind, ['value']));
102
+ }
103
+ default:
104
+ return [];
105
+ }
106
+ }
@@ -0,0 +1,6 @@
1
+ export interface RunOptions {
2
+ typeCheck: boolean;
3
+ markup: boolean | undefined;
4
+ watch: boolean;
5
+ }
6
+ export declare function runFile(file: string, options: RunOptions): void;
@@ -0,0 +1,111 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, unwatchFile, watchFile, } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { displayPath } from "./build.js";
7
+ /** The signals that `rook run` gives to the program, or leaves to it, rather than ending at them. */
8
+ const SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
9
+ /** How often the files are looked at, and how long a program has to stop before it is killed. */
10
+ const POLL_MS = 200;
11
+ const KILL_MS = 2000;
12
+ export function runFile(file, options) {
13
+ // The build rewrites the extensions of imports, not of these URLs: in the package they are .js.
14
+ const extension = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
15
+ const loader = new URL(`./loader${extension}`, import.meta.url).href;
16
+ const run = fileURLToPath(new URL(`./run${extension}`, import.meta.url));
17
+ const watched = new Set();
18
+ let child = null;
19
+ let cache = '';
20
+ let restart = null;
21
+ let stopping = false;
22
+ let killer;
23
+ // The files of the run and the time each was changed when it was compiled.
24
+ const files = () => existsSync(join(cache, 'files'))
25
+ ? readFileSync(join(cache, 'files'), 'utf8')
26
+ .split('\n')
27
+ .filter(Boolean)
28
+ .map((line) => line.split('\t'))
29
+ .map(([path, time]) => [path, Number(time)])
30
+ : [];
31
+ // A file is compared with what was compiled, as it may change before it is watched.
32
+ const watch = ([path, compiled]) => {
33
+ if (!watched.has(path)) {
34
+ watched.add(path);
35
+ watchFile(path, { interval: POLL_MS }, (now, before) => {
36
+ if (now.mtimeMs !== before.mtimeMs)
37
+ changed(path);
38
+ });
39
+ }
40
+ if (existsSync(path) && statSync(path).mtimeMs !== compiled)
41
+ changed(path);
42
+ };
43
+ const changed = (path) => {
44
+ if (stopping || restart !== null)
45
+ return;
46
+ restart = path;
47
+ if (child === null) {
48
+ start();
49
+ return;
50
+ }
51
+ child.kill('SIGTERM');
52
+ killer = setTimeout(() => child?.kill('SIGKILL'), KILL_MS);
53
+ };
54
+ const stop = () => {
55
+ stopping = true;
56
+ for (const path of watched)
57
+ unwatchFile(path);
58
+ unwatchFile(join(cache, 'files'));
59
+ };
60
+ const forward = (name) => {
61
+ // Without --watch Ctrl+C is the program's; with it, it ends the watching too.
62
+ if (name !== 'SIGINT')
63
+ child?.kill(name);
64
+ if (options.watch)
65
+ stop();
66
+ if (options.watch && child === null)
67
+ process.exitCode = 0;
68
+ };
69
+ const start = () => {
70
+ if (restart !== null)
71
+ console.error(`rook: ${displayPath(restart)} changed, running again`);
72
+ restart = null;
73
+ cache = mkdtempSync(join(tmpdir(), 'rook-run-'));
74
+ const args = ['--enable-source-maps', '--import', loader, run, file];
75
+ child = spawn(process.execPath, args, {
76
+ stdio: 'inherit',
77
+ env: {
78
+ ...process.env,
79
+ ROOK_RUN_CACHE: cache,
80
+ ...(options.typeCheck ? {} : { ROOK_NO_CHECK: '1' }),
81
+ ...(options.markup === undefined ? {} : { ROOK_MARKUP: options.markup ? '1' : '0' }),
82
+ },
83
+ });
84
+ // The files of the program are watched as it loads them.
85
+ if (options.watch) {
86
+ watchFile(join(cache, 'files'), { interval: POLL_MS }, () => files().forEach(watch));
87
+ }
88
+ child.on('close', (status, signal) => {
89
+ clearTimeout(killer);
90
+ if (options.watch)
91
+ files().forEach(watch);
92
+ unwatchFile(join(cache, 'files'));
93
+ rmSync(cache, { recursive: true, force: true });
94
+ child = null;
95
+ if (!options.watch || stopping) {
96
+ for (const name of SIGNALS)
97
+ process.off(name, forward);
98
+ process.exitCode = status ?? (signal ? 1 : 0);
99
+ }
100
+ else if (restart !== null) {
101
+ start();
102
+ }
103
+ else {
104
+ console.error('rook: waiting for a change to run again');
105
+ }
106
+ });
107
+ };
108
+ for (const name of SIGNALS)
109
+ process.on(name, forward);
110
+ start();
111
+ }
@@ -2,13 +2,19 @@
2
2
  // and `npm run format` writes it again.
3
3
  /** The code of the runtime by file, for modules that have it in them (codegen/runtime.ts). */
4
4
  export const RUNTIME_FILES = {
5
- 'chan.js': "// Channels: sending, receiving and closing, here or in the thread that made the channel\n// (threads.js), and the queues of a channel. Receiving from a closed channel gives\n// the zero value of its type, as in Go.\n\nimport { $$Queue } from './queue.js';\nimport { $$commit, $$lock, $$select, $$try, $$unlock, $$unwait } from './select.js';\nimport { $$adopt, $$ask, $$ownerOf, $$post, $$registry, $$stopped } from './threads.js';\n\n$$registry().handle ??= { message: $$message, pump: $$pump };\n\nexport function $$chan(size, zero) {\n const registry = $$registry();\n const id = `${registry.key}:${registry.channels++}`;\n const channel = { $$chan: id, size, zero };\n\n registry.states.set(id, {\n size,\n zero,\n buffer: new $$Queue(),\n receivers: new $$Queue(),\n senders: new $$Queue(),\n closed: false,\n holders: 1,\n busy: false,\n });\n $$adopt(channel);\n\n return channel;\n}\n\n// A local operation waits in the queue of its channel; one of another thread asks the thread\n// that made the channel.\nexport function $$send(channel, value) {\n const id = channel.$$chan;\n const owner = $$ownerOf(id);\n\n if (owner !== $$registry().key) return $$ask(owner, { op: 'send', channel: id, value });\n\n return new Promise((resolve, reject) =>\n $$wait(id, 'senders', { value, done: resolve, fail: reject }),\n );\n}\n\nexport function $$receive(channel, withOk = false) {\n const id = channel.$$chan;\n const owner = $$ownerOf(id);\n const received =\n owner !== $$registry().key\n ? $$ask(owner, { op: 'receive', channel: id })\n : new Promise((resolve, reject) => $$wait(id, 'receivers', { done: resolve, fail: reject }));\n\n return withOk ? received : received.then(([value]) => value);\n}\n\n// Closing a channel of another thread fails this one if it was closed already, as a panic would.\nexport function $$close(channel) {\n const id = channel.$$chan;\n const owner = $$ownerOf(id);\n\n if (owner === $$registry().key) return $$closeHere(id);\n $$ask(owner, { op: 'close', channel: id }).catch((error) => {\n setTimeout(() => {\n throw error;\n });\n });\n}\n\nexport function $$closeHere(id) {\n const state = $$registry().states.get(id);\n\n if (state.closed) throw new Error('close of a closed channel');\n state.closed = true;\n $$pump(id);\n}\n\n// What a closed channel gives: a value of its own for each receiver, if it is an array or a queue.\nexport function $$zero({ zero }) {\n return Array.isArray(zero) ? [] : zero instanceof $$Queue ? new $$Queue() : zero;\n}\n\nexport async function* $$range(channel) {\n while (true) {\n const [value, ok] = await $$receive(channel, true);\n\n if (!ok) return;\n yield value;\n }\n}\n\nexport function $$wait(id, queue, entry) {\n const state = $$registry().states.get(id);\n\n if (!state) return entry.fail?.($$stopped());\n state[queue].push(entry);\n $$pump(id);\n}\n\n// The operations of other threads on the channels of this one.\nexport function $$message(from, data) {\n const answer = (value) => $$post(from, { answer: data.ask, value });\n const fail = (error) => $$post(from, { answer: data.ask, error });\n const select = { key: from, sid: data.sid };\n\n switch (data.op) {\n case 'send':\n return $$wait(data.channel, 'senders', { from, value: data.value, done: answer, fail });\n\n case 'receive':\n return $$wait(data.channel, 'receivers', { from, done: answer, fail });\n\n case 'close':\n try {\n $$closeHere(data.channel);\n\n return answer();\n } catch (error) {\n return fail(error);\n }\n\n case 'try':\n try {\n return answer($$try(data.channel, data.send, data.value));\n } catch (error) {\n return fail(error);\n }\n\n case 'select':\n return $$select(data.cases, data.hasDefault).then(answer, fail);\n\n case 'wait':\n return $$wait(data.channel, data.send ? 'senders' : 'receivers', {\n select,\n case: data.case,\n value: data.value,\n });\n\n case 'cancel':\n return $$unwait(select, data.channels);\n\n case 'lock':\n return answer($$lock(data.sid));\n\n case 'unlock':\n return $$unlock(data.sid);\n\n case 'commit':\n return $$commit(data.sid, data.case, data.result, data.error);\n }\n}\n\n// Matches what waits in a channel: a receiver with a value of the buffer, a sender or the zero value\n// after close; a sender with room in the buffer. A case of a select goes on only if its select\n// has not gone on with another case, which the thread of the select tells.\nexport function $$pump(id) {\n const state = $$registry().states.get(id);\n\n while (state && !state.busy) {\n const receiver = state.receivers.at(0);\n const sender = state.senders.at(0);\n\n if (receiver && (state.buffer.length > 0 || state.closed)) {\n if (!$$pair(id, state, receiver, null)) return;\n } else if (receiver && sender) {\n if (!$$pair(id, state, receiver, sender)) return;\n } else if (sender && (state.closed || state.buffer.length < state.size)) {\n if (!$$pair(id, state, null, sender)) return;\n } else {\n return;\n }\n }\n}\n\n// Whether the pump can go on at once: a pair whose selects are all in this thread is decided here.\nexport function $$pair(id, state, receiver, sender) {\n const entries = [receiver, sender].filter((entry) => entry !== null);\n const key = $$registry().key;\n\n if (entries.some((entry) => entry.select && entry.select.key !== key)) {\n $$pairLater(id, state, receiver, sender);\n\n return false;\n }\n\n const locks = entries.map((entry) => (entry.select ? $$lock(entry.select.sid) : 'locked'));\n\n if (locks.every((lock) => lock === 'locked')) {\n $$complete(state, receiver, sender);\n\n return true;\n }\n\n entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));\n\n return $$settle(id, state, entries, locks);\n}\n\n// A pair with a select of another thread: the channel waits for the answers of that thread.\nexport async function $$pairLater(id, state, receiver, sender) {\n state.busy = true;\n\n const entries = [receiver, sender].filter((entry) => entry !== null);\n const locks = [];\n\n for (const { select } of entries) {\n const lock = !select\n ? 'locked'\n : select.key === $$registry().key\n ? $$lock(select.sid)\n : await $$ask(select.key, { op: 'lock', sid: select.sid }).catch(() => 'done');\n\n locks.push(lock);\n if (lock !== 'locked') break;\n }\n\n state.busy = false;\n if (locks.length === entries.length && locks.every((lock) => lock === 'locked')) {\n $$complete(state, receiver, sender);\n } else {\n entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));\n if (!$$settle(id, state, entries, locks)) return;\n }\n\n $$pump(id);\n}\n\n// After a pair that could not go on: a select that has gone on leaves the queue; one that another\n// channel is deciding is tried again a little later.\nexport function $$settle(id, state, entries, locks) {\n entries.forEach((entry, i) => {\n if (locks[i] === 'done') $$remove(state, entry);\n });\n if (locks.includes('done')) return true;\n setTimeout(() => $$pump(id), 1 + Math.random() * 4);\n\n return false;\n}\n\nexport function $$remove(state, entry) {\n state.receivers.$$delete(entry);\n state.senders.$$delete(entry);\n}\n\n// Takes the pair off the queues and passes the value.\nexport function $$complete(state, receiver, sender) {\n if (receiver) state.receivers.$$delete(receiver);\n if (sender) state.senders.$$delete(sender);\n if (receiver) {\n const result =\n state.buffer.length > 0\n ? [state.buffer.shift(), true]\n : sender\n ? [sender.value, true]\n : [$$zero(state), false];\n\n $$finish(receiver, result);\n if (sender) $$finish(sender);\n } else if (state.closed) {\n $$finish(sender, undefined, new Error('send on a closed channel'));\n } else {\n state.buffer.push(sender.value);\n $$finish(sender);\n }\n}\n\nexport function $$finish(entry, result, error) {\n const { select } = entry;\n\n if (!select) return error ? entry.fail(error) : entry.done(result);\n if (select.key === $$registry().key) return $$commit(select.sid, entry.case, result, error);\n $$post(select.key, { op: 'commit', sid: select.sid, case: entry.case, result, error });\n}\n\nexport function $$unlockAt(select) {\n if (!select) return;\n if (select.key === $$registry().key) $$unlock(select.sid);\n else $$post(select.key, { op: 'unlock', sid: select.sid });\n}\n",
6
- 'core.js': "// What the code of any module may need: `for` over a value of unknown type, and `defer`.\n\n// `for v in x` over an untyped value: a Map gives its values, as a typed one does.\nexport function $$values(iterable) {\n if (typeof iterable === 'function') return iterable();\n\n return iterable instanceof Map ? iterable.values() : iterable;\n}\n\n// Runs deferred calls in reverse order; every call runs even if an earlier one throws.\nexport function $$runDeferred(deferred) {\n let failure;\n\n for (let i = deferred.length - 1; i >= 0; i--) {\n try {\n deferred[i]();\n } catch (error) {\n failure = { error };\n }\n }\n\n if (failure) throw failure.error;\n}\n",
7
- 'go.js': "// `go`: the pool of workers of the program, and the side of the worker. They work\n// with Web Workers and with the worker threads of Node, whose API differs: `on(\"message\")` instead\n// of `onmessage`, and `unref()`, so that idle workers do not keep the program running.\n\nimport {\n $$adopt,\n $$connect,\n $$copy,\n $$gone,\n $$incoming,\n $$post,\n $$registry,\n $$transfer,\n} from './threads.js';\n\n// `go` in a module: a call in a worker of the pool. `start` starts a worker of the module, with its\n// worker module: it is $$startWorker of the module, whose URL a bundler needs to see there.\nexport function $$goWorker(start, name, args) {\n return (globalThis[Symbol.for('rook.goroutines')] ??= $$pool()).call(\n start,\n name,\n $$copy(args, true),\n );\n}\n\n// The workers of the program, shared by its modules: up to one per processor. Each runs the worker\n// module of one module, `start`. Calls wait in one queue and go to a worker of their module that is\n// free: one whose thread is not running code, though its earlier calls may wait in `await`. A\n// worker that is still busy keeps its calls for later; an idle worker of another module makes room\n// for a new one, unless it keeps channels.\nexport function $$pool() {\n const size = globalThis.navigator?.hardwareConcurrency ?? 4;\n const registry = $$registry();\n const workers = [];\n const queue = [];\n const calls = new Map();\n let ids = 0;\n // A worker without calls lets the program end, unless something here waits for it.\n const unref = (worker) => {\n if (worker.calls === 0 && !(registry.pins.get(worker.key) > 0)) worker.thread.unref?.();\n };\n\n registry.idle = (key) => {\n const worker = workers.find((each) => each.key === key);\n\n if (worker) unref(worker);\n };\n\n const send = (worker, call) => {\n worker.busy = true;\n worker.calls++;\n worker.thread.ref?.();\n call.worker = worker;\n calls.set(call.id, call);\n $$post(worker.key, { call: call.id, name: call.name, args: call.args });\n call.args = null;\n };\n const dispatch = () => {\n const starting = new Map();\n\n for (const worker of workers) {\n if (!worker.key) starting.set(worker.start, (starting.get(worker.start) ?? 0) + 1);\n }\n\n for (let i = 0; i < queue.length; i++) {\n const call = queue[i];\n let free = null;\n\n for (const worker of workers) {\n if (worker.start !== call.start || !worker.key || worker.busy) continue;\n if (!free || worker.calls < free.calls) free = worker;\n }\n\n // A free worker without calls; else a new worker; else one whose calls wait in await.\n if (free?.calls === 0 || (free && workers.length >= size)) {\n queue.splice(i--, 1);\n send(free, call);\n } else if (starting.get(call.start) > 0) {\n starting.set(call.start, starting.get(call.start) - 1);\n } else if (workers.length < size || retire(call.start)) {\n add(call.start);\n } else {\n // It waits for a busy worker of its module, which may have let go of the program when\n // its last call returned: the program runs on until that worker is free and takes it.\n for (const worker of workers)\n if (worker.start === call.start && worker.key) worker.thread.ref?.();\n }\n }\n };\n const retire = (start) => {\n const idle = workers.find(\n (worker) =>\n worker.key && worker.start !== start && !worker.busy && worker.calls === 0 && !worker.owns,\n );\n\n if (!idle) return false;\n idle.thread.terminate();\n remove(idle, null);\n\n return true;\n };\n // A worker that stopped: its calls fail, and the threads that talked to it forget it.\n const remove = (worker, error) => {\n workers.splice(workers.indexOf(worker), 1);\n for (const [id, call] of calls) {\n if (call.worker !== worker) continue;\n calls.delete(id);\n call.reject(error);\n }\n\n if (worker.key) {\n $$gone(worker.key);\n for (const other of workers) {\n if (other.key) other.thread.postMessage({ gone: worker.key });\n }\n }\n };\n const fail = (worker, error) => {\n remove(worker, error);\n dispatch();\n };\n const receive = (worker, data) => {\n if ('op' in data || 'answer' in data) return $$incoming(worker.key, data);\n if ('hello' in data) {\n worker.key = data.hello;\n $$connect(worker.key, worker.thread, false);\n worker.thread.postMessage({ init: registry.key });\n // Workers talk to each other through ports of their own, also while this thread is busy.\n for (const other of workers) {\n if (other === worker || !other.key) continue;\n\n const { port1, port2 } = new MessageChannel();\n\n worker.thread.postMessage({ port: other.key, through: port1 }, [port1]);\n other.thread.postMessage({ port: worker.key, through: port2 }, [port2]);\n }\n\n worker.busy = false;\n unref(worker);\n } else if ('free' in data) {\n worker.busy = false;\n worker.owns = data.owns;\n } else if ('returned' in data) {\n $$adopt(data);\n\n const call = calls.get(data.returned);\n\n calls.delete(data.returned);\n worker.owns = data.owns;\n worker.calls--;\n unref(worker);\n if ('error' in data) call?.reject(data.error);\n else call?.resolve(data.value);\n }\n\n dispatch();\n // A worker that a waiting call held on to, now free and without a call, lets go again.\n if ('free' in data) unref(worker);\n };\n const add = (start) => {\n const worker = { start, key: null, thread: null, busy: true, calls: 0, owns: false };\n\n workers.push(worker);\n start().then(\n (thread) => {\n worker.thread = thread;\n if (thread.on) {\n thread.on('message', (data) => receive(worker, data));\n thread.on('error', (error) => fail(worker, error));\n } else {\n thread.onmessage = (event) => receive(worker, event.data);\n thread.onerror = (event) => fail(worker, new Error(event.message ?? 'a worker failed'));\n }\n },\n (error) => {\n remove(worker, error);\n for (const call of queue.filter((each) => each.start === start)) {\n queue.splice(queue.indexOf(call), 1);\n call.reject(error);\n }\n },\n );\n };\n const tasks = new WeakMap();\n\n return {\n call: (start, name, args) => {\n const call = { id: ids++, start, name, args, resolve: null, reject: null, worker: null };\n const task = new Promise((resolve, reject) => Object.assign(call, { resolve, reject }));\n\n tasks.set(task, call);\n queue.push(call);\n dispatch();\n\n return task;\n },\n // A call that waits leaves the queue; a worker cannot stop a call, so the worker stops, and\n // the other calls in it fail too.\n cancel: (task) => {\n const call = tasks.get(task);\n\n tasks.delete(task);\n if (!call || (!queue.includes(call) && !calls.has(call.id))) return;\n task.catch(() => {});\n if (queue.includes(call)) {\n queue.splice(queue.indexOf(call), 1);\n\n return call.reject(new Error('goroutine cancelled'));\n }\n\n calls.delete(call.id);\n call.reject(new Error('goroutine cancelled'));\n call.worker.thread.terminate();\n remove(\n call.worker,\n new Error('the worker of the goroutine stopped: another goroutine in it was cancelled'),\n );\n dispatch();\n },\n };\n}\n\n// `go` inside a goroutine: the function runs in the same worker, with copies of the arguments. A\n// name is one of the `functions` of the worker module, a function literal is itself.\nexport function $$goHere(functions, fn, args) {\n const copies = $$copy(args);\n const run = typeof fn === 'function' ? fn : functions[fn];\n let cancel;\n const cancelled = new Promise((_, reject) => (cancel = reject));\n const task = Promise.race([cancelled, Promise.resolve().then(() => cancel && run(...copies))]);\n\n $$registry().cancels.set(task, () => {\n task.catch(() => {});\n cancel(new Error('goroutine cancelled'));\n cancel = null;\n });\n\n return task;\n}\n\n// `go for`: the array is split into parts, each part runs as a goroutine of `go` of the module, and\n// the loop waits for all of them. The pool gives a part to the first worker that is free, so with\n// several parts per processor the workers end at about the same time, also when some elements take\n// longer than others or some processors are slower. The names that the body uses are copied with\n// each part: if one of them is an object, which may be large, there is one part per processor. A\n// typed array on a SharedArrayBuffer is shared with the workers, which write into it; other parts\n// are copies, moved to the workers and back into the array.\nexport async function $$goFor(go, name, items, ...captured) {\n const partsPerProcessor = 4;\n const processors = globalThis.navigator?.hardwareConcurrency ?? 4;\n const small = captured.every((value) => typeof value !== 'object' || value === null);\n const count = Math.min(small ? processors * partsPerProcessor : processors, items.length);\n const shared =\n typeof SharedArrayBuffer === 'function' && items.buffer instanceof SharedArrayBuffer;\n const parts = [];\n\n for (let k = 0; k < count; k++) {\n const from = Math.floor((items.length * k) / count);\n const to = Math.floor((items.length * (k + 1)) / count);\n const part = shared ? items.subarray(from, to) : items.slice(from, to);\n\n if (ArrayBuffer.isView(part) && !shared) $$transfer(part);\n parts.push(\n go(name, [part, from, ...captured]).then((written) => {\n if (written === undefined || shared) return;\n if (ArrayBuffer.isView(items)) items.set(written, from);\n else for (let i = 0; i < written.length; i++) items[from + i] = written[i];\n }),\n );\n }\n\n await Promise.all(parts);\n}\n\n// `make([]T, n, shared)`: the memory of a typed array that the workers of `go for` write into,\n// without copies. A page has SharedArrayBuffer only with cross-origin isolation.\nexport function $$shared(bytes) {\n if (typeof SharedArrayBuffer !== 'function') {\n throw new Error(\n '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',\n );\n }\n\n return new SharedArrayBuffer(bytes);\n}\n\n// The worker: runs the calls that the pool sends and sends back their results or errors. Once the\n// thread is free again, it says so, and the pool may send another call.\nexport async function $$serve(functions) {\n const threads = 'node:worker_threads';\n const port =\n typeof postMessage === 'function'\n ? globalThis\n : (await import(/* @vite-ignore */ threads)).parentPort;\n const registry = $$registry();\n\n // The port to the main thread keeps the worker running.\n registry.idle = (key) => key !== registry.main && registry.ports.get(key)?.unref?.();\n port.postMessage({ hello: registry.key });\n\n const free = () => port.postMessage({ free: true, owns: registry.states.size > 0 });\n const receive = async (data) => {\n if ('op' in data || 'answer' in data) return $$incoming(registry.main, data);\n if ('init' in data) {\n registry.main = data.init;\n\n return $$connect(data.init, port, false);\n }\n\n if ('port' in data) return $$connect(data.port, data.through, true);\n if ('gone' in data) return $$gone(data.gone);\n setTimeout(free);\n $$adopt(data.args);\n\n const { main } = registry;\n\n try {\n const value = await functions[data.name](...data.args);\n\n $$post(main, { returned: data.call, value, owns: registry.states.size > 0 });\n } catch (error) {\n try {\n $$post(main, { returned: data.call, error, owns: registry.states.size > 0 });\n } catch {\n $$post(main, { returned: data.call, error: new Error(String(error)), owns: true });\n }\n }\n };\n\n if (port.on) port.on('message', receive);\n else port.onmessage = (event) => receive(event.data);\n}\n\n// `cancel(task)`: stops a goroutine of `go`, whose Promise fails with \"goroutine cancelled\". A call\n// in a worker stops with its worker; inside a goroutine, `go` runs in the same worker, so its\n// Promise fails but code that has started goes on.\nexport function $$cancel(task) {\n globalThis[Symbol.for('rook.goroutines')]?.cancel(task);\n $$registry().cancels.get(task)?.();\n}\n",
5
+ 'chan.js': "// Channels: sending, receiving and closing, here or in the thread that made the channel\n// (threads.js), and the queues of a channel. Receiving from a closed channel gives\n// the zero value of its type, as in Go. Channel.EMPTY is closed and belongs to no thread: a send to\n// it goes nowhere, and receiving gives the zero value that the code that receives passes.\n\nimport { $$Queue } from './queue.js';\nimport { $$commit, $$lock, $$select, $$try, $$unlock, $$unwait } from './select.js';\nimport { $$adopt, $$ask, $$isEmpty, $$ownerOf, $$post, $$registry, $$stopped } from './threads.js';\n\n$$registry().handle ??= { message: $$message, pump: $$pump };\n\nexport function $$chan(size, zero) {\n const registry = $$registry();\n const id = `${registry.key}:${registry.channels++}`;\n const channel = { $$chan: id, size, zero };\n\n registry.states.set(id, {\n size,\n zero,\n buffer: new $$Queue(),\n receivers: new $$Queue(),\n senders: new $$Queue(),\n closed: false,\n holders: 1,\n busy: false,\n });\n $$adopt(channel);\n\n return channel;\n}\n\n// A local operation waits in the queue of its channel; one of another thread asks the thread\n// that made the channel.\nexport function $$send(channel, value) {\n const id = channel.$$chan;\n\n if ($$isEmpty(id)) return Promise.resolve();\n\n const owner = $$ownerOf(id);\n\n if (owner !== $$registry().key) return $$ask(owner, { op: 'send', channel: id, value });\n\n return new Promise((resolve, reject) =>\n $$wait(id, 'senders', { value, done: resolve, fail: reject }),\n );\n}\n\nexport function $$receive(channel, withOk = false, zero = null) {\n const id = channel.$$chan;\n const owner = $$ownerOf(id);\n const received = $$isEmpty(id)\n ? Promise.resolve([zero, false])\n : owner !== $$registry().key\n ? $$ask(owner, { op: 'receive', channel: id })\n : new Promise((resolve, reject) => $$wait(id, 'receivers', { done: resolve, fail: reject }));\n\n return withOk ? received : received.then(([value]) => value);\n}\n\n// Closing a channel of another thread fails this one if it was closed already, as a panic would.\nexport function $$close(channel) {\n const id = channel.$$chan;\n\n if ($$isEmpty(id)) return;\n\n const owner = $$ownerOf(id);\n\n if (owner === $$registry().key) return $$closeHere(id);\n $$ask(owner, { op: 'close', channel: id }).catch((error) => {\n setTimeout(() => {\n throw error;\n });\n });\n}\n\nexport function $$closeHere(id) {\n const state = $$registry().states.get(id);\n\n if (state.closed) throw new Error('close of a closed channel');\n state.closed = true;\n // What made the channel stops with it, as signal() does (signals.js).\n state.onClose?.();\n $$pump(id);\n}\n\n// What a closed channel gives: a value of its own for each receiver, if it is an array or a queue.\nexport function $$zero({ zero }) {\n return Array.isArray(zero) ? [] : zero instanceof $$Queue ? new $$Queue() : zero;\n}\n\nexport async function* $$range(channel) {\n while (true) {\n const [value, ok] = await $$receive(channel, true);\n\n if (!ok) return;\n yield value;\n }\n}\n\nexport function $$wait(id, queue, entry) {\n const state = $$registry().states.get(id);\n\n if (!state) return entry.fail?.($$stopped());\n state[queue].push(entry);\n $$pump(id);\n}\n\n// The operations of other threads on the channels of this one.\nexport function $$message(from, data) {\n const answer = (value) => $$post(from, { answer: data.ask, value });\n const fail = (error) => $$post(from, { answer: data.ask, error });\n const select = { key: from, sid: data.sid };\n\n switch (data.op) {\n case 'send':\n return $$wait(data.channel, 'senders', { from, value: data.value, done: answer, fail });\n\n case 'receive':\n return $$wait(data.channel, 'receivers', { from, done: answer, fail });\n\n case 'close':\n try {\n $$closeHere(data.channel);\n\n return answer();\n } catch (error) {\n return fail(error);\n }\n\n case 'try':\n try {\n return answer($$try(data.channel, data.send, data.value));\n } catch (error) {\n return fail(error);\n }\n\n case 'select':\n return $$select(data.cases, data.hasDefault).then(answer, fail);\n\n case 'wait':\n return $$wait(data.channel, data.send ? 'senders' : 'receivers', {\n select,\n case: data.case,\n value: data.value,\n });\n\n case 'cancel':\n return $$unwait(select, data.channels);\n\n case 'lock':\n return answer($$lock(data.sid));\n\n case 'unlock':\n return $$unlock(data.sid);\n\n case 'commit':\n return $$commit(data.sid, data.case, data.result, data.error);\n }\n}\n\n// Matches what waits in a channel: a receiver with a value of the buffer, a sender or the zero value\n// after close; a sender with room in the buffer. A case of a select goes on only if its select\n// has not gone on with another case, which the thread of the select tells.\nexport function $$pump(id) {\n const state = $$registry().states.get(id);\n\n while (state && !state.busy) {\n const receiver = state.receivers.at(0);\n const sender = state.senders.at(0);\n\n if (receiver && (state.buffer.length > 0 || state.closed)) {\n if (!$$pair(id, state, receiver, null)) return;\n } else if (receiver && sender) {\n if (!$$pair(id, state, receiver, sender)) return;\n } else if (sender && (state.closed || state.buffer.length < state.size)) {\n if (!$$pair(id, state, null, sender)) return;\n } else {\n return;\n }\n }\n}\n\n// Whether the pump can go on at once: a pair whose selects are all in this thread is decided here.\nexport function $$pair(id, state, receiver, sender) {\n const entries = [receiver, sender].filter((entry) => entry !== null);\n const key = $$registry().key;\n\n if (entries.some((entry) => entry.select && entry.select.key !== key)) {\n $$pairLater(id, state, receiver, sender);\n\n return false;\n }\n\n const locks = entries.map((entry) => (entry.select ? $$lock(entry.select.sid) : 'locked'));\n\n if (locks.every((lock) => lock === 'locked')) {\n $$complete(state, receiver, sender);\n\n return true;\n }\n\n entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));\n\n return $$settle(id, state, entries, locks);\n}\n\n// A pair with a select of another thread: the channel waits for the answers of that thread.\nexport async function $$pairLater(id, state, receiver, sender) {\n state.busy = true;\n\n const entries = [receiver, sender].filter((entry) => entry !== null);\n const locks = [];\n\n for (const { select } of entries) {\n const lock = !select\n ? 'locked'\n : select.key === $$registry().key\n ? $$lock(select.sid)\n : await $$ask(select.key, { op: 'lock', sid: select.sid }).catch(() => 'done');\n\n locks.push(lock);\n if (lock !== 'locked') break;\n }\n\n state.busy = false;\n if (locks.length === entries.length && locks.every((lock) => lock === 'locked')) {\n $$complete(state, receiver, sender);\n } else {\n entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));\n if (!$$settle(id, state, entries, locks)) return;\n }\n\n $$pump(id);\n}\n\n// After a pair that could not go on: a select that has gone on leaves the queue; one that another\n// channel is deciding is tried again a little later.\nexport function $$settle(id, state, entries, locks) {\n entries.forEach((entry, i) => {\n if (locks[i] === 'done') $$remove(state, entry);\n });\n if (locks.includes('done')) return true;\n setTimeout(() => $$pump(id), 1 + Math.random() * 4);\n\n return false;\n}\n\nexport function $$remove(state, entry) {\n state.receivers.$$delete(entry);\n state.senders.$$delete(entry);\n}\n\n// Takes the pair off the queues and passes the value.\nexport function $$complete(state, receiver, sender) {\n if (receiver) state.receivers.$$delete(receiver);\n if (sender) state.senders.$$delete(sender);\n if (receiver) {\n const result =\n state.buffer.length > 0\n ? [state.buffer.shift(), true]\n : sender\n ? [sender.value, true]\n : [$$zero(state), false];\n\n $$finish(receiver, result);\n if (sender) $$finish(sender);\n } else if (state.closed) {\n $$finish(sender, undefined, new Error('send on a closed channel'));\n } else {\n state.buffer.push(sender.value);\n $$finish(sender);\n }\n}\n\nexport function $$finish(entry, result, error) {\n const { select } = entry;\n\n if (!select) return error ? entry.fail(error) : entry.done(result);\n if (select.key === $$registry().key) return $$commit(select.sid, entry.case, result, error);\n $$post(select.key, { op: 'commit', sid: select.sid, case: entry.case, result, error });\n}\n\nexport function $$unlockAt(select) {\n if (!select) return;\n if (select.key === $$registry().key) $$unlock(select.sid);\n else $$post(select.key, { op: 'unlock', sid: select.sid });\n}\n",
6
+ 'channel.js': "// `Channel`: what Rook code and the code compiled from it do with channels, by name. The functions\n// themselves are in chan.js, select.js, signals.js, streams.js and timers.js.\n\nimport { $$chan, $$close, $$range, $$receive, $$send } from './chan.js';\nimport { $$select, $$try } from './select.js';\nimport { $$abortSignal, $$done, $$signal } from './signals.js';\nimport { $$fromStream, $$stream } from './streams.js';\nimport { $$after, $$tick } from './timers.js';\n\nexport const $$Channel = Object.freeze({\n // The zero value of channel types: closed, and the same in every thread (threads.js).\n EMPTY: Object.freeze({ $$chan: 'empty', size: 0, zero: null }),\n make: $$chan,\n send: $$send,\n receive: $$receive,\n close: $$close,\n range: $$range,\n select: $$select,\n // [] for a send and [value, ok] for a receive that goes on at once, else null.\n try: (channel, send, value) => $$try(channel.$$chan, send, value),\n fromStream: $$fromStream,\n stream: $$stream,\n after: $$after,\n tick: $$tick,\n signal: $$signal,\n done: $$done,\n abortSignal: $$abortSignal,\n});\n",
7
+ 'classes.js': "// Data classes: `data class Point` registers its class with an id of its own, the name and a hash\n// of its code, the same in a module and in its worker module, which has a copy of the class. A copy\n// of a value that goes to another thread loses the class of its instances (structuredClone); the\n// thread that sends lists them with their ids (threads.js), and the one that gets the copy gives\n// them their class again.\n\nfunction registry() {\n return (globalThis[Symbol.for('rook.classes')] ??= { byId: new Map(), ids: new WeakMap() });\n}\n\nexport function $$dataClass(cls, id) {\n const { byId, ids } = registry();\n\n byId.set(id, cls.prototype);\n ids.set(cls.prototype, id);\n}\n\n/** The id of the data class of an instance, or undefined. */\nexport function $$classOf(value) {\n return registry().ids.get(Object.getPrototypeOf(value));\n}\n\n/** Gives the copies of instances their classes back: `[object, id]` pairs of a copied message. */\nexport function $$reclass(instances) {\n const { byId } = registry();\n\n for (const [object, id] of instances) {\n const prototype = byId.get(id);\n\n if (prototype) Object.setPrototypeOf(object, prototype);\n }\n}\n",
8
+ 'core.js': '// What the code of any module may need: `for` over a value of unknown type, `defer`, and wrapped\n// errors.\n\n// `for v in x` over an untyped value: a Map gives its values, as a typed one does.\nexport function $$values(iterable) {\n if (typeof iterable === \'function\') return iterable();\n\n return iterable instanceof Map ? iterable.values() : iterable;\n}\n\n// Runs deferred calls in reverse order; every call runs even if an earlier one throws.\nexport function $$runDeferred(deferred) {\n let failure;\n\n for (let i = deferred.length - 1; i >= 0; i--) {\n try {\n deferred[i]();\n } catch (error) {\n failure = { error };\n }\n }\n\n if (failure) throw failure.error;\n}\n\n// `error("load user", err)`: an error whose message ends with that of its cause, as `%w` of Go\n// gives "load user: not found", and whose `cause` is the error itself, for code that looks at it.\nexport function $$wrap(message, cause) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n\n return new Error(`${message}: ${reason}`, { cause });\n}\n',
9
+ 'decode.js': "// `decode<T>(x)`: JSON text, or a value from JS, checked against the type T, which the compiler\n// writes as data: \"number\", \"string\", \"bool\", \"any\", { literal: [values] }, { nullable: T },\n// { array: T }, { record: T }, { union: [types] } and $$type(name, fields) for an object. A value\n// that fits is returned as it is, with the fields that T does not have; a nullable field may be\n// missing. The error says where the value does not fit: `decode User: tags[1]: expected string,\n// got 42`.\n\n/** An object type. Its fields come from a function, so that types can refer to each other. */\nexport function $$type(name, fields) {\n let resolved = null;\n\n return {\n name,\n get fields() {\n return (resolved ??= fields());\n },\n };\n}\n\n/** `decode<T>(text)` of a string: the JSON text is parsed, then checked. */\nexport function $$decodeJson(text, type) {\n let value;\n\n try {\n value = JSON.parse(text);\n } catch (error) {\n return [null, new Error(`decode ${describe(type)}: invalid JSON: ${error.message}`)];\n }\n\n return $$decode(value, type);\n}\n\n/**\n * `decode<T>(value)` of a value from JS. Without the types of the checker (`rook build --no-check`)\n * the type is null: then a string is parsed as JSON text, and nothing is checked.\n */\nexport function $$decode(value, type) {\n if (type === null) return typeof value === 'string' ? $$decodeJson(value, 'any') : [value, null];\n\n const problem = check(value, type, []);\n\n if (problem === null) return [value, null];\n\n const where = problem.path.length > 0 ? `${pathText(problem.path)}: ` : '';\n\n return [null, new Error(`decode ${describe(type)}: ${where}${problem.message}`)];\n}\n\nconst PRIMITIVES = { number: 'number', string: 'string', bool: 'boolean' };\n\n/** Where and how the value does not fit the type, or null if it does. */\nfunction check(value, type, path) {\n if (typeof type === 'string') {\n return type === 'any' || typeof value === PRIMITIVES[type] ? null : mismatch(value, type, path);\n }\n\n if ('literal' in type) return type.literal.includes(value) ? null : mismatch(value, type, path);\n\n if ('nullable' in type) {\n if (value == null) return null;\n\n const problem = check(value, type.nullable, path);\n\n // The value itself is of another type: the message names ?T, not T.\n return problem !== null && problem.path.length === path.length\n ? mismatch(value, type, path)\n : problem;\n }\n\n if ('array' in type) {\n if (!Array.isArray(value)) return mismatch(value, type, path);\n\n for (let i = 0; i < value.length; i++) {\n const problem = check(value[i], type.array, [...path, i]);\n\n if (problem !== null) return problem;\n }\n\n return null;\n }\n\n if ('union' in type) return checkUnion(value, type, path);\n if (!isObject(value)) return mismatch(value, type, path);\n\n if ('record' in type) {\n for (const key of Object.keys(value)) {\n const problem = check(value[key], type.record, [...path, key]);\n\n if (problem !== null) return problem;\n }\n\n return null;\n }\n\n for (const [name, field] of Object.entries(type.fields)) {\n // Only own fields: a field named `toString` is not the method of every object.\n if (!Object.hasOwn(value, name)) {\n if (isOptional(field)) continue;\n\n return { path: [...path, name], message: 'missing' };\n }\n\n const problem = check(value[name], field, [...path, name]);\n\n if (problem !== null) return problem;\n }\n\n return null;\n}\n\n/**\n * The value fits one of the types. If none fits, the type whose check went deepest into the value\n * is most likely the one meant, `{ kind: \"circle\", r: \"2\" }` of `Circle | Square`, so its problem\n * is the one reported.\n */\nfunction checkUnion(value, type, path) {\n let deepest = null;\n\n for (const member of type.union) {\n const problem = check(value, member, path);\n\n if (problem === null) return null;\n if (problem.path.length > (deepest?.path.length ?? path.length)) deepest = problem;\n }\n\n return deepest ?? mismatch(value, type, path);\n}\n\nfunction isObject(value) {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isOptional(type) {\n return type === 'any' || (typeof type === 'object' && 'nullable' in type);\n}\n\nfunction mismatch(value, type, path) {\n return { path, message: `expected ${describe(type)}, got ${shown(value)}` };\n}\n\n/** The type as Rook writes it: `[]string`, `?User`, `\"admin\" | \"user\"`. */\nfunction describe(type) {\n if (typeof type === 'string') return type;\n if ('literal' in type) return type.literal.map((value) => JSON.stringify(value)).join(' | ');\n if ('nullable' in type) return `?${grouped(type.nullable)}`;\n if ('array' in type) return `[]${grouped(type.array)}`;\n if ('record' in type) return `{ [key string]: ${describe(type.record)} }`;\n if ('union' in type) return type.union.map(describe).join(' | ');\n\n return type.name;\n}\n\nfunction grouped(type) {\n const several = typeof type === 'object' && ('union' in type || type.literal?.length > 1);\n\n return several ? `(${describe(type)})` : describe(type);\n}\n\n/** The value in a message: a primitive itself, a long string cut, and the kind of anything else. */\nfunction shown(value) {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'an array';\n\n switch (typeof value) {\n case 'string':\n return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value);\n\n case 'number':\n case 'boolean':\n return String(value);\n\n case 'object':\n return 'an object';\n\n default:\n return typeof value;\n }\n}\n\n/** `tags[1]`, `boss.name`, `[\"first name\"]`. */\nfunction pathText(path) {\n return path\n .map((key, i) => {\n if (typeof key === 'number') return `[${key}]`;\n if (/^[A-Za-z_$][\\w$]*$/.test(key)) return i === 0 ? key : `.${key}`;\n\n return `[${JSON.stringify(key)}]`;\n })\n .join('');\n}\n",
10
+ 'go.js': "// `go`: the pool of workers of the program, and the side of the worker. They work\n// with Web Workers and with the worker threads of Node, whose API differs: `on(\"message\")` instead\n// of `onmessage`, and `unref()`, so that idle workers do not keep the program running.\n\nimport {\n $$adopt,\n $$connect,\n $$copy,\n $$gone,\n $$incoming,\n $$post,\n $$reclassed,\n $$registry,\n $$transfer,\n} from './threads.js';\n\n// `go` in a module: a call in a worker of the pool. `start` starts a worker of the module, with its\n// worker module: it is $$startWorker of the module, whose URL a bundler needs to see there.\nexport function $$goWorker(start, name, args) {\n return (globalThis[Symbol.for('rook.goroutines')] ??= $$pool()).call(\n start,\n name,\n $$copy(args, true),\n );\n}\n\n// The workers of the program, shared by its modules: up to one per processor. Each runs the worker\n// module of one module, `start`. Calls wait in one queue and go to a worker of their module that is\n// free: one whose thread is not running code, though its earlier calls may wait in `await`. A\n// worker that is still busy keeps its calls for later; an idle worker of another module makes room\n// for a new one, unless it keeps channels.\nexport function $$pool() {\n const size = globalThis.navigator?.hardwareConcurrency ?? 4;\n const registry = $$registry();\n const workers = [];\n const queue = [];\n const calls = new Map();\n let ids = 0;\n // A worker without calls lets the program end, unless something here waits for it.\n const unref = (worker) => {\n if (worker.calls === 0 && !(registry.pins.get(worker.key) > 0)) worker.thread.unref?.();\n };\n\n registry.idle = (key) => {\n const worker = workers.find((each) => each.key === key);\n\n if (worker) unref(worker);\n };\n\n const send = (worker, call) => {\n worker.busy = true;\n worker.calls++;\n worker.thread.ref?.();\n call.worker = worker;\n calls.set(call.id, call);\n $$post(worker.key, { call: call.id, name: call.name, args: call.args });\n call.args = null;\n };\n const dispatch = () => {\n const starting = new Map();\n\n for (const worker of workers) {\n if (!worker.key) starting.set(worker.start, (starting.get(worker.start) ?? 0) + 1);\n }\n\n for (let i = 0; i < queue.length; i++) {\n const call = queue[i];\n let free = null;\n\n for (const worker of workers) {\n if (worker.start !== call.start || !worker.key || worker.busy) continue;\n if (!free || worker.calls < free.calls) free = worker;\n }\n\n // A free worker without calls; else a new worker; else one whose calls wait in await.\n if (free?.calls === 0 || (free && workers.length >= size)) {\n queue.splice(i--, 1);\n send(free, call);\n } else if (starting.get(call.start) > 0) {\n starting.set(call.start, starting.get(call.start) - 1);\n } else if (workers.length < size || retire(call.start)) {\n add(call.start);\n } else {\n // It waits for a busy worker of its module, which may have let go of the program when\n // its last call returned: the program runs on until that worker is free and takes it.\n for (const worker of workers)\n if (worker.start === call.start && worker.key) worker.thread.ref?.();\n }\n }\n };\n const retire = (start) => {\n const idle = workers.find(\n (worker) =>\n worker.key && worker.start !== start && !worker.busy && worker.calls === 0 && !worker.owns,\n );\n\n if (!idle) return false;\n idle.thread.terminate();\n remove(idle, null);\n\n return true;\n };\n // A worker that stopped: its calls fail, and the threads that talked to it forget it.\n const remove = (worker, error) => {\n workers.splice(workers.indexOf(worker), 1);\n for (const [id, call] of calls) {\n if (call.worker !== worker) continue;\n calls.delete(id);\n call.reject(error);\n }\n\n if (worker.key) {\n $$gone(worker.key);\n for (const other of workers) {\n if (other.key) other.thread.postMessage({ gone: worker.key });\n }\n }\n };\n const fail = (worker, error) => {\n remove(worker, error);\n dispatch();\n };\n const receive = (worker, data) => {\n if ('op' in data || 'answer' in data) return $$incoming(worker.key, data);\n if ('hello' in data) {\n worker.key = data.hello;\n $$connect(worker.key, worker.thread, false);\n worker.thread.postMessage({ init: registry.key });\n // Workers talk to each other through ports of their own, also while this thread is busy.\n for (const other of workers) {\n if (other === worker || !other.key) continue;\n\n const { port1, port2 } = new MessageChannel();\n\n worker.thread.postMessage({ port: other.key, through: port1 }, [port1]);\n other.thread.postMessage({ port: worker.key, through: port2 }, [port2]);\n }\n\n worker.busy = false;\n unref(worker);\n } else if ('free' in data) {\n worker.busy = false;\n worker.owns = data.owns;\n } else if ('returned' in data) {\n $$adopt(data);\n $$reclassed(data);\n\n const call = calls.get(data.returned);\n\n calls.delete(data.returned);\n worker.owns = data.owns;\n worker.calls--;\n unref(worker);\n if ('error' in data) call?.reject(data.error);\n else call?.resolve(data.value);\n }\n\n dispatch();\n // A worker that a waiting call held on to, now free and without a call, lets go again.\n if ('free' in data) unref(worker);\n };\n const add = (start) => {\n const worker = { start, key: null, thread: null, busy: true, calls: 0, owns: false };\n\n workers.push(worker);\n start().then(\n (thread) => {\n worker.thread = thread;\n if (thread.on) {\n thread.on('message', (data) => receive(worker, data));\n thread.on('error', (error) => fail(worker, error));\n } else {\n thread.onmessage = (event) => receive(worker, event.data);\n thread.onerror = (event) => fail(worker, new Error(event.message ?? 'a worker failed'));\n }\n },\n (error) => {\n remove(worker, error);\n for (const call of queue.filter((each) => each.start === start)) {\n queue.splice(queue.indexOf(call), 1);\n call.reject(error);\n }\n },\n );\n };\n const tasks = new WeakMap();\n\n return {\n call: (start, name, args) => {\n const call = { id: ids++, start, name, args, resolve: null, reject: null, worker: null };\n const task = new Promise((resolve, reject) => Object.assign(call, { resolve, reject }));\n\n tasks.set(task, call);\n queue.push(call);\n dispatch();\n\n return task;\n },\n // A call that waits leaves the queue; a worker cannot stop a call, so the worker stops, and\n // the other calls in it fail too.\n cancel: (task) => {\n const call = tasks.get(task);\n\n tasks.delete(task);\n if (!call || (!queue.includes(call) && !calls.has(call.id))) return;\n task.catch(() => {});\n if (queue.includes(call)) {\n queue.splice(queue.indexOf(call), 1);\n\n return call.reject(new Error('goroutine cancelled'));\n }\n\n calls.delete(call.id);\n call.reject(new Error('goroutine cancelled'));\n call.worker.thread.terminate();\n remove(\n call.worker,\n new Error('the worker of the goroutine stopped: another goroutine in it was cancelled'),\n );\n dispatch();\n },\n };\n}\n\n// `go` inside a goroutine: the function runs in the same worker, with copies of the arguments. A\n// name is one of the `functions` of the worker module, a function literal is itself.\nexport function $$goHere(functions, fn, args) {\n const copies = $$copy(args);\n const run = typeof fn === 'function' ? fn : functions[fn];\n let cancel;\n const cancelled = new Promise((_, reject) => (cancel = reject));\n const task = Promise.race([cancelled, Promise.resolve().then(() => cancel && run(...copies))]);\n\n $$registry().cancels.set(task, () => {\n task.catch(() => {});\n cancel(new Error('goroutine cancelled'));\n cancel = null;\n });\n\n return task;\n}\n\n// `go for`: the array is split into parts, each part runs as a goroutine of `go` of the module, and\n// the loop waits for all of them. The pool gives a part to the first worker that is free, so with\n// several parts per processor the workers end at about the same time, also when some elements take\n// longer than others or some processors are slower. The names that the body uses are copied with\n// each part: if one of them is an object, which may be large, there is one part per processor. A\n// typed array on a SharedArrayBuffer is shared with the workers, which write into it; other parts\n// are copies, moved to the workers and back into the array.\nexport async function $$goFor(go, name, items, ...captured) {\n const partsPerProcessor = 4;\n const processors = globalThis.navigator?.hardwareConcurrency ?? 4;\n const small = captured.every((value) => typeof value !== 'object' || value === null);\n const count = Math.min(small ? processors * partsPerProcessor : processors, items.length);\n const shared =\n typeof SharedArrayBuffer === 'function' && items.buffer instanceof SharedArrayBuffer;\n const parts = [];\n\n for (let k = 0; k < count; k++) {\n const from = Math.floor((items.length * k) / count);\n const to = Math.floor((items.length * (k + 1)) / count);\n const part = shared ? items.subarray(from, to) : items.slice(from, to);\n\n if (ArrayBuffer.isView(part) && !shared) $$transfer(part);\n parts.push(\n go(name, [part, from, ...captured]).then((written) => {\n if (written === undefined || shared) return;\n if (ArrayBuffer.isView(items)) items.set(written, from);\n else for (let i = 0; i < written.length; i++) items[from + i] = written[i];\n }),\n );\n }\n\n await Promise.all(parts);\n}\n\n// `make([]T, n, shared)`: the memory of a typed array that the workers of `go for` write into,\n// without copies. A page has SharedArrayBuffer only with cross-origin isolation.\nexport function $$shared(bytes) {\n if (typeof SharedArrayBuffer !== 'function') {\n throw new Error(\n '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',\n );\n }\n\n return new SharedArrayBuffer(bytes);\n}\n\n// The worker: runs the calls that the pool sends and sends back their results or errors. Once the\n// thread is free again, it says so, and the pool may send another call.\nexport async function $$serve(functions) {\n const threads = 'node:worker_threads';\n const port =\n typeof postMessage === 'function'\n ? globalThis\n : (await import(/* @vite-ignore */ threads)).parentPort;\n const registry = $$registry();\n\n // The port to the main thread keeps the worker running.\n registry.idle = (key) => key !== registry.main && registry.ports.get(key)?.unref?.();\n port.postMessage({ hello: registry.key });\n\n const free = () => port.postMessage({ free: true, owns: registry.states.size > 0 });\n const receive = async (data) => {\n if ('op' in data || 'answer' in data) return $$incoming(registry.main, data);\n if ('init' in data) {\n registry.main = data.init;\n\n return $$connect(data.init, port, false);\n }\n\n if ('port' in data) return $$connect(data.port, data.through, true);\n if ('gone' in data) return $$gone(data.gone);\n setTimeout(free);\n $$adopt(data.args);\n $$reclassed(data);\n\n const { main } = registry;\n\n try {\n const value = await functions[data.name](...data.args);\n\n $$post(main, { returned: data.call, value, owns: registry.states.size > 0 });\n } catch (error) {\n try {\n $$post(main, { returned: data.call, error, owns: registry.states.size > 0 });\n } catch {\n $$post(main, { returned: data.call, error: new Error(String(error)), owns: true });\n }\n }\n };\n\n if (port.on) port.on('message', receive);\n else port.onmessage = (event) => receive(event.data);\n}\n\n// `cancel(task)`: stops a goroutine of `go`, whose Promise fails with \"goroutine cancelled\". A call\n// in a worker stops with its worker; inside a goroutine, `go` runs in the same worker, so its\n// Promise fails but code that has started goes on.\nexport function $$cancel(task) {\n globalThis[Symbol.for('rook.goroutines')]?.cancel(task);\n $$registry().cancels.get(task)?.();\n}\n",
8
11
  'markup.js': "// Markup: the owners of the blocks that `{if}`, `{switch}` and `{for}` remove, the\n// `mount()` of components, the blocks themselves, elements and the CSS of components.\n\nimport { $$runDeferred } from './core.js';\n\n// Owners of markup that `{if}`, `{switch}` and `{for}` may remove: the functions returned by the\n// `mount()` of its components, and the removal of the blocks inside it, run when it is removed.\n// The current owner is shared by the modules of a page, since the blocks of one module may hold\n// the components of another.\nexport const $$owner = Symbol.for('rook.owner');\n\nexport function $$owned(create) {\n const owner = { removed: false, cleanups: [] };\n const parent = globalThis[$$owner];\n\n globalThis[$$owner] = owner;\n\n let result;\n\n try {\n result = create();\n } finally {\n globalThis[$$owner] = parent;\n }\n\n const remove = () => {\n if (owner.removed) return;\n owner.removed = true;\n $$runDeferred(owner.cleanups);\n };\n\n return [result, remove];\n}\n\nexport function $$onRemove(cleanup) {\n globalThis[$$owner]?.cleanups.push(cleanup);\n}\n\n// `mount() { ... }` of a component: runs when its markup is in the document. That is checked after\n// the code that creates the component; markup that is not in the document yet is waited for with a\n// MutationObserver, which watches only while some markup waits. Markup that its owner removes\n// before that never mounts. The function `mount` returns runs when the owner is removed.\nexport const $$waiting = new Set();\nexport let $$observer = null;\n\nexport function $$mount(node, mount) {\n const entry = { node, mount, owner: globalThis[$$owner] };\n\n entry.owner?.cleanups.push(() => $$waiting.delete(entry));\n queueMicrotask(() => $$tryMount(entry));\n}\n\nexport function $$tryMount(entry) {\n if (entry.owner?.removed) return;\n\n const node = entry.node();\n\n if (node != null && !node.isConnected) {\n $$waiting.add(entry);\n if (!$$observer) {\n $$observer = new MutationObserver($$mountWaiting);\n $$observer.observe(node.ownerDocument ?? document, { childList: true, subtree: true });\n }\n\n return;\n }\n\n const cleanup = entry.mount();\n\n if (typeof cleanup === 'function') entry.owner?.cleanups.push(cleanup);\n}\n\nexport function $$mountWaiting() {\n for (const entry of $$waiting) {\n if (!entry.node().isConnected) continue;\n $$waiting.delete(entry);\n $$tryMount(entry);\n }\n\n if ($$waiting.size === 0) {\n $$observer.disconnect();\n $$observer = null;\n }\n}\n\n// Appends element content whose type is not known: null adds nothing, arrays add each item.\nexport function $$append(parent, value) {\n if (value == null || value === false) return;\n if (Array.isArray(value)) {\n for (const item of value) $$append(parent, item);\n } else {\n parent.append(value);\n }\n}\n\n// Nodes of markup that change, `{list}` or `{ok ? <b /> : null}`: they are kept before an end\n// marker. Nodes that stay are moved rather than created again. Returns the markers with the first\n// nodes, to be inserted, and the update.\nexport function $$content(value) {\n const end = document.createTextNode('');\n const fragment = document.createDocumentFragment();\n\n fragment.append(end);\n\n let nodes = [];\n let remove;\n const update = () => {\n const next = [];\n const add = (item) => {\n if (item == null || item === false) return;\n if (Array.isArray(item)) {\n for (const each of item) add(each);\n } else if (item.nodeType === 11) {\n next.push(...item.childNodes);\n } else {\n next.push(typeof item === 'object' ? item : document.createTextNode(String(item)));\n }\n };\n const [created, removeCreated] = $$owned(value);\n\n add(created);\n remove?.();\n remove = removeCreated;\n\n const kept = new Set(next);\n\n for (const node of nodes) if (!kept.has(node)) node.remove();\n end.before(...next);\n nodes = next;\n };\n\n update();\n $$onRemove(() => remove?.());\n\n return [fragment, update];\n}\n\n// `{if ...}` and `{switch ...}` in markup: shows the block that `choose` picks (-1 for none) between\n// two markers. The block is created again only when the choice changes; otherwise it is updated.\nexport function $$branches(choose, blocks) {\n const start = document.createTextNode('');\n const end = document.createTextNode('');\n const fragment = document.createDocumentFragment();\n\n fragment.append(start, end);\n\n let current = null;\n let update;\n let remove;\n const render = () => {\n const next = choose();\n\n if (next === current) {\n update?.();\n\n return;\n }\n\n current = next;\n remove?.();\n while (start.nextSibling !== end) start.nextSibling.remove();\n update = undefined;\n remove = undefined;\n if (next >= 0) {\n const [[content, blockUpdate], removeBlock] = $$owned(blocks[next]);\n\n end.before(content);\n update = blockUpdate;\n remove = removeBlock;\n }\n };\n\n render();\n $$onRemove(() => remove?.());\n\n return [fragment, render];\n}\n\n// `<Page />` whose component is a value that changes: when it does, the block that `create` makes\n// with it is created again between two markers; otherwise the block is updated.\nexport function $$dynamic(component, create) {\n const start = document.createTextNode('');\n const end = document.createTextNode('');\n const fragment = document.createDocumentFragment();\n\n fragment.append(start, end);\n\n let current;\n let update;\n let remove;\n const render = () => {\n const next = component();\n\n if (remove && next === current) {\n update?.();\n\n return;\n }\n\n current = next;\n remove?.();\n while (start.nextSibling !== end) start.nextSibling.remove();\n\n const [[content, blockUpdate], removeBlock] = $$owned(() => create(next));\n\n end.before(content);\n update = blockUpdate;\n remove = removeBlock;\n };\n\n render();\n $$onRemove(() => remove?.());\n\n return [fragment, render];\n}\n\n// `{for item in items { ... }}` in markup: a block for each item, between its own markers. On\n// updates the blocks are found again by the item itself: blocks of items that stay are updated and\n// moved into place, others are created or removed.\nexport function $$list(items, create) {\n const end = document.createTextNode('');\n const fragment = document.createDocumentFragment();\n\n fragment.append(end);\n\n let blocks = [];\n const nodesOf = (block) => {\n const nodes = [];\n\n for (let node = block.start; node !== block.end; node = node.nextSibling) nodes.push(node);\n nodes.push(block.end);\n\n return nodes;\n };\n const update = () => {\n const old = new Map();\n\n for (const block of blocks) {\n const same = old.get(block.item);\n\n if (same) same.push(block);\n else old.set(block.item, [block]);\n }\n\n const next = [];\n let index = 0;\n\n for (const item of items()) {\n let block = old.get(item)?.shift();\n\n if (block) {\n block.update?.(index);\n } else {\n const [[content, blockUpdate], remove] = $$owned(() => create(item, index));\n\n block = { item, start: document.createTextNode(''), end: document.createTextNode('') };\n block.update = blockUpdate;\n block.remove = remove;\n block.content = document.createDocumentFragment();\n block.content.append(block.start, content, block.end);\n }\n\n next.push(block);\n index++;\n }\n\n for (const same of old.values()) {\n for (const block of same) {\n block.remove();\n for (const node of nodesOf(block)) node.remove();\n }\n }\n\n let before = end;\n\n for (let i = next.length - 1; i >= 0; i--) {\n const block = next[i];\n\n if (block.content) {\n before.before(block.content);\n block.content = null;\n } else if (block.end.nextSibling !== before) {\n before.before(...nodesOf(block));\n }\n\n before = block.start;\n }\n\n blocks = next;\n };\n\n update();\n $$onRemove(() => {\n for (const block of blocks) block.remove();\n });\n\n return [fragment, update];\n}\n\n// Creates an element with its properties and the attributes that are set with setAttribute.\nexport function $$element(tag, properties, attributes) {\n const element = Object.assign(document.createElement(tag), properties);\n\n for (const name in attributes) element.setAttribute(name, attributes[name]);\n\n return element;\n}\n\n// Creates an SVG element with its attributes.\nexport function $$svgElement(tag, attributes) {\n const element = document.createElementNS('http://www.w3.org/2000/svg', tag);\n\n for (const name in attributes) element.setAttribute(name, attributes[name]);\n\n return element;\n}\n\n// Sets an attribute whose value may be null: then the attribute is removed.\nexport function $$attribute(element, name, value) {\n if (value == null) element.removeAttribute(name);\n else element.setAttribute(name, value);\n}\n\n// Adds the CSS of a component to the page once: modules that inline the same component add it with\n// the same id. `styles` keeps the CSS of the module for the shadow roots of its web components,\n// which the styles of the page do not reach.\nexport function $$style(styles, id, css) {\n styles.push(css);\n if (document.getElementById(id)) return;\n\n const style = document.createElement('style');\n\n style.id = id;\n style.textContent = css;\n document.head.append(style);\n}\n",
9
12
  'queue.js': '// The class of `queue T`. Its values are a ring in an array: `shift()` and `unshift()` move the\n// start instead of the other values, as they do in an array. A full ring is copied into one twice\n// its size, and one that is mostly empty into one half its size, so each operation costs a few\n// steps on average. The fields are data, so a copy for another thread keeps the queue, and the\n// thread that gets it gives it back its class (threads.js).\n\nexport class $$Queue {\n constructor(items = []) {\n this.$$queue = [...items];\n this.$$start = 0;\n this.$$length = this.$$queue.length;\n }\n\n get length() {\n return this.$$length;\n }\n\n push(...items) {\n for (const item of items) {\n if (this.$$length === this.$$queue.length) this.$$resize();\n this.$$queue[(this.$$start + this.$$length++) % this.$$queue.length] = item;\n }\n\n return this.$$length;\n }\n\n unshift(...items) {\n for (let i = items.length - 1; i >= 0; i--) {\n if (this.$$length === this.$$queue.length) this.$$resize();\n this.$$start = (this.$$start || this.$$queue.length) - 1;\n this.$$queue[this.$$start] = items[i];\n this.$$length++;\n }\n\n return this.$$length;\n }\n\n // An empty queue gives null; a value that is taken out is not held any more.\n shift() {\n if (this.$$length === 0) return null;\n\n const item = this.$$queue[this.$$start];\n\n this.$$queue[this.$$start] = undefined;\n this.$$start = (this.$$start + 1) % this.$$queue.length;\n this.$$length--;\n this.$$shrink();\n\n return item;\n }\n\n pop() {\n if (this.$$length === 0) return null;\n\n const at = (this.$$start + --this.$$length) % this.$$queue.length;\n const item = this.$$queue[at];\n\n this.$$queue[at] = undefined;\n this.$$shrink();\n\n return item;\n }\n\n // A position from the start, or from the end if it is negative, as with an array.\n at(index) {\n const i = Math.trunc(index) || 0;\n const at = i < 0 ? i + this.$$length : i;\n\n if (at < 0 || at >= this.$$length) return null;\n\n return this.$$queue[(this.$$start + at) % this.$$queue.length];\n }\n\n clear() {\n this.$$queue = [];\n this.$$start = 0;\n this.$$length = 0;\n }\n\n *[Symbol.iterator]() {\n for (let i = 0; i < this.$$length; i++) {\n yield this.$$queue[(this.$$start + i) % this.$$queue.length];\n }\n }\n\n // Removes a value, for the channels: at the start it costs a step, elsewhere the whole queue.\n $$delete(item) {\n if (this.$$length > 0 && this.$$queue[this.$$start] === item) return this.shift();\n\n const items = [...this];\n const at = items.indexOf(item);\n\n if (at >= 0) this.$$keep((_, i) => i !== at);\n }\n\n // Keeps the values that pass the test, in their order.\n $$keep(test) {\n const items = [...this].filter(test);\n\n this.$$queue = items;\n this.$$start = 0;\n this.$$length = items.length;\n this.$$shrink();\n }\n\n // The values from the start of a new ring with room for as many again, and at least 8.\n $$resize() {\n const items = [...this];\n\n items.length = Math.max(8, this.$$length * 2);\n this.$$queue = items;\n this.$$start = 0;\n }\n\n $$shrink() {\n if (this.$$queue.length > 8 && this.$$length * 4 <= this.$$queue.length) this.$$resize();\n }\n}\n',
10
- 'select.js': "// `select`. Its cases may wait in channels of several threads; a channel that can go on with a case\n// asks the thread of the select to lock it first, so that only one case goes on, as Go locks the\n// channels of a select.\n\nimport { $$finish, $$pump, $$zero } from './chan.js';\nimport { $$ask, $$ownerOf, $$pin, $$post, $$registry, $$stopped } from './threads.js';\n\n// `select`: the index of the case that went on, and what it received. A case is { channel } to\n// receive or { channel, value } to send; from another thread, { channel, send, value }. One that\n// can go on at once wins, a random one as in Go; else `default`, -1, if there is one; else the\n// first that can go on later. A case with a null channel never goes on.\nexport async function $$select(cases, hasDefault) {\n const registry = $$registry();\n const all = cases.map((each, i) => ({\n i,\n id: typeof each.channel === 'string' ? each.channel : (each.channel?.$$chan ?? null),\n send: each.send ?? 'value' in each,\n value: each.value,\n }));\n const live = all.filter(({ id }) => id !== null);\n const owners = new Set(live.map(({ id }) => $$ownerOf(id)));\n\n // The channels of one other thread: the select runs there.\n if (owners.size === 1 && !owners.has(registry.key)) {\n const [owner] = owners;\n\n return $$ask(owner, {\n op: 'select',\n cases: all.map(({ id: channel, send, value }) => ({ channel, send, value })),\n hasDefault,\n });\n }\n\n const order = live.map(({ i }) => i).sort(() => Math.random() - 0.5);\n\n for (const i of order) {\n const { id, send, value } = all[i];\n const owner = $$ownerOf(id);\n const ready =\n owner === registry.key\n ? $$try(id, send, value)\n : await $$ask(owner, { op: 'try', channel: id, send, value });\n\n if (ready) return [i, ...ready];\n }\n\n if (hasDefault) return [-1];\n\n return new Promise((resolve, reject) => {\n const sid = registry.asks++;\n const channels = new Map();\n\n registry.selects.set(sid, { locked: false, resolve, reject, channels });\n live.forEach(({ i, id, send, value }) => {\n const owner = $$ownerOf(id);\n\n if (!channels.has(owner)) channels.set(owner, []);\n channels.get(owner).push(id);\n if (owner === registry.key) {\n const entry = { select: { key: owner, sid }, case: i, value };\n\n registry.states.get(id)[send ? 'senders' : 'receivers'].push(entry);\n } else {\n $$post(owner, { op: 'wait', sid, case: i, channel: id, send, value });\n }\n });\n for (const owner of channels.keys()) if (owner !== registry.key) $$pin(owner, 1);\n for (const id of channels.get(registry.key) ?? []) $$pump(id);\n });\n}\n\n// What a case of a select gets at once, if it can go on: [] for a send, [value, ok] for a receive.\n// A receiver or sender that a select of another thread waits with is left to $$pump.\nexport function $$try(id, send, value) {\n const state = $$registry().states.get(id);\n\n if (!state) throw $$stopped();\n if (state.busy) return null;\n if (send && state.closed) throw new Error('send on a closed channel');\n if (!send && state.buffer.length > 0) {\n const received = state.buffer.shift();\n\n $$pump(id);\n\n return [received, true];\n }\n\n const queue = send ? state.receivers : state.senders;\n\n while (queue.length > 0) {\n const entry = queue.at(0);\n\n if (entry.select && entry.select.key !== $$registry().key) break;\n\n const lock = entry.select ? $$lock(entry.select.sid) : 'locked';\n\n if (lock === 'busy') break;\n queue.shift();\n if (lock === 'done') continue;\n if (!send) {\n $$finish(entry);\n\n return [entry.value, true];\n }\n\n $$finish(entry, [value, true]);\n\n return [];\n }\n\n if (send && state.buffer.length < state.size) {\n state.buffer.push(value);\n $$pump(id);\n\n return [];\n }\n\n return !send && state.closed ? [$$zero(state), false] : null;\n}\n\n// The thread of a select decides which of its cases goes on: a channel locks the select, then\n// commits a case or unlocks it.\nexport function $$lock(sid) {\n const select = $$registry().selects.get(sid);\n\n if (!select) return 'done';\n if (select.locked) return 'busy';\n select.locked = true;\n\n return 'locked';\n}\n\nexport function $$unlock(sid) {\n const select = $$registry().selects.get(sid);\n\n if (select) select.locked = false;\n}\n\nexport function $$commit(sid, i, result, error) {\n const registry = $$registry();\n const select = registry.selects.get(sid);\n\n if (!select) return;\n registry.selects.delete(sid);\n // The other cases leave the queues of their channels.\n for (const [owner, channels] of select.channels) {\n if (owner === registry.key) {\n $$unwait({ key: owner, sid }, channels);\n } else {\n $$post(owner, { op: 'cancel', sid, channels });\n $$pin(owner, -1);\n }\n }\n\n if (error) select.reject(error);\n else select.resolve([i, ...(result ?? [])]);\n}\n\nexport function $$unwait(select, channels) {\n const { states } = $$registry();\n const other = (entry) => entry.select?.key !== select.key || entry.select.sid !== select.sid;\n\n for (const id of channels) {\n const state = states.get(id);\n\n if (!state) continue;\n state.receivers.$$keep(other);\n state.senders.$$keep(other);\n }\n}\n",
13
+ 'select.js': "// `select`. Its cases may wait in channels of several threads; a channel that can go on with a case\n// asks the thread of the select to lock it first, so that only one case goes on, as Go locks the\n// channels of a select.\n\nimport { $$finish, $$pump, $$zero } from './chan.js';\nimport { $$ask, $$isEmpty, $$ownerOf, $$pin, $$post, $$registry, $$stopped } from './threads.js';\n\n// `select`: the index of the case that went on, and what it received. A case is { channel } to\n// receive or { channel, value } to send, with the zero value that Channel.EMPTY gives, if it is\n// not null; from another thread, { channel, send, value }. One that can go on at once wins, a\n// random one as in Go; else `default`, -1, if there is one; else the first that can go on later.\n// A case with a null channel never goes on, and one with Channel.EMPTY always can.\nexport async function $$select(cases, hasDefault) {\n const registry = $$registry();\n const all = cases.map((each, i) => ({\n i,\n id: typeof each.channel === 'string' ? each.channel : (each.channel?.$$chan ?? null),\n send: each.send ?? 'value' in each,\n value: each.value,\n zero: each.zero ?? null,\n }));\n const live = all.filter(({ id }) => id !== null);\n const owners = new Set(live.filter(({ id }) => !$$isEmpty(id)).map(({ id }) => $$ownerOf(id)));\n\n // The channels of one other thread: the select runs there.\n if (owners.size === 1 && !owners.has(registry.key) && live.every(({ id }) => !$$isEmpty(id))) {\n const [owner] = owners;\n\n return $$ask(owner, {\n op: 'select',\n cases: all.map(({ id: channel, send, value }) => ({ channel, send, value })),\n hasDefault,\n });\n }\n\n const order = live.map(({ i }) => i).sort(() => Math.random() - 0.5);\n\n for (const i of order) {\n const { id, send, value, zero } = all[i];\n const owner = $$ownerOf(id);\n const ready = $$isEmpty(id)\n ? send\n ? []\n : [zero, false]\n : owner === registry.key\n ? $$try(id, send, value)\n : await $$ask(owner, { op: 'try', channel: id, send, value });\n\n if (ready) return [i, ...ready];\n }\n\n if (hasDefault) return [-1];\n\n return new Promise((resolve, reject) => {\n const sid = registry.asks++;\n const channels = new Map();\n\n registry.selects.set(sid, { locked: false, resolve, reject, channels });\n live.forEach(({ i, id, send, value }) => {\n const owner = $$ownerOf(id);\n\n if (!channels.has(owner)) channels.set(owner, []);\n channels.get(owner).push(id);\n if (owner === registry.key) {\n const entry = { select: { key: owner, sid }, case: i, value };\n\n registry.states.get(id)[send ? 'senders' : 'receivers'].push(entry);\n } else {\n $$post(owner, { op: 'wait', sid, case: i, channel: id, send, value });\n }\n });\n for (const owner of channels.keys()) if (owner !== registry.key) $$pin(owner, 1);\n for (const id of channels.get(registry.key) ?? []) $$pump(id);\n });\n}\n\n// What a case of a select gets at once, if it can go on: [] for a send, [value, ok] for a receive.\n// A receiver or sender that a select of another thread waits with is left to $$pump.\nexport function $$try(id, send, value) {\n const state = $$registry().states.get(id);\n\n if (!state) throw $$stopped();\n if (state.busy) return null;\n if (send && state.closed) throw new Error('send on a closed channel');\n if (!send && state.buffer.length > 0) {\n const received = state.buffer.shift();\n\n $$pump(id);\n\n return [received, true];\n }\n\n const queue = send ? state.receivers : state.senders;\n\n while (queue.length > 0) {\n const entry = queue.at(0);\n\n if (entry.select && entry.select.key !== $$registry().key) break;\n\n const lock = entry.select ? $$lock(entry.select.sid) : 'locked';\n\n if (lock === 'busy') break;\n queue.shift();\n if (lock === 'done') continue;\n if (!send) {\n $$finish(entry);\n\n return [entry.value, true];\n }\n\n $$finish(entry, [value, true]);\n\n return [];\n }\n\n if (send && state.buffer.length < state.size) {\n state.buffer.push(value);\n $$pump(id);\n\n return [];\n }\n\n return !send && state.closed ? [$$zero(state), false] : null;\n}\n\n// The thread of a select decides which of its cases goes on: a channel locks the select, then\n// commits a case or unlocks it.\nexport function $$lock(sid) {\n const select = $$registry().selects.get(sid);\n\n if (!select) return 'done';\n if (select.locked) return 'busy';\n select.locked = true;\n\n return 'locked';\n}\n\nexport function $$unlock(sid) {\n const select = $$registry().selects.get(sid);\n\n if (select) select.locked = false;\n}\n\nexport function $$commit(sid, i, result, error) {\n const registry = $$registry();\n const select = registry.selects.get(sid);\n\n if (!select) return;\n registry.selects.delete(sid);\n // The other cases leave the queues of their channels.\n for (const [owner, channels] of select.channels) {\n if (owner === registry.key) {\n $$unwait({ key: owner, sid }, channels);\n } else {\n $$post(owner, { op: 'cancel', sid, channels });\n $$pin(owner, -1);\n }\n }\n\n if (error) select.reject(error);\n else select.resolve([i, ...(result ?? [])]);\n}\n\nexport function $$unwait(select, channels) {\n const { states } = $$registry();\n const other = (entry) => entry.select?.key !== select.key || entry.select.sid !== select.sid;\n\n for (const id of channels) {\n const state = states.get(id);\n\n if (!state) continue;\n state.receivers.$$keep(other);\n state.senders.$$keep(other);\n }\n}\n",
14
+ 'signals.js': "// Signals of the process and AbortSignal as channels: `signal(...names)`, `done(abortSignal)` and\n// `abortSignal(ch)`, as signal.Notify and the Done() of a context are in Go.\n\nimport { $$chan, $$closeHere, $$receive } from './chan.js';\nimport { $$try } from './select.js';\nimport { $$registry } from './threads.js';\n\n// `signal(\"SIGINT\", \"SIGTERM\")`: a channel that gets the name of each signal that comes. While it\n// listens, the signals do not end the process, and it keeps the program running, as a tick does;\n// `close(ch)` stops it, and the signals do what they did before. A signal that finds the last one\n// not received yet is dropped. Only the main thread of Node gets the signals of the process.\nexport function $$signal(...names) {\n // Node's own, which a browser does not have.\n const { process } = globalThis;\n\n if (process === undefined) throw new Error('signal() needs Node: a browser has no signals');\n\n const channel = $$chan(1, '');\n const listener = (name) => $$try(channel.$$chan, true, name);\n // A listener of a signal does not keep the program running, and a timer that never fires does.\n const timer = setInterval(() => {}, 2 ** 31 - 1);\n\n for (const name of names) process.on(name, listener);\n $$registry().states.get(channel.$$chan).onClose = () => {\n clearInterval(timer);\n for (const name of names) process.off(name, listener);\n };\n\n return channel;\n}\n\n// `done(signal)`: a channel that gets the reason of an AbortSignal when it aborts, and is closed\n// then, so that `case <-done(request.signal):` goes on once the request is cancelled.\nexport function $$done(signal) {\n const channel = $$chan(1, null);\n const abort = () => {\n $$try(channel.$$chan, true, signal.reason);\n $$closeHere(channel.$$chan);\n };\n\n if (signal.aborted) abort();\n else signal.addEventListener('abort', abort, { once: true });\n\n return channel;\n}\n\n// `abortSignal(ch)`: an AbortSignal that aborts when the channel gets a value, with the value as\n// its reason, or is closed, for `fetch(url, { signal: abortSignal(stop) })`. It receives that one\n// value, as any receiver of the channel would.\nexport function $$abortSignal(channel) {\n const controller = new AbortController();\n\n $$receive(channel, true).then(\n ([value, ok]) => controller.abort(ok ? value : undefined),\n (error) => controller.abort(error),\n );\n\n return controller.signal;\n}\n",
11
15
  'streams.js': "// Streams and channels: `chan(stream, size)` and `stream(ch)`.\n\nimport { $$chan, $$closeHere, $$receive, $$send } from './chan.js';\nimport { $$try } from './select.js';\nimport { $$registry } from './threads.js';\n\n// `chan(stream, size)`: a channel of the chunks of a stream and one of its error. The next chunk is\n// read only when the last one is in the channel, so a full channel stops the stream; closing\n// the channel cancels it.\nexport function $$fromStream(stream, size, zero) {\n const channel = $$chan(size, zero);\n const failed = $$chan(1, null);\n const id = channel.$$chan;\n\n (async () => {\n const reader = stream?.getReader();\n let failure = null;\n\n while (reader) {\n let chunk;\n\n try {\n chunk = await reader.read();\n } catch (error) {\n failure = error;\n break;\n }\n\n if (chunk.done) break;\n try {\n await $$send(channel, chunk.value);\n } catch {\n reader.cancel().catch(() => {});\n break;\n }\n }\n\n // The pump holds both channels, so they are not forgotten; the one of the error has room.\n if (!$$registry().states.get(id).closed) $$closeHere(id);\n $$try(failed.$$chan, true, failure);\n $$closeHere(failed.$$chan);\n })();\n\n return [channel, failed];\n}\n\n// `stream(ch)`: a stream that reads one value of the channel for each read, so the room of the\n// channel is all it buffers, and ends when the channel is closed.\nexport function $$stream(channel) {\n return new ReadableStream(\n {\n async pull(controller) {\n const [value, ok] = await $$receive(channel, true);\n\n if (ok) controller.enqueue(value);\n else controller.close();\n },\n },\n { highWaterMark: 0 },\n );\n}\n",
12
- 'threads.js': "// What threads share: a channel is data, `{ $$chan: id, size, zero }`, copied to other threads like\n// any value, and what it holds lives in the thread that made it, whose key starts its id. Each\n// thread has a registry of its channels and of its ports to the other threads, shared by its\n// modules; the operations of a thread on the channels of another go straight to that thread\n// through a MessagePort, which the main thread sets up on the first need (go.js).\n\nimport { $$Queue } from './queue.js';\n\n// The state of this thread that its modules share: the channels it made, its ports to the other\n// threads by their keys, and the requests that wait for an answer.\nexport function $$registry() {\n return (globalThis[Symbol.for('rook.threads')] ??= {\n key: Math.random().toString(36).slice(2),\n channels: 0,\n states: new Map(),\n held: new FinalizationRegistry((id) => $$hold(id, -1)),\n ports: new Map(),\n pins: new Map(),\n idle: null,\n waiting: new Map(),\n gone: new Set(),\n main: null,\n requests: new Map(),\n asks: 0,\n selects: new Map(),\n moved: new WeakSet(),\n cancels: new WeakMap(),\n handle: null,\n });\n}\n\n// The key of the thread that made a channel, which keeps what it holds.\nexport function $$ownerOf(id) {\n return id.slice(0, id.lastIndexOf(':'));\n}\n\n// Sends a message to a thread. The channels in it are counted first, as the thread gets copies,\n// and the buffers of transfer() move there. A message waits for the port of a thread that has just\n// started.\nexport function $$post(key, message) {\n const registry = $$registry();\n\n if (registry.gone.has(key)) return;\n\n const { channels, moved } = $$walk(message);\n\n for (const channel of channels) $$hold(channel.$$chan, 1);\n\n const port = registry.ports.get(key);\n\n if (port) return port.postMessage(message, moved);\n if (!registry.waiting.has(key)) registry.waiting.set(key, []);\n registry.waiting.get(key).push([message, moved]);\n}\n\n// A request to a thread, answered by a message with its number.\nexport function $$ask(key, message) {\n const registry = $$registry();\n\n if (registry.gone.has(key)) return Promise.reject($$stopped());\n $$pin(key, 1);\n\n return new Promise((resolve, reject) => {\n const ask = registry.asks++;\n\n registry.requests.set(ask, { resolve, reject, key });\n $$post(key, { ...message, ask });\n });\n}\n\n// A thread that this one waits for keeps the program running: in Node, its worker or port is\n// referenced while some request or select waits for it.\nexport function $$pin(key, count) {\n const registry = $$registry();\n const pins = (registry.pins.get(key) ?? 0) + count;\n\n registry.pins.set(key, pins);\n\n const port = registry.ports.get(key);\n\n if (pins > 0) port?.ref?.();\n else if (registry.idle) registry.idle(key);\n else port?.unref?.();\n}\n\nexport function $$stopped() {\n return new Error('the goroutine that made the channel has stopped');\n}\n\n// A port to a thread: messages that waited for it go now.\nexport function $$connect(key, port, listen) {\n const registry = $$registry();\n\n registry.ports.set(key, port);\n if (registry.pins.get(key) > 0) port.ref?.();\n if (listen) {\n const receive = (data) => $$incoming(key, data);\n\n if (port.on) {\n port.on('message', receive);\n if (!(registry.pins.get(key) > 0)) port.unref?.();\n } else {\n port.onmessage = (event) => receive(event.data);\n }\n }\n\n for (const [message, moved] of registry.waiting.get(key) ?? []) port.postMessage(message, moved);\n registry.waiting.delete(key);\n}\n\n// A thread has stopped: what waits for it fails, and what it waited for is dropped.\nexport function $$gone(key) {\n const registry = $$registry();\n\n registry.gone.add(key);\n registry.ports.get(key)?.close?.();\n registry.ports.delete(key);\n registry.waiting.delete(key);\n for (const [ask, request] of registry.requests) {\n if (request.key !== key) continue;\n registry.requests.delete(ask);\n request.reject($$stopped());\n }\n\n for (const [id, state] of registry.states) {\n const alive = (entry) => (entry.select?.key ?? entry.from) !== key;\n\n state.receivers.$$keep(alive);\n state.senders.$$keep(alive);\n registry.handle?.pump(id);\n }\n}\n\n// A message from another thread: an answer to a request, or an operation on a channel of this one.\nexport function $$incoming(key, data) {\n const registry = $$registry();\n\n $$adopt(data);\n if ('answer' in data) {\n const request = registry.requests.get(data.answer);\n\n registry.requests.delete(data.answer);\n if (request) $$pin(request.key, -1);\n if ('error' in data) request?.reject(data.error);\n else request?.resolve(data.value);\n } else if (data.op === 'hold') {\n $$hold(data.channel, data.count);\n } else if ('op' in data) {\n registry.handle.message(key, data);\n }\n}\n\n// Each value of a channel, in any thread, holds it: the thread that made it forgets it when none\n// is left. A thread counts the channels of a value before a copy of it goes to another thread, and\n// a copy lets go of them when it is collected.\nexport function $$hold(id, count) {\n const registry = $$registry();\n const owner = $$ownerOf(id);\n\n if (owner !== registry.key) return $$post(owner, { op: 'hold', channel: id, count });\n\n const state = registry.states.get(id);\n\n if (!state) return;\n state.holders += count;\n if (state.holders <= 0) registry.states.delete(id);\n}\n\nexport function $$adopt(value) {\n const { held } = $$registry();\n\n for (const channel of $$walk(value).channels) held.register(channel, channel.$$chan);\n}\n\n// A copy in this thread. One that `own` the runtime, such as the arguments of `go` that wait for\n// a worker, moves its buffers on.\nexport function $$copy(value, own = false) {\n const { channels, moved } = $$walk(value);\n\n for (const channel of channels) $$hold(channel.$$chan, 1);\n\n const copy = structuredClone(value, { transfer: moved });\n const { held } = $$registry();\n\n for (const channel of $$walk(copy, own).channels) held.register(channel, channel.$$chan);\n\n return copy;\n}\n\n// `transfer(x)`: the buffer of x moves to the thread that a value with it goes to, instead of a\n// copy; here it is left empty.\nexport function $$transfer(value) {\n $$registry().moved.add(ArrayBuffer.isView(value) ? value.buffer : value);\n\n return value;\n}\n\n// The channels in a value, each of which becomes a copy in the thread that gets the value, and the\n// buffers that move there. A copy of a queue is its fields: here it gets its class again.\nexport function $$walk(\n value,\n own = false,\n found = { channels: new Set(), moved: [] },\n seen = new Set(),\n) {\n if (typeof value !== 'object' || value === null || seen.has(value)) return found;\n seen.add(value);\n\n const buffer =\n value instanceof ArrayBuffer ? value : ArrayBuffer.isView(value) ? value.buffer : null;\n\n // A SharedArrayBuffer is shared with the other thread, not moved.\n if (buffer && !(buffer instanceof ArrayBuffer)) return found;\n if (buffer) {\n const { moved } = $$registry();\n\n if (own) moved.add(buffer);\n if (moved.has(buffer) && !found.moved.includes(buffer)) found.moved.push(buffer);\n\n return found;\n }\n\n if (Array.isArray(value.$$queue)) Object.setPrototypeOf(value, $$Queue.prototype);\n if (typeof value.$$chan === 'string') {\n found.channels.add(value);\n\n return found;\n }\n\n const items =\n value instanceof Map\n ? [...value.keys(), ...value.values()]\n : value instanceof Set || Array.isArray(value)\n ? value\n : Object.values(value);\n\n for (const item of items) $$walk(item, own, found, seen);\n\n return found;\n}\n",
16
+ 'threads.js': "// What threads share: a channel is data, `{ $$chan: id, size, zero }`, copied to other threads like\n// any value, and what it holds lives in the thread that made it, whose key starts its id. Each\n// thread has a registry of its channels and of its ports to the other threads, shared by its\n// modules; the operations of a thread on the channels of another go straight to that thread\n// through a MessagePort, which the main thread sets up on the first need (go.js).\n\nimport { $$classOf, $$reclass } from './classes.js';\nimport { $$Queue } from './queue.js';\n\n// The state of this thread that its modules share: the channels it made, its ports to the other\n// threads by their keys, and the requests that wait for an answer.\nexport function $$registry() {\n return (globalThis[Symbol.for('rook.threads')] ??= {\n key: Math.random().toString(36).slice(2),\n channels: 0,\n states: new Map(),\n held: new FinalizationRegistry((id) => $$hold(id, -1)),\n ports: new Map(),\n pins: new Map(),\n idle: null,\n waiting: new Map(),\n gone: new Set(),\n main: null,\n requests: new Map(),\n asks: 0,\n selects: new Map(),\n moved: new WeakSet(),\n cancels: new WeakMap(),\n handle: null,\n });\n}\n\n// Channel.EMPTY, the zero value of channel types, has this id in every thread: it is closed, and\n// no thread keeps it (channel.js).\nexport function $$isEmpty(id) {\n return id === 'empty';\n}\n\n// The key of the thread that made a channel, which keeps what it holds.\nexport function $$ownerOf(id) {\n return id.slice(0, id.lastIndexOf(':'));\n}\n\n// Sends a message to a thread. The channels in it are counted first, as the thread gets copies,\n// and the buffers of transfer() move there. A message waits for the port of a thread that has just\n// started.\nexport function $$post(key, message) {\n const registry = $$registry();\n\n if (registry.gone.has(key)) return;\n\n const { channels, moved, instances } = $$walk(message);\n\n for (const channel of channels) $$hold(channel.$$chan, 1);\n // The copies of the instances of data classes are listed in the copy, by reference.\n if (instances.length > 0) message = { ...message, $$instances: instances };\n\n const port = registry.ports.get(key);\n\n if (port) return port.postMessage(message, moved);\n if (!registry.waiting.has(key)) registry.waiting.set(key, []);\n registry.waiting.get(key).push([message, moved]);\n}\n\n// A request to a thread, answered by a message with its number.\nexport function $$ask(key, message) {\n const registry = $$registry();\n\n if (registry.gone.has(key)) return Promise.reject($$stopped());\n $$pin(key, 1);\n\n return new Promise((resolve, reject) => {\n const ask = registry.asks++;\n\n registry.requests.set(ask, { resolve, reject, key });\n $$post(key, { ...message, ask });\n });\n}\n\n// A thread that this one waits for keeps the program running: in Node, its worker or port is\n// referenced while some request or select waits for it.\nexport function $$pin(key, count) {\n const registry = $$registry();\n const pins = (registry.pins.get(key) ?? 0) + count;\n\n registry.pins.set(key, pins);\n\n const port = registry.ports.get(key);\n\n if (pins > 0) port?.ref?.();\n else if (registry.idle) registry.idle(key);\n else port?.unref?.();\n}\n\nexport function $$stopped() {\n return new Error('the goroutine that made the channel has stopped');\n}\n\n// A port to a thread: messages that waited for it go now.\nexport function $$connect(key, port, listen) {\n const registry = $$registry();\n\n registry.ports.set(key, port);\n if (registry.pins.get(key) > 0) port.ref?.();\n if (listen) {\n const receive = (data) => $$incoming(key, data);\n\n if (port.on) {\n port.on('message', receive);\n if (!(registry.pins.get(key) > 0)) port.unref?.();\n } else {\n port.onmessage = (event) => receive(event.data);\n }\n }\n\n for (const [message, moved] of registry.waiting.get(key) ?? []) port.postMessage(message, moved);\n registry.waiting.delete(key);\n}\n\n// A thread has stopped: what waits for it fails, and what it waited for is dropped.\nexport function $$gone(key) {\n const registry = $$registry();\n\n registry.gone.add(key);\n registry.ports.get(key)?.close?.();\n registry.ports.delete(key);\n registry.waiting.delete(key);\n for (const [ask, request] of registry.requests) {\n if (request.key !== key) continue;\n registry.requests.delete(ask);\n request.reject($$stopped());\n }\n\n for (const [id, state] of registry.states) {\n const alive = (entry) => (entry.select?.key ?? entry.from) !== key;\n\n state.receivers.$$keep(alive);\n state.senders.$$keep(alive);\n registry.handle?.pump(id);\n }\n}\n\n// A message from another thread: an answer to a request, or an operation on a channel of this one.\nexport function $$incoming(key, data) {\n const registry = $$registry();\n\n $$adopt(data);\n $$reclassed(data);\n if ('answer' in data) {\n const request = registry.requests.get(data.answer);\n\n registry.requests.delete(data.answer);\n if (request) $$pin(request.key, -1);\n if ('error' in data) request?.reject(data.error);\n else request?.resolve(data.value);\n } else if (data.op === 'hold') {\n $$hold(data.channel, data.count);\n } else if ('op' in data) {\n registry.handle.message(key, data);\n }\n}\n\n// Each value of a channel, in any thread, holds it: the thread that made it forgets it when none\n// is left. A thread counts the channels of a value before a copy of it goes to another thread, and\n// a copy lets go of them when it is collected.\nexport function $$hold(id, count) {\n const registry = $$registry();\n const owner = $$ownerOf(id);\n\n if (owner !== registry.key) return $$post(owner, { op: 'hold', channel: id, count });\n\n const state = registry.states.get(id);\n\n if (!state) return;\n state.holders += count;\n if (state.holders <= 0) registry.states.delete(id);\n}\n\n// A message with copies of the instances of data classes gives them their classes back.\nexport function $$reclassed(data) {\n if (data.$$instances) $$reclass(data.$$instances);\n}\n\nexport function $$adopt(value) {\n const { held } = $$registry();\n\n for (const channel of $$walk(value).channels) held.register(channel, channel.$$chan);\n}\n\n// A copy in this thread. One that `own` the runtime, such as the arguments of `go` that wait for\n// a worker, moves its buffers on.\nexport function $$copy(value, own = false) {\n const { channels, moved, instances } = $$walk(value);\n\n for (const channel of channels) $$hold(channel.$$chan, 1);\n\n const { copy, $$instances } = structuredClone(\n { copy: value, $$instances: instances },\n {\n transfer: moved,\n },\n );\n\n $$reclass($$instances);\n\n const { held } = $$registry();\n\n for (const channel of $$walk(copy, own).channels) held.register(channel, channel.$$chan);\n\n return copy;\n}\n\n// `transfer(x)`: the buffer of x moves to the thread that a value with it goes to, instead of a\n// copy; here it is left empty.\nexport function $$transfer(value) {\n $$registry().moved.add(ArrayBuffer.isView(value) ? value.buffer : value);\n\n return value;\n}\n\n// The channels in a value, each of which becomes a copy in the thread that gets the value, and the\n// buffers that move there. A copy of a queue is its fields: here it gets its class again.\nexport function $$walk(\n value,\n own = false,\n found = { channels: new Set(), moved: [], instances: [] },\n seen = new Set(),\n) {\n if (typeof value !== 'object' || value === null || seen.has(value)) return found;\n seen.add(value);\n\n const buffer =\n value instanceof ArrayBuffer ? value : ArrayBuffer.isView(value) ? value.buffer : null;\n\n // A SharedArrayBuffer is shared with the other thread, not moved.\n if (buffer && !(buffer instanceof ArrayBuffer)) return found;\n if (buffer) {\n const { moved } = $$registry();\n\n if (own) moved.add(buffer);\n if (moved.has(buffer) && !found.moved.includes(buffer)) found.moved.push(buffer);\n\n return found;\n }\n\n if (Array.isArray(value.$$queue)) Object.setPrototypeOf(value, $$Queue.prototype);\n\n const id = $$classOf(value);\n\n if (id !== undefined) found.instances.push([value, id]);\n if (typeof value.$$chan === 'string') {\n if (!$$isEmpty(value.$$chan)) found.channels.add(value);\n\n return found;\n }\n\n const items =\n value instanceof Map\n ? [...value.keys(), ...value.values()]\n : value instanceof Set || Array.isArray(value)\n ? value\n : Object.values(value);\n\n for (const item of items) $$walk(item, own, found, seen);\n\n return found;\n}\n",
13
17
  'timers.js': "// Channels of time, as in Go: `after(ms)` and `tick(ms)`.\n\nimport { $$chan, $$send } from './chan.js';\nimport { $$try } from './select.js';\nimport { $$registry } from './threads.js';\n\n// `after(ms)`: a channel that gets the time once, after `ms`. `tick(ms)`: one that gets it every\n// `ms` until it is closed; a tick that finds the last one still there is dropped, as in Go.\nexport function $$after(ms) {\n const channel = $$chan(1, 0);\n\n setTimeout(() => $$send(channel, Date.now()), ms);\n\n return channel;\n}\n\nexport function $$tick(ms) {\n const channel = $$chan(1, 0);\n const timer = setInterval(() => {\n const state = $$registry().states.get(channel.$$chan);\n\n if (!state || state.closed) clearInterval(timer);\n else $$try(channel.$$chan, true, Date.now());\n }, ms);\n\n return channel;\n}\n",
18
+ 'topic.js': "// Topics: values that go to the other tabs of a site, and to its workers, over a BroadcastChannel\n// of their name. A topic is data, `{ $$topic: name, zero }`, so it goes to a worker as its name,\n// and there it is a topic of its own. Each topic listens from its first receive on, into a channel\n// without a limit, so that `<-t`, `for v in t` and `select` are those of channels. `Topic` has\n// what Rook code and the code compiled from it do with topics, by name.\n\nimport { $$chan, $$closeHere, $$send } from './chan.js';\nimport { $$Channel } from './channel.js';\nimport { $$walk } from './threads.js';\n\nexport const $$Topic = Object.freeze({\n // The zero value of topic types: it has no name, it is closed, and what is sent to it goes\n // nowhere.\n EMPTY: Object.freeze({ $$topic: null, zero: null }),\n make: $$topic,\n publish: $$publish,\n listen: $$listen,\n close: $$closeTopic,\n});\n\n// What the modules of a thread share: one BroadcastChannel for each name, so that the topics of\n// this thread do not hear what it sends, as one BroadcastChannel does not hear itself; the channel\n// that each topic listens into, and the topics that are closed.\nfunction $$topics() {\n return (globalThis[Symbol.for('rook.topics')] ??= {\n names: new Map(),\n channels: new WeakMap(),\n closed: new WeakSet(),\n });\n}\n\nexport function $$topic(name, zero) {\n return { $$topic: name, zero };\n}\n\n// `t <- v`: the value goes to the other threads that listen and is not kept here, so nothing waits.\nexport function $$publish(topic, value) {\n if (topic.$$topic === null) return;\n if ($$topics().closed.has(topic)) throw new Error('send on a closed topic');\n $$named(topic.$$topic).channel.postMessage(value);\n}\n\n// The channel that a topic listens into, from the first receive on; a closed topic gives a closed\n// one, and Topic.EMPTY gives Channel.EMPTY. A null topic turns off its case of `select`, as a null\n// channel does.\nexport function $$listen(topic) {\n if (topic === null) return null;\n if (topic.$$topic === null) return $$Channel.EMPTY;\n\n const { channels, closed } = $$topics();\n let channel = channels.get(topic);\n\n if (channel) return channel;\n channel = $$chan(Infinity, topic.zero);\n channels.set(topic, channel);\n if (closed.has(topic)) $$closeHere(channel.$$chan);\n else $$listening(topic, true);\n\n return channel;\n}\n\n// `close(t)`: the topic stops listening and sending; what it has received can still be received.\nexport function $$closeTopic(topic) {\n const { channels, closed } = $$topics();\n\n if (topic.$$topic === null) return;\n if (closed.has(topic)) throw new Error('close of a closed topic');\n closed.add(topic);\n\n const channel = channels.get(topic);\n\n if (!channel) return;\n $$closeHere(channel.$$chan);\n $$listening(topic, false);\n}\n\n// The BroadcastChannel of a name and the topics of this thread that listen to it, each of which\n// gets a copy of its own.\nfunction $$named(name) {\n const { names, channels } = $$topics();\n const found = names.get(name);\n\n if (found) return found;\n\n const entry = { channel: new BroadcastChannel(name), listening: new Set() };\n\n entry.channel.onmessage = ({ data }) => {\n let first = true;\n\n for (const topic of entry.listening) {\n const value = first ? data : structuredClone(data);\n\n first = false;\n $$walk(value);\n $$send(channels.get(topic), value);\n }\n };\n\n names.set(name, entry);\n $$keepAlive(entry);\n\n return entry;\n}\n\nfunction $$listening(topic, on) {\n const entry = $$named(topic.$$topic);\n\n if (on) entry.listening.add(topic);\n else entry.listening.delete(topic);\n $$keepAlive(entry);\n}\n\n// In Node, a BroadcastChannel keeps the program running only while a topic listens to it, as a\n// `tick` does until it is closed.\nfunction $$keepAlive({ channel, listening }) {\n if (listening.size > 0) channel.ref?.();\n else channel.unref?.();\n}\n",
19
+ 'try.js': '// `try expr`: an exception in the expression becomes the error of `(T, error)`. `results` is how\n// many values the expression gives: 0 for none, and then the error comes alone, 1 for a value, more\n// for several. With `withError` the last of them is an error already, as of `(User, error)` or of a\n// function that returns only an error, and an exception takes its place.\n\nexport function $$catch(f, results = 1, withError = false) {\n let value;\n\n try {\n value = f();\n } catch (error) {\n return failed(error, results, withError);\n }\n\n return succeeded(value, results, withError);\n}\n\n/** `try` of an expression that waits: a rejected Promise is caught as an exception is. */\nexport async function $$catchAsync(f, results = 1, withError = false) {\n let value;\n\n try {\n value = await f();\n } catch (error) {\n return failed(error, results, withError);\n }\n\n return succeeded(value, results, withError);\n}\n\nfunction succeeded(value, results, withError) {\n if (results === 0) return null;\n if (withError) return value;\n\n return results === 1 ? [value, null] : [...value, null];\n}\n\n/** JS can throw anything; what is not an Error becomes one, so that the error has a message. */\nfunction failed(thrown, results, withError) {\n const error = thrown instanceof Error ? thrown : new Error(String(thrown));\n\n if (results === 0 || (withError && results === 1)) return error;\n\n return [...Array(withError ? results - 1 : results).fill(null), error];\n}\n',
14
20
  };
@@ -0,0 +1,17 @@
1
+ export interface SourceMap {
2
+ version: 3;
3
+ file?: string;
4
+ sources: string[];
5
+ sourcesContent: string[];
6
+ names: string[];
7
+ mappings: string;
8
+ }
9
+ /** The mark of a place in the source, `<offset in base 36>`, which code cannot contain. */
10
+ export declare function sourceMark(offset: number): string;
11
+ /** The code without its marks, and the source map of them. */
12
+ export declare function withoutMarks(code: string, source: string, sourceName: string): {
13
+ code: string;
14
+ map: SourceMap;
15
+ };
16
+ /** The comment at the end of a module that tells where its source map is. */
17
+ export declare function sourceMapComment(url: string): string;
@@ -0,0 +1,75 @@
1
+ import { SourceFile } from "./source.js";
2
+ const MARK = /([0-9a-z]+)/g;
3
+ /** The mark of a place in the source, `<offset in base 36>`, which code cannot contain. */
4
+ export function sourceMark(offset) {
5
+ return `${offset.toString(36)}`;
6
+ }
7
+ /** The code without its marks, and the source map of them. */
8
+ export function withoutMarks(code, source, sourceName) {
9
+ const file = new SourceFile(sourceName, source);
10
+ const lines = [];
11
+ const segments = [];
12
+ // The previous segment, relative to which the next one is written.
13
+ let previous = { line: 0, column: 0 };
14
+ let started = false;
15
+ for (const text of code.split('\n')) {
16
+ const places = [];
17
+ let removed = 0;
18
+ const clean = text.replace(MARK, (mark, offset, index) => {
19
+ const { line, column } = file.position(parseInt(offset, 36));
20
+ places.push({ generated: index - removed, line: line - 1, column: column - 1 });
21
+ removed += mark.length;
22
+ return '';
23
+ });
24
+ if (places.length === 0 && started && clean.trim() !== '') {
25
+ places.push({ generated: 0, ...previous });
26
+ }
27
+ let generated = 0;
28
+ segments.push(places
29
+ .map((place) => {
30
+ const segment = vlq([
31
+ place.generated - generated,
32
+ // The index of the source, which is always the one.
33
+ 0,
34
+ place.line - previous.line,
35
+ place.column - previous.column,
36
+ ]);
37
+ generated = place.generated;
38
+ previous = { line: place.line, column: place.column };
39
+ started = true;
40
+ return segment;
41
+ })
42
+ .join(','));
43
+ lines.push(clean);
44
+ }
45
+ return {
46
+ code: lines.join('\n'),
47
+ map: {
48
+ version: 3,
49
+ sources: [sourceName],
50
+ sourcesContent: [source],
51
+ names: [],
52
+ mappings: segments.join(';'),
53
+ },
54
+ };
55
+ }
56
+ /** The comment at the end of a module that tells where its source map is. */
57
+ export function sourceMapComment(url) {
58
+ return `//# sourceMappingURL=${url}\n`;
59
+ }
60
+ const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
61
+ /** Numbers in base 64 VLQ: five bits a digit, the lowest first, and the sign in the lowest bit. */
62
+ function vlq(numbers) {
63
+ let text = '';
64
+ for (const number of numbers) {
65
+ let value = number < 0 ? (-number << 1) | 1 : number << 1;
66
+ do {
67
+ let digit = value & 31;
68
+ value >>>= 5;
69
+ if (value > 0)
70
+ digit |= 32;
71
+ text += BASE64[digit];
72
+ } while (value > 0);
73
+ }
74
+ return text;
75
+ }
package/dist/ts-types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CONTENT } from "./checker/dom.js";
2
2
  import { guardedByError } from "./checker/error-results.js";
3
- import { ERROR_CLASS, isQueue, memberNames, queueElement, } from "./checker/types.js";
3
+ import { ERROR_CLASS, isQueue, isTopic, memberNames, queueElement, } from "./checker/types.js";
4
4
  // Types of the checker in TypeScript syntax, for declaration files (declarations.ts).
5
5
  /** A nullable property may be left out of an object: `name?: string | null`. */
6
6
  export function property(name, type) {
@@ -108,6 +108,9 @@ function objectType(type) {
108
108
  if (type.name === 'chan' || type.name === 'chan<-' || type.name === '<-chan') {
109
109
  return `{ readonly $$chan: string }`;
110
110
  }
111
+ // A topic is an object with its name, and so is it for TypeScript code.
112
+ if (isTopic(type))
113
+ return `{ readonly $$topic: string }`;
111
114
  // A queue is an instance of $$Queue of the runtime (src/runtime/queue.js): TypeScript code uses its
112
115
  // members.
113
116
  if (isQueue(type)) {
@@ -17,7 +17,8 @@ export declare abstract class ObjectConverter extends ConverterBase {
17
17
  protected object(type: TS.Type, name: string | null, typeParams?: TypeParam[]): ObjectType;
18
18
  /**
19
19
  * What `for v in x` gives: the `value` of what `next()` of `[Symbol.iterator]()` returns while
20
- * it is not `done`. Found when it is first needed, as most types are never looped over.
20
+ * it is not `done`, or of what its Promise gives for `[Symbol.asyncIterator]()`. Found when it is
21
+ * first needed, as most types are never looped over.
21
22
  */
22
23
  private iterates;
23
24
  protected member(type: TS.Type, name: string, owner: ClassInfo | null): Member | undefined;