tova 0.5.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/bin/tova.js +261 -60
  2. package/package.json +1 -1
  3. package/src/analyzer/analyzer.js +351 -11
  4. package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
  5. package/src/analyzer/deploy-analyzer.js +44 -0
  6. package/src/analyzer/form-analyzer.js +113 -0
  7. package/src/analyzer/scope.js +2 -2
  8. package/src/codegen/base-codegen.js +1160 -10
  9. package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
  10. package/src/codegen/codegen.js +119 -28
  11. package/src/codegen/deploy-codegen.js +49 -0
  12. package/src/codegen/edge-codegen.js +1351 -0
  13. package/src/codegen/form-codegen.js +553 -0
  14. package/src/codegen/security-codegen.js +5 -5
  15. package/src/codegen/server-codegen.js +88 -7
  16. package/src/codegen/shared-codegen.js +5 -0
  17. package/src/codegen/wasm-codegen.js +6 -0
  18. package/src/config/edit-toml.js +6 -2
  19. package/src/config/git-resolver.js +128 -0
  20. package/src/config/lock-file.js +57 -0
  21. package/src/config/module-cache.js +58 -0
  22. package/src/config/module-entry.js +37 -0
  23. package/src/config/module-path.js +31 -0
  24. package/src/config/pkg-errors.js +62 -0
  25. package/src/config/resolve.js +17 -0
  26. package/src/config/resolver.js +139 -0
  27. package/src/config/search.js +28 -0
  28. package/src/config/semver.js +72 -0
  29. package/src/config/toml.js +48 -5
  30. package/src/deploy/deploy.js +217 -0
  31. package/src/deploy/infer.js +218 -0
  32. package/src/deploy/provision.js +311 -0
  33. package/src/diagnostics/error-codes.js +1 -1
  34. package/src/docs/generator.js +1 -1
  35. package/src/formatter/formatter.js +4 -4
  36. package/src/lexer/tokens.js +12 -2
  37. package/src/lsp/server.js +483 -1
  38. package/src/parser/ast.js +60 -5
  39. package/src/parser/{client-ast.js → browser-ast.js} +3 -3
  40. package/src/parser/{client-parser.js → browser-parser.js} +42 -15
  41. package/src/parser/concurrency-ast.js +15 -0
  42. package/src/parser/concurrency-parser.js +236 -0
  43. package/src/parser/deploy-ast.js +37 -0
  44. package/src/parser/deploy-parser.js +132 -0
  45. package/src/parser/edge-ast.js +83 -0
  46. package/src/parser/edge-parser.js +262 -0
  47. package/src/parser/form-ast.js +80 -0
  48. package/src/parser/form-parser.js +206 -0
  49. package/src/parser/parser.js +82 -14
  50. package/src/parser/select-ast.js +39 -0
  51. package/src/registry/plugins/browser-plugin.js +30 -0
  52. package/src/registry/plugins/concurrency-plugin.js +32 -0
  53. package/src/registry/plugins/deploy-plugin.js +33 -0
  54. package/src/registry/plugins/edge-plugin.js +32 -0
  55. package/src/registry/register-all.js +8 -2
  56. package/src/runtime/ssr.js +2 -2
  57. package/src/stdlib/inline.js +38 -6
  58. package/src/stdlib/runtime-bridge.js +152 -0
  59. package/src/version.js +1 -1
  60. package/src/registry/plugins/client-plugin.js +0 -30
@@ -1,5 +1,6 @@
1
- import { TokenType } from '../lexer/tokens.js';
1
+ import { TokenType, Keywords } from '../lexer/tokens.js';
2
2
  import * as AST from './ast.js';
3
+ import { FormValidator } from './form-ast.js';
3
4
  import { BlockRegistry } from '../registry/register-all.js';
4
5
 
