render-workflows-dart 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 (61) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/LICENSE +21 -0
  3. package/README.md +678 -0
  4. package/dart/generator/bin/generate.dart +419 -0
  5. package/dart/generator/pubspec.lock +149 -0
  6. package/dart/generator/pubspec.yaml +9 -0
  7. package/examples/README.md +56 -0
  8. package/examples/default/README.md +18 -0
  9. package/examples/default/gitignore +7 -0
  10. package/examples/default/index.js +2 -0
  11. package/examples/default/package.json +17 -0
  12. package/examples/default/pubspec.yaml +7 -0
  13. package/examples/default/tasks.dart +39 -0
  14. package/examples/http/README.md +26 -0
  15. package/examples/http/gitignore +7 -0
  16. package/examples/http/index.js +2 -0
  17. package/examples/http/package.json +18 -0
  18. package/examples/http/pubspec.yaml +13 -0
  19. package/examples/http/tasks.dart +100 -0
  20. package/examples/introspect/README.md +50 -0
  21. package/examples/introspect/gitignore +7 -0
  22. package/examples/introspect/index.js +2 -0
  23. package/examples/introspect/node_env.dart +35 -0
  24. package/examples/introspect/package.json +18 -0
  25. package/examples/introspect/pubspec.yaml +23 -0
  26. package/examples/introspect/tasks.dart +144 -0
  27. package/examples/native/README.md +32 -0
  28. package/examples/native/gitignore +7 -0
  29. package/examples/native/index.js +2 -0
  30. package/examples/native/native/tools_impl.dart +105 -0
  31. package/examples/native/package.json +23 -0
  32. package/examples/native/pubspec.yaml +7 -0
  33. package/examples/native/tasks.dart +35 -0
  34. package/examples/postgres/README.md +73 -0
  35. package/examples/postgres/gitignore +7 -0
  36. package/examples/postgres/index.js +2 -0
  37. package/examples/postgres/native/db_impl.dart +205 -0
  38. package/examples/postgres/package.json +26 -0
  39. package/examples/postgres/pubspec.yaml +14 -0
  40. package/examples/postgres/seed/bin/seed.dart +56 -0
  41. package/examples/postgres/seed/bin/show.dart +64 -0
  42. package/examples/postgres/seed/lib/src/connect.dart +93 -0
  43. package/examples/postgres/seed/lib/src/schema.dart +46 -0
  44. package/examples/postgres/seed/pubspec.yaml +17 -0
  45. package/examples/postgres/tasks.dart +66 -0
  46. package/package.json +51 -0
  47. package/runtime/AGENTS.md +141 -0
  48. package/runtime/CLAUDE.md +2 -0
  49. package/runtime/native_task.dart +115 -0
  50. package/runtime/render_dart.dart +428 -0
  51. package/src/cli.js +396 -0
  52. package/src/native-worker.js +196 -0
  53. package/src/node-bridge.js +118 -0
  54. package/src/runtime.js +108 -0
  55. package/src/toolchain/compile.js +153 -0
  56. package/src/toolchain/dart-sdk.js +216 -0
  57. package/src/toolchain/dart-version.js +113 -0
  58. package/src/toolchain/generate.js +112 -0
  59. package/src/toolchain/index.js +8 -0
  60. package/src/toolchain/native.js +217 -0
  61. package/src/web-shims.js +281 -0
