rooklang 0.3.1 → 0.3.3

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 (107) hide show
  1. package/README.md +17 -4
  2. package/README.ru.md +17 -4
  3. package/dist/ast/expressions.d.ts +16 -1
  4. package/dist/ast/markup.d.ts +6 -1
  5. package/dist/browser.d.ts +5 -0
  6. package/dist/browser.js +1 -0
  7. package/dist/checker/assignments.d.ts +5 -0
  8. package/dist/checker/assignments.js +34 -8
  9. package/dist/checker/base.d.ts +2 -1
  10. package/dist/checker/base.js +4 -10
  11. package/dist/checker/builtins.js +4 -35
  12. package/dist/checker/bytes.d.ts +2 -2
  13. package/dist/checker/bytes.js +2 -2
  14. package/dist/checker/channels.d.ts +3 -0
  15. package/dist/checker/channels.js +39 -1
  16. package/dist/checker/context.d.ts +6 -0
  17. package/dist/checker/control-flow.d.ts +1 -0
  18. package/dist/checker/control-flow.js +17 -0
  19. package/dist/checker/error-results.d.ts +6 -0
  20. package/dist/checker/error-results.js +31 -0
  21. package/dist/checker/expressions.d.ts +2 -0
  22. package/dist/checker/expressions.js +4 -0
  23. package/dist/checker/helpers.js +2 -0
  24. package/dist/checker/queues.d.ts +6 -0
  25. package/dist/checker/queues.js +24 -0
  26. package/dist/checker/resolution.js +6 -2
  27. package/dist/checker/statements.d.ts +5 -0
  28. package/dist/checker/statements.js +19 -0
  29. package/dist/checker/transfer.js +4 -1
  30. package/dist/checker/types/printing.js +7 -0
  31. package/dist/checker/types/queue.d.ts +6 -0
  32. package/dist/checker/types/queue.js +42 -0
  33. package/dist/checker/types/relations.js +12 -1
  34. package/dist/checker/types.d.ts +1 -0
  35. package/dist/checker/types.js +1 -0
  36. package/dist/checker/untyped-globals.d.ts +1 -0
  37. package/dist/checker/untyped-globals.js +47 -0
  38. package/dist/codegen/attributes.js +0 -1
  39. package/dist/codegen/calls.d.ts +9 -0
  40. package/dist/codegen/calls.js +41 -1
  41. package/dist/codegen/component-values.js +0 -1
  42. package/dist/codegen/components.js +0 -1
  43. package/dist/codegen/control-flow.js +0 -3
  44. package/dist/codegen/control-statements.d.ts +9 -1
  45. package/dist/codegen/control-statements.js +51 -7
  46. package/dist/codegen/creation.d.ts +6 -8
  47. package/dist/codegen/creation.js +1 -10
  48. package/dist/codegen/early-returns.js +0 -1
  49. package/dist/codegen/elements.js +0 -3
  50. package/dist/codegen/emitter.d.ts +3 -5
  51. package/dist/codegen/emitter.js +0 -2
  52. package/dist/codegen/expressions.d.ts +4 -1
  53. package/dist/codegen/expressions.js +9 -13
  54. package/dist/codegen/functions.js +0 -2
  55. package/dist/codegen/js.d.ts +0 -7
  56. package/dist/codegen/js.js +33 -59
  57. package/dist/codegen/parallel.js +2 -4
  58. package/dist/codegen/refs.js +0 -1
  59. package/dist/codegen/runtime.d.ts +13 -0
  60. package/dist/codegen/runtime.js +93 -0
  61. package/dist/codegen/simple-statements.js +3 -1
  62. package/dist/codegen/styles.d.ts +1 -1
  63. package/dist/codegen/styles.js +2 -2
  64. package/dist/codegen/web-components.js +0 -1
  65. package/dist/compile.d.ts +7 -0
  66. package/dist/compile.js +1 -0
  67. package/dist/completion.js +2 -1
  68. package/dist/index.d.ts +2 -0
  69. package/dist/index.js +1 -0
  70. package/dist/loader.js +19 -6
  71. package/dist/parser/base.d.ts +3 -1
  72. package/dist/parser/expressions.js +9 -2
  73. package/dist/parser/literals.d.ts +7 -0
  74. package/dist/parser/literals.js +45 -1
  75. package/dist/parser/simple-statements.js +3 -0
  76. package/dist/parser/types.d.ts +1 -1
  77. package/dist/parser/types.js +10 -3
  78. package/dist/runtime/sources.d.ts +2 -0
  79. package/dist/runtime/sources.js +14 -0
  80. package/dist/runtime-module.d.ts +1 -0
  81. package/dist/runtime-module.js +3 -0
  82. package/dist/ts-types.d.ts +4 -0
  83. package/dist/ts-types.js +25 -3
  84. package/dist/vite.js +7 -1
  85. package/dist/walk-types.js +1 -0
  86. package/dist/walk.js +9 -0
  87. package/package.json +6 -4
  88. package/src/runtime/chan.js +277 -0
  89. package/src/runtime/core.js +23 -0
  90. package/src/runtime/go.js +340 -0
  91. package/src/runtime/index.js +13 -0
  92. package/src/runtime/markup.js +336 -0
  93. package/src/runtime/queue.js +118 -0
  94. package/src/runtime/select.js +170 -0
  95. package/src/runtime/streams.js +61 -0
  96. package/src/runtime/threads.js +240 -0
  97. package/src/runtime/timers.js +27 -0
  98. package/dist/codegen/chan-helpers.d.ts +0 -2
  99. package/dist/codegen/chan-helpers.js +0 -253
  100. package/dist/codegen/go-helpers.d.ts +0 -9
  101. package/dist/codegen/go-helpers.js +0 -307
  102. package/dist/codegen/helpers.d.ts +0 -10
  103. package/dist/codegen/helpers.js +0 -356
  104. package/dist/codegen/select-helpers.d.ts +0 -2
  105. package/dist/codegen/select-helpers.js +0 -135
  106. package/dist/codegen/thread-helpers.d.ts +0 -2
  107. package/dist/codegen/thread-helpers.js +0 -198