5
6
  export class Parser {
@@ -75,6 +76,16 @@ export class Parser {
75
76
  this.error(message || `Expected ${type}, got ${this.current().type}`);
76
77
  }
77
78
 
79
+ // Accept IDENTIFIER or any keyword token as a property name (e.g., obj.field, obj.state).
80
+ // Keywords are valid property names after '.' and '?.' just like in JavaScript.
81
+ expectPropertyName(message) {
82
+ const tok = this.current();
83
+ if (tok.type === TokenType.IDENTIFIER || (typeof tok.value === 'string' && tok.type !== TokenType.EOF && tok.type !== TokenType.NUMBER && tok.type !== TokenType.STRING && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(tok.value))) {
84
+ return this.advance();
85
+ }
86
+ this.error(message || `Expected property name, got ${tok.type}`);
87
+ }
88
+
78
89
  loc() {
79
90
  const tok = this.current();
80
91
  return { line: tok.line, column: tok.column, file: this.filename };
@@ -95,7 +106,7 @@ export class Parser {
95
106
  tok.type === TokenType.WHILE || tok.type === TokenType.RETURN ||
96
107
  tok.type === TokenType.IMPORT || tok.type === TokenType.MATCH ||
97
108
  tok.type === TokenType.TRY || tok.type === TokenType.SERVER ||
98
- tok.type === TokenType.CLIENT || tok.type === TokenType.SHARED ||
109
+ tok.type === TokenType.BROWSER || tok.type === TokenType.SHARED ||
99
110
  tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
100
111
  tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
101
112
  tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
@@ -120,13 +131,15 @@ export class Parser {
120
131
  _isContextualKeyword() {
121
132
  const t = this.current().type;
122
133
  return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
123
- t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE;
134
+ t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE ||
135
+ t === TokenType.FORM || t === TokenType.FIELD || t === TokenType.GROUP || t === TokenType.STEPS;
124
136
  }
125
137
 
126
138
  _isContextualKeywordToken(token) {
127
139
  const t = token.type;
128
140
  return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
129
- t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE;
141
+ t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE ||
142
+ t === TokenType.FORM || t === TokenType.FIELD || t === TokenType.GROUP || t === TokenType.STEPS;
130
143
  }
131
144
 
132
145
  _synchronizeBlock() {
@@ -144,7 +157,7 @@ export class Parser {
144
157
  tok.type === TokenType.WHILE || tok.type === TokenType.RETURN ||
145
158
  tok.type === TokenType.IMPORT || tok.type === TokenType.MATCH ||
146
159
  tok.type === TokenType.TRY || tok.type === TokenType.SERVER ||
147
- tok.type === TokenType.CLIENT || tok.type === TokenType.SHARED ||
160
+ tok.type === TokenType.BROWSER || tok.type === TokenType.SHARED ||
148
161
  tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
149
162
  tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
150
163
  tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
@@ -163,7 +176,8 @@ export class Parser {
163
176
  const next = this.peek(1);
164
177
  // Fragment: <>
165
178
  if (next.type === TokenType.GREATER) return true;
166
- if (next.type !== TokenType.IDENTIFIER) return false;
179
+ // Accept identifiers and keywords as JSX tag names (e.g., <form>, <label>, <field>)
180
+ if (next.type !== TokenType.IDENTIFIER && !(next.value in Keywords)) return false;
167
181
  // Uppercase tag is always a component reference, never a comparison variable
168
182
  if (/^[A-Z]/.test(next.value)) return true;
169
183
  const afterIdent = this.peek(2);
@@ -198,6 +212,13 @@ export class Parser {
198
212
  // ─── Program ───────────────────────────────────────────────
199
213
 
200
214
  parse() {
215
+ // Eagerly install all block-plugin parser extensions so they work inside function bodies
216
+ for (const plugin of BlockRegistry.all()) {
217
+ const p = plugin.parser;
218
+ if (p.install && p.installedFlag && !Parser.prototype[p.installedFlag]) {
219
+ p.install(Parser);
220
+ }
221
+ }
201
222
  const body = [];
202
223
  const maxErrors = 50; // Stop after 50 errors to avoid cascading noise
203
224
  while (!this.isAtEnd()) {
@@ -378,7 +399,7 @@ export class Parser {
378
399
  }
379
400
 
380
401
  // ─── Full-stack blocks ────────────────────────────────────
381
- // parseClientBlock() and client-specific methods are in client-parser.js (lazy-loaded)
402
+ // parseBrowserBlock() and browser-specific methods are in browser-parser.js (lazy-loaded)
382
403
 
383
404
  parseSharedBlock() {
384
405
  const l = this.loc();
@@ -521,7 +542,7 @@ export class Parser {
521
542
  return new AST.RefreshPolicy(sourceName, { value, unit }, l);
522
543
  }
523
544
 
524
- // Client-specific statements and JSX parsing are in client-parser.js (lazy-loaded)
545
+ // Browser-specific statements and JSX parsing are in browser-parser.js (lazy-loaded)
525
546
 
526
547
  // ─── Statements ───────────────────────────────────────────
527
548
 
@@ -980,6 +1001,43 @@ export class Parser {
980
1001
  return new AST.TypeAnnotation(name, typeParams, l);
981
1002
  }
982
1003
 
1004
+ // Parse inline validators for type fields: { required, email, min(18) }
1005
+ // Uses comma-separated validator names, supports args in parens
1006
+ _parseTypeFieldValidators() {
1007
+ const validators = [];
1008
+ if (this.check(TokenType.LBRACE)) {
1009
+ this.advance(); // consume {
1010
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
1011
+ validators.push(this._parseInlineValidator());
1012
+ this.match(TokenType.COMMA); // optional comma separator
1013
+ }
1014
+ this.expect(TokenType.RBRACE, "Expected '}' to close validator block");
1015
+ }
1016
+ return validators;
1017
+ }
1018
+
1019
+ // Parse a single inline validator: name or name(args...)
1020
+ _parseInlineValidator() {
1021
+ const l = this.loc();
1022
+ let isAsync = false;
1023
+ if (this.check(TokenType.ASYNC)) {
1024
+ isAsync = true;
1025
+ this.advance();
1026
+ }
1027
+ const name = this.expect(TokenType.IDENTIFIER, "Expected validator name").value;
1028
+ const args = [];
1029
+ if (this.match(TokenType.LPAREN)) {
1030
+ if (!this.check(TokenType.RPAREN)) {
1031
+ args.push(this.parseExpression());
1032
+ while (this.match(TokenType.COMMA)) {
1033
+ args.push(this.parseExpression());
1034
+ }
1035
+ }
1036
+ this.expect(TokenType.RPAREN, "Expected ')' after validator arguments");
1037
+ }
1038
+ return new FormValidator(name, args, isAsync, l);
1039
+ }
1040
+
983
1041
  parseTypeDeclaration() {
984
1042
  const l = this.loc();
985
1043
  this.expect(TokenType.TYPE);
@@ -1066,9 +1124,10 @@ export class Parser {
1066
1124
  this.expect(TokenType.RPAREN, "Expected ')' after variant fields");
1067
1125
  variants.push(new AST.TypeVariant(vname, fields, vl));
1068
1126
  } else if (this.match(TokenType.COLON)) {
1069
- // Simple field: name: String
1127
+ // Simple field: name: String or name: String { required, email }
1070
1128
  const ftype = this.parseTypeAnnotation();
1071
- variants.push(new AST.TypeField(vname, ftype, vl));
1129
+ const validators = this._parseTypeFieldValidators();
1130
+ variants.push(new AST.TypeField(vname, ftype, vl, validators));
1072
1131
  } else {
1073
1132
  // Bare variant: None
1074
1133
  variants.push(new AST.TypeVariant(vname, [], vl));
@@ -1168,7 +1227,12 @@ export class Parser {
1168
1227
  const elements = [];
1169
1228
 
1170
1229
  while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
1171
- if (this.check(TokenType.IDENTIFIER) && this.current().value === '_') {
1230
+ if (this.check(TokenType.SPREAD)) {
1231
+ this.advance(); // consume ...
1232
+ const restName = this.expect(TokenType.IDENTIFIER, "Expected identifier after '...'").value;
1233
+ elements.push('...' + restName);
1234
+ break; // rest must be last
1235
+ } else if (this.check(TokenType.IDENTIFIER) && this.current().value === '_') {
1172
1236
  elements.push(null); // skip placeholder
1173
1237
  this.advance();
1174
1238
  } else {
@@ -1744,7 +1808,7 @@ export class Parser {
1744
1808
  expr = new AST.MemberExpression(expr, new AST.NumberLiteral(idx, l), true, l);
1745
1809
  continue;
1746
1810
  }
1747
- const prop = this.expect(TokenType.IDENTIFIER, "Expected property name after '.'").value;
1811
+ const prop = this.expectPropertyName("Expected property name after '.'").value;
1748
1812
  expr = new AST.MemberExpression(expr, prop, false, l);
1749
1813
  continue;
1750
1814
  }
@@ -1752,7 +1816,7 @@ export class Parser {
1752
1816
  if (this.check(TokenType.QUESTION_DOT)) {
1753
1817
  const l = this.loc();
1754
1818
  this.advance();
1755
- const prop = this.expect(TokenType.IDENTIFIER, "Expected property name after '?.'").value;
1819
+ const prop = this.expectPropertyName("Expected property name after '?.'").value;
1756
1820
  expr = new AST.OptionalChain(expr, prop, false, l);
1757
1821
  continue;
1758
1822
  }
@@ -1950,9 +2014,13 @@ export class Parser {
1950
2014
  return this.parseParenOrArrowLambda();
1951
2015
 
1952
2016
  case TokenType.SERVER:
1953
- case TokenType.CLIENT:
2017
+ case TokenType.BROWSER:
1954
2018
  case TokenType.SHARED:
1955
2019
  case TokenType.DERIVE:
2020
+ case TokenType.FORM:
2021
+ case TokenType.FIELD:
2022
+ case TokenType.GROUP:
2023
+ case TokenType.STEPS:
1956
2024
  return new AST.Identifier(this.advance().value, l);
1957
2025
 
1958
2026
  case TokenType.IDENTIFIER: {
@@ -0,0 +1,39 @@
1
+ // Select-specific AST Node definitions for the Tova language
2
+ // Extracted for lazy loading -- only loaded when select { } blocks are used.
3
+
4
+ /**
5
+ * select {
6
+ * msg from ch => { ... }
7
+ * ch.send(val) => { ... }
8
+ * timeout(5000) => { ... }
9
+ * _ => { ... }
10
+ * }
11
+ */
12
+ export class SelectStatement {
13
+ constructor(cases, loc) {
14
+ this.type = 'SelectStatement';
15
+ this.cases = cases; // Array of SelectCase
16
+ this.loc = loc;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * A single case arm inside a select block.
22
+ *
23
+ * kind: "receive" | "send" | "timeout" | "default"
24
+ * channel: Expression | null (identifier for the channel; null for timeout/default)
25
+ * binding: string | null (variable name bound on receive; null otherwise)
26
+ * value: Expression | null (value to send, or timeout duration; null for receive/default)
27
+ * body: [Statement] (statements executed when this case fires)
28
+ */
29
+ export class SelectCase {
30
+ constructor(kind, channel, binding, value, body, loc) {
31
+ this.type = 'SelectCase';
32
+ this.kind = kind; // "receive" | "send" | "timeout" | "default"
33
+ this.channel = channel; // Expression | null
34
+ this.binding = binding; // string | null
35
+ this.value = value; // Expression | null
36
+ this.body = body; // [Statement]
37
+ this.loc = loc;
38
+ }
39
+ }
@@ -0,0 +1,30 @@
1
+ import { installBrowserParser } from '../../parser/browser-parser.js';
2
+ import { installBrowserAnalyzer } from '../../analyzer/browser-analyzer.js';
3
+
4
+ export const browserPlugin = {
5
+ name: 'browser',
6
+ astNodeType: 'BrowserBlock',
7
+ detection: {
8
+ strategy: 'keyword',
9
+ tokenType: 'BROWSER',
10
+ },
11
+ parser: {
12
+ install: installBrowserParser,
13
+ installedFlag: '_browserParserInstalled',
14
+ method: 'parseBrowserBlock',
15
+ },
16
+ analyzer: {
17
+ visit: (analyzer, node) => {
18
+ if (!analyzer.constructor.prototype._browserAnalyzerInstalled) {
19
+ installBrowserAnalyzer(analyzer.constructor);
20
+ }
21
+ const methodName = 'visit' + node.type;
22
+ return analyzer[methodName](node);
23
+ },
24
+ childNodeTypes: [
25
+ 'StateDeclaration', 'ComputedDeclaration', 'EffectDeclaration',
26
+ 'ComponentDeclaration', 'StoreDeclaration', 'FormDeclaration',
27
+ ],
28
+ },
29
+ codegen: {},
30
+ };
@@ -0,0 +1,32 @@
1
+ import { TokenType } from '../../lexer/tokens.js';
2
+ import { installConcurrencyParser } from '../../parser/concurrency-parser.js';
3
+
4
+ export const concurrencyPlugin = {
5
+ name: 'concurrency',
6
+ astNodeType: 'ConcurrentBlock',
7
+ detection: {
8
+ strategy: 'identifier',
9
+ identifierValue: 'concurrent',
10
+ lookahead: (parser) => {
11
+ const next = parser.peek(1);
12
+ // concurrent {} or concurrent mode {}
13
+ return next.type === TokenType.LBRACE ||
14
+ (next.type === TokenType.IDENTIFIER &&
15
+ ['cancel_on_error', 'first', 'timeout'].includes(next.value));
16
+ },
17
+ },
18
+ parser: {
19
+ install: installConcurrencyParser,
20
+ installedFlag: '_concurrencyParserInstalled',
21
+ method: 'parseConcurrentBlock',
22
+ },
23
+ analyzer: {
24
+ visit: (analyzer, node) => {
25
+ if (node.type === 'SelectStatement') return analyzer.visitSelectStatement(node);
26
+ return analyzer.visitConcurrentBlock(node);
27
+ },
28
+ childNodeTypes: ['SelectStatement'],
29
+ noopNodeTypes: ['SpawnExpression', 'SelectCase'],
30
+ },
31
+ codegen: {},
32
+ };
@@ -0,0 +1,33 @@
1
+ import { installDeployParser } from '../../parser/deploy-parser.js';
2
+ import { installDeployAnalyzer } from '../../analyzer/deploy-analyzer.js';
3
+ import { TokenType } from '../../lexer/tokens.js';
4
+
5
+ export const deployPlugin = {
6
+ name: 'deploy',
7
+ astNodeType: 'DeployBlock',
8
+ detection: {
9
+ strategy: 'identifier',
10
+ identifierValue: 'deploy',
11
+ lookahead: (parser) => {
12
+ const next = parser.peek(1);
13
+ // deploy "name" {} — name is required
14
+ return next.type === TokenType.STRING;
15
+ },
16
+ },
17
+ parser: {
18
+ install: installDeployParser,
19
+ installedFlag: '_deployParserInstalled',
20
+ method: 'parseDeployBlock',
21
+ },
22
+ analyzer: {
23
+ visit: (analyzer, node) => {
24
+ installDeployAnalyzer(analyzer.constructor);
25
+ analyzer.visitDeployBlock(node);
26
+ },
27
+ childNodeTypes: [],
28
+ noopNodeTypes: [
29
+ 'DeployConfigField', 'DeployEnvBlock', 'DeployDbBlock',
30
+ ],
31
+ },
32
+ codegen: {},
33
+ };
@@ -0,0 +1,32 @@
1
+ import { installEdgeParser } from '../../parser/edge-parser.js';
2
+ import { TokenType } from '../../lexer/tokens.js';
3
+
4
+ export const edgePlugin = {
5
+ name: 'edge',
6
+ astNodeType: 'EdgeBlock',
7
+ detection: {
8
+ strategy: 'identifier',
9
+ identifierValue: 'edge',
10
+ lookahead: (parser) => {
11
+ const next = parser.peek(1);
12
+ // edge {} or edge "name" {}
13
+ return next.type === TokenType.LBRACE || next.type === TokenType.STRING;
14
+ },
15
+ },
16
+ parser: {
17
+ install: installEdgeParser,
18
+ installedFlag: '_edgeParserInstalled',
19
+ method: 'parseEdgeBlock',
20
+ },
21
+ analyzer: {
22
+ visit: (analyzer, node) => analyzer.visitEdgeBlock(node),
23
+ childNodeTypes: [],
24
+ noopNodeTypes: [
25
+ 'EdgeKVDeclaration', 'EdgeSQLDeclaration', 'EdgeStorageDeclaration',
26
+ 'EdgeQueueDeclaration', 'EdgeEnvDeclaration', 'EdgeSecretDeclaration',
27
+ 'EdgeScheduleDeclaration', 'EdgeConsumeDeclaration', 'EdgeConfigField',
28
+ ],
29
+ crossBlockValidate: (analyzer) => analyzer._validateEdgeCrossBlock(),
30
+ },
31
+ codegen: {},
32
+ };
@@ -3,21 +3,27 @@
3
3
 
4
4
  import { BlockRegistry } from './block-registry.js';
5
5
  import { serverPlugin } from './plugins/server-plugin.js';
6
- import { clientPlugin } from './plugins/client-plugin.js';
6
+ import { browserPlugin } from './plugins/browser-plugin.js';
7
7
  import { sharedPlugin } from './plugins/shared-plugin.js';
8
8
  import { securityPlugin } from './plugins/security-plugin.js';
9
9
  import { cliPlugin } from './plugins/cli-plugin.js';
10
10
  import { dataPlugin } from './plugins/data-plugin.js';
11
11
  import { testPlugin } from './plugins/test-plugin.js';
12
12
  import { benchPlugin } from './plugins/bench-plugin.js';
13
+ import { edgePlugin } from './plugins/edge-plugin.js';
14
+ import { concurrencyPlugin } from './plugins/concurrency-plugin.js';
15
+ import { deployPlugin } from './plugins/deploy-plugin.js';
13
16
 
14
17
  BlockRegistry.register(serverPlugin);
15
- BlockRegistry.register(clientPlugin);
18
+ BlockRegistry.register(browserPlugin);
16
19
  BlockRegistry.register(sharedPlugin);
17
20
  BlockRegistry.register(securityPlugin);
18
21
  BlockRegistry.register(cliPlugin);
19
22
  BlockRegistry.register(dataPlugin);
20
23
  BlockRegistry.register(testPlugin);
21
24
  BlockRegistry.register(benchPlugin);
25
+ BlockRegistry.register(edgePlugin);
26
+ BlockRegistry.register(concurrencyPlugin);
27
+ BlockRegistry.register(deployPlugin);
22
28
 
23
29
  export { BlockRegistry };
@@ -243,7 +243,7 @@ export function renderHeadTags(tags) {
243
243
  // of tag descriptors for safe rendering: [{ tag: 'meta', attrs: { name: 'desc', content: '...' } }]
244
244
  // SECURITY: Raw string `head` must contain only developer-authored content — never user input.
245
245
  // Use the array form or renderHeadTags() for safe user-controlled head content.
246
- export function renderPage(component, { title = 'Tova App', head = '', scriptSrc = '/client.js', cspNonce } = {}) {
246
+ export function renderPage(component, { title = 'Tova App', head = '', scriptSrc = '/browser.js', cspNonce } = {}) {
247
247
  const appHtml = renderToString(typeof component === 'function' ? component() : component);
248
248
  const headHtml = Array.isArray(head) ? renderHeadTags(head) : head;
249
249
  const nonceAttr = cspNonce ? ` nonce="${escapeAttr(cspNonce)}"` : '';
@@ -403,7 +403,7 @@ export function renderToReadableStream(vnode, options = {}) {
403
403
 
404
404
  // Render a full HTML page as a stream
405
405
  export function renderPageToStream(component, options = {}) {
406
- const { title = 'Tova App', head = '', scriptSrc = '/client.js', onError, bufferSize, cspNonce } = options;
406
+ const { title = 'Tova App', head = '', scriptSrc = '/browser.js', onError, bufferSize, cspNonce } = options;
407
407
  const headHtml = Array.isArray(head) ? renderHeadTags(head) : head;
408
408
  const nonceAttr = cspNonce ? ` nonce="${escapeAttr(cspNonce)}"` : '';
409
409
 
@@ -1,6 +1,6 @@
1
1
  // Tova standard library — inline string versions for codegen
2
2
  // Single source of truth for all inline stdlib code used in code generation.
3
- // Used by: base-codegen.js, client-codegen.js, bin/tova.js
3
+ // Used by: base-codegen.js, browser-codegen.js, bin/tova.js
4
4
 
5
5
  export const RESULT_OPTION = `class _Ok { constructor(value) { this.value = value; } }
6
6
  _Ok.prototype.__tag = "Ok";
@@ -117,7 +117,7 @@ export const BUILTIN_FUNCTIONS = {
117
117
  if (!parallel_map._pool) {
118
118
  const { Worker } = await import("worker_threads");
119
119
  const wc = 'const{parentPort}=require("worker_threads");parentPort.on("message",m=>{const fn=(0,eval)("("+m.f+")");try{const r=m.c.map(fn);parentPort.postMessage({i:m.i,r})}catch(e){parentPort.postMessage({i:m.i,e:e.message})}})';
120
- parallel_map._pool = Array.from({length: n}, () => { const w = new Worker(wc, {eval: true}); w.unref(); return w; });
120
+ parallel_map._pool = Array.from({length: n}, () => new Worker(wc, {eval: true}));
121
121
  parallel_map._cid = 0;
122
122
  }
123
123
  const pool = parallel_map._pool;
@@ -125,17 +125,20 @@ export const BUILTIN_FUNCTIONS = {
125
125
  const fnStr = fn.toString();
126
126
  const cid = ++parallel_map._cid;
127
127
  const promises = [];
128
+ const usedWorkers = [];
128
129
  for (let ci = 0; ci < pool.length && ci * cs < arr.length; ci++) {
129
130
  const chunk = arr.slice(ci * cs, (ci + 1) * cs);
130
131
  const mid = cid * 1000 + ci;
132
+ const w = pool[ci];
133
+ w.ref();
134
+ usedWorkers.push(w);
131
135
  promises.push(new Promise((resolve, reject) => {
132
- const w = pool[ci];
133
136
  const h = (msg) => { if (msg.i === mid) { w.removeListener("message", h); if (msg.e) reject(new Error(msg.e)); else resolve(msg.r); } };
134
137
  w.on("message", h);
135
138
  w.postMessage({i: mid, c: chunk, f: fnStr});
136
139
  }));
137
140
  }
138
- return (await Promise.all(promises)).flat();
141
+ try { return (await Promise.all(promises)).flat(); } finally { for (const w of usedWorkers) w.unref(); }
139
142
  }`,
140
143
  upper: `function upper(s) { return s.toUpperCase(); }`,
141
144
  lower: `function lower(s) { return s.toLowerCase(); }`,
@@ -1054,6 +1057,35 @@ Table.prototype = { get rows() { return this._rows.length; }, get columns() { re
1054
1057
  this._recvWaiters.push(resolve);
1055
1058
  }.bind(this));
1056
1059
  }
1060
+ _tryReceive() {
1061
+ if (this._buffer.length > 0) {
1062
+ const value = this._buffer.shift();
1063
+ if (this._sendWaiters.length > 0) {
1064
+ const waiter = this._sendWaiters.shift();
1065
+ this._buffer.push(waiter.value);
1066
+ waiter.resolve();
1067
+ }
1068
+ return Some(value);
1069
+ }
1070
+ if (this._sendWaiters.length > 0) {
1071
+ const waiter = this._sendWaiters.shift();
1072
+ waiter.resolve();
1073
+ return Some(waiter.value);
1074
+ }
1075
+ return None;
1076
+ }
1077
+ _trySend(value) {
1078
+ if (this._recvWaiters.length > 0) {
1079
+ const waiter = this._recvWaiters.shift();
1080
+ waiter(Some(value));
1081
+ return true;
1082
+ }
1083
+ if (this._capacity > 0 && this._buffer.length < this._capacity) {
1084
+ this._buffer.push(value);
1085
+ return true;
1086
+ }
1087
+ return false;
1088
+ }
1057
1089
  close() {
1058
1090
  this._closed = true;
1059
1091
  for (const waiter of this._recvWaiters) waiter(None);
@@ -1461,7 +1493,7 @@ export function getFullStdlib() {
1461
1493
  return `${NATIVE_INIT_SYNC}\n${buildSelectiveStdlib(BUILTIN_NAMES)}\n${RESULT_OPTION}\n${PROPAGATE}`;
1462
1494
  }
1463
1495
 
1464
- // Stdlib for client codegen (includes builtins + result/option + propagate)
1465
- export function getClientStdlib() {
1496
+ // Stdlib for browser codegen (includes builtins + result/option + propagate)
1497
+ export function getBrowserStdlib() {
1466
1498
  return `${buildSelectiveStdlib(BUILTIN_NAMES)}\n${RESULT_OPTION}\n${PROPAGATE}`;
1467
1499
  }
@@ -0,0 +1,152 @@
1
+ // Tova Runtime Bridge
2
+ // Auto-discovers the native tova_runtime addon (napi-rs) and exposes a clean API.
3
+ // Falls back gracefully when the addon is not available.
4
+
5
+ 'use strict';
6
+
7
+ let _runtime = null; // null = not yet attempted, false = failed, object = loaded
8
+ let _available = false;
9
+
10
+ function _findAndLoad() {
11
+ const { existsSync, readdirSync } = require('fs');
12
+ const { join, dirname } = require('path');
13
+
14
+ const searchDirs = [
15
+ // Relative to this file (src/stdlib/) -> project root tova_runtime/
16
+ join(dirname(__filename), '..', '..', 'tova_runtime'),
17
+ join(dirname(__filename), '..', '..', 'tova_runtime', 'target', 'release'),
18
+ // System-wide install
19
+ join(process.env.HOME || '', '.tova', 'lib'),
20
+ // Next to the running script
21
+ dirname(process.argv[1] || ''),
22
+ ];
23
+
24
+ for (const dir of searchDirs) {
25
+ if (!existsSync(dir)) continue;
26
+
27
+ let entries;
28
+ try { entries = readdirSync(dir); } catch (_) { continue; }
29
+
30
+ // Search .node files first (napi-rs convention)
31
+ const nodeFiles = entries.filter(f => f.endsWith('.node'));
32
+ for (const f of nodeFiles) {
33
+ try { return require(join(dir, f)); } catch (_) { continue; }
34
+ }
35
+
36
+ // Fall back to .dylib files (macOS raw cargo output)
37
+ const dylibFiles = entries.filter(f => f.endsWith('.dylib') && f.startsWith('libtova_runtime'));
38
+ for (const f of dylibFiles) {
39
+ try { return require(join(dir, f)); } catch (_) { continue; }
40
+ }
41
+ }
42
+
43
+ return null;
44
+ }
45
+
46
+ function _init() {
47
+ if (_runtime !== null) return _available;
48
+
49
+ try {
50
+ const loaded = _findAndLoad();
51
+ if (loaded && typeof loaded.healthCheck === 'function') {
52
+ _runtime = loaded;
53
+ _available = true;
54
+ } else {
55
+ _runtime = false;
56
+ _available = false;
57
+ }
58
+ } catch (_) {
59
+ _runtime = false;
60
+ _available = false;
61
+ }
62
+
63
+ return _available;
64
+ }
65
+
66
+ // --- Public API ---
67
+
68
+ function isRuntimeAvailable() {
69
+ return _init();
70
+ }
71
+
72
+ function healthCheck() {
73
+ if (!_init()) return null;
74
+ return _runtime.healthCheck();
75
+ }
76
+
77
+ function channelCreate(capacity) {
78
+ if (!_init()) throw new Error('tova_runtime not available');
79
+ return _runtime.channelCreate(capacity);
80
+ }
81
+
82
+ function channelSend(id, value) {
83
+ if (!_init()) throw new Error('tova_runtime not available');
84
+ return _runtime.channelSend(id, value);
85
+ }
86
+
87
+ function channelReceive(id) {
88
+ if (!_init()) throw new Error('tova_runtime not available');
89
+ return _runtime.channelReceive(id);
90
+ }
91
+
92
+ function channelClose(id) {
93
+ if (!_init()) throw new Error('tova_runtime not available');
94
+ _runtime.channelClose(id);
95
+ }
96
+
97
+ function execWasm(bytes, func, args) {
98
+ if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
99
+ return _runtime.execWasm(bytes, func, args);
100
+ }
101
+
102
+ function execWasmWithChannels(bytes, func, args) {
103
+ if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
104
+ return _runtime.execWasmWithChannels(bytes, func, args);
105
+ }
106
+
107
+ function concurrentWasm(tasks) {
108
+ if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
109
+ return _runtime.concurrentWasm(tasks);
110
+ }
111
+
112
+ function concurrentWasmWithChannels(tasks) {
113
+ if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
114
+ return _runtime.concurrentWasmWithChannels(tasks);
115
+ }
116
+
117
+ function concurrentWasmShared(tasks) {
118
+ if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
119
+ return _runtime.concurrentWasmShared(tasks);
120
+ }
121
+
122
+ function concurrentWasmFirst(tasks) {
123
+ if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
124
+ return _runtime.concurrentWasmFirst(tasks);
125
+ }
126
+
127
+ function concurrentWasmTimeout(tasks, timeoutMs) {
128
+ if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
129
+ return _runtime.concurrentWasmTimeout(tasks, timeoutMs);
130
+ }
131
+
132
+ function concurrentWasmCancelOnError(tasks) {
133
+ if (!_init()) return Promise.reject(new Error('tova_runtime not available'));
134
+ return _runtime.concurrentWasmCancelOnError(tasks);
135
+ }
136
+
137
+ module.exports = {
138
+ isRuntimeAvailable,
139
+ healthCheck,
140
+ channelCreate,
141
+ channelSend,
142
+ channelReceive,
143
+ channelClose,
144
+ execWasm,
145
+ execWasmWithChannels,
146
+ concurrentWasm,
147
+ concurrentWasmWithChannels,
148
+ concurrentWasmShared,
149
+ concurrentWasmFirst,
150
+ concurrentWasmTimeout,
151
+ concurrentWasmCancelOnError,
152
+ };
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/embed-runtime.js — do not edit
2
- export const VERSION = "0.5.1";
2
+ export const VERSION = "0.8.2";