@@ -0,0 +1,66 @@
1
+ import 'render_dart.dart';
2
+ // The natural name. Nothing here says these run in another process, or that
3
+ // they touch dart:io — the facade resolves to a stub under dart2js.
4
+ import 'native/db.dart';
5
+
6
+ /// Render Workflows tasks backed by a Render Postgres database.
7
+ ///
8
+ /// package:postgres cannot run under dart2js, so every one of these reaches a
9
+ /// natively compiled binary. Nothing at the call site says so.
10
+ ///
11
+ /// The table is created and filled by `seed/`, which runs on your machine over
12
+ /// the database's *external* connection string. These tasks read the same rows
13
+ /// from inside Render over the *internal* one — so a change made by a task and
14
+ /// seen by `seed/bin/show.dart` has crossed two machines and two connections.
15
+ void main() {
16
+ /// Query, optionally filtered.
17
+ task('listWidgets', (args) async => await listWidgets(
18
+ sku: args.isEmpty ? null : args[0] as String?,
19
+ limit: args.length < 2 ? 20 : args[1]! as int,
20
+ ));
21
+
22
+ /// A single aggregate row.
23
+ task('widgetStats', (args) async => await widgetStats());
24
+
25
+ /// Insert or update.
26
+ task('upsertWidget', (args) async => await upsertWidget(
27
+ args[0]! as String,
28
+ args[1]! as String,
29
+ args[2]! as int,
30
+ args[3]! as int,
31
+ ));
32
+
33
+ /// Update one row and return it.
34
+ task('restock', (args) async => await restock(
35
+ args[0]! as String,
36
+ args[1]! as int,
37
+ ));
38
+
39
+ /// Calls dbConnection repeatedly to show that a worker holds one Postgres
40
+ /// session: the backend pid stays put while the call counter climbs.
41
+ ///
42
+ /// Reconnecting per call would show a different pid every time — and pay for
43
+ /// a TCP handshake, TLS negotiation and authentication each round.
44
+ task('connectionReuse', (args) async {
45
+ final n = args.isEmpty ? 5 : args[0]! as int;
46
+ final started = DateTime.now();
47
+
48
+ final backendPids = <Object?>{};
49
+ Map<String, Object?> last = const {};
50
+ for (var i = 0; i < n; i++) {
51
+ last = await dbConnection();
52
+ backendPids.add(last['backend_pid']);
53
+ }
54
+
55
+ return {
56
+ 'calls': n,
57
+ 'ms': DateTime.now().difference(started).inMilliseconds,
58
+ 'distinctBackendPids': backendPids.length,
59
+ 'callsThisProcess': last['callsThisProcess'],
60
+ 'connectionAgeMs': last['connectionAgeMs'],
61
+ 'database': last['database'],
62
+ };
63
+ }, timeoutSeconds: 120);
64
+
65
+ start();
66
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "render-workflows-dart",
3
+ "version": "0.8.2",
4
+ "description": "Unofficial: write Render Workflows tasks in Dart, compiled to JavaScript. Not affiliated with or endorsed by Render.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/timmaffett/render_workflows_dart.git"
9
+ },
10
+ "keywords": [
11
+ "render",
12
+ "workflows",
13
+ "dart",
14
+ "dart2js"
15
+ ],
16
+ "bin": {
17
+ "render-workflows-dart": "src/cli.js",
18
+ "render-dart": "src/cli.js"
19
+ },
20
+ "main": "src/runtime.js",
21
+ "exports": {
22
+ ".": "./src/runtime.js",
23
+ "./runtime": "./src/runtime.js",
24
+ "./toolchain": "./src/toolchain/index.js",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "runtime",
30
+ "examples",
31
+ "README.md",
32
+ "CHANGELOG.md",
33
+ "LICENSE",
34
+ "dart"
35
+ ],
36
+ "scripts": {
37
+ "test": "node --test \"test/**/*.test.js\""
38
+ },
39
+ "peerDependencies": {
40
+ "@renderinc/sdk": ">=0.5.0",
41
+ "@bjorn3/browser_wasi_shim": ">=0.2.9"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@bjorn3/browser_wasi_shim": {
48
+ "optional": true
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,141 @@
1
+ # Working in this project
2
+
3
+ Render Workflows tasks written in Dart, built by
4
+ [`render-dart`](https://github.com/timmaffett/render_workflows_dart).
5
+
6
+ Task bodies are compiled to JavaScript with `dart compile js` and registered
7
+ through Render's official `@renderinc/sdk` on the `node` runtime. Where
8
+ JavaScript is not enough, a task calls into Dart compiled natively.
9
+
10
+ ## Commands
11
+
12
+ ```bash
13
+ npx render-dart build # compile; --force skips the freshness check
14
+ npx render-dart dev # build, then start Render's local task server
15
+ npx render-dart dart # which Dart this project uses, and why
16
+ render workflows tasks list --local
17
+ render workflows start <task> --local --input='[1, 2]'
18
+ ```
19
+
20
+ No Dart installation is needed. If one is on `PATH` the build uses it;
21
+ otherwise an SDK is fetched into `node_modules`. Either way the build prints
22
+ which version it used and where it came from, so `render-dart dart` is only
23
+ needed before building.
24
+
25
+ To pin a version, put it in `package.json` — that is the form that travels
26
+ with the project:
27
+
28
+ ```json
29
+ "renderDart": { "dartVersion": "3.13.1" }
30
+ ```
31
+
32
+ `--dart-version` and `RENDER_DART_VERSION` override it, in that order, for
33
+ trying one without a commit. **A version asked for explicitly beats a Dart on
34
+ `PATH`** and is downloaded if it has to be, so a pin means the same version
35
+ here and on Render. `latest` and the channel names work too, but an exact
36
+ version is the one that reproduces.
37
+
38
+ ## Writing a task
39
+
40
+ `tasks.dart` registers functions by name and must end with `start()`:
41
+
42
+ ```dart
43
+ task('doThing', (args) async => args[0]! as int * 2);
44
+ ```
45
+
46
+ Arguments and return values are JSON. Render caps one invocation's input at
47
+ 4 MB. `callTask('other', [...])` starts a **subtask of this workflow**, each on
48
+ its own instance — that is how work fans out.
49
+
50
+ ## The rule that catches people
51
+
52
+ **`dart:io` does not work in task code.** It compiles under dart2js and then
53
+ throws `Unsupported operation` on first use, so the build refuses a direct
54
+ import. That rules out files, sockets, subprocesses and any package needing
55
+ them.
56
+
57
+ Three ways forward:
58
+
59
+ | Need | Use |
60
+ | --- | --- |
61
+ | HTTP | `package:http` — works, it goes through the platform's networking |
62
+ | A package shipping a `.wasm` | just use it; modules resolve automatically |
63
+ | Files, FFI, a database, real parallelism | a **native task** |
64
+
65
+ ## Native tasks
66
+
67
+ Write the implementation in `native/<name>_impl.dart`, annotate top-level
68
+ functions, and list the file in `renderDart.native` in `package.json`:
69
+
70
+ ```dart
71
+ // native/tools_impl.dart
72
+ import 'dart:io';
73
+ import '../native_task.dart';
74
+
75
+ @nativeTask
76
+ Map<String, Object?> inspect(String path) => {'bytes': File(path).lengthSync()};
77
+ ```
78
+
79
+ ```dart
80
+ // tasks.dart — nothing here says it is native
81
+ import 'native/tools.dart';
82
+
83
+ task('inspect', (args) async => await inspect(args[0]! as String));
84
+ ```
85
+
86
+ The build generates `native/tools.dart` and `native/tools.stub.dart`. Do not
87
+ edit or commit them.
88
+
89
+ - **Always `await`** a native task; the stub returns a `Future` where the
90
+ implementation may not.
91
+ - **Return JSON-representable types only.** `DateTime` is not — convert to an
92
+ ISO-8601 string. Anything unsupported is rejected at build time, naming the
93
+ parameter.
94
+ - **`@NativeTask(worker: true)`** keeps the process alive between calls, which
95
+ matters for anything holding a connection. It also keeps top-level state, so
96
+ one call can observe what the last left behind.
97
+
98
+ **Native is not faster at arithmetic.** Measured on Render, V8 matches Dart AOT
99
+ on integer work and beats it at small n. Choose native for *capability* — or
100
+ for parallelism, which is the one real speed win, since dart2js is
101
+ single-threaded.
102
+
103
+ ## Reaching a database
104
+
105
+ `package:postgres` cannot run under dart2js at all — it needs a raw socket, and
106
+ pub.dev marks it `runtime:native-aot` with no `runtime:web`. So a database
107
+ needs a native task, holding the connection open with `worker: true`.
108
+
109
+ `render-dart init db --template postgres` scaffolds a working version, seeder
110
+ included.
111
+
112
+ The connection string belongs in `DATABASE_URL` on the service, set to the
113
+ database's **internal** string. A workflow's environment can only be set when
114
+ the service is created; afterwards, the Dashboard is the only route.
115
+
116
+ ## Deploying
117
+
118
+ | Field | Value |
119
+ | --- | --- |
120
+ | Runtime | Node |
121
+ | Build Command | `npm install && npm run build` |
122
+ | Start Command | `node index.js` |
123
+ | Root Directory | where this `package.json` lives |
124
+
125
+ **Autodeploy fires only for commits touching the root directory.** A commit
126
+ elsewhere being ignored is correct, not a broken webhook.
127
+
128
+ ## Do not commit
129
+
130
+ `build/`, `node_modules/`, and the generated `native/*.dart` files. The build
131
+ regenerates all of them and maintains a `native/.gitignore`.
132
+
133
+ `render_dart.dart` and `native_task.dart` are render-dart's bridge files. They
134
+ are boilerplate — the build refreshes them when missing and warns when a local
135
+ copy is older than the installed package.
136
+
137
+ ## More
138
+
139
+ Runnable examples covering HTTP, native tasks, Postgres, WebAssembly and
140
+ calling Render's own API from a task:
141
+ <https://github.com/timmaffett/render_workflows_dart/tree/main/examples>
@@ -0,0 +1,2 @@
1
+ See [AGENTS.md](AGENTS.md) — the guidance for working in this project lives there,
2
+ so there is one file to keep current rather than two.
@@ -0,0 +1,115 @@
1
+ /// The native side of a render-dart native task.
2
+ ///
3
+ /// This file is boilerplate — you shouldn't need to edit it. A file compiled
4
+ /// AOT by `render-dart build` runs a generated `main()` that calls
5
+ /// [nativeTaskMain] with its `@nativeTask` functions.
6
+ ///
7
+ /// The protocol is JSONL: one JSON object per line, in and out. That framing
8
+ /// is what lets the same binary serve a one-shot call and, later, a persistent
9
+ /// worker — a one-shot writes a line and reads a line, a worker just keeps
10
+ /// looping.
11
+ ///
12
+ /// → {"id":1,"method":"inspect","args":["a.png"],"named":{"sha":true}}
13
+ /// ← {"id":1,"$log":"reading a.png"}
14
+ /// ← {"id":1,"$ok":{"bytes":8192}}
15
+ ///
16
+ /// Every message carries an `id` even though a one-shot call has only one
17
+ /// request in flight, so nothing about the wire format changes when a worker
18
+ /// starts multiplexing several.
19
+ library;
20
+
21
+ import 'dart:async';
22
+ import 'dart:convert';
23
+ import 'dart:io';
24
+
25
+ /// Marks a top-level function as callable from a dart2js task.
26
+ ///
27
+ /// Parameters and the return value cross a JSON boundary, so they must be
28
+ /// JSON-representable: bool, int, double, num, String, List<T>, Map<String, T>,
29
+ /// Object?, dynamic, or Future<T> of those. `render-dart build` rejects
30
+ /// anything else by name rather than letting it fail as a decode error at run
31
+ /// time.
32
+ class NativeTask {
33
+ const NativeTask({this.worker, this.idleTimeout, this.timeout});
34
+
35
+ /// Keep the executable alive between calls.
36
+ ///
37
+ /// Much faster in a loop — 10 ms against 132 ms over 20 calls on Render —
38
+ /// but the process keeps its top-level state, so call N can observe what
39
+ /// call N-1 left behind and a leak is never cleaned up by process exit.
40
+ /// Defaults to false.
41
+ final bool? worker;
42
+
43
+ /// How long an idle worker lingers before it is reaped. Defaults to 30 s.
44
+ final Duration? idleTimeout;
45
+
46
+ /// How long one call may take before it is abandoned. Unset means no limit.
47
+ final Duration? timeout;
48
+ }
49
+
50
+ /// The annotation itself, for the common case with no options: `@nativeTask`.
51
+ const nativeTask = NativeTask();
52
+
53
+ /// What a generated dispatcher entry looks like.
54
+ typedef NativeHandler = Future<Object?> Function(
55
+ List<Object?> args,
56
+ Map<String, Object?> named,
57
+ );
58
+
59
+ /// Serves JSONL requests on stdin until it closes.
60
+ ///
61
+ /// Handlers run one at a time. A native task therefore never has to be
62
+ /// reentrant, which is the reason worker mode can reuse this unchanged.
63
+ Future<void> nativeTaskMain(
64
+ List<String> argv,
65
+ Map<String, NativeHandler> handlers,
66
+ ) async {
67
+ final out = stdout;
68
+
69
+ final lines = stdin.transform(utf8.decoder).transform(const LineSplitter());
70
+
71
+ await for (final line in lines) {
72
+ if (line.trim().isEmpty) continue;
73
+
74
+ Object? id;
75
+ try {
76
+ final request = jsonDecode(line) as Map<String, Object?>;
77
+ id = request['id'];
78
+ final method = request['method'] as String?;
79
+ final args = (request['args'] as List?)?.cast<Object?>() ?? const [];
80
+ final named = (request['named'] as Map?)?.cast<String, Object?>() ?? const {};
81
+
82
+ final handler = handlers[method];
83
+ if (handler == null) {
84
+ _write(out, {
85
+ 'id': id,
86
+ r'$err': 'no @nativeTask named "$method" in this executable',
87
+ r'$known': handlers.keys.toList(),
88
+ });
89
+ continue;
90
+ }
91
+
92
+ // print() from a task body would otherwise land on stdout and corrupt
93
+ // the framing, so it is rerouted to a $log line instead of becoming a
94
+ // baffling parse error on the Node side.
95
+ final result = await runZoned(
96
+ () => handler(args, named),
97
+ zoneSpecification: ZoneSpecification(
98
+ print: (_, __, ___, String message) =>
99
+ _write(out, {'id': id, r'$log': message}),
100
+ ),
101
+ );
102
+
103
+ _write(out, {'id': id, r'$ok': result});
104
+ } catch (e, stackTrace) {
105
+ // Mirrors the {$ok}/{$err} envelope the JS boundary uses, for the same
106
+ // reason: a real message is worth more than a generic failure.
107
+ _write(out, {'id': id, r'$err': '$e', r'$stack': '$stackTrace'});
108
+ }
109
+ }
110
+ }
111
+
112
+ void _write(IOSink out, Map<String, Object?> message) {
113
+ // jsonEncode escapes newlines, so one message is always exactly one line.
114
+ out.writeln(jsonEncode(message));
115
+ }