rooklang 0.3.3 → 0.4.0

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 (244) hide show
  1. package/README.md +34 -7
  2. package/README.ru.md +34 -7
  3. package/dist/ast/declarations.d.ts +39 -4
  4. package/dist/ast/expressions.d.ts +34 -8
  5. package/dist/ast/markup.d.ts +6 -1
  6. package/dist/ast/statements.d.ts +28 -2
  7. package/dist/auto-import.d.ts +11 -0
  8. package/dist/auto-import.js +81 -0
  9. package/dist/breaks.d.ts +6 -0
  10. package/dist/breaks.js +36 -0
  11. package/dist/build.d.ts +7 -0
  12. package/dist/build.js +23 -2
  13. package/dist/checker/array-literals.d.ts +6 -0
  14. package/dist/checker/array-literals.js +59 -0
  15. package/dist/checker/assignments.d.ts +4 -0
  16. package/dist/checker/assignments.js +23 -3
  17. package/dist/checker/base.d.ts +8 -2
  18. package/dist/checker/base.js +7 -7
  19. package/dist/checker/builtins.js +22 -8
  20. package/dist/checker/bytes.d.ts +2 -2
  21. package/dist/checker/bytes.js +2 -2
  22. package/dist/checker/calls.js +3 -0
  23. package/dist/checker/channel-objects.d.ts +3 -0
  24. package/dist/checker/channel-objects.js +40 -0
  25. package/dist/checker/channels.d.ts +1 -0
  26. package/dist/checker/channels.js +17 -4
  27. package/dist/checker/checker.d.ts +1 -1
  28. package/dist/checker/checker.js +19 -0
  29. package/dist/checker/class-members.d.ts +22 -0
  30. package/dist/checker/class-members.js +105 -0
  31. package/dist/checker/classes.d.ts +5 -4
  32. package/dist/checker/classes.js +48 -13
  33. package/dist/checker/closing.d.ts +21 -0
  34. package/dist/checker/closing.js +164 -0
  35. package/dist/checker/completion.d.ts +3 -3
  36. package/dist/checker/completion.js +4 -3
  37. package/dist/checker/component-bindings.d.ts +7 -0
  38. package/dist/checker/component-bindings.js +45 -0
  39. package/dist/checker/component-values.js +10 -7
  40. package/dist/checker/components.d.ts +2 -2
  41. package/dist/checker/components.js +8 -3
  42. package/dist/checker/context.d.ts +41 -5
  43. package/dist/checker/context.js +23 -0
  44. package/dist/checker/control-flow.d.ts +11 -5
  45. package/dist/checker/control-flow.js +65 -40
  46. package/dist/checker/declarations.d.ts +2 -2
  47. package/dist/checker/declarations.js +4 -2
  48. package/dist/checker/decode.d.ts +8 -0
  49. package/dist/checker/decode.js +32 -0
  50. package/dist/checker/decorator-exports.js +4 -0
  51. package/dist/checker/decorator-includes.d.ts +9 -0
  52. package/dist/checker/decorator-includes.js +71 -0
  53. package/dist/checker/decorator-members.d.ts +8 -10
  54. package/dist/checker/decorator-members.js +28 -31
  55. package/dist/checker/decorator-wrappers.d.ts +1 -1
  56. package/dist/checker/decorator-wrappers.js +3 -2
  57. package/dist/checker/decorators.d.ts +14 -5
  58. package/dist/checker/decorators.js +52 -25
  59. package/dist/checker/expressions.d.ts +6 -2
  60. package/dist/checker/expressions.js +20 -40
  61. package/dist/checker/flow.d.ts +25 -3
  62. package/dist/checker/flow.js +56 -12
  63. package/dist/checker/functions.d.ts +2 -2
  64. package/dist/checker/functions.js +9 -5
  65. package/dist/checker/helpers.d.ts +5 -2
  66. package/dist/checker/helpers.js +16 -4
  67. package/dist/checker/imports.js +5 -2
  68. package/dist/checker/iterables.d.ts +3 -1
  69. package/dist/checker/iterables.js +13 -0
  70. package/dist/checker/iterators.d.ts +4 -1
  71. package/dist/checker/iterators.js +12 -4
  72. package/dist/checker/json.d.ts +5 -0
  73. package/dist/checker/json.js +92 -0
  74. package/dist/checker/loops.d.ts +2 -0
  75. package/dist/checker/loops.js +50 -5
  76. package/dist/checker/markup.js +14 -2
  77. package/dist/checker/members.js +10 -1
  78. package/dist/checker/operators.d.ts +2 -2
  79. package/dist/checker/operators.js +7 -5
  80. package/dist/checker/queues.d.ts +2 -2
  81. package/dist/checker/queues.js +2 -2
  82. package/dist/checker/resolution.js +5 -3
  83. package/dist/checker/shared-state.d.ts +11 -0
  84. package/dist/checker/shared-state.js +37 -0
  85. package/dist/checker/signals.d.ts +8 -0
  86. package/dist/checker/signals.js +48 -0
  87. package/dist/checker/statements.d.ts +5 -2
  88. package/dist/checker/statements.js +51 -22
  89. package/dist/checker/tagged-unions.d.ts +37 -0
  90. package/dist/checker/tagged-unions.js +132 -0
  91. package/dist/checker/topics.d.ts +6 -0
  92. package/dist/checker/topics.js +17 -0
  93. package/dist/checker/transfer.d.ts +3 -1
  94. package/dist/checker/transfer.js +25 -5
  95. package/dist/checker/try.d.ts +6 -0
  96. package/dist/checker/try.js +19 -0
  97. package/dist/checker/types/constructors.d.ts +1 -1
  98. package/dist/checker/types/constructors.js +2 -2
  99. package/dist/checker/types/generics.js +1 -1
  100. package/dist/checker/types/iterables.d.ts +13 -0
  101. package/dist/checker/types/iterables.js +52 -0
  102. package/dist/checker/types/model.d.ts +25 -0
  103. package/dist/checker/types/params.js +22 -3
  104. package/dist/checker/types/printing.js +4 -3
  105. package/dist/checker/types/relations.js +19 -25
  106. package/dist/checker/types/topic.d.ts +6 -0
  107. package/dist/checker/types/topic.js +23 -0
  108. package/dist/checker/types.d.ts +2 -0
  109. package/dist/checker/types.js +2 -0
  110. package/dist/checker/untyped-globals.d.ts +1 -0
  111. package/dist/checker/untyped-globals.js +3 -1
  112. package/dist/cli.js +18 -36
  113. package/dist/codegen/attributes.d.ts +4 -0
  114. package/dist/codegen/attributes.js +24 -2
  115. package/dist/codegen/calls.d.ts +23 -0
  116. package/dist/codegen/calls.js +88 -7
  117. package/dist/codegen/component-helpers.d.ts +6 -1
  118. package/dist/codegen/component-helpers.js +6 -1
  119. package/dist/codegen/component-props.d.ts +11 -2
  120. package/dist/codegen/component-props.js +50 -5
  121. package/dist/codegen/components.d.ts +6 -5
  122. package/dist/codegen/components.js +44 -21
  123. package/dist/codegen/control-flow.d.ts +2 -0
  124. package/dist/codegen/control-flow.js +23 -9
  125. package/dist/codegen/control-statements.d.ts +10 -2
  126. package/dist/codegen/control-statements.js +56 -19
  127. package/dist/codegen/creation.js +5 -2
  128. package/dist/codegen/decode.d.ts +14 -0
  129. package/dist/codegen/decode.js +93 -0
  130. package/dist/codegen/early-returns.d.ts +4 -3
  131. package/dist/codegen/early-returns.js +17 -5
  132. package/dist/codegen/emitter.d.ts +13 -41
  133. package/dist/codegen/emitter.js +27 -8
  134. package/dist/codegen/expressions.d.ts +9 -0
  135. package/dist/codegen/expressions.js +15 -26
  136. package/dist/codegen/functions.d.ts +11 -1
  137. package/dist/codegen/functions.js +31 -3
  138. package/dist/codegen/js.d.ts +2 -2
  139. package/dist/codegen/js.js +5 -2
  140. package/dist/codegen/live.d.ts +60 -0
  141. package/dist/codegen/live.js +29 -0
  142. package/dist/codegen/mutations.d.ts +7 -0
  143. package/dist/codegen/mutations.js +43 -2
  144. package/dist/codegen/options.d.ts +42 -0
  145. package/dist/codegen/options.js +1 -0
  146. package/dist/codegen/reactive.d.ts +4 -59
  147. package/dist/codegen/reactive.js +34 -72
  148. package/dist/codegen/runtime.d.ts +7 -3
  149. package/dist/codegen/runtime.js +33 -14
  150. package/dist/codegen/shared.d.ts +22 -0
  151. package/dist/codegen/shared.js +93 -0
  152. package/dist/codegen/simple-statements.d.ts +11 -5
  153. package/dist/codegen/simple-statements.js +40 -15
  154. package/dist/codegen/statements.d.ts +5 -0
  155. package/dist/codegen/statements.js +99 -4
  156. package/dist/codegen/try.d.ts +5 -0
  157. package/dist/codegen/try.js +40 -0
  158. package/dist/codegen/updates.d.ts +11 -3
  159. package/dist/codegen/updates.js +52 -4
  160. package/dist/codegen/web-components.d.ts +2 -2
  161. package/dist/codegen/web-components.js +2 -2
  162. package/dist/compile.d.ts +9 -0
  163. package/dist/compile.js +25 -8
  164. package/dist/completion.d.ts +4 -2
  165. package/dist/completion.js +10 -5
  166. package/dist/declarations.js +32 -6
  167. package/dist/decorator-copy.d.ts +1 -1
  168. package/dist/decorator-copy.js +6 -2
  169. package/dist/decorator-names.d.ts +14 -0
  170. package/dist/decorator-names.js +56 -0
  171. package/dist/decorators.d.ts +2 -3
  172. package/dist/decorators.js +36 -14
  173. package/dist/goroutines.d.ts +1 -1
  174. package/dist/goroutines.js +5 -4
  175. package/dist/index.d.ts +6 -2
  176. package/dist/index.js +14 -5
  177. package/dist/inline-packages.d.ts +2 -1
  178. package/dist/inline-packages.js +9 -4
  179. package/dist/lexer/base.d.ts +2 -0
  180. package/dist/lexer/base.js +6 -1
  181. package/dist/lexer/lexer.js +3 -0
  182. package/dist/lexer/literals.d.ts +5 -0
  183. package/dist/lexer/literals.js +51 -1
  184. package/dist/lexer/markup.js +3 -4
  185. package/dist/lexer/tables.js +2 -0
  186. package/dist/lexer/token.d.ts +4 -3
  187. package/dist/lexer/token.js +0 -2
  188. package/dist/loader.js +30 -8
  189. package/dist/names.d.ts +4 -0
  190. package/dist/names.js +29 -0
  191. package/dist/parser/base.d.ts +5 -0
  192. package/dist/parser/base.js +2 -1
  193. package/dist/parser/classes.d.ts +2 -0
  194. package/dist/parser/classes.js +88 -23
  195. package/dist/parser/control-flow.d.ts +8 -1
  196. package/dist/parser/control-flow.js +49 -26
  197. package/dist/parser/declarations.d.ts +5 -12
  198. package/dist/parser/declarations.js +14 -79
  199. package/dist/parser/decorators.d.ts +6 -2
  200. package/dist/parser/decorators.js +43 -19
  201. package/dist/parser/expressions.js +27 -3
  202. package/dist/parser/jumps.d.ts +11 -0
  203. package/dist/parser/jumps.js +77 -0
  204. package/dist/parser/literals.d.ts +9 -0
  205. package/dist/parser/literals.js +45 -6
  206. package/dist/parser/markup.js +1 -1
  207. package/dist/parser/params.d.ts +21 -0
  208. package/dist/parser/params.js +143 -0
  209. package/dist/parser/simple-statements.d.ts +2 -1
  210. package/dist/parser/simple-statements.js +38 -15
  211. package/dist/parser/statements.d.ts +5 -5
  212. package/dist/parser/statements.js +39 -35
  213. package/dist/parser/syntax.d.ts +11 -1
  214. package/dist/parser/syntax.js +16 -2
  215. package/dist/parser/types.js +8 -3
  216. package/dist/project-modules.d.ts +12 -0
  217. package/dist/project-modules.js +106 -0
  218. package/dist/run-file.d.ts +6 -0
  219. package/dist/run-file.js +111 -0
  220. package/dist/runtime/sources.js +13 -6
  221. package/dist/source-map.d.ts +17 -0
  222. package/dist/source-map.js +75 -0
  223. package/dist/ts-types.js +4 -1
  224. package/dist/typescript/convert-objects.d.ts +2 -1
  225. package/dist/typescript/convert-objects.js +18 -9
  226. package/dist/vite.js +12 -4
  227. package/dist/walk-types.js +1 -0
  228. package/dist/walk.d.ts +0 -2
  229. package/dist/walk.js +14 -31
  230. package/package.json +2 -2
  231. package/src/runtime/chan.js +15 -5
  232. package/src/runtime/channel.js +28 -0
  233. package/src/runtime/classes.js +32 -0
  234. package/src/runtime/core.js +10 -1
  235. package/src/runtime/decode.js +221 -0
  236. package/src/runtime/go.js +3 -0
  237. package/src/runtime/index.js +7 -0
  238. package/src/runtime/markup.js +27 -17
  239. package/src/runtime/select.js +14 -9
  240. package/src/runtime/shared.js +17 -0
  241. package/src/runtime/signals.js +59 -0
  242. package/src/runtime/threads.js +32 -5
  243. package/src/runtime/topic.js +118 -0
  244. package/src/runtime/try.js +45 -0
