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.
- package/CHANGELOG.md +193 -0
- package/LICENSE +21 -0
- package/README.md +678 -0
- package/dart/generator/bin/generate.dart +419 -0
- package/dart/generator/pubspec.lock +149 -0
- package/dart/generator/pubspec.yaml +9 -0
- package/examples/README.md +56 -0
- package/examples/default/README.md +18 -0
- package/examples/default/gitignore +7 -0
- package/examples/default/index.js +2 -0
- package/examples/default/package.json +17 -0
- package/examples/default/pubspec.yaml +7 -0
- package/examples/default/tasks.dart +39 -0
- package/examples/http/README.md +26 -0
- package/examples/http/gitignore +7 -0
- package/examples/http/index.js +2 -0
- package/examples/http/package.json +18 -0
- package/examples/http/pubspec.yaml +13 -0
- package/examples/http/tasks.dart +100 -0
- package/examples/introspect/README.md +50 -0
- package/examples/introspect/gitignore +7 -0
- package/examples/introspect/index.js +2 -0
- package/examples/introspect/node_env.dart +35 -0
- package/examples/introspect/package.json +18 -0
- package/examples/introspect/pubspec.yaml +23 -0
- package/examples/introspect/tasks.dart +144 -0
- package/examples/native/README.md +32 -0
- package/examples/native/gitignore +7 -0
- package/examples/native/index.js +2 -0
- package/examples/native/native/tools_impl.dart +105 -0
- package/examples/native/package.json +23 -0
- package/examples/native/pubspec.yaml +7 -0
- package/examples/native/tasks.dart +35 -0
- package/examples/postgres/README.md +73 -0
- package/examples/postgres/gitignore +7 -0
- package/examples/postgres/index.js +2 -0
- package/examples/postgres/native/db_impl.dart +205 -0
- package/examples/postgres/package.json +26 -0
- package/examples/postgres/pubspec.yaml +14 -0
- package/examples/postgres/seed/bin/seed.dart +56 -0
- package/examples/postgres/seed/bin/show.dart +64 -0
- package/examples/postgres/seed/lib/src/connect.dart +93 -0
- package/examples/postgres/seed/lib/src/schema.dart +46 -0
- package/examples/postgres/seed/pubspec.yaml +17 -0
- package/examples/postgres/tasks.dart +66 -0
- package/package.json +51 -0
- package/runtime/AGENTS.md +141 -0
- package/runtime/CLAUDE.md +2 -0
- package/runtime/native_task.dart +115 -0
- package/runtime/render_dart.dart +428 -0
- package/src/cli.js +396 -0
- package/src/native-worker.js +196 -0
- package/src/node-bridge.js +118 -0
- package/src/runtime.js +108 -0
- package/src/toolchain/compile.js +153 -0
- package/src/toolchain/dart-sdk.js +216 -0
- package/src/toolchain/dart-version.js +113 -0
- package/src/toolchain/generate.js +112 -0
- package/src/toolchain/index.js +8 -0
- package/src/toolchain/native.js +217 -0
- package/src/web-shims.js +281 -0
package/README.md
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
# render-workflows-dart
|
|
2
|
+
|
|
3
|
+
> **Renamed.** This package was `render-dart` up to 0.8.1 and is
|
|
4
|
+
> `render-workflows-dart` from 0.8.2. The `render-dart` command still works, so
|
|
5
|
+
> an existing project only changes one line:
|
|
6
|
+
>
|
|
7
|
+
> ```diff
|
|
8
|
+
> - "render-dart": "^0.8.1"
|
|
9
|
+
> + "render-workflows-dart": "^0.8.2"
|
|
10
|
+
> ```
|
|
11
|
+
|
|
12
|
+
> **This is an _unofficial_, independent, community-built package.**
|
|
13
|
+
> Not affiliated with, endorsed by, or supported by
|
|
14
|
+
> [Render](https://render.com).
|
|
15
|
+
>
|
|
16
|
+
> Render's own SDKs and documentation are at
|
|
17
|
+
> [render.com/docs](https://render.com/docs).
|
|
18
|
+
|
|
19
|
+
[](https://render.com) [\*](#note)
|
|
20
|
+
|
|
21
|
+
Write [Render Workflows](https://render.com/docs/workflows) tasks in **Dart**,
|
|
22
|
+
on [Render](https://render.com).
|
|
23
|
+
|
|
24
|
+
[Render Workflows](https://render.com/docs/workflows) is in public beta and has
|
|
25
|
+
no built-in way to use Dart: tasks are defined with Render's own SDK, which is
|
|
26
|
+
available for **TypeScript and Python** only. Their docs say SDKs for more
|
|
27
|
+
languages are planned.
|
|
28
|
+
|
|
29
|
+
`render-dart` provides first class dart language support, without reaching for Docker (which
|
|
30
|
+
would cost API provisioning and local development). It can compile Dart **two
|
|
31
|
+
ways**:
|
|
32
|
+
|
|
33
|
+
- **Task bodies to JavaScript**, with `dart compile js`, registered through
|
|
34
|
+
Render's `@renderinc/sdk`. Render sees an ordinary Node workflow.
|
|
35
|
+
- **Anything needing the real platform to a native executable**, with
|
|
36
|
+
`dart compile exe`. The JavaScript side becomes a generated shim that calls
|
|
37
|
+
it, so the task code still reads as a plain Dart call. That is what reaches
|
|
38
|
+
`dart:io`, `dart:ffi`, isolates, and packages like `postgres` that have no
|
|
39
|
+
web build at all.
|
|
40
|
+
|
|
41
|
+
Both are compiled during the deploy, on Render's own hardware — nothing binary
|
|
42
|
+
is committed. You get real static analysis over the whole thing.
|
|
43
|
+
|
|
44
|
+
## The Dart side of Render
|
|
45
|
+
|
|
46
|
+
This package covers **writing** workflow tasks. Two companion Dart packages optionally provide
|
|
47
|
+
access to render's apis (neither are required to use this one):
|
|
48
|
+
|
|
49
|
+
| | |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `render-dart` (this) | **Writing** tasks in Dart |
|
|
52
|
+
| [`render_workflows`](https://pub.dev/packages/render_workflows) | **Running** workflows/tasks — start, watch, cancel, fan out |
|
|
53
|
+
| [`render_api`](https://pub.dev/packages/render_api) | **Managing** the services — create, deploy, inspect |
|
|
54
|
+
|
|
55
|
+
So a Flutter app can trigger a task with `render_workflows` that a Dart
|
|
56
|
+
workflow, built here, then executes.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx render-workflows-dart init my-workflow
|
|
62
|
+
cd my-workflow
|
|
63
|
+
npm install
|
|
64
|
+
npm run dev
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then, in another terminal:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
render workflows tasks list --local
|
|
71
|
+
render workflows start sumSquares --local --input='[[2,3,4]]'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Examples, which are also templates
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx render-workflows-dart init my-app --template postgres
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| | Answers |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| [`default`](examples/default) | Writing a task, fanning out, retrying |
|
|
83
|
+
| [`http`](examples/http) | Calling an external API |
|
|
84
|
+
| [`native`](examples/native) | Files, subprocesses, FFI, more than one core |
|
|
85
|
+
| [`postgres`](examples/postgres) | Reaching a database |
|
|
86
|
+
| [`introspect`](examples/introspect) | Inspecting Render, or running a task in another workflow |
|
|
87
|
+
|
|
88
|
+
Each is a complete service you can run, and `init` scaffolds from it — so a
|
|
89
|
+
template that does not work is a test failure rather than a surprise.
|
|
90
|
+
|
|
91
|
+
## Coding agents
|
|
92
|
+
|
|
93
|
+
Agents never read `node_modules`, so nothing shipped inside this package
|
|
94
|
+
reaches one helping in your project. `render-dart init` therefore writes an
|
|
95
|
+
`AGENTS.md` (and a `CLAUDE.md` pointing at it) **into the project it creates**,
|
|
96
|
+
covering the traps, when to reach for a native task, and how to get at a
|
|
97
|
+
database.
|
|
98
|
+
|
|
99
|
+
If you are an agent reading this page: the short version is that task bodies
|
|
100
|
+
are dart2js, `dart:io` does not work there, `package:http` does, and anything
|
|
101
|
+
needing files, FFI, a database or a second core goes through a native task —
|
|
102
|
+
see [Two ways past dart2js](#two-ways-past-dart2js). Native is **not** faster
|
|
103
|
+
at arithmetic.
|
|
104
|
+
|
|
105
|
+
There is also a Claude skill in the repository at
|
|
106
|
+
`.claude/skills/render-dart/`. It is not in the npm tarball, since skills are
|
|
107
|
+
not loaded from `node_modules`:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cp -r render-dart/.claude/skills/render-dart ~/.claude/skills/
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`AGENTS.md` in the repository root is guidance for working *on* this package,
|
|
114
|
+
which is a different thing.
|
|
115
|
+
|
|
116
|
+
## Writing tasks
|
|
117
|
+
|
|
118
|
+
```dart
|
|
119
|
+
import 'render_dart.dart';
|
|
120
|
+
|
|
121
|
+
void main() {
|
|
122
|
+
task('calculateSquare', (args) async {
|
|
123
|
+
final n = args[0]! as int;
|
|
124
|
+
return n * n;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Each callTask becomes its own task run on its own Render instance.
|
|
128
|
+
task('sumSquares', (args) async {
|
|
129
|
+
var total = 0;
|
|
130
|
+
for (final v in args[0]! as List<Object?>) {
|
|
131
|
+
total += (await callTask('calculateSquare', [v]))! as int;
|
|
132
|
+
}
|
|
133
|
+
return total;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
start();
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Deploying
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
render workflows create --name my-workflow --repo <url> \
|
|
144
|
+
--runtime node --root-directory my-workflow \
|
|
145
|
+
--build-command "npm install && npm run build" \
|
|
146
|
+
--run-command "node index.js"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
No Dart is needed on Render's builder — `render-dart build` fetches a pinned
|
|
150
|
+
SDK when none is present.
|
|
151
|
+
|
|
152
|
+
> Blueprints (`render.yaml`) do not support Workflows, so provisioning is via
|
|
153
|
+
> the CLI, the API, or the Dashboard.
|
|
154
|
+
|
|
155
|
+
## Commands
|
|
156
|
+
|
|
157
|
+
| | |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `render-dart build` | Compile `tasks.dart` to `build/tasks.js`, skipping if fresh |
|
|
160
|
+
| `render-dart dev` | Build, then start Render's local task server |
|
|
161
|
+
| `render-dart init [dir] [--template <name>]` | Scaffold a new project from an [example](#examples-which-are-also-templates) |
|
|
162
|
+
| `render-dart dart` | Which Dart this project will use, and why |
|
|
163
|
+
| `render-dart dart --list` | Every version the archive offers |
|
|
164
|
+
|
|
165
|
+
`--template` takes any directory name under
|
|
166
|
+
[`examples/`](examples) — `default` (the default), `http`, `native`,
|
|
167
|
+
`postgres` or `introspect`. An unknown name fails listing the real ones.
|
|
168
|
+
|
|
169
|
+
A scaffold also gets an `AGENTS.md`, and a `CLAUDE.md` pointing at it, so a
|
|
170
|
+
coding agent working in that project has the guidance it would otherwise never
|
|
171
|
+
see — see [Coding agents](#coding-agents).
|
|
172
|
+
|
|
173
|
+
Configure through `renderDart` in `package.json`:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"renderDart": {
|
|
178
|
+
"entry": "tasks.dart",
|
|
179
|
+
"out": "build/tasks.js",
|
|
180
|
+
"dartVersion": "3.13.1",
|
|
181
|
+
"optimize": "O2",
|
|
182
|
+
"sourceMaps": false,
|
|
183
|
+
"allowDartIo": false,
|
|
184
|
+
"allowDartIoIn": [],
|
|
185
|
+
"native": []
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`native` lists Dart files to compile to native executables — see
|
|
191
|
+
[Native tasks](#native-tasks). Each entry is a path, or an object that can
|
|
192
|
+
override what the source declared:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
"native": [
|
|
196
|
+
"native/tools_impl.dart",
|
|
197
|
+
{ "entry": "native/raw_impl.dart", "mode": "exe" },
|
|
198
|
+
{ "entry": "native/hot_impl.dart", "worker": false }
|
|
199
|
+
]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`allowDartIoIn` exempts named directories from the `dart:io` guard — for a
|
|
203
|
+
local tool sitting beside the workflow rather than running on it, like the
|
|
204
|
+
seeder in the `postgres` example. Narrower than `allowDartIo: true`, which
|
|
205
|
+
switches the check off for task code too.
|
|
206
|
+
|
|
207
|
+
## Choosing a Dart version
|
|
208
|
+
|
|
209
|
+
Three places, highest first. The flag is for trying one once, the environment
|
|
210
|
+
for varying a build without a commit — Render's dashboard sets those — and
|
|
211
|
+
`package.json` for the answer that should travel with the project:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npx render-dart build --dart-version 3.12.2
|
|
215
|
+
RENDER_DART_VERSION=3.12.2 npx render-dart build
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
"renderDart": { "dartVersion": "3.12.2" }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
A version can be exact, `latest`, or a channel name — `stable`, `beta` or
|
|
223
|
+
`dev`. An exact version needs no network to interpret, so a pinned project
|
|
224
|
+
keeps building when the archive is unreachable; an alias is resolved against
|
|
225
|
+
the archive on each build, which is the point of asking for one.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npx render-dart dart # what this project will use, and why
|
|
229
|
+
npx render-dart dart --list # 176 stable releases, newest first
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**A pin set explicitly wins over a Dart already on `PATH`.** If they differ, the
|
|
233
|
+
requested version is downloaded and used. Only the built-in default defers to a
|
|
234
|
+
local toolchain — it exists so a first build on Render has something to fetch,
|
|
235
|
+
not to override a Dart you installed deliberately.
|
|
236
|
+
|
|
237
|
+
Before 0.8.0 that was not true: the pin was consulted only when downloading, so
|
|
238
|
+
it worked on a first Render build and was silently ignored everywhere else. On a
|
|
239
|
+
laptop `PATH` always won, and on later Render builds whatever had been vendored
|
|
240
|
+
first won for ever, because the cache key was "does the directory exist". If
|
|
241
|
+
setting `dartVersion` ever appeared to do nothing, that is why. The vendored SDK
|
|
242
|
+
now records its version and is replaced when the pin changes.
|
|
243
|
+
|
|
244
|
+
Every build says which Dart it used and where it came from:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
[render-dart] Dart 3.12.2 requested by RENDER_DART_VERSION
|
|
248
|
+
[render-dart] using Dart 3.12.2 (downloaded)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Downloads are checked against the archive's published SHA-256 before being
|
|
252
|
+
unpacked. The hash is computed while the archive streams to disk, so it costs no
|
|
253
|
+
extra I/O — 0.18s of CPU for a 228 MB file, against roughly 30s to fetch it.
|
|
254
|
+
Releases old enough to predate the published sums say so rather than implying a
|
|
255
|
+
check happened.
|
|
256
|
+
|
|
257
|
+
## Using pub.dev packages
|
|
258
|
+
|
|
259
|
+
Add them to `pubspec.yaml` as normal; `render-dart build` runs `dart pub get`
|
|
260
|
+
for you. What works is determined by dart2js, not by Render:
|
|
261
|
+
|
|
262
|
+
| | Works? | |
|
|
263
|
+
| --- | --- | --- |
|
|
264
|
+
| Pure Dart (`collection`, `crypto`, `intl`, `path`) | yes | Nothing to think about |
|
|
265
|
+
| `package:http` | yes | Goes through `fetch`, which Node 18+ provides |
|
|
266
|
+
| Anything importing `dart:io` | **no** | `File`, `Process`, `Socket`, `HttpClient` |
|
|
267
|
+
|
|
268
|
+
**`dart:io` is the trap.** dart2js compiles it without complaint and then
|
|
269
|
+
throws `Unsupported operation` at runtime — so a task using it deploys cleanly
|
|
270
|
+
and fails on its first real run, potentially burning up to the task timeout
|
|
271
|
+
first. `render-dart build` therefore refuses to build a project that imports
|
|
272
|
+
`dart:io` directly, and tells you what to use instead. Conditional imports
|
|
273
|
+
(`if (dart.library.io)`) are left alone, and `allowDartIo` opts out.
|
|
274
|
+
|
|
275
|
+
For Node APIs beyond HTTP, use `dart:js_interop` directly.
|
|
276
|
+
|
|
277
|
+
### WebAssembly
|
|
278
|
+
|
|
279
|
+
Packages that ship a `.wasm` module work, and they are often the **simpler**
|
|
280
|
+
choice: the module runs inside the Node process, so there is no subprocess, no
|
|
281
|
+
second binary, and no build step beyond the one you already have.
|
|
282
|
+
|
|
283
|
+
When a package has no wasm build — or the work needs files, sockets or more
|
|
284
|
+
than one core — [native tasks](#two-ways-past-dart2js) are the other route.
|
|
285
|
+
|
|
286
|
+
Two shapes, both verified on Render, differing in how much the runtime has to
|
|
287
|
+
supply:
|
|
288
|
+
|
|
289
|
+
| | Needs | Verified |
|
|
290
|
+
| --- | --- | --- |
|
|
291
|
+
| Uses the platform's `WebAssembly` API (`forge2d`) | asset resolution only | Box2D v3, zero config |
|
|
292
|
+
| Carries a JS wasm runtime (`rust_crypto` → `wasm_run`) | asset resolution, `XMLHttpRequest`, and two pre-seeded globals | SHA/MD5/HMAC, cross-checked against pure-Dart `crypto` |
|
|
293
|
+
|
|
294
|
+
`wasm_run` looks browser-only at first: it loads its WASI shim by injecting a
|
|
295
|
+
`<script>` tag into an HTML document. But its setup checks whether the global
|
|
296
|
+
is *already* present and skips injection if so. The runtime seeds both:
|
|
297
|
+
`wasmFeatureDetect` comes from a UMD bundle shipped inside the pub package, and
|
|
298
|
+
`browser_wasi_shim` from npm. It then loads its module over `XMLHttpRequest`,
|
|
299
|
+
which the runtime also provides, on top of `fetch`.
|
|
300
|
+
|
|
301
|
+
For `wasm_run`-based packages, add the shim to your project — it is an
|
|
302
|
+
*optional* peer dependency, so nothing else pays for it:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
npm install @bjorn3/browser_wasi_shim
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Then the package works unmodified, with no `loadModule` callback and no other
|
|
309
|
+
setup. Its **native** path stays unavailable, needing `dart:ffi` and a wasmtime
|
|
310
|
+
binary; the web executor is what runs here, on the host's own `WebAssembly`.
|
|
311
|
+
|
|
312
|
+
One caveat worth knowing before combining packages: `rust_crypto` and
|
|
313
|
+
`forge2d 0.15` cannot share a pubspec, because `wasm_run` pulls
|
|
314
|
+
`build_rust_binaries` → `hooks ^1.0.0` while forge2d needs `hooks ^2.0.0`. Put
|
|
315
|
+
them in separate workflows.
|
|
316
|
+
|
|
317
|
+
`forge2d` — a `dart:ffi` binding to Box2D v3 — selects a bundled 227 KB
|
|
318
|
+
WebAssembly build under dart2js, and runs on Render unchanged:
|
|
319
|
+
|
|
320
|
+
```dart
|
|
321
|
+
await initializeForge2D(wasmUri: Uri.parse(fileUri('web/box2d.wasm')));
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
No configuration, no staging step, no `wasmUri`.
|
|
325
|
+
|
|
326
|
+
A Dart web app serves each package's `lib/` at `packages/<name>/`, and packages
|
|
327
|
+
that ship assets ask for them at exactly that path. Nothing serves it under
|
|
328
|
+
Node, so the request fails. The runtime resolves those paths from
|
|
329
|
+
`.dart_tool/package_config.json` — written by `dart pub get`, so the mapping is
|
|
330
|
+
exact rather than guessed — and reads the file directly. Node's `fetch` also
|
|
331
|
+
has no `file:` scheme, which the runtime adds for the same reason.
|
|
332
|
+
|
|
333
|
+
For assets of your own rather than a package's, `fileUri()` resolves a
|
|
334
|
+
project-relative path:
|
|
335
|
+
|
|
336
|
+
```dart
|
|
337
|
+
final data = await http.get(Uri.parse(fileUri('data/table.json')));
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`node:wasi` is not required: forge2d supplies its own WASI shims. It is
|
|
341
|
+
available in Node if a module ever needs the real thing.
|
|
342
|
+
|
|
343
|
+
## Reaching Node from a task
|
|
344
|
+
|
|
345
|
+
`dart:io` compiles under dart2js and then throws at runtime, so a task cannot
|
|
346
|
+
open a file, spawn a process, or reach the npm ecosystem on its own. Two
|
|
347
|
+
helpers close that gap.
|
|
348
|
+
|
|
349
|
+
**Any npm package or Node built-in:**
|
|
350
|
+
|
|
351
|
+
```dart
|
|
352
|
+
@JS()
|
|
353
|
+
extension type _Crypto(JSObject _) implements JSObject {
|
|
354
|
+
external String randomUUID();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
final crypto = _Crypto(requireModule('node:crypto'));
|
|
358
|
+
print(crypto.randomUUID());
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Dart cannot call `require` itself — in CommonJS it is module-scoped, and
|
|
362
|
+
`globalThis.require` is undefined in both CommonJS and ESM — so the runtime
|
|
363
|
+
hoists it. Resolution is rooted at your project directory, so
|
|
364
|
+
`requireModule('lodash')` means whatever *your* package.json depends on.
|
|
365
|
+
|
|
366
|
+
**Shelling out** to a CLI tool. (For calling *Dart* compiled natively, use
|
|
367
|
+
[native tasks](#native-tasks) rather than driving a process by hand.)
|
|
368
|
+
|
|
369
|
+
```dart
|
|
370
|
+
final result = await runProcess('git', args: ['rev-parse', 'HEAD']);
|
|
371
|
+
if (result.ok) print(result.stdout.trim());
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`runProcess` takes `args`, `workingDirectory`, `environment`, `stdin`,
|
|
375
|
+
`timeout` and `runInShell`. A non-zero exit is **returned, not thrown** — an
|
|
376
|
+
exit code is a result, and the caller usually wants `stderr` with it. It throws
|
|
377
|
+
only when the process could not be started, or when `timeout` elapses (SIGKILL,
|
|
378
|
+
since a task run is already bounded by Render's own timeout).
|
|
379
|
+
|
|
380
|
+
## Two ways past dart2js
|
|
381
|
+
|
|
382
|
+
dart2js cannot open a file, use a second core, or run a package that needs
|
|
383
|
+
`dart:io`. There are two escapes, and they are complementary rather than
|
|
384
|
+
ranked.
|
|
385
|
+
|
|
386
|
+
| | WebAssembly | Native task |
|
|
387
|
+
| --- | --- | --- |
|
|
388
|
+
| Runs in | the Node process | a subprocess |
|
|
389
|
+
| Needs | the package to ship a `.wasm` | nothing — any Dart compiles |
|
|
390
|
+
| `dart:io`, sockets, files | no | **yes** |
|
|
391
|
+
| `dart:ffi` | no | **yes** |
|
|
392
|
+
| More than one core | no | **yes**, isolates |
|
|
393
|
+
| Per-call cost | none | ~0.5 ms with a worker |
|
|
394
|
+
| Extra artefact | none | a binary, built during the deploy |
|
|
395
|
+
|
|
396
|
+
**Reach for wasm when the package already has one.** `forge2d` and
|
|
397
|
+
`rust_crypto` both do, and `render-dart` resolves their modules without
|
|
398
|
+
configuration. Nothing is spawned and nothing is compiled.
|
|
399
|
+
|
|
400
|
+
**Reach for native when there is no wasm build, when the work needs I/O or
|
|
401
|
+
FFI, or when it needs to use more than one core.** `package:postgres` is the
|
|
402
|
+
clearest case: it speaks the wire protocol over a raw socket, and pub.dev marks
|
|
403
|
+
it `runtime:native-aot` with no `runtime:web`. There is no wasm alternative and
|
|
404
|
+
no dart2js path — native or nothing.
|
|
405
|
+
|
|
406
|
+
### What native is *not*
|
|
407
|
+
|
|
408
|
+
It is not a way to make computation faster. The same recursive fib, compiled
|
|
409
|
+
both ways and run on Render:
|
|
410
|
+
|
|
411
|
+
| n | dart2js | native |
|
|
412
|
+
| --- | --- | --- |
|
|
413
|
+
| 30 | 8 ms | 23 ms |
|
|
414
|
+
| 34 | 60 ms | 50 ms |
|
|
415
|
+
| 36 | 146 ms | 131 ms |
|
|
416
|
+
|
|
417
|
+
V8 matches Dart AOT on pure integer work, and beats it at small n. If your task
|
|
418
|
+
is arithmetic, dart2js is already fine.
|
|
419
|
+
|
|
420
|
+
The speed win that *is* real is **parallelism**, because dart2js inherits
|
|
421
|
+
JavaScript's single thread. The same batch of fib(32), run sequentially in
|
|
422
|
+
JavaScript and across isolates natively:
|
|
423
|
+
|
|
424
|
+
| jobs | dart2js seq | native parallel | speedup |
|
|
425
|
+
| ---: | ---: | ---: | ---: |
|
|
426
|
+
| 2 | 46 ms | 31 ms | 1.5x |
|
|
427
|
+
| 4 | 88 ms | 48 ms | 1.8x |
|
|
428
|
+
| 8 | 177 ms | 84 ms | 2.1x |
|
|
429
|
+
| 16 | 367 ms | 219 ms | 1.7x |
|
|
430
|
+
| 32 | 706 ms | 533 ms | 1.3x |
|
|
431
|
+
|
|
432
|
+
**Treat that as anecdote.** It is one workload on one Render instance, on the
|
|
433
|
+
default `starter` task plan in a free workspace — the smallest there is. A
|
|
434
|
+
different plan, or different work, would produce a different curve.
|
|
435
|
+
|
|
436
|
+
What it does illustrate is a shape worth expecting: the benefit is real, it
|
|
437
|
+
does not grow indefinitely, and past some point more isolates cost more than
|
|
438
|
+
they return. The dart2js column stays flat at ~22 ms per job throughout, which
|
|
439
|
+
is the control confirming the native side's rise is not noise.
|
|
440
|
+
|
|
441
|
+
`Platform.numberOfProcessors` reported 32 the whole time, which was not a
|
|
442
|
+
useful guide to any of this. Measure the workload on the plan it will run on.
|
|
443
|
+
|
|
444
|
+
## Native tasks
|
|
445
|
+
|
|
446
|
+
Write the function once, compile it AOT, and call it from task code as if it
|
|
447
|
+
were local — no process handling, no serialisation, nothing at the call site
|
|
448
|
+
that says it is native.
|
|
449
|
+
|
|
450
|
+
Write the implementation in `<name>_impl.dart`:
|
|
451
|
+
|
|
452
|
+
```dart
|
|
453
|
+
// native/tools_impl.dart
|
|
454
|
+
import 'dart:io';
|
|
455
|
+
|
|
456
|
+
import '../native_task.dart';
|
|
457
|
+
|
|
458
|
+
@nativeTask
|
|
459
|
+
Map<String, Object?> inspect(String path) => {
|
|
460
|
+
'bytes': File(path).lengthSync(),
|
|
461
|
+
'lines': File(path).readAsLinesSync().length,
|
|
462
|
+
};
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Declare it, and call it by its plain name:
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
"renderDart": { "native": ["native/tools_impl.dart"] }
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
```dart
|
|
472
|
+
// tasks.dart — nothing here says "native"
|
|
473
|
+
import 'native/tools.dart';
|
|
474
|
+
|
|
475
|
+
task('inspect', (args) async => await inspect(args[0]! as String));
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
`render-dart build` generates `native/tools.dart` as a conditional export:
|
|
479
|
+
|
|
480
|
+
```dart
|
|
481
|
+
export 'tools.stub.dart' if (dart.library.io) 'tools_impl.dart';
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
so the **same source** compiles to a process call under dart2js and a direct
|
|
485
|
+
call natively. That also means native code can be unit-tested on the Dart VM,
|
|
486
|
+
and a native function calling a sibling skips the process hop entirely.
|
|
487
|
+
|
|
488
|
+
Always `await` a native task — the stub returns a `Future` where the
|
|
489
|
+
implementation may return a plain value, and awaiting is what makes one piece of
|
|
490
|
+
code valid on both sides.
|
|
491
|
+
|
|
492
|
+
### What can cross
|
|
493
|
+
|
|
494
|
+
Parameters and return values are JSON, so: `bool`, `int`, `double`, `num`,
|
|
495
|
+
`String`, `List<T>`, `Map<String, T>`, `Object?`, `dynamic`, and `Future<T>` of
|
|
496
|
+
those, nullable included. Required, optional and named parameters all work,
|
|
497
|
+
with their defaults.
|
|
498
|
+
|
|
499
|
+
Anything else — a custom class, `Uint8List`, `Set`, a record — is **rejected at
|
|
500
|
+
build time**, naming the parameter, rather than failing as a decode error on a
|
|
501
|
+
live run.
|
|
502
|
+
|
|
503
|
+
### Options ride with the declaration
|
|
504
|
+
|
|
505
|
+
So a call site never has to know, and never has to be updated when you change
|
|
506
|
+
your mind:
|
|
507
|
+
|
|
508
|
+
```dart
|
|
509
|
+
@NativeTask(worker: true, idleTimeout: Duration(seconds: 30))
|
|
510
|
+
Future<int> hot(int a) async => a;
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
| | |
|
|
514
|
+
| --- | --- |
|
|
515
|
+
| `worker` | Keep the executable alive between calls. Default `false` |
|
|
516
|
+
| `idleTimeout` | How long an idle worker lingers. Default 30 s |
|
|
517
|
+
| `timeout` | How long one call may take. Default none |
|
|
518
|
+
|
|
519
|
+
`renderDart.native` can override any of them per entry, so a deployment can
|
|
520
|
+
change behaviour without editing code. To vary them for one caller — without
|
|
521
|
+
changing any signature, which is what keeps the one-source property:
|
|
522
|
+
|
|
523
|
+
```dart
|
|
524
|
+
await NativeCall.scope(worker: false, () async => hot(1));
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Worker mode
|
|
528
|
+
|
|
529
|
+
Measured on Render, 20 calls:
|
|
530
|
+
|
|
531
|
+
| | processes | time |
|
|
532
|
+
| --- | --- | --- |
|
|
533
|
+
| spawn per call | 20 | 112 ms |
|
|
534
|
+
| worker | 1 | **9 ms** |
|
|
535
|
+
|
|
536
|
+
It is opt-in because a worker keeps top-level state between calls. That is what
|
|
537
|
+
makes it fast, and it also means a leak accumulates instead of being cleaned up
|
|
538
|
+
by process exit, and one call can observe what the last one left behind. A call
|
|
539
|
+
that throws does *not* kill the worker; a process that dies rejects everything
|
|
540
|
+
in flight with its exit code and stderr, then respawns on the next call.
|
|
541
|
+
|
|
542
|
+
### The wire, and errors
|
|
543
|
+
|
|
544
|
+
One JSON object per line (JSONL) over stdin/stdout. `print()` on the native side
|
|
545
|
+
arrives as a `$log` line and is forwarded to the task log — on stdout it would
|
|
546
|
+
corrupt the framing, so it is rerouted rather than left to break things. A
|
|
547
|
+
native `throw` arrives as a `NativeTaskException` carrying the real message and
|
|
548
|
+
the native stack trace.
|
|
549
|
+
|
|
550
|
+
### A worked example: Postgres
|
|
551
|
+
|
|
552
|
+
`package:postgres` speaks the wire protocol over a raw socket. pub.dev marks it
|
|
553
|
+
`runtime:native-aot` and `runtime:native-jit`, with **no** `runtime:web` — it
|
|
554
|
+
cannot run under dart2js at all, and there is no wasm build to fall back on.
|
|
555
|
+
Native is the only route to a database from a Dart workflow.
|
|
556
|
+
|
|
557
|
+
```dart
|
|
558
|
+
// native/db_impl.dart
|
|
559
|
+
@NativeTask(worker: true, idleTimeout: Duration(minutes: 2))
|
|
560
|
+
Future<List<Map<String, Object?>>> listWidgets({int limit = 20}) async {
|
|
561
|
+
final db = await _db(); // held open between calls
|
|
562
|
+
final rows = await db.execute(
|
|
563
|
+
Sql.named('select * from widgets limit @limit'),
|
|
564
|
+
parameters: {'limit': limit},
|
|
565
|
+
);
|
|
566
|
+
return rows.map(_jsonRow).toList();
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
```dart
|
|
571
|
+
// tasks.dart
|
|
572
|
+
import 'native/db.dart';
|
|
573
|
+
|
|
574
|
+
task('listWidgets', (args) async => await listWidgets(limit: 20));
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Worker mode earns its keep here: the process stays alive, so the TCP handshake,
|
|
578
|
+
TLS negotiation and Postgres authentication happen once rather than per call.
|
|
579
|
+
`pg_backend_pid()` proves it from the server's side — it stays constant across
|
|
580
|
+
calls while a counter climbs.
|
|
581
|
+
|
|
582
|
+
Two things this example ran into, both worth knowing before you hit them:
|
|
583
|
+
|
|
584
|
+
- **`timestamptz` arrives as a `DateTime`, which is not JSON.** Convert before
|
|
585
|
+
returning, or the build rejects the signature — the right failure, but a
|
|
586
|
+
puzzling one if unexpected.
|
|
587
|
+
- **A held connection can be dropped** by the server, a deploy, or idling. Check
|
|
588
|
+
and reconnect rather than surfacing a broken socket; that is the honest cost
|
|
589
|
+
of keeping state in a worker.
|
|
590
|
+
|
|
591
|
+
A full version, with a local seeding program that creates the table over the
|
|
592
|
+
*external* connection string while the tasks read it over the *internal* one,
|
|
593
|
+
is in [`examples/postgres`](examples/postgres) — which is also what
|
|
594
|
+
`init --template postgres` scaffolds.
|
|
595
|
+
|
|
596
|
+
### `mode: "exe"`
|
|
597
|
+
|
|
598
|
+
For a program that owns its own `main()` and wants no wrapper. It is compiled
|
|
599
|
+
to `build/native/<name>` and left alone; call it with `runProcess`.
|
|
600
|
+
|
|
601
|
+
### Building
|
|
602
|
+
|
|
603
|
+
The vendored SDK carries `gen_snapshot`, so this needs nothing extra — and
|
|
604
|
+
**nothing is cross-compiled and no binary is committed**. Render's build host is
|
|
605
|
+
already `linux/x64`, so the executable is produced from the source in the commit
|
|
606
|
+
that deploys it.
|
|
607
|
+
|
|
608
|
+
Generated files (`tools.dart`, `tools.stub.dart`) are listed in a
|
|
609
|
+
`native/.gitignore` the build maintains, because the facade takes a plain name
|
|
610
|
+
and would otherwise read as hand-written source.
|
|
611
|
+
|
|
612
|
+
Native sources need `dart:io`, so declared native directories — and
|
|
613
|
+
`native_task.dart` — are exempt from the `dart:io` guard. Everything else stays
|
|
614
|
+
strict.
|
|
615
|
+
|
|
616
|
+
## Two things this package exists to get right
|
|
617
|
+
|
|
618
|
+
**`RENDER_SDK_AUTO_START` must be `false` before the SDK loads.** The SDK's
|
|
619
|
+
`task()` schedules its own `startTaskServer()` via `setImmediate`. Combined
|
|
620
|
+
with an explicit start, that produces two task servers and runs **every task
|
|
621
|
+
body twice** — doubled side effects and doubled billing. Neither
|
|
622
|
+
`render workflows dev` nor Render sets this for you.
|
|
623
|
+
|
|
624
|
+
**Dart must never throw across the JS boundary.** A Dart exception converted
|
|
625
|
+
by `Future.toJS` reaches Render as the opaque *"Dart exception thrown from
|
|
626
|
+
converted Future…"*, with the real message boxed out of reach. Task bodies
|
|
627
|
+
return a `{$ok}`/`{$err}` envelope instead, and the runtime rethrows a genuine
|
|
628
|
+
`Error`, so the actual message lands in the run record.
|
|
629
|
+
|
|
630
|
+
Both are handled for you. They are documented because they cost real debugging
|
|
631
|
+
time to find.
|
|
632
|
+
|
|
633
|
+
## Build caching on Render
|
|
634
|
+
|
|
635
|
+
The Dart SDK is unpacked into `node_modules/.dart-sdk`, and the pub cache into
|
|
636
|
+
`node_modules/.pub-cache`. Render preserves `node_modules` between builds but
|
|
637
|
+
not arbitrary top-level directories — measured, with the SDK elsewhere it was
|
|
638
|
+
re-downloaded on every deploy, 33s of a 52s build. Cached, the build step is
|
|
639
|
+
about a second.
|
|
640
|
+
|
|
641
|
+
Native executables are cached the same way, in `node_modules/.native-cache`,
|
|
642
|
+
keyed on the **content** of their sources rather than mtime — every deploy is a
|
|
643
|
+
fresh git checkout that restamps mtimes, so an mtime-keyed cache could never
|
|
644
|
+
hit. A deploy that changes only `tasks.dart` reuses the executable instead of
|
|
645
|
+
paying for another AOT compile.
|
|
646
|
+
|
|
647
|
+
Use *Clear build cache & deploy* in the Dashboard to force a clean fetch.
|
|
648
|
+
|
|
649
|
+
## Layout
|
|
650
|
+
|
|
651
|
+
src/runtime.js Loaded by your workflow; bridges Dart to the SDK
|
|
652
|
+
src/web-shims.js Browser-shaped APIs Node lacks: self, file: fetch,
|
|
653
|
+
Dart package assets, XMLHttpRequest
|
|
654
|
+
src/node-bridge.js Node access Dart lacks: require, subprocesses
|
|
655
|
+
src/native-worker.js Keeping native executables alive between calls
|
|
656
|
+
src/cli.js build / dev / init
|
|
657
|
+
src/toolchain/ SDK resolution and compilation, free of Render
|
|
658
|
+
specifics so it can be extracted later
|
|
659
|
+
dart/generator/ Reads @nativeTask with package:analyzer and writes
|
|
660
|
+
the dispatcher, stubs and facade. Its own pubspec,
|
|
661
|
+
so your project never depends on the analyzer
|
|
662
|
+
examples/ Five runnable services, which are also the `init`
|
|
663
|
+
templates — so a template cannot drift from a
|
|
664
|
+
working example
|
|
665
|
+
runtime/ Copied into a scaffold: the two Dart bridge files
|
|
666
|
+
(render_dart.dart, native_task.dart) and the
|
|
667
|
+
AGENTS.md that lands in your project
|
|
668
|
+
|
|
669
|
+
## Licence
|
|
670
|
+
|
|
671
|
+
MIT
|
|
672
|
+
|
|
673
|
+
## Note
|
|
674
|
+
|
|
675
|
+
\* The Render name and logo are trademarks of Render Services, Inc. The mark
|
|
676
|
+
itself is unmodified, shown on white with the clear space Render's brand kit
|
|
677
|
+
specifies, referentially — to identify the service these packages work with,
|
|
678
|
+
not to suggest any endorsement.
|