@@ -0,0 +1,336 @@
1
+ // Markup: the owners of the blocks that `{if}`, `{switch}` and `{for}` remove, the
2
+ // `mount()` of components, the blocks themselves, elements and the CSS of components.
3
+
4
+ import { $$runDeferred } from './core.js';
5
+
6
+ // Owners of markup that `{if}`, `{switch}` and `{for}` may remove: the functions returned by the
7
+ // `mount()` of its components, and the removal of the blocks inside it, run when it is removed.
8
+ // The current owner is shared by the modules of a page, since the blocks of one module may hold
9
+ // the components of another.
10
+ export const $$owner = Symbol.for('rook.owner');
11
+
12
+ export function $$owned(create) {
13
+ const owner = { removed: false, cleanups: [] };
14
+ const parent = globalThis[$$owner];
15
+
16
+ globalThis[$$owner] = owner;
17
+
18
+ let result;
19
+
20
+ try {
21
+ result = create();
22
+ } finally {
23
+ globalThis[$$owner] = parent;
24
+ }
25
+
26
+ const remove = () => {
27
+ if (owner.removed) return;
28
+ owner.removed = true;
29
+ $$runDeferred(owner.cleanups);
30
+ };
31
+
32
+ return [result, remove];
33
+ }
34
+
35
+ export function $$onRemove(cleanup) {
36
+ globalThis[$$owner]?.cleanups.push(cleanup);
37
+ }
38
+
39
+ // `mount() { ... }` of a component: runs when its markup is in the document. That is checked after
40
+ // the code that creates the component; markup that is not in the document yet is waited for with a
41
+ // MutationObserver, which watches only while some markup waits. Markup that its owner removes
42
+ // before that never mounts. The function `mount` returns runs when the owner is removed.
43
+ export const $$waiting = new Set();
44
+ export let $$observer = null;
45
+
46
+ export function $$mount(node, mount) {
47
+ const entry = { node, mount, owner: globalThis[$$owner] };
48
+
49
+ entry.owner?.cleanups.push(() => $$waiting.delete(entry));
50
+ queueMicrotask(() => $$tryMount(entry));
51
+ }
52
+
53
+ export function $$tryMount(entry) {
54
+ if (entry.owner?.removed) return;
55
+
56
+ const node = entry.node();
57
+
58
+ if (node != null && !node.isConnected) {
59
+ $$waiting.add(entry);
60
+ if (!$$observer) {
61
+ $$observer = new MutationObserver($$mountWaiting);
62
+ $$observer.observe(node.ownerDocument ?? document, { childList: true, subtree: true });
63
+ }
64
+
65
+ return;
66
+ }
67
+
68
+ const cleanup = entry.mount();
69
+
70
+ if (typeof cleanup === 'function') entry.owner?.cleanups.push(cleanup);
71
+ }
72
+
73
+ export function $$mountWaiting() {
74
+ for (const entry of $$waiting) {
75
+ if (!entry.node().isConnected) continue;
76
+ $$waiting.delete(entry);
77
+ $$tryMount(entry);
78
+ }
79
+
80
+ if ($$waiting.size === 0) {
81
+ $$observer.disconnect();
82
+ $$observer = null;
83
+ }
84
+ }
85
+
86
+ // Appends element content whose type is not known: null adds nothing, arrays add each item.
87
+ export function $$append(parent, value) {
88
+ if (value == null || value === false) return;
89
+ if (Array.isArray(value)) {
90
+ for (const item of value) $$append(parent, item);
91
+ } else {
92
+ parent.append(value);
93
+ }
94
+ }
95
+
96
+ // Nodes of markup that change, `{list}` or `{ok ? <b /> : null}`: they are kept before an end
97
+ // marker. Nodes that stay are moved rather than created again. Returns the markers with the first
98
+ // nodes, to be inserted, and the update.
99
+ export function $$content(value) {
100
+ const end = document.createTextNode('');
101
+ const fragment = document.createDocumentFragment();
102
+
103
+ fragment.append(end);
104
+
105
+ let nodes = [];
106
+ let remove;
107
+ const update = () => {
108
+ const next = [];
109
+ const add = (item) => {
110
+ if (item == null || item === false) return;
111
+ if (Array.isArray(item)) {
112
+ for (const each of item) add(each);
113
+ } else if (item.nodeType === 11) {
114
+ next.push(...item.childNodes);
115
+ } else {
116
+ next.push(typeof item === 'object' ? item : document.createTextNode(String(item)));
117
+ }
118
+ };
119
+ const [created, removeCreated] = $$owned(value);
120
+
121
+ add(created);
122
+ remove?.();
123
+ remove = removeCreated;
124
+
125
+ const kept = new Set(next);
126
+
127
+ for (const node of nodes) if (!kept.has(node)) node.remove();
128
+ end.before(...next);
129
+ nodes = next;
130
+ };
131
+
132
+ update();
133
+ $$onRemove(() => remove?.());
134
+
135
+ return [fragment, update];
136
+ }
137
+
138
+ // `{if ...}` and `{switch ...}` in markup: shows the block that `choose` picks (-1 for none) between
139
+ // two markers. The block is created again only when the choice changes; otherwise it is updated.
140
+ export function $$branches(choose, blocks) {
141
+ const start = document.createTextNode('');
142
+ const end = document.createTextNode('');
143
+ const fragment = document.createDocumentFragment();
144
+
145
+ fragment.append(start, end);
146
+
147
+ let current = null;
148
+ let update;
149
+ let remove;
150
+ const render = () => {
151
+ const next = choose();
152
+
153
+ if (next === current) {
154
+ update?.();
155
+
156
+ return;
157
+ }
158
+
159
+ current = next;
160
+ remove?.();
161
+ while (start.nextSibling !== end) start.nextSibling.remove();
162
+ update = undefined;
163
+ remove = undefined;
164
+ if (next >= 0) {
165
+ const [[content, blockUpdate], removeBlock] = $$owned(blocks[next]);
166
+
167
+ end.before(content);
168
+ update = blockUpdate;
169
+ remove = removeBlock;
170
+ }
171
+ };
172
+
173
+ render();
174
+ $$onRemove(() => remove?.());
175
+
176
+ return [fragment, render];
177
+ }
178
+
179
+ // `<Page />` whose component is a value that changes: when it does, the block that `create` makes
180
+ // with it is created again between two markers; otherwise the block is updated.
181
+ export function $$dynamic(component, create) {
182
+ const start = document.createTextNode('');
183
+ const end = document.createTextNode('');
184
+ const fragment = document.createDocumentFragment();
185
+
186
+ fragment.append(start, end);
187
+
188
+ let current;
189
+ let update;
190
+ let remove;
191
+ const render = () => {
192
+ const next = component();
193
+
194
+ if (remove && next === current) {
195
+ update?.();
196
+
197
+ return;
198
+ }
199
+
200
+ current = next;
201
+ remove?.();
202
+ while (start.nextSibling !== end) start.nextSibling.remove();
203
+
204
+ const [[content, blockUpdate], removeBlock] = $$owned(() => create(next));
205
+
206
+ end.before(content);
207
+ update = blockUpdate;
208
+ remove = removeBlock;
209
+ };
210
+
211
+ render();
212
+ $$onRemove(() => remove?.());
213
+
214
+ return [fragment, render];
215
+ }
216
+
217
+ // `{for item in items { ... }}` in markup: a block for each item, between its own markers. On
218
+ // updates the blocks are found again by the item itself: blocks of items that stay are updated and
219
+ // moved into place, others are created or removed.
220
+ export function $$list(items, create) {
221
+ const end = document.createTextNode('');
222
+ const fragment = document.createDocumentFragment();
223
+
224
+ fragment.append(end);
225
+
226
+ let blocks = [];
227
+ const nodesOf = (block) => {
228
+ const nodes = [];
229
+
230
+ for (let node = block.start; node !== block.end; node = node.nextSibling) nodes.push(node);
231
+ nodes.push(block.end);
232
+
233
+ return nodes;
234
+ };
235
+ const update = () => {
236
+ const old = new Map();
237
+
238
+ for (const block of blocks) {
239
+ const same = old.get(block.item);
240
+
241
+ if (same) same.push(block);
242
+ else old.set(block.item, [block]);
243
+ }
244
+
245
+ const next = [];
246
+ let index = 0;
247
+
248
+ for (const item of items()) {
249
+ let block = old.get(item)?.shift();
250
+
251
+ if (block) {
252
+ block.update?.(index);
253
+ } else {
254
+ const [[content, blockUpdate], remove] = $$owned(() => create(item, index));
255
+
256
+ block = { item, start: document.createTextNode(''), end: document.createTextNode('') };
257
+ block.update = blockUpdate;
258
+ block.remove = remove;
259
+ block.content = document.createDocumentFragment();
260
+ block.content.append(block.start, content, block.end);
261
+ }
262
+
263
+ next.push(block);
264
+ index++;
265
+ }
266
+
267
+ for (const same of old.values()) {
268
+ for (const block of same) {
269
+ block.remove();
270
+ for (const node of nodesOf(block)) node.remove();
271
+ }
272
+ }
273
+
274
+ let before = end;
275
+
276
+ for (let i = next.length - 1; i >= 0; i--) {
277
+ const block = next[i];
278
+
279
+ if (block.content) {
280
+ before.before(block.content);
281
+ block.content = null;
282
+ } else if (block.end.nextSibling !== before) {
283
+ before.before(...nodesOf(block));
284
+ }
285
+
286
+ before = block.start;
287
+ }
288
+
289
+ blocks = next;
290
+ };
291
+
292
+ update();
293
+ $$onRemove(() => {
294
+ for (const block of blocks) block.remove();
295
+ });
296
+
297
+ return [fragment, update];
298
+ }
299
+
300
+ // Creates an element with its properties and the attributes that are set with setAttribute.
301
+ export function $$element(tag, properties, attributes) {
302
+ const element = Object.assign(document.createElement(tag), properties);
303
+
304
+ for (const name in attributes) element.setAttribute(name, attributes[name]);
305
+
306
+ return element;
307
+ }
308
+
309
+ // Creates an SVG element with its attributes.
310
+ export function $$svgElement(tag, attributes) {
311
+ const element = document.createElementNS('http://www.w3.org/2000/svg', tag);
312
+
313
+ for (const name in attributes) element.setAttribute(name, attributes[name]);
314
+
315
+ return element;
316
+ }
317
+
318
+ // Sets an attribute whose value may be null: then the attribute is removed.
319
+ export function $$attribute(element, name, value) {
320
+ if (value == null) element.removeAttribute(name);
321
+ else element.setAttribute(name, value);
322
+ }
323
+
324
+ // Adds the CSS of a component to the page once: modules that inline the same component add it with
325
+ // the same id. `styles` keeps the CSS of the module for the shadow roots of its web components,
326
+ // which the styles of the page do not reach.
327
+ export function $$style(styles, id, css) {
328
+ styles.push(css);
329
+ if (document.getElementById(id)) return;
330
+
331
+ const style = document.createElement('style');
332
+
333
+ style.id = id;
334
+ style.textContent = css;
335
+ document.head.append(style);
336
+ }
@@ -0,0 +1,118 @@
1
+ // The class of `queue T`. Its values are a ring in an array: `shift()` and `unshift()` move the
2
+ // start instead of the other values, as they do in an array. A full ring is copied into one twice
3
+ // its size, and one that is mostly empty into one half its size, so each operation costs a few
4
+ // steps on average. The fields are data, so a copy for another thread keeps the queue, and the
5
+ // thread that gets it gives it back its class (threads.js).
6
+
7
+ export class $$Queue {
8
+ constructor(items = []) {
9
+ this.$$queue = [...items];
10
+ this.$$start = 0;
11
+ this.$$length = this.$$queue.length;
12
+ }
13
+
14
+ get length() {
15
+ return this.$$length;
16
+ }
17
+
18
+ push(...items) {
19
+ for (const item of items) {
20
+ if (this.$$length === this.$$queue.length) this.$$resize();
21
+ this.$$queue[(this.$$start + this.$$length++) % this.$$queue.length] = item;
22
+ }
23
+
24
+ return this.$$length;
25
+ }
26
+
27
+ unshift(...items) {
28
+ for (let i = items.length - 1; i >= 0; i--) {
29
+ if (this.$$length === this.$$queue.length) this.$$resize();
30
+ this.$$start = (this.$$start || this.$$queue.length) - 1;
31
+ this.$$queue[this.$$start] = items[i];
32
+ this.$$length++;
33
+ }
34
+
35
+ return this.$$length;
36
+ }
37
+
38
+ // An empty queue gives null; a value that is taken out is not held any more.
39
+ shift() {
40
+ if (this.$$length === 0) return null;
41
+
42
+ const item = this.$$queue[this.$$start];
43
+
44
+ this.$$queue[this.$$start] = undefined;
45
+ this.$$start = (this.$$start + 1) % this.$$queue.length;
46
+ this.$$length--;
47
+ this.$$shrink();
48
+
49
+ return item;
50
+ }
51
+
52
+ pop() {
53
+ if (this.$$length === 0) return null;
54
+
55
+ const at = (this.$$start + --this.$$length) % this.$$queue.length;
56
+ const item = this.$$queue[at];
57
+
58
+ this.$$queue[at] = undefined;
59
+ this.$$shrink();
60
+
61
+ return item;
62
+ }
63
+
64
+ // A position from the start, or from the end if it is negative, as with an array.
65
+ at(index) {
66
+ const i = Math.trunc(index) || 0;
67
+ const at = i < 0 ? i + this.$$length : i;
68
+
69
+ if (at < 0 || at >= this.$$length) return null;
70
+
71
+ return this.$$queue[(this.$$start + at) % this.$$queue.length];
72
+ }
73
+
74
+ clear() {
75
+ this.$$queue = [];
76
+ this.$$start = 0;
77
+ this.$$length = 0;
78
+ }
79
+
80
+ *[Symbol.iterator]() {
81
+ for (let i = 0; i < this.$$length; i++) {
82
+ yield this.$$queue[(this.$$start + i) % this.$$queue.length];
83
+ }
84
+ }
85
+
86
+ // Removes a value, for the channels: at the start it costs a step, elsewhere the whole queue.
87
+ $$delete(item) {
88
+ if (this.$$length > 0 && this.$$queue[this.$$start] === item) return this.shift();
89
+
90
+ const items = [...this];
91
+ const at = items.indexOf(item);
92
+
93
+ if (at >= 0) this.$$keep((_, i) => i !== at);
94
+ }
95
+
96
+ // Keeps the values that pass the test, in their order.
97
+ $$keep(test) {
98
+ const items = [...this].filter(test);
99
+
100
+ this.$$queue = items;
101
+ this.$$start = 0;
102
+ this.$$length = items.length;
103
+ this.$$shrink();
104
+ }
105
+
106
+ // The values from the start of a new ring with room for as many again, and at least 8.
107
+ $$resize() {
108
+ const items = [...this];
109
+
110
+ items.length = Math.max(8, this.$$length * 2);
111
+ this.$$queue = items;
112
+ this.$$start = 0;
113
+ }
114
+
115
+ $$shrink() {
116
+ if (this.$$queue.length > 8 && this.$$length * 4 <= this.$$queue.length) this.$$resize();
117
+ }
118
+ }
@@ -0,0 +1,170 @@
1
+ // `select`. Its cases may wait in channels of several threads; a channel that can go on with a case
2
+ // asks the thread of the select to lock it first, so that only one case goes on, as Go locks the
3
+ // channels of a select.
4
+
5
+ import { $$finish, $$pump, $$zero } from './chan.js';
6
+ import { $$ask, $$ownerOf, $$pin, $$post, $$registry, $$stopped } from './threads.js';
7
+
8
+ // `select`: the index of the case that went on, and what it received. A case is { channel } to
9
+ // receive or { channel, value } to send; from another thread, { channel, send, value }. One that
10
+ // can go on at once wins, a random one as in Go; else `default`, -1, if there is one; else the
11
+ // first that can go on later. A case with a null channel never goes on.
12
+ export async function $$select(cases, hasDefault) {
13
+ const registry = $$registry();
14
+ const all = cases.map((each, i) => ({
15
+ i,
16
+ id: typeof each.channel === 'string' ? each.channel : (each.channel?.$$chan ?? null),
17
+ send: each.send ?? 'value' in each,
18
+ value: each.value,
19
+ }));
20
+ const live = all.filter(({ id }) => id !== null);
21
+ const owners = new Set(live.map(({ id }) => $$ownerOf(id)));
22
+
23
+ // The channels of one other thread: the select runs there.
24
+ if (owners.size === 1 && !owners.has(registry.key)) {
25
+ const [owner] = owners;
26
+
27
+ return $$ask(owner, {
28
+ op: 'select',
29
+ cases: all.map(({ id: channel, send, value }) => ({ channel, send, value })),
30
+ hasDefault,
31
+ });
32
+ }
33
+
34
+ const order = live.map(({ i }) => i).sort(() => Math.random() - 0.5);
35
+
36
+ for (const i of order) {
37
+ const { id, send, value } = all[i];
38
+ const owner = $$ownerOf(id);
39
+ const ready =
40
+ owner === registry.key
41
+ ? $$try(id, send, value)
42
+ : await $$ask(owner, { op: 'try', channel: id, send, value });
43
+
44
+ if (ready) return [i, ...ready];
45
+ }
46
+
47
+ if (hasDefault) return [-1];
48
+
49
+ return new Promise((resolve, reject) => {
50
+ const sid = registry.asks++;
51
+ const channels = new Map();
52
+
53
+ registry.selects.set(sid, { locked: false, resolve, reject, channels });
54
+ live.forEach(({ i, id, send, value }) => {
55
+ const owner = $$ownerOf(id);
56
+
57
+ if (!channels.has(owner)) channels.set(owner, []);
58
+ channels.get(owner).push(id);
59
+ if (owner === registry.key) {
60
+ const entry = { select: { key: owner, sid }, case: i, value };
61
+
62
+ registry.states.get(id)[send ? 'senders' : 'receivers'].push(entry);
63
+ } else {
64
+ $$post(owner, { op: 'wait', sid, case: i, channel: id, send, value });
65
+ }
66
+ });
67
+ for (const owner of channels.keys()) if (owner !== registry.key) $$pin(owner, 1);
68
+ for (const id of channels.get(registry.key) ?? []) $$pump(id);
69
+ });
70
+ }
71
+
72
+ // What a case of a select gets at once, if it can go on: [] for a send, [value, ok] for a receive.
73
+ // A receiver or sender that a select of another thread waits with is left to $$pump.
74
+ export function $$try(id, send, value) {
75
+ const state = $$registry().states.get(id);
76
+
77
+ if (!state) throw $$stopped();
78
+ if (state.busy) return null;
79
+ if (send && state.closed) throw new Error('send on a closed channel');
80
+ if (!send && state.buffer.length > 0) {
81
+ const received = state.buffer.shift();
82
+
83
+ $$pump(id);
84
+
85
+ return [received, true];
86
+ }
87
+
88
+ const queue = send ? state.receivers : state.senders;
89
+
90
+ while (queue.length > 0) {
91
+ const entry = queue.at(0);
92
+
93
+ if (entry.select && entry.select.key !== $$registry().key) break;
94
+
95
+ const lock = entry.select ? $$lock(entry.select.sid) : 'locked';
96
+
97
+ if (lock === 'busy') break;
98
+ queue.shift();
99
+ if (lock === 'done') continue;
100
+ if (!send) {
101
+ $$finish(entry);
102
+
103
+ return [entry.value, true];
104
+ }
105
+
106
+ $$finish(entry, [value, true]);
107
+
108
+ return [];
109
+ }
110
+
111
+ if (send && state.buffer.length < state.size) {
112
+ state.buffer.push(value);
113
+ $$pump(id);
114
+
115
+ return [];
116
+ }
117
+
118
+ return !send && state.closed ? [$$zero(state), false] : null;
119
+ }
120
+
121
+ // The thread of a select decides which of its cases goes on: a channel locks the select, then
122
+ // commits a case or unlocks it.
123
+ export function $$lock(sid) {
124
+ const select = $$registry().selects.get(sid);
125
+
126
+ if (!select) return 'done';
127
+ if (select.locked) return 'busy';
128
+ select.locked = true;
129
+
130
+ return 'locked';
131
+ }
132
+
133
+ export function $$unlock(sid) {
134
+ const select = $$registry().selects.get(sid);
135
+
136
+ if (select) select.locked = false;
137
+ }
138
+
139
+ export function $$commit(sid, i, result, error) {
140
+ const registry = $$registry();
141
+ const select = registry.selects.get(sid);
142
+
143
+ if (!select) return;
144
+ registry.selects.delete(sid);
145
+ // The other cases leave the queues of their channels.
146
+ for (const [owner, channels] of select.channels) {
147
+ if (owner === registry.key) {
148
+ $$unwait({ key: owner, sid }, channels);
149
+ } else {
150
+ $$post(owner, { op: 'cancel', sid, channels });
151
+ $$pin(owner, -1);
152
+ }
153
+ }
154
+
155
+ if (error) select.reject(error);
156
+ else select.resolve([i, ...(result ?? [])]);
157
+ }
158
+
159
+ export function $$unwait(select, channels) {
160
+ const { states } = $$registry();
161
+ const other = (entry) => entry.select?.key !== select.key || entry.select.sid !== select.sid;
162
+
163
+ for (const id of channels) {
164
+ const state = states.get(id);
165
+
166
+ if (!state) continue;
167
+ state.receivers.$$keep(other);
168
+ state.senders.$$keep(other);
169
+ }
170
+ }
@@ -0,0 +1,61 @@
1
+ // Streams and channels: `chan(stream, size)` and `stream(ch)`.
2
+
3
+ import { $$chan, $$closeHere, $$receive, $$send } from './chan.js';
4
+ import { $$try } from './select.js';
5
+ import { $$registry } from './threads.js';
6
+
7
+ // `chan(stream, size)`: a channel of the chunks of a stream and one of its error. The next chunk is
8
+ // read only when the last one is in the channel, so a full channel stops the stream; closing
9
+ // the channel cancels it.
10
+ export function $$fromStream(stream, size, zero) {
11
+ const channel = $$chan(size, zero);
12
+ const failed = $$chan(1, null);
13
+ const id = channel.$$chan;
14
+
15
+ (async () => {
16
+ const reader = stream?.getReader();
17
+ let failure = null;
18
+
19
+ while (reader) {
20
+ let chunk;
21
+
22
+ try {
23
+ chunk = await reader.read();
24
+ } catch (error) {
25
+ failure = error;
26
+ break;
27
+ }
28
+
29
+ if (chunk.done) break;
30
+ try {
31
+ await $$send(channel, chunk.value);
32
+ } catch {
33
+ reader.cancel().catch(() => {});
34
+ break;
35
+ }
36
+ }
37
+
38
+ // The pump holds both channels, so they are not forgotten; the one of the error has room.
39
+ if (!$$registry().states.get(id).closed) $$closeHere(id);
40
+ $$try(failed.$$chan, true, failure);
41
+ $$closeHere(failed.$$chan);
42
+ })();
43
+
44
+ return [channel, failed];
45
+ }
46
+
47
+ // `stream(ch)`: a stream that reads one value of the channel for each read, so the room of the
48
+ // channel is all it buffers, and ends when the channel is closed.
49
+ export function $$stream(channel) {
50
+ return new ReadableStream(
51
+ {
52
+ async pull(controller) {
53
+ const [value, ok] = await $$receive(channel, true);
54
+
55
+ if (ok) controller.enqueue(value);
56
+ else controller.close();
57
+ },
58
+ },
59
+ { highWaterMark: 0 },
60
+ );
61
+ }