rooklang 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/README.ru.md +31 -2
- package/dist/bun-register.d.ts +1 -0
- package/dist/bun-register.js +9 -0
- package/dist/bun.d.ts +34 -0
- package/dist/bun.js +99 -0
- package/dist/codegen/runtime.js +18 -6
- package/dist/compile-file.d.ts +4 -1
- package/dist/compile.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/loader.js +6 -19
- package/dist/run-cache.d.ts +7 -0
- package/dist/run-cache.js +24 -0
- package/dist/run-file.js +6 -1
- package/dist/runtime/sources.js +1 -1
- package/package.json +9 -1
- package/src/runtime/go.js +10 -4
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ version: [CHANGELOG.md](CHANGELOG.md).
|
|
|
63
63
|
|
|
64
64
|
## Installation
|
|
65
65
|
|
|
66
|
-
Rook needs Node.js ≥ 24.
|
|
66
|
+
Rook needs Node.js ≥ 24 or [Bun](#bun).
|
|
67
67
|
|
|
68
68
|
```sh
|
|
69
69
|
npm install rooklang
|
|
@@ -77,7 +77,8 @@ The compiled code imports what channels, goroutines and markup need from `rookla
|
|
|
77
77
|
the package is a dependency of the project, not only of its build.
|
|
78
78
|
|
|
79
79
|
From code, the compiler is `import { compile } from 'rooklang'`. The package also has the
|
|
80
|
-
[Vite](#vite) plugin, `rooklang/vite`,
|
|
80
|
+
[Vite](#vite) plugin, `rooklang/vite`, the [Bun](#bun) plugin, `rooklang/bun`, and
|
|
81
|
+
`rooklang/browser`, the compiler without the
|
|
81
82
|
TypeScript compiler for running in a browser, as in the site's playground: there imports are not
|
|
82
83
|
type checked.
|
|
83
84
|
|
|
@@ -210,6 +211,34 @@ import { add } from './math.rk';
|
|
|
210
211
|
it('adds', () => expect(add(1, 2)).toBe(3));
|
|
211
212
|
```
|
|
212
213
|
|
|
214
|
+
## Bun
|
|
215
|
+
|
|
216
|
+
Rook works in Bun too: `bun add rooklang`, then `bunx rook build app.rk` or `bunx rook run
|
|
217
|
+
app.rk`. Under the runtime of Bun, `bun --bun rook run app.rk`, the program runs in Bun.
|
|
218
|
+
|
|
219
|
+
To let Bun load `.rk` modules itself, preload the plugin `rooklang/bun` in `bunfig.toml`:
|
|
220
|
+
|
|
221
|
+
```toml
|
|
222
|
+
preload = ["rooklang/bun/register"]
|
|
223
|
+
|
|
224
|
+
[test]
|
|
225
|
+
preload = ["rooklang/bun/register"]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Then `bun app.rk` runs a module, TypeScript imports `.rk` modules, `bun --watch` and `bun --hot`
|
|
229
|
+
follow them, and `bun test` runs tests of them; goroutines work there too. The plugin itself is
|
|
230
|
+
`import { rook } from 'rooklang/bun'`, for `Bun.plugin(rook())` or `Bun.build({ plugins: [rook()] })`;
|
|
231
|
+
a bundle of `Bun.build` cannot have the workers of `go` yet, so build those with `rook build` or
|
|
232
|
+
Vite.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// math.test.ts
|
|
236
|
+
import { expect, test } from 'bun:test';
|
|
237
|
+
import { add } from './math.rk';
|
|
238
|
+
|
|
239
|
+
test('adds', () => expect(add(1, 2)).toBe(3));
|
|
240
|
+
```
|
|
241
|
+
|
|
213
242
|
## Conventions
|
|
214
243
|
|
|
215
244
|
- Imports are written with the `.ts` extension (`import { x } from './lexer.ts'`).
|
package/README.ru.md
CHANGED
|
@@ -63,7 +63,7 @@ close(results)
|
|
|
63
63
|
|
|
64
64
|
## Установка
|
|
65
65
|
|
|
66
|
-
Нужен Node.js ≥ 24.
|
|
66
|
+
Нужен Node.js ≥ 24 или [Bun](#bun).
|
|
67
67
|
|
|
68
68
|
```sh
|
|
69
69
|
npm install rooklang
|
|
@@ -77,7 +77,8 @@ npx rook run --watch app.rk # и запускать снова, ког
|
|
|
77
77
|
`rooklang/runtime`, поэтому пакет — зависимость проекта, а не только его сборки.
|
|
78
78
|
|
|
79
79
|
Из кода компилятор подключается как `import { compile } from 'rooklang'`. Ещё в пакете есть
|
|
80
|
-
плагин для [Vite](#vite) — `rooklang/vite
|
|
80
|
+
плагин для [Vite](#vite) — `rooklang/vite`, — плагин для [Bun](#bun) — `rooklang/bun` — и
|
|
81
|
+
`rooklang/browser`: компилятор без
|
|
81
82
|
компилятора TypeScript, для работы в браузере, как в песочнице сайта; импорты там не проверяются
|
|
82
83
|
по типам.
|
|
83
84
|
|
|
@@ -210,6 +211,34 @@ import { add } from './math.rk';
|
|
|
210
211
|
it('adds', () => expect(add(1, 2)).toBe(3));
|
|
211
212
|
```
|
|
212
213
|
|
|
214
|
+
## Bun
|
|
215
|
+
|
|
216
|
+
Rook работает и в Bun: `bun add rooklang`, потом `bunx rook build app.rk` или `bunx rook run
|
|
217
|
+
app.rk`. Под рантаймом Bun, `bun --bun rook run app.rk`, программа выполняется в Bun.
|
|
218
|
+
|
|
219
|
+
Чтобы Bun сам загружал `.rk`-модули, плагин `rooklang/bun` подключают заранее в `bunfig.toml`:
|
|
220
|
+
|
|
221
|
+
```toml
|
|
222
|
+
preload = ["rooklang/bun/register"]
|
|
223
|
+
|
|
224
|
+
[test]
|
|
225
|
+
preload = ["rooklang/bun/register"]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Тогда `bun app.rk` запускает модуль, TypeScript импортирует `.rk`-модули, `bun --watch` и
|
|
229
|
+
`bun --hot` следят за ними, а `bun test` запускает их тесты; горутины там тоже работают. Сам плагин —
|
|
230
|
+
`import { rook } from 'rooklang/bun'`, для `Bun.plugin(rook())` или
|
|
231
|
+
`Bun.build({ plugins: [rook()] })`; в бандле `Bun.build` воркеров `go` пока быть не может, поэтому
|
|
232
|
+
такой код собирают `rook build` или Vite.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// math.test.ts
|
|
236
|
+
import { expect, test } from 'bun:test';
|
|
237
|
+
import { add } from './math.rk';
|
|
238
|
+
|
|
239
|
+
test('adds', () => expect(add(1, 2)).toBe(3));
|
|
240
|
+
```
|
|
241
|
+
|
|
213
242
|
## Соглашения
|
|
214
243
|
|
|
215
244
|
- Импорты пишутся с расширением `.ts` (`import { x } from './lexer.ts'`).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { rook } from "./bun.js";
|
|
2
|
+
// `preload = ["rooklang/bun/register"]` in bunfig.toml: Bun then compiles `.rk` modules when they
|
|
3
|
+
// load, in `bun app.rk`, in imports from TypeScript and in `bun test` (bun.ts). `rook run` under
|
|
4
|
+
// Bun preloads it too, and the workers of `go` import it before their module.
|
|
5
|
+
const { Bun } = globalThis;
|
|
6
|
+
if (Bun === undefined) {
|
|
7
|
+
throw new Error('rooklang/bun/register is for Bun; in Node, rook run compiles .rk files');
|
|
8
|
+
}
|
|
9
|
+
Bun.plugin(rook());
|
package/dist/bun.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** What a plugin of Bun is given, of what Rook uses: the package needs no types of Bun. */
|
|
2
|
+
interface PluginBuilder {
|
|
3
|
+
onResolve(constraints: {
|
|
4
|
+
filter: RegExp;
|
|
5
|
+
namespace?: string;
|
|
6
|
+
}, callback: (args: {
|
|
7
|
+
path: string;
|
|
8
|
+
importer: string;
|
|
9
|
+
}) => {
|
|
10
|
+
path: string;
|
|
11
|
+
namespace?: string;
|
|
12
|
+
}): void;
|
|
13
|
+
onLoad(constraints: {
|
|
14
|
+
filter: RegExp;
|
|
15
|
+
namespace?: string;
|
|
16
|
+
}, callback: (args: {
|
|
17
|
+
path: string;
|
|
18
|
+
}) => {
|
|
19
|
+
contents: string;
|
|
20
|
+
loader: 'js';
|
|
21
|
+
}): void;
|
|
22
|
+
}
|
|
23
|
+
export interface BunPlugin {
|
|
24
|
+
name: string;
|
|
25
|
+
setup(build: PluginBuilder): void;
|
|
26
|
+
}
|
|
27
|
+
export interface RookBunOptions {
|
|
28
|
+
/** Check types before generating code. Defaults to `true`. */
|
|
29
|
+
typeCheck?: boolean;
|
|
30
|
+
/** Markup and components; by default as the package.json of the file says. */
|
|
31
|
+
markup?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function rook(options?: RookBunOptions): BunPlugin;
|
|
34
|
+
export {};
|
package/dist/bun.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { isMainThread } from 'node:worker_threads';
|
|
6
|
+
import { displayPath } from "./build.js";
|
|
7
|
+
import { compileFile } from "./compile-file.js";
|
|
8
|
+
import { cachedFile, listFiles, withMap } from "./run-cache.js";
|
|
9
|
+
import { RUNTIME_MODULE } from "./runtime-module.js";
|
|
10
|
+
// The plugin of Bun: `bun app.rk`, `import "./cart.rk"` from TypeScript and `bun test` compile the
|
|
11
|
+
// `.rk` modules when they load, as `rook run` does in Node (loader.ts). `preload =
|
|
12
|
+
// ["rooklang/bun/register"]` in bunfig.toml registers it (bun-register.ts), and so does
|
|
13
|
+
// `rook run` under Bun.
|
|
14
|
+
//
|
|
15
|
+
// `app.rk.goroutines.js` is the worker module of `go` in app.rk: Bun drops the query of
|
|
16
|
+
// `app.rk?goroutines` before a plugin sees it, as Vite does (vite.ts). A Web Worker of Bun has no
|
|
17
|
+
// preload and loads its module without the plugins, so it starts with a module that registers the
|
|
18
|
+
// plugin first (startWorker in codegen/runtime.ts). The main thread compiles a module once and
|
|
19
|
+
// leaves its code and that of its worker module in ROOK_RUN_CACHE, a directory of the run, which
|
|
20
|
+
// the workers take instead of compiling again; without a run of `rook run`, the plugin makes one
|
|
21
|
+
// and removes it when the process ends.
|
|
22
|
+
/** The worker module of `go` in a module, `app.rk.goroutines.js`, next to it. */
|
|
23
|
+
const GOROUTINES = '.goroutines.js';
|
|
24
|
+
// The files next to this one: .ts in src, .js in the dist of the package.
|
|
25
|
+
const extension = extname(fileURLToPath(import.meta.url));
|
|
26
|
+
// The worker of `go` registers the plugin with this module before it imports its module.
|
|
27
|
+
const register = new URL(`./bun-register${extension}`, import.meta.url).href;
|
|
28
|
+
// The compiled code imports the runtime of this compiler, wherever the program is, by its URL: a
|
|
29
|
+
// worker of Bun does not give a plugin the bare `rooklang/runtime`, and looks for the package.
|
|
30
|
+
const runtimeImport = `from "${RUNTIME_MODULE}";`;
|
|
31
|
+
const runtime = `from ${JSON.stringify(import.meta.resolve(RUNTIME_MODULE))};`;
|
|
32
|
+
export function rook(options = {}) {
|
|
33
|
+
return {
|
|
34
|
+
name: 'rook',
|
|
35
|
+
setup(build) {
|
|
36
|
+
build.onResolve({ filter: /\.rk\.goroutines\.js$/ }, (args) => ({
|
|
37
|
+
path: args.path.startsWith('file:')
|
|
38
|
+
? fileURLToPath(args.path)
|
|
39
|
+
: resolve(dirname(args.importer), args.path),
|
|
40
|
+
namespace: 'rook-goroutines',
|
|
41
|
+
}));
|
|
42
|
+
build.onLoad({ filter: /.*/, namespace: 'rook-goroutines' }, (args) => ({
|
|
43
|
+
contents: load(args.path.slice(0, -GOROUTINES.length), true, options),
|
|
44
|
+
loader: 'js',
|
|
45
|
+
}));
|
|
46
|
+
build.onLoad({ filter: /\.rk$/ }, (args) => ({
|
|
47
|
+
contents: load(args.path, false, options),
|
|
48
|
+
loader: 'js',
|
|
49
|
+
}));
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The code of a module or of its worker module, from the cache of the run when it is there. The
|
|
55
|
+
* key has the time the file was changed, since `bun --hot` loads a changed module again.
|
|
56
|
+
*/
|
|
57
|
+
function load(path, worker, options) {
|
|
58
|
+
const url = `${pathToFileURL(path).href}#${statSync(path).mtimeMs}`;
|
|
59
|
+
const cache = runCache();
|
|
60
|
+
const file = cachedFile(cache, worker ? `${url}${GOROUTINES}` : url);
|
|
61
|
+
if (existsSync(file))
|
|
62
|
+
return readFileSync(file, 'utf8');
|
|
63
|
+
const result = compile(path, options);
|
|
64
|
+
const code = withMap(result.code.replaceAll(runtimeImport, runtime), result.map);
|
|
65
|
+
const workerCode = withMap((result.worker ?? '').replaceAll(runtimeImport, runtime), result.workerMap);
|
|
66
|
+
if (isMainThread) {
|
|
67
|
+
// The CSS modules it imports; the .rk modules are listed when they are loaded.
|
|
68
|
+
const read = result.dependencies.map((each) => resolve(dirname(path), each));
|
|
69
|
+
listFiles(cache, [path, ...read.filter((each) => existsSync(each))]);
|
|
70
|
+
writeFileSync(cachedFile(cache, url), code);
|
|
71
|
+
writeFileSync(cachedFile(cache, `${url}${GOROUTINES}`), workerCode);
|
|
72
|
+
}
|
|
73
|
+
return worker ? workerCode : code;
|
|
74
|
+
}
|
|
75
|
+
function compile(path, options) {
|
|
76
|
+
return compileFile(displayPath(path), {
|
|
77
|
+
rewriteImports: false,
|
|
78
|
+
typeCheck: options.typeCheck ?? process.env['ROOK_NO_CHECK'] === undefined,
|
|
79
|
+
markup: options.markup ?? markupSetting(),
|
|
80
|
+
quiet: !isMainThread,
|
|
81
|
+
workerUrl: `./${basename(path)}${GOROUTINES}`,
|
|
82
|
+
nodeLoader: register,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** ROOK_MARKUP of `rook run`, 1 or 0, turns markup on or off for every file. */
|
|
86
|
+
function markupSetting() {
|
|
87
|
+
const setting = process.env['ROOK_MARKUP'];
|
|
88
|
+
return setting === undefined ? undefined : setting === '1';
|
|
89
|
+
}
|
|
90
|
+
/** The directory of the run: that of `rook run`, or one of this process, which its workers share. */
|
|
91
|
+
function runCache() {
|
|
92
|
+
const existing = process.env['ROOK_RUN_CACHE'];
|
|
93
|
+
if (existing !== undefined)
|
|
94
|
+
return existing;
|
|
95
|
+
const dir = mkdtempSync(join(tmpdir(), 'rook-bun-'));
|
|
96
|
+
process.env['ROOK_RUN_CACHE'] = dir;
|
|
97
|
+
process.on('exit', () => rmSync(dir, { recursive: true, force: true }));
|
|
98
|
+
return dir;
|
|
99
|
+
}
|
package/dist/codegen/runtime.js
CHANGED
|
@@ -86,16 +86,28 @@ export function moduleGo(workerUrl, nodeLoader) {
|
|
|
86
86
|
* The start of a worker of a module: a Web Worker, or a worker thread of Node. Node takes only the
|
|
87
87
|
* path of a `file:` URL, without the query of `app.rk?goroutines`, so its worker starts with a
|
|
88
88
|
* module that imports the whole URL. An absolute URL, such as the Blob of the playground, is used
|
|
89
|
-
* as it is. With `nodeLoader`, as under Vitest
|
|
90
|
-
* files first, since its thread has neither Vite nor the
|
|
89
|
+
* as it is. With `nodeLoader`, as under Vitest or the plugin of Bun, the worker imports the loader
|
|
90
|
+
* of `.rk` files first, since its thread has neither Vite nor the hooks of the main thread: so does
|
|
91
|
+
* the Web Worker of Bun, which also loads its module without the plugins and the query.
|
|
91
92
|
*/
|
|
92
93
|
function startWorker(url, nodeLoader) {
|
|
93
|
-
// The loader registers its hooks when it runs, so the module comes after it, by a dynamic import.
|
|
94
|
-
const code = nodeLoader === undefined
|
|
95
|
-
? '`import ${JSON.stringify(url.href)};`'
|
|
96
|
-
: `\`import ${JSON.stringify(nodeLoader)}; await import(\${JSON.stringify(url.href)});\``;
|
|
97
94
|
const base = /^[a-z][a-z\d+.-]*:/i.test(url) ? '' : ', import.meta.url';
|
|
98
95
|
const workerUrl = `new URL(${JSON.stringify(url)}${base})`;
|
|
96
|
+
if (nodeLoader !== undefined) {
|
|
97
|
+
// The loader registers its hooks when it runs, so the module comes after it, by a dynamic import.
|
|
98
|
+
const code = `\`import ${JSON.stringify(nodeLoader)}; await import(\${JSON.stringify(url.href)});\``;
|
|
99
|
+
return [
|
|
100
|
+
'async function $$startWorker() {',
|
|
101
|
+
` const url = new URL(/* @vite-ignore */ ${JSON.stringify(url)}${base});`,
|
|
102
|
+
` const start = new URL(\`data:text/javascript,\${encodeURIComponent(${code})}\`);`,
|
|
103
|
+
' if (typeof Worker === "function") return new Worker(start, { type: "module" });',
|
|
104
|
+
' const threads = "node:worker_threads";',
|
|
105
|
+
' const { Worker: NodeWorker } = await import(/* @vite-ignore */ threads);',
|
|
106
|
+
' return new NodeWorker(start);',
|
|
107
|
+
'}',
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
const code = '`import ${JSON.stringify(url.href)};`';
|
|
99
111
|
return [
|
|
100
112
|
'async function $$startWorker() {',
|
|
101
113
|
' if (typeof Worker === "function") {',
|
package/dist/compile-file.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export declare class CompileError extends Error {
|
|
|
6
6
|
export declare function compileFile(path: string, options: {
|
|
7
7
|
rewriteImports: boolean;
|
|
8
8
|
typeCheck: boolean;
|
|
9
|
-
markup?: boolean;
|
|
9
|
+
markup?: boolean | undefined;
|
|
10
10
|
quiet?: boolean;
|
|
11
|
+
/** Where the worker module of `go` is, and what its worker imports first (bun.ts). */
|
|
12
|
+
workerUrl?: string;
|
|
13
|
+
nodeLoader?: string;
|
|
11
14
|
}): CompileResult;
|
package/dist/compile.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface CoreCompileOptions {
|
|
|
23
23
|
readStyleModule?: ((specifier: string) => string | null) | undefined;
|
|
24
24
|
/** Where the worker module of `go` is, relative to the module; see workerUrl(). */
|
|
25
25
|
workerUrl?: string | undefined;
|
|
26
|
-
/** A module that a worker
|
|
26
|
+
/** A module that a worker of Node or Bun imports first, to load `.rk` files (vite.ts, bun.ts). */
|
|
27
27
|
nodeLoader?: string | undefined;
|
|
28
28
|
/**
|
|
29
29
|
* How the code gets the runtime of Rook: `import`, the default, imports it from
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface CompileOptions {
|
|
|
20
20
|
declarations?: boolean;
|
|
21
21
|
/** Where the worker module of `go` is, relative to the module; see workerUrl() in compile.ts. */
|
|
22
22
|
workerUrl?: string;
|
|
23
|
-
/** A module that a worker
|
|
23
|
+
/** A module that a worker of Node or Bun imports first, to load `.rk` files (vite.ts, bun.ts). */
|
|
24
24
|
nodeLoader?: string;
|
|
25
25
|
/** Markup and components; by default as the package.json of the file says (src/config.ts). */
|
|
26
26
|
markup?: boolean;
|
package/dist/loader.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { appendFileSync, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { createRequire, registerHooks } from 'node:module';
|
|
4
|
-
import { dirname, extname,
|
|
3
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { isMainThread } from 'node:worker_threads';
|
|
7
6
|
import { RUNTIME_MODULE } from "./runtime-module.js";
|
|
8
|
-
import {
|
|
7
|
+
import { cachedFile, listFiles, withMap } from "./run-cache.js";
|
|
9
8
|
// `rook run` imports this module first (`node --import`): Node then imports `.rk` files,
|
|
10
9
|
// compiled when they load, and `app.rk?goroutines` is the worker module of `go` in app.rk.
|
|
11
10
|
// The worker threads of `go` import it too, since they get the `--import` of the process. The main
|
|
@@ -24,7 +23,7 @@ const markup = markupSetting === undefined ? undefined : markupSetting === '1';
|
|
|
24
23
|
const cache = process.env['ROOK_RUN_CACHE'];
|
|
25
24
|
/** The file of the cache for the code of a URL, query included. */
|
|
26
25
|
function cached(url) {
|
|
27
|
-
return cache ?
|
|
26
|
+
return cache ? cachedFile(cache, url) : null;
|
|
28
27
|
}
|
|
29
28
|
// The compiled code imports the runtime of this compiler, wherever the program is.
|
|
30
29
|
const runtime = import.meta.resolve(RUNTIME_MODULE);
|
|
@@ -50,7 +49,7 @@ registerHooks({
|
|
|
50
49
|
const { compileFile } = require(`./compile-file${extension}`);
|
|
51
50
|
const path = displayPath(fileURLToPath(moduleUrl));
|
|
52
51
|
if (isMainThread && cache)
|
|
53
|
-
|
|
52
|
+
listFiles(cache, [resolve(path)]);
|
|
54
53
|
const result = compileFile(path, {
|
|
55
54
|
rewriteImports: false,
|
|
56
55
|
typeCheck,
|
|
@@ -62,7 +61,7 @@ registerHooks({
|
|
|
62
61
|
if (isMainThread && cache) {
|
|
63
62
|
// The CSS modules it imports; the .rk modules are listed when they are loaded.
|
|
64
63
|
const read = result.dependencies.map((each) => resolve(dirname(path), each));
|
|
65
|
-
|
|
64
|
+
listFiles(cache, read.filter((each) => existsSync(each)));
|
|
66
65
|
writeFileSync(cached(moduleUrl), code);
|
|
67
66
|
if (worker !== undefined)
|
|
68
67
|
writeFileSync(cached(`${moduleUrl}?goroutines`), worker);
|
|
@@ -71,15 +70,3 @@ registerHooks({
|
|
|
71
70
|
return { format: 'module', source, shortCircuit: true };
|
|
72
71
|
},
|
|
73
72
|
});
|
|
74
|
-
/** The code with its source map in it. */
|
|
75
|
-
function withMap(code, map) {
|
|
76
|
-
if (map === undefined || code === '')
|
|
77
|
-
return code;
|
|
78
|
-
const data = Buffer.from(JSON.stringify(map)).toString('base64');
|
|
79
|
-
return `${code}${sourceMapComment(`data:application/json;base64,${data}`)}`;
|
|
80
|
-
}
|
|
81
|
-
/** Adds files to the list of the run: a line of the path and the time it was changed. */
|
|
82
|
-
function list(paths) {
|
|
83
|
-
const lines = paths.map((path) => `${path}\t${statSync(path).mtimeMs}\n`);
|
|
84
|
-
appendFileSync(join(cache, 'files'), lines.join(''));
|
|
85
|
-
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SourceMap } from './source-map.ts';
|
|
2
|
+
/** The file of the cache for the code of a URL, query included. */
|
|
3
|
+
export declare function cachedFile(cache: string, url: string): string;
|
|
4
|
+
/** The code with its source map in it. */
|
|
5
|
+
export declare function withMap(code: string, map: SourceMap | undefined): string;
|
|
6
|
+
/** Adds files to the list of the run: a line of the path and the time it was changed. */
|
|
7
|
+
export declare function listFiles(cache: string, paths: readonly string[]): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { appendFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { sourceMapComment } from "./source-map.js";
|
|
5
|
+
// The directory of a run of a program, ROOK_RUN_CACHE: the compiled code of its modules, which the
|
|
6
|
+
// worker threads of `go` take instead of compiling them again, and `files`, the list of the files
|
|
7
|
+
// that the run compiled or read, which `rook run --watch` watches (run-file.ts). The loader of
|
|
8
|
+
// Node (loader.ts) and the plugin of Bun (bun.ts) keep it; neither loads the compiler for it.
|
|
9
|
+
/** The file of the cache for the code of a URL, query included. */
|
|
10
|
+
export function cachedFile(cache, url) {
|
|
11
|
+
return join(cache, `${createHash('sha1').update(url).digest('hex')}.js`);
|
|
12
|
+
}
|
|
13
|
+
/** The code with its source map in it. */
|
|
14
|
+
export function withMap(code, map) {
|
|
15
|
+
if (map === undefined || code === '')
|
|
16
|
+
return code;
|
|
17
|
+
const data = Buffer.from(JSON.stringify(map)).toString('base64');
|
|
18
|
+
return `${code}${sourceMapComment(`data:application/json;base64,${data}`)}`;
|
|
19
|
+
}
|
|
20
|
+
/** Adds files to the list of the run: a line of the path and the time it was changed. */
|
|
21
|
+
export function listFiles(cache, paths) {
|
|
22
|
+
const lines = paths.map((path) => `${path}\t${statSync(path).mtimeMs}\n`);
|
|
23
|
+
appendFileSync(join(cache, 'files'), lines.join(''));
|
|
24
|
+
}
|
package/dist/run-file.js
CHANGED
|
@@ -13,6 +13,9 @@ export function runFile(file, options) {
|
|
|
13
13
|
// The build rewrites the extensions of imports, not of these URLs: in the package they are .js.
|
|
14
14
|
const extension = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
15
15
|
const loader = new URL(`./loader${extension}`, import.meta.url).href;
|
|
16
|
+
// Under Bun the program runs in Bun, with its plugin preloaded instead of the hooks of Node.
|
|
17
|
+
const bun = process.versions['bun'] !== undefined;
|
|
18
|
+
const register = fileURLToPath(new URL(`./bun-register${extension}`, import.meta.url));
|
|
16
19
|
const run = fileURLToPath(new URL(`./run${extension}`, import.meta.url));
|
|
17
20
|
const watched = new Set();
|
|
18
21
|
let child = null;
|
|
@@ -71,7 +74,9 @@ export function runFile(file, options) {
|
|
|
71
74
|
console.error(`rook: ${displayPath(restart)} changed, running again`);
|
|
72
75
|
restart = null;
|
|
73
76
|
cache = mkdtempSync(join(tmpdir(), 'rook-run-'));
|
|
74
|
-
const args =
|
|
77
|
+
const args = bun
|
|
78
|
+
? ['--preload', register, run, file]
|
|
79
|
+
: ['--enable-source-maps', '--import', loader, run, file];
|
|
75
80
|
child = spawn(process.execPath, args, {
|
|
76
81
|
stdio: 'inherit',
|
|
77
82
|
env: {
|
package/dist/runtime/sources.js
CHANGED
|
@@ -7,7 +7,7 @@ export const RUNTIME_FILES = {
|
|
|
7
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
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
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/**\n * Where and how the value does not fit the type, or null if it does. For a member of a union,\n * `tagsFirst` checks its fields of literal types first: a member whose tag does not fit reports\n * that, not another field.\n */\nfunction check(value, type, path, tagsFirst = false) {\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) {\n // Remembered for unions: a tag that fits no member lists the tags of all of them.\n return type.literal.includes(value)\n ? null\n : { ...mismatch(value, type, path), literal: type.literal, value };\n }\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 const fields = Object.entries(type.fields);\n\n if (tagsFirst) fields.sort(([, a], [, b]) => Number(isLiteral(b)) - Number(isLiteral(a)));\n\n for (const [name, field] of 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, and of those the one whose tag fits: `{ kind: \"circle\", r: \"2\" }`\n * of `Circle | Square` is a Circle, so its problem is the one reported. When no tag fits, the\n * message lists them all: `kind: expected \"circle\" | \"square\", got \"oval\"`.\n */\nfunction checkUnion(value, type, path) {\n const problems = [];\n\n for (const member of type.union) {\n const problem = check(value, member, path, true);\n\n if (problem === null) return null;\n problems.push(problem);\n }\n\n const depth = Math.max(...problems.map((problem) => problem.path.length));\n\n if (depth === path.length) return mismatch(value, type, path);\n\n const deepest = problems.filter((problem) => problem.path.length === depth);\n const fitting = deepest.find((problem) => !problem.literal);\n\n if (fitting) return fitting;\n\n const [first] = deepest;\n const same = deepest.filter((problem) => pathText(problem.path) === pathText(first.path));\n const literal = [...new Set(same.flatMap((problem) => problem.literal))];\n\n return { ...mismatch(first.value, { literal }, first.path), literal, value: first.value };\n}\n\nfunction isObject(value) {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isLiteral(type) {\n return typeof type === 'object' && 'literal' in type;\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",
|
|
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 // A worker that failed before it started, as when its module does not load, would fail again:\n // the calls of its module that wait fail with it, instead of starting worker after worker.\n const fail = (worker, error) => {\n remove(worker, error);\n if (!worker.key) failQueued(worker.start, error);\n dispatch();\n };\n const failQueued = (start, 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 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 failQueued(start, error);\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",
|
|
11
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: what the `mount` and `effect`\n// of its components deferred, 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. What its `defer`s put into `cleanups` runs when the owner is removed;\n// they are added to the owner once it mounts, so the cleanups of the decorators, which mount after\n// the component, run before those of the component.\nexport const $$waiting = new Set();\nexport let $$observer = null;\n\nexport function $$mount(node, mount, cleanups) {\n const entry = { node, mount, cleanups, 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 entry.mount();\n if (entry.cleanups) entry.owner?.cleanups.push(() => $$runDeferred(entry.cleanups.splice(0)));\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. With `key`, of `with key item.id`, they are\n// found by the key instead, so an item replaced by another with the same key keeps its block,\n// which gets the new item.\nexport function $$list(items, create, key = (item) => item) {\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.key);\n\n if (same) same.push(block);\n else old.set(block.key, [block]);\n }\n\n const next = [];\n let index = 0;\n\n for (const item of items()) {\n const id = key(item, index);\n let block = old.get(id)?.shift();\n\n if (block) {\n block.update?.(index, item);\n } else {\n const [[content, blockUpdate], remove] = $$owned(() => create(item, index));\n\n block = { key: id, 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// The class of `class={[styles.tab, active ? styles.active : null]}`: the classes that are not\n// null or empty, separated by spaces.\nexport function $$classes(list) {\n return list.filter((each) => each).join(' ');\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",
|
|
12
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',
|
|
13
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",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rooklang",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "A language with JavaScript semantics, Go-style syntax, goroutines on real threads and static types that compiles to readable JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rooklang",
|
|
@@ -47,6 +47,14 @@
|
|
|
47
47
|
"types": "./dist/browser.d.ts",
|
|
48
48
|
"default": "./dist/browser.js"
|
|
49
49
|
},
|
|
50
|
+
"./bun": {
|
|
51
|
+
"types": "./dist/bun.d.ts",
|
|
52
|
+
"default": "./dist/bun.js"
|
|
53
|
+
},
|
|
54
|
+
"./bun/register": {
|
|
55
|
+
"types": "./dist/bun-register.d.ts",
|
|
56
|
+
"default": "./dist/bun-register.js"
|
|
57
|
+
},
|
|
50
58
|
"./runtime": "./src/runtime/index.js",
|
|
51
59
|
"./package.json": "./package.json"
|
|
52
60
|
},
|
package/src/runtime/go.js
CHANGED
|
@@ -116,10 +116,19 @@ export function $$pool() {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
|
+
// A worker that failed before it started, as when its module does not load, would fail again:
|
|
120
|
+
// the calls of its module that wait fail with it, instead of starting worker after worker.
|
|
119
121
|
const fail = (worker, error) => {
|
|
120
122
|
remove(worker, error);
|
|
123
|
+
if (!worker.key) failQueued(worker.start, error);
|
|
121
124
|
dispatch();
|
|
122
125
|
};
|
|
126
|
+
const failQueued = (start, error) => {
|
|
127
|
+
for (const call of queue.filter((each) => each.start === start)) {
|
|
128
|
+
queue.splice(queue.indexOf(call), 1);
|
|
129
|
+
call.reject(error);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
123
132
|
const receive = (worker, data) => {
|
|
124
133
|
if ('op' in data || 'answer' in data) return $$incoming(worker.key, data);
|
|
125
134
|
if ('hello' in data) {
|
|
@@ -176,10 +185,7 @@ export function $$pool() {
|
|
|
176
185
|
},
|
|
177
186
|
(error) => {
|
|
178
187
|
remove(worker, error);
|
|
179
|
-
|
|
180
|
-
queue.splice(queue.indexOf(call), 1);
|
|
181
|
-
call.reject(error);
|
|
182
|
-
}
|
|
188
|
+
failQueued(start, error);
|
|
183
189
|
},
|
|
184
190
|
);
|
|
185
191
|
};
|