@@ -0,0 +1,221 @@
1
+ // `decode<T>(x)`: JSON text, or a value from JS, checked against the type T, which the compiler
2
+ // writes as data: "number", "string", "bool", "any", { literal: [values] }, { nullable: T },
3
+ // { array: T }, { record: T }, { union: [types] } and $$type(name, fields) for an object. A value
4
+ // that fits is returned as it is, with the fields that T does not have; a nullable field may be
5
+ // missing. The error says where the value does not fit: `decode User: tags[1]: expected string,
6
+ // got 42`.
7
+
8
+ /** An object type. Its fields come from a function, so that types can refer to each other. */
9
+ export function $$type(name, fields) {
10
+ let resolved = null;
11
+
12
+ return {
13
+ name,
14
+ get fields() {
15
+ return (resolved ??= fields());
16
+ },
17
+ };
18
+ }
19
+
20
+ /** `decode<T>(text)` of a string: the JSON text is parsed, then checked. */
21
+ export function $$decodeJson(text, type) {
22
+ let value;
23
+
24
+ try {
25
+ value = JSON.parse(text);
26
+ } catch (error) {
27
+ return [null, new Error(`decode ${describe(type)}: invalid JSON: ${error.message}`)];
28
+ }
29
+
30
+ return $$decode(value, type);
31
+ }
32
+
33
+ /**
34
+ * `decode<T>(value)` of a value from JS. Without the types of the checker (`rook build --no-check`)
35
+ * the type is null: then a string is parsed as JSON text, and nothing is checked.
36
+ */
37
+ export function $$decode(value, type) {
38
+ if (type === null) return typeof value === 'string' ? $$decodeJson(value, 'any') : [value, null];
39
+
40
+ const problem = check(value, type, []);
41
+
42
+ if (problem === null) return [value, null];
43
+
44
+ const where = problem.path.length > 0 ? `${pathText(problem.path)}: ` : '';
45
+
46
+ return [null, new Error(`decode ${describe(type)}: ${where}${problem.message}`)];
47
+ }
48
+
49
+ const PRIMITIVES = { number: 'number', string: 'string', bool: 'boolean' };
50
+
51
+ /**
52
+ * Where and how the value does not fit the type, or null if it does. For a member of a union,
53
+ * `tagsFirst` checks its fields of literal types first: a member whose tag does not fit reports
54
+ * that, not another field.
55
+ */
56
+ function check(value, type, path, tagsFirst = false) {
57
+ if (typeof type === 'string') {
58
+ return type === 'any' || typeof value === PRIMITIVES[type] ? null : mismatch(value, type, path);
59
+ }
60
+
61
+ if ('literal' in type) {
62
+ // Remembered for unions: a tag that fits no member lists the tags of all of them.
63
+ return type.literal.includes(value)
64
+ ? null
65
+ : { ...mismatch(value, type, path), literal: type.literal, value };
66
+ }
67
+
68
+ if ('nullable' in type) {
69
+ if (value == null) return null;
70
+
71
+ const problem = check(value, type.nullable, path);
72
+
73
+ // The value itself is of another type: the message names ?T, not T.
74
+ return problem !== null && problem.path.length === path.length
75
+ ? mismatch(value, type, path)
76
+ : problem;
77
+ }
78
+
79
+ if ('array' in type) {
80
+ if (!Array.isArray(value)) return mismatch(value, type, path);
81
+
82
+ for (let i = 0; i < value.length; i++) {
83
+ const problem = check(value[i], type.array, [...path, i]);
84
+
85
+ if (problem !== null) return problem;
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ if ('union' in type) return checkUnion(value, type, path);
92
+ if (!isObject(value)) return mismatch(value, type, path);
93
+
94
+ if ('record' in type) {
95
+ for (const key of Object.keys(value)) {
96
+ const problem = check(value[key], type.record, [...path, key]);
97
+
98
+ if (problem !== null) return problem;
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ const fields = Object.entries(type.fields);
105
+
106
+ if (tagsFirst) fields.sort(([, a], [, b]) => Number(isLiteral(b)) - Number(isLiteral(a)));
107
+
108
+ for (const [name, field] of fields) {
109
+ // Only own fields: a field named `toString` is not the method of every object.
110
+ if (!Object.hasOwn(value, name)) {
111
+ if (isOptional(field)) continue;
112
+
113
+ return { path: [...path, name], message: 'missing' };
114
+ }
115
+
116
+ const problem = check(value[name], field, [...path, name]);
117
+
118
+ if (problem !== null) return problem;
119
+ }
120
+
121
+ return null;
122
+ }
123
+
124
+ /**
125
+ * The value fits one of the types. If none fits, the type whose check went deepest into the value
126
+ * is most likely the one meant, and of those the one whose tag fits: `{ kind: "circle", r: "2" }`
127
+ * of `Circle | Square` is a Circle, so its problem is the one reported. When no tag fits, the
128
+ * message lists them all: `kind: expected "circle" | "square", got "oval"`.
129
+ */
130
+ function checkUnion(value, type, path) {
131
+ const problems = [];
132
+
133
+ for (const member of type.union) {
134
+ const problem = check(value, member, path, true);
135
+
136
+ if (problem === null) return null;
137
+ problems.push(problem);
138
+ }
139
+
140
+ const depth = Math.max(...problems.map((problem) => problem.path.length));
141
+
142
+ if (depth === path.length) return mismatch(value, type, path);
143
+
144
+ const deepest = problems.filter((problem) => problem.path.length === depth);
145
+ const fitting = deepest.find((problem) => !problem.literal);
146
+
147
+ if (fitting) return fitting;
148
+
149
+ const [first] = deepest;
150
+ const same = deepest.filter((problem) => pathText(problem.path) === pathText(first.path));
151
+ const literal = [...new Set(same.flatMap((problem) => problem.literal))];
152
+
153
+ return { ...mismatch(first.value, { literal }, first.path), literal, value: first.value };
154
+ }
155
+
156
+ function isObject(value) {
157
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
158
+ }
159
+
160
+ function isLiteral(type) {
161
+ return typeof type === 'object' && 'literal' in type;
162
+ }
163
+
164
+ function isOptional(type) {
165
+ return type === 'any' || (typeof type === 'object' && 'nullable' in type);
166
+ }
167
+
168
+ function mismatch(value, type, path) {
169
+ return { path, message: `expected ${describe(type)}, got ${shown(value)}` };
170
+ }
171
+
172
+ /** The type as Rook writes it: `[]string`, `?User`, `"admin" | "user"`. */
173
+ function describe(type) {
174
+ if (typeof type === 'string') return type;
175
+ if ('literal' in type) return type.literal.map((value) => JSON.stringify(value)).join(' | ');
176
+ if ('nullable' in type) return `?${grouped(type.nullable)}`;
177
+ if ('array' in type) return `[]${grouped(type.array)}`;
178
+ if ('record' in type) return `{ [key string]: ${describe(type.record)} }`;
179
+ if ('union' in type) return type.union.map(describe).join(' | ');
180
+
181
+ return type.name;
182
+ }
183
+
184
+ function grouped(type) {
185
+ const several = typeof type === 'object' && ('union' in type || type.literal?.length > 1);
186
+
187
+ return several ? `(${describe(type)})` : describe(type);
188
+ }
189
+
190
+ /** The value in a message: a primitive itself, a long string cut, and the kind of anything else. */
191
+ function shown(value) {
192
+ if (value === null) return 'null';
193
+ if (Array.isArray(value)) return 'an array';
194
+
195
+ switch (typeof value) {
196
+ case 'string':
197
+ return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value);
198
+
199
+ case 'number':
200
+ case 'boolean':
201
+ return String(value);
202
+
203
+ case 'object':
204
+ return 'an object';
205
+
206
+ default:
207
+ return typeof value;
208
+ }
209
+ }
210
+
211
+ /** `tags[1]`, `boss.name`, `["first name"]`. */
212
+ function pathText(path) {
213
+ return path
214
+ .map((key, i) => {
215
+ if (typeof key === 'number') return `[${key}]`;
216
+ if (/^[A-Za-z_$][\w$]*$/.test(key)) return i === 0 ? key : `.${key}`;
217
+
218
+ return `[${JSON.stringify(key)}]`;
219
+ })
220
+ .join('');
221
+ }
package/src/runtime/go.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  $$gone,
10
10
  $$incoming,
11
11
  $$post,
12
+ $$reclassed,
12
13
  $$registry,
13
14
  $$transfer,
14
15
  } from './threads.js';
@@ -142,6 +143,7 @@ export function $$pool() {
142
143
  worker.owns = data.owns;
143
144
  } else if ('returned' in data) {
144
145
  $$adopt(data);
146
+ $$reclassed(data);
145
147
 
146
148
  const call = calls.get(data.returned);
147
149
 
@@ -311,6 +313,7 @@ export async function $$serve(functions) {
311
313
  if ('gone' in data) return $$gone(data.gone);
312
314
  setTimeout(free);
313
315
  $$adopt(data.args);
316
+ $$reclassed(data);
314
317
 
315
318
  const { main } = registry;
316
319
 
@@ -3,11 +3,18 @@
3
3
  // The compiler can also put the parts that a module needs into the module itself (runtime.ts).
4
4
 
5
5
  export * from './chan.js';
6
+ export * from './channel.js';
7
+ export * from './classes.js';
6
8
  export * from './core.js';
9
+ export * from './decode.js';
7
10
  export * from './go.js';
8
11
  export * from './markup.js';
9
12
  export * from './queue.js';
10
13
  export * from './select.js';
14
+ export * from './shared.js';
15
+ export * from './signals.js';
11
16
  export * from './streams.js';
12
17
  export * from './threads.js';
13
18
  export * from './timers.js';
19
+ export * from './topic.js';
20
+ export * from './try.js';
@@ -1,10 +1,10 @@
1
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.
2
+ // `mount` of components, the blocks themselves, elements and the CSS of components.
3
3
 
4
4
  import { $$runDeferred } from './core.js';
5
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.
6
+ // Owners of markup that `{if}`, `{switch}` and `{for}` may remove: what the `mount` and `effect`
7
+ // of its components deferred, and the removal of the blocks inside it, run when it is removed.
8
8
  // The current owner is shared by the modules of a page, since the blocks of one module may hold
9
9
  // the components of another.
10
10
  export const $$owner = Symbol.for('rook.owner');
@@ -36,15 +36,17 @@ export function $$onRemove(cleanup) {
36
36
  globalThis[$$owner]?.cleanups.push(cleanup);
37
37
  }
38
38
 
39
- // `mount() { ... }` of a component: runs when its markup is in the document. That is checked after
39
+ // `mount { ... }` of a component: runs when its markup is in the document. That is checked after
40
40
  // the code that creates the component; markup that is not in the document yet is waited for with a
41
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.
42
+ // before that never mounts. What its `defer`s put into `cleanups` runs when the owner is removed;
43
+ // they are added to the owner once it mounts, so the cleanups of the decorators, which mount after
44
+ // the component, run before those of the component.
43
45
  export const $$waiting = new Set();
44
46
  export let $$observer = null;
45
47
 
46
- export function $$mount(node, mount) {
47
- const entry = { node, mount, owner: globalThis[$$owner] };
48
+ export function $$mount(node, mount, cleanups) {
49
+ const entry = { node, mount, cleanups, owner: globalThis[$$owner] };
48
50
 
49
51
  entry.owner?.cleanups.push(() => $$waiting.delete(entry));
50
52
  queueMicrotask(() => $$tryMount(entry));
@@ -65,9 +67,8 @@ export function $$tryMount(entry) {
65
67
  return;
66
68
  }
67
69
 
68
- const cleanup = entry.mount();
69
-
70
- if (typeof cleanup === 'function') entry.owner?.cleanups.push(cleanup);
70
+ entry.mount();
71
+ if (entry.cleanups) entry.owner?.cleanups.push(() => $$runDeferred(entry.cleanups.splice(0)));
71
72
  }
72
73
 
73
74
  export function $$mountWaiting() {
@@ -216,8 +217,10 @@ export function $$dynamic(component, create) {
216
217
 
217
218
  // `{for item in items { ... }}` in markup: a block for each item, between its own markers. On
218
219
  // 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) {
220
+ // moved into place, others are created or removed. With `key`, of `with key item.id`, they are
221
+ // found by the key instead, so an item replaced by another with the same key keeps its block,
222
+ // which gets the new item.
223
+ export function $$list(items, create, key = (item) => item) {
221
224
  const end = document.createTextNode('');
222
225
  const fragment = document.createDocumentFragment();
223
226
 
@@ -236,24 +239,25 @@ export function $$list(items, create) {
236
239
  const old = new Map();
237
240
 
238
241
  for (const block of blocks) {
239
- const same = old.get(block.item);
242
+ const same = old.get(block.key);
240
243
 
241
244
  if (same) same.push(block);
242
- else old.set(block.item, [block]);
245
+ else old.set(block.key, [block]);
243
246
  }
244
247
 
245
248
  const next = [];
246
249
  let index = 0;
247
250
 
248
251
  for (const item of items()) {
249
- let block = old.get(item)?.shift();
252
+ const id = key(item, index);
253
+ let block = old.get(id)?.shift();
250
254
 
251
255
  if (block) {
252
- block.update?.(index);
256
+ block.update?.(index, item);
253
257
  } else {
254
258
  const [[content, blockUpdate], remove] = $$owned(() => create(item, index));
255
259
 
256
- block = { item, start: document.createTextNode(''), end: document.createTextNode('') };
260
+ block = { key: id, start: document.createTextNode(''), end: document.createTextNode('') };
257
261
  block.update = blockUpdate;
258
262
  block.remove = remove;
259
263
  block.content = document.createDocumentFragment();
@@ -315,6 +319,12 @@ export function $$svgElement(tag, attributes) {
315
319
  return element;
316
320
  }
317
321
 
322
+ // The class of `class={[styles.tab, active ? styles.active : null]}`: the classes that are not
323
+ // null or empty, separated by spaces.
324
+ export function $$classes(list) {
325
+ return list.filter((each) => each).join(' ');
326
+ }
327
+
318
328
  // Sets an attribute whose value may be null: then the attribute is removed.
319
329
  export function $$attribute(element, name, value) {
320
330
  if (value == null) element.removeAttribute(name);
@@ -3,12 +3,13 @@
3
3
  // channels of a select.
4
4
 
5
5
  import { $$finish, $$pump, $$zero } from './chan.js';
6
- import { $$ask, $$ownerOf, $$pin, $$post, $$registry, $$stopped } from './threads.js';
6
+ import { $$ask, $$isEmpty, $$ownerOf, $$pin, $$post, $$registry, $$stopped } from './threads.js';
7
7
 
8
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.
9
+ // receive or { channel, value } to send, with the zero value that Channel.EMPTY gives, if it is
10
+ // not null; from another thread, { channel, send, value }. One that can go on at once wins, a
11
+ // random one as in Go; else `default`, -1, if there is one; else the first that can go on later.
12
+ // A case with a null channel never goes on, and one with Channel.EMPTY always can.
12
13
  export async function $$select(cases, hasDefault) {
13
14
  const registry = $$registry();
14
15
  const all = cases.map((each, i) => ({
@@ -16,12 +17,13 @@ export async function $$select(cases, hasDefault) {
16
17
  id: typeof each.channel === 'string' ? each.channel : (each.channel?.$$chan ?? null),
17
18
  send: each.send ?? 'value' in each,
18
19
  value: each.value,
20
+ zero: each.zero ?? null,
19
21
  }));
20
22
  const live = all.filter(({ id }) => id !== null);
21
- const owners = new Set(live.map(({ id }) => $$ownerOf(id)));
23
+ const owners = new Set(live.filter(({ id }) => !$$isEmpty(id)).map(({ id }) => $$ownerOf(id)));
22
24
 
23
25
  // The channels of one other thread: the select runs there.
24
- if (owners.size === 1 && !owners.has(registry.key)) {
26
+ if (owners.size === 1 && !owners.has(registry.key) && live.every(({ id }) => !$$isEmpty(id))) {
25
27
  const [owner] = owners;
26
28
 
27
29
  return $$ask(owner, {
@@ -34,10 +36,13 @@ export async function $$select(cases, hasDefault) {
34
36
  const order = live.map(({ i }) => i).sort(() => Math.random() - 0.5);
35
37
 
36
38
  for (const i of order) {
37
- const { id, send, value } = all[i];
39
+ const { id, send, value, zero } = all[i];
38
40
  const owner = $$ownerOf(id);
39
- const ready =
40
- owner === registry.key
41
+ const ready = $$isEmpty(id)
42
+ ? send
43
+ ? []
44
+ : [zero, false]
45
+ : owner === registry.key
41
46
  ? $$try(id, send, value)
42
47
  : await $$ask(owner, { op: 'try', channel: id, send, value });
43
48
 
@@ -0,0 +1,17 @@
1
+ // State of a module, `state items []Item = []` at its top level: the set of the updates of the
2
+ // components that read it, which every change of it runs.
3
+
4
+ import { $$onRemove } from './markup.js';
5
+
6
+ // A component adds its update while it is on the page: `{if}`, `{switch}` or `{for}` that removes
7
+ // it takes the update away.
8
+ export function $$watch(listeners, update) {
9
+ listeners.add(update);
10
+ $$onRemove(() => listeners.delete(update));
11
+ }
12
+
13
+ // After a change of the state: the updates run on a copy, since one may remove a component, or
14
+ // add one, that listens too.
15
+ export function $$changed(listeners) {
16
+ for (const update of [...listeners]) update();
17
+ }
@@ -0,0 +1,59 @@
1
+ // Signals of the process and AbortSignal as channels: `signal(...names)`, `done(abortSignal)` and
2
+ // `abortSignal(ch)`, as signal.Notify and the Done() of a context are in Go.
3
+
4
+ import { $$chan, $$closeHere, $$receive } from './chan.js';
5
+ import { $$try } from './select.js';
6
+ import { $$registry } from './threads.js';
7
+
8
+ // `signal("SIGINT", "SIGTERM")`: a channel that gets the name of each signal that comes. While it
9
+ // listens, the signals do not end the process, and it keeps the program running, as a tick does;
10
+ // `close(ch)` stops it, and the signals do what they did before. A signal that finds the last one
11
+ // not received yet is dropped. Only the main thread of Node gets the signals of the process.
12
+ export function $$signal(...names) {
13
+ // Node's own, which a browser does not have.
14
+ const { process } = globalThis;
15
+
16
+ if (process === undefined) throw new Error('signal() needs Node: a browser has no signals');
17
+
18
+ const channel = $$chan(1, '');
19
+ const listener = (name) => $$try(channel.$$chan, true, name);
20
+ // A listener of a signal does not keep the program running, and a timer that never fires does.
21
+ const timer = setInterval(() => {}, 2 ** 31 - 1);
22
+
23
+ for (const name of names) process.on(name, listener);
24
+ $$registry().states.get(channel.$$chan).onClose = () => {
25
+ clearInterval(timer);
26
+ for (const name of names) process.off(name, listener);
27
+ };
28
+
29
+ return channel;
30
+ }
31
+
32
+ // `done(signal)`: a channel that gets the reason of an AbortSignal when it aborts, and is closed
33
+ // then, so that `case <-done(request.signal):` goes on once the request is cancelled.
34
+ export function $$done(signal) {
35
+ const channel = $$chan(1, null);
36
+ const abort = () => {
37
+ $$try(channel.$$chan, true, signal.reason);
38
+ $$closeHere(channel.$$chan);
39
+ };
40
+
41
+ if (signal.aborted) abort();
42
+ else signal.addEventListener('abort', abort, { once: true });
43
+
44
+ return channel;
45
+ }
46
+
47
+ // `abortSignal(ch)`: an AbortSignal that aborts when the channel gets a value, with the value as
48
+ // its reason, or is closed, for `fetch(url, { signal: abortSignal(stop) })`. It receives that one
49
+ // value, as any receiver of the channel would.
50
+ export function $$abortSignal(channel) {
51
+ const controller = new AbortController();
52
+
53
+ $$receive(channel, true).then(
54
+ ([value, ok]) => controller.abort(ok ? value : undefined),
55
+ (error) => controller.abort(error),
56
+ );
57
+
58
+ return controller.signal;
59
+ }
@@ -4,6 +4,7 @@
4
4
  // modules; the operations of a thread on the channels of another go straight to that thread
5
5
  // through a MessagePort, which the main thread sets up on the first need (go.js).
6
6
 
7
+ import { $$classOf, $$reclass } from './classes.js';
7
8
  import { $$Queue } from './queue.js';
8
9
 
9
10
  // The state of this thread that its modules share: the channels it made, its ports to the other
@@ -29,6 +30,12 @@ export function $$registry() {
29
30
  });
30
31
  }
31
32
 
33
+ // Channel.EMPTY, the zero value of channel types, has this id in every thread: it is closed, and
34
+ // no thread keeps it (channel.js).
35
+ export function $$isEmpty(id) {
36
+ return id === 'empty';
37
+ }
38
+
32
39
  // The key of the thread that made a channel, which keeps what it holds.
33
40
  export function $$ownerOf(id) {
34
41
  return id.slice(0, id.lastIndexOf(':'));
@@ -42,9 +49,11 @@ export function $$post(key, message) {
42
49
 
43
50
  if (registry.gone.has(key)) return;
44
51
 
45
- const { channels, moved } = $$walk(message);
52
+ const { channels, moved, instances } = $$walk(message);
46
53
 
47
54
  for (const channel of channels) $$hold(channel.$$chan, 1);
55
+ // The copies of the instances of data classes are listed in the copy, by reference.
56
+ if (instances.length > 0) message = { ...message, $$instances: instances };
48
57
 
49
58
  const port = registry.ports.get(key);
50
59
 
@@ -136,6 +145,7 @@ export function $$incoming(key, data) {
136
145
  const registry = $$registry();
137
146
 
138
147
  $$adopt(data);
148
+ $$reclassed(data);
139
149
  if ('answer' in data) {
140
150
  const request = registry.requests.get(data.answer);
141
151
 
@@ -166,6 +176,11 @@ export function $$hold(id, count) {
166
176
  if (state.holders <= 0) registry.states.delete(id);
167
177
  }
168
178
 
179
+ // A message with copies of the instances of data classes gives them their classes back.
180
+ export function $$reclassed(data) {
181
+ if (data.$$instances) $$reclass(data.$$instances);
182
+ }
183
+
169
184
  export function $$adopt(value) {
170
185
  const { held } = $$registry();
171
186
 
@@ -175,11 +190,19 @@ export function $$adopt(value) {
175
190
  // A copy in this thread. One that `own` the runtime, such as the arguments of `go` that wait for
176
191
  // a worker, moves its buffers on.
177
192
  export function $$copy(value, own = false) {
178
- const { channels, moved } = $$walk(value);
193
+ const { channels, moved, instances } = $$walk(value);
179
194
 
180
195
  for (const channel of channels) $$hold(channel.$$chan, 1);
181
196
 
182
- const copy = structuredClone(value, { transfer: moved });
197
+ const { copy, $$instances } = structuredClone(
198
+ { copy: value, $$instances: instances },
199
+ {
200
+ transfer: moved,
201
+ },
202
+ );
203
+
204
+ $$reclass($$instances);
205
+
183
206
  const { held } = $$registry();
184
207
 
185
208
  for (const channel of $$walk(copy, own).channels) held.register(channel, channel.$$chan);
@@ -200,7 +223,7 @@ export function $$transfer(value) {
200
223
  export function $$walk(
201
224
  value,
202
225
  own = false,
203
- found = { channels: new Set(), moved: [] },
226
+ found = { channels: new Set(), moved: [], instances: [] },
204
227
  seen = new Set(),
205
228
  ) {
206
229
  if (typeof value !== 'object' || value === null || seen.has(value)) return found;
@@ -221,8 +244,12 @@ export function $$walk(
221
244
  }
222
245
 
223
246
  if (Array.isArray(value.$$queue)) Object.setPrototypeOf(value, $$Queue.prototype);
247
+
248
+ const id = $$classOf(value);
249
+
250
+ if (id !== undefined) found.instances.push([value, id]);
224
251
  if (typeof value.$$chan === 'string') {
225
- found.channels.add(value);
252
+ if (!$$isEmpty(value.$$chan)) found.channels.add(value);
226
253
 
227
254
  return found;
228
255
  }