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
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/// The Dart side of the render-dart bridge.
|
|
2
|
+
///
|
|
3
|
+
/// This file is boilerplate — you shouldn't need to edit it. It will become a
|
|
4
|
+
/// pub package (`render_workflows_node`); for now it ships with the template
|
|
5
|
+
/// so a project stays self-contained.
|
|
6
|
+
library;
|
|
7
|
+
|
|
8
|
+
import 'dart:async';
|
|
9
|
+
import 'dart:convert';
|
|
10
|
+
import 'dart:js_interop';
|
|
11
|
+
import 'dart:js_interop_unsafe';
|
|
12
|
+
|
|
13
|
+
@JS('__registerTask')
|
|
14
|
+
external void _registerTask(String name, JSFunction fn, JSObject? options);
|
|
15
|
+
|
|
16
|
+
@JS('__callTask')
|
|
17
|
+
external JSPromise<JSAny?> _callTask(String name, JSArray args);
|
|
18
|
+
|
|
19
|
+
@JS('__start')
|
|
20
|
+
external JSPromise<JSAny?> _start();
|
|
21
|
+
|
|
22
|
+
@JS('__nativeCall')
|
|
23
|
+
external JSPromise<JSArray<JSString>> _nativeCall(
|
|
24
|
+
String binary,
|
|
25
|
+
String request,
|
|
26
|
+
JSObject options,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
@JS('__fileUri')
|
|
30
|
+
external String _fileUri(String relativePath);
|
|
31
|
+
|
|
32
|
+
/// Resolves a project-relative path to a `file:` URI.
|
|
33
|
+
///
|
|
34
|
+
/// Useful for packages that load bundled assets through `fetch`. Node's fetch
|
|
35
|
+
/// has no `file:` support of its own; the render-dart runtime adds it, so a
|
|
36
|
+
/// URI from here can be handed straight to such a package:
|
|
37
|
+
///
|
|
38
|
+
/// ```dart
|
|
39
|
+
/// await initializeForge2D(wasmUri: Uri.parse(fileUri('web/box2d.wasm')));
|
|
40
|
+
/// ```
|
|
41
|
+
String fileUri(String relativePath) => _fileUri(relativePath);
|
|
42
|
+
|
|
43
|
+
@JS('__require')
|
|
44
|
+
external JSObject _require(String id);
|
|
45
|
+
|
|
46
|
+
@JS('__run')
|
|
47
|
+
external JSPromise<JSObject> _run(String command, JSArray args, JSObject options);
|
|
48
|
+
|
|
49
|
+
/// Loads an npm package or Node built-in, by name.
|
|
50
|
+
///
|
|
51
|
+
/// This is how a Dart task reaches the npm ecosystem. Bind what you need with
|
|
52
|
+
/// an extension type:
|
|
53
|
+
///
|
|
54
|
+
/// ```dart
|
|
55
|
+
/// @JS()
|
|
56
|
+
/// extension type _Crypto(JSObject _) implements JSObject {
|
|
57
|
+
/// external String randomUUID();
|
|
58
|
+
/// }
|
|
59
|
+
///
|
|
60
|
+
/// final crypto = _Crypto(requireModule('node:crypto'));
|
|
61
|
+
/// print(crypto.randomUUID());
|
|
62
|
+
/// ```
|
|
63
|
+
///
|
|
64
|
+
/// Dart cannot call `require` directly — in CommonJS it is module-scoped, and
|
|
65
|
+
/// `globalThis.require` is undefined — so the runtime hoists it for us.
|
|
66
|
+
JSObject requireModule(String id) => _require(id);
|
|
67
|
+
|
|
68
|
+
/// What a finished process left behind.
|
|
69
|
+
class ProcessResult {
|
|
70
|
+
const ProcessResult({
|
|
71
|
+
required this.exitCode,
|
|
72
|
+
required this.stdout,
|
|
73
|
+
required this.stderr,
|
|
74
|
+
this.signal,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/// The process's exit code, or -1 if it was killed before exiting.
|
|
78
|
+
final int exitCode;
|
|
79
|
+
final String stdout;
|
|
80
|
+
final String stderr;
|
|
81
|
+
|
|
82
|
+
/// The signal that killed the process, if one did.
|
|
83
|
+
final String? signal;
|
|
84
|
+
|
|
85
|
+
bool get ok => exitCode == 0;
|
|
86
|
+
|
|
87
|
+
@override
|
|
88
|
+
String toString() => 'ProcessResult(exitCode: $exitCode'
|
|
89
|
+
'${signal == null ? '' : ', signal: $signal'})';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Runs [command] to completion and captures its output.
|
|
93
|
+
///
|
|
94
|
+
/// `dart:io` compiles under dart2js and then fails at runtime, so `Process` is
|
|
95
|
+
/// unavailable here. This is the way to shell out — to a CLI tool, or to a
|
|
96
|
+
/// natively compiled Dart binary shipped alongside the workflow.
|
|
97
|
+
///
|
|
98
|
+
/// A non-zero exit is returned, not thrown: an exit code is a result, and the
|
|
99
|
+
/// caller usually wants [ProcessResult.stderr] with it. It throws only when the
|
|
100
|
+
/// process could not be started, or when [timeout] elapses.
|
|
101
|
+
///
|
|
102
|
+
/// ```dart
|
|
103
|
+
/// final result = await runProcess('git', args: ['rev-parse', 'HEAD']);
|
|
104
|
+
/// if (result.ok) print(result.stdout.trim());
|
|
105
|
+
/// ```
|
|
106
|
+
Future<ProcessResult> runProcess(
|
|
107
|
+
String command, {
|
|
108
|
+
List<String> args = const [],
|
|
109
|
+
String? workingDirectory,
|
|
110
|
+
Map<String, String>? environment,
|
|
111
|
+
String? stdin,
|
|
112
|
+
Duration? timeout,
|
|
113
|
+
bool runInShell = false,
|
|
114
|
+
}) async {
|
|
115
|
+
final options = JSObject();
|
|
116
|
+
if (workingDirectory != null) options['cwd'] = workingDirectory.toJS;
|
|
117
|
+
if (stdin != null) options['stdin'] = stdin.toJS;
|
|
118
|
+
if (timeout != null) options['timeoutMs'] = timeout.inMilliseconds.toJS;
|
|
119
|
+
if (runInShell) options['shell'] = true.toJS;
|
|
120
|
+
if (environment != null) {
|
|
121
|
+
final env = JSObject();
|
|
122
|
+
environment.forEach((key, value) => env[key] = value.toJS);
|
|
123
|
+
options['env'] = env;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
final raw = await _run(command, args.map((a) => a.toJS).toList().toJS, options)
|
|
127
|
+
.toDart;
|
|
128
|
+
|
|
129
|
+
return ProcessResult(
|
|
130
|
+
exitCode: (raw['code']! as JSNumber).toDartInt,
|
|
131
|
+
stdout: (raw['stdout']! as JSString).toDart,
|
|
132
|
+
stderr: (raw['stderr']! as JSString).toDart,
|
|
133
|
+
signal: raw['signal'].isUndefinedOrNull
|
|
134
|
+
? null
|
|
135
|
+
: (raw['signal']! as JSString).toDart,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Thrown when a native task reports a failure.
|
|
140
|
+
///
|
|
141
|
+
/// Carries the message and stack trace from the native side rather than the
|
|
142
|
+
/// spawn's exit code, so a `throw` inside AOT-compiled Dart reads the same way
|
|
143
|
+
/// it would if the call had been local.
|
|
144
|
+
class NativeTaskException implements Exception {
|
|
145
|
+
NativeTaskException(this.message, {this.nativeStackTrace, this.binary, this.method});
|
|
146
|
+
|
|
147
|
+
final String message;
|
|
148
|
+
final String? nativeStackTrace;
|
|
149
|
+
final String? binary;
|
|
150
|
+
final String? method;
|
|
151
|
+
|
|
152
|
+
@override
|
|
153
|
+
String toString() {
|
|
154
|
+
final where = binary == null ? '' : ' in $binary/$method';
|
|
155
|
+
return 'NativeTaskException$where: $message'
|
|
156
|
+
'${nativeStackTrace == null ? '' : '\n$nativeStackTrace'}';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Calls a `@nativeTask` function in an AOT-compiled executable.
|
|
161
|
+
///
|
|
162
|
+
/// Generated stubs call this; you rarely call it directly. The executable is
|
|
163
|
+
/// produced by `render-dart build` from the file declared in
|
|
164
|
+
/// `renderDart.native`, and lives at `build/native/<binary>` relative to the
|
|
165
|
+
/// project root — which is the working directory both on Render and under
|
|
166
|
+
/// `render workflows dev`.
|
|
167
|
+
///
|
|
168
|
+
/// One JSONL line goes in and the reply lines come back. `print()` on the
|
|
169
|
+
/// native side arrives as a `\$log` line and is forwarded here, so native
|
|
170
|
+
/// output still reaches the task log instead of corrupting the framing.
|
|
171
|
+
/// Per-call overrides for native tasks, scoped to a block.
|
|
172
|
+
///
|
|
173
|
+
/// Settings normally travel with the `@NativeTask` declaration, so nothing at
|
|
174
|
+
/// a call site needs to know a function is native. This is the escape hatch
|
|
175
|
+
/// for the times one caller wants something different — it changes no
|
|
176
|
+
/// signatures, which is what keeps the same source compiling both natively and
|
|
177
|
+
/// under dart2js.
|
|
178
|
+
///
|
|
179
|
+
/// ```dart
|
|
180
|
+
/// await NativeCall.scope(worker: false, () async => probe(30));
|
|
181
|
+
/// ```
|
|
182
|
+
class NativeCall {
|
|
183
|
+
const NativeCall._();
|
|
184
|
+
|
|
185
|
+
static const _key = #renderDartNativeCall;
|
|
186
|
+
|
|
187
|
+
/// Runs [body] with these settings applied to every native call inside it,
|
|
188
|
+
/// including nested ones.
|
|
189
|
+
static Future<T> scope<T>(
|
|
190
|
+
Future<T> Function() body, {
|
|
191
|
+
bool? worker,
|
|
192
|
+
Duration? idleTimeout,
|
|
193
|
+
Duration? timeout,
|
|
194
|
+
}) {
|
|
195
|
+
final outer = Zone.current[_key] as _NativeOverrides?;
|
|
196
|
+
return runZoned(
|
|
197
|
+
body,
|
|
198
|
+
zoneValues: {
|
|
199
|
+
// An inner scope refines the outer one rather than replacing it.
|
|
200
|
+
_key: _NativeOverrides(
|
|
201
|
+
worker: worker ?? outer?.worker,
|
|
202
|
+
idleTimeoutMs: idleTimeout?.inMilliseconds ?? outer?.idleTimeoutMs,
|
|
203
|
+
timeoutMs: timeout?.inMilliseconds ?? outer?.timeoutMs,
|
|
204
|
+
),
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class _NativeOverrides {
|
|
211
|
+
const _NativeOverrides({this.worker, this.idleTimeoutMs, this.timeoutMs});
|
|
212
|
+
|
|
213
|
+
final bool? worker;
|
|
214
|
+
final int? idleTimeoutMs;
|
|
215
|
+
final int? timeoutMs;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Future<Object?> callNativeTask(
|
|
219
|
+
String binary,
|
|
220
|
+
String method,
|
|
221
|
+
List<Object?> args, [
|
|
222
|
+
Map<String, Object?> named = const {},
|
|
223
|
+
bool worker = false,
|
|
224
|
+
int idleTimeoutMs = 30000,
|
|
225
|
+
int timeoutMs = 0,
|
|
226
|
+
]) async {
|
|
227
|
+
// A surrounding NativeCall.scope wins over what the declaration asked for.
|
|
228
|
+
final overrides = Zone.current[NativeCall._key] as _NativeOverrides?;
|
|
229
|
+
final useWorker = overrides?.worker ?? worker;
|
|
230
|
+
final idle = overrides?.idleTimeoutMs ?? idleTimeoutMs;
|
|
231
|
+
final limit = overrides?.timeoutMs ?? timeoutMs;
|
|
232
|
+
final request = jsonEncode({
|
|
233
|
+
'id': 1,
|
|
234
|
+
'method': method,
|
|
235
|
+
'args': args,
|
|
236
|
+
'named': named,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Both paths speak the same JSONL, so the reply handling below is shared.
|
|
240
|
+
// A worker keeps the executable alive and assigns its own ids, since it can
|
|
241
|
+
// have several calls in flight; a one-shot spawn writes a line and reads the
|
|
242
|
+
// answer back.
|
|
243
|
+
final List<String> lines;
|
|
244
|
+
ProcessResult? result;
|
|
245
|
+
|
|
246
|
+
if (useWorker) {
|
|
247
|
+
final options = JSObject()
|
|
248
|
+
..['idleTimeoutMs'] = idle.toJS
|
|
249
|
+
..['timeoutMs'] = limit.toJS;
|
|
250
|
+
final replies =
|
|
251
|
+
await _nativeCall('build/native/$binary', request, options).toDart;
|
|
252
|
+
lines = replies.toDart.map((line) => line.toDart).toList();
|
|
253
|
+
} else {
|
|
254
|
+
result = await runProcess(
|
|
255
|
+
'build/native/$binary',
|
|
256
|
+
stdin: '$request\n',
|
|
257
|
+
timeout: limit > 0 ? Duration(milliseconds: limit) : null,
|
|
258
|
+
);
|
|
259
|
+
lines = const LineSplitter().convert(result.stdout);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
for (final line in lines) {
|
|
263
|
+
if (line.trim().isEmpty) continue;
|
|
264
|
+
|
|
265
|
+
final Map<String, Object?> message;
|
|
266
|
+
try {
|
|
267
|
+
message = (jsonDecode(line) as Map).cast<String, Object?>();
|
|
268
|
+
} catch (_) {
|
|
269
|
+
// Anything that is not JSON came from the program writing to stdout
|
|
270
|
+
// directly, which the generated wrapper avoids. Surface it rather than
|
|
271
|
+
// failing on a parse error nobody can act on.
|
|
272
|
+
print('[native $binary] $line');
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (message.containsKey(r'$log')) {
|
|
277
|
+
print('[native $binary] ${message[r'$log']}');
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (message.containsKey(r'$err')) {
|
|
281
|
+
throw NativeTaskException(
|
|
282
|
+
message[r'$err'] as String,
|
|
283
|
+
nativeStackTrace: message[r'$stack'] as String?,
|
|
284
|
+
binary: binary,
|
|
285
|
+
method: method,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (message.containsKey(r'$ok')) return message[r'$ok'];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// No reply at all: the process died before it could answer. A worker
|
|
292
|
+
// rejects on its own with the child's exit status, so this only covers the
|
|
293
|
+
// one-shot path.
|
|
294
|
+
throw NativeTaskException(
|
|
295
|
+
result == null || result.exitCode == 0
|
|
296
|
+
? 'no response from build/native/$binary'
|
|
297
|
+
: 'build/native/$binary exited ${result.exitCode}'
|
|
298
|
+
'${result.stderr.trim().isEmpty ? '' : ': ${result.stderr.trim()}'}',
|
|
299
|
+
binary: binary,
|
|
300
|
+
method: method,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// How Render should retry a failing task.
|
|
305
|
+
class Retry {
|
|
306
|
+
const Retry({required this.maxRetries, this.waitDurationMs, this.backoffScaling});
|
|
307
|
+
|
|
308
|
+
final int maxRetries;
|
|
309
|
+
final int? waitDurationMs;
|
|
310
|
+
final double? backoffScaling;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// Instance size a task runs on. Larger tiers need access from Render.
|
|
314
|
+
enum TaskPlan { starter, standard, pro, proPlus, proMax, proUltra }
|
|
315
|
+
|
|
316
|
+
const _planWire = {
|
|
317
|
+
TaskPlan.starter: 'starter',
|
|
318
|
+
TaskPlan.standard: 'standard',
|
|
319
|
+
TaskPlan.pro: 'pro',
|
|
320
|
+
TaskPlan.proPlus: 'pro_plus',
|
|
321
|
+
TaskPlan.proMax: 'pro_max',
|
|
322
|
+
TaskPlan.proUltra: 'pro_ultra',
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/// Registers [body] as a Render task named [name].
|
|
326
|
+
///
|
|
327
|
+
/// Call this at top level, before [start]. Arguments arrive as a positional
|
|
328
|
+
/// list matching whatever the caller passed; the return value must be
|
|
329
|
+
/// JSON-serialisable, and Render caps input at 4 MB.
|
|
330
|
+
///
|
|
331
|
+
/// [timeoutSeconds] accepts 30–86,400 and defaults to two hours.
|
|
332
|
+
void task(
|
|
333
|
+
String name,
|
|
334
|
+
Future<Object?> Function(List<Object?> args) body, {
|
|
335
|
+
Retry? retry,
|
|
336
|
+
int? timeoutSeconds,
|
|
337
|
+
TaskPlan? plan,
|
|
338
|
+
}) {
|
|
339
|
+
if (timeoutSeconds != null &&
|
|
340
|
+
(timeoutSeconds < 30 || timeoutSeconds > 86400)) {
|
|
341
|
+
throw ArgumentError.value(
|
|
342
|
+
timeoutSeconds,
|
|
343
|
+
'timeoutSeconds',
|
|
344
|
+
'Render allows 30 to 86,400 seconds.',
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
JSPromise<JSAny?> wrapper(JSArray args) => _guard(() async {
|
|
349
|
+
final dartArgs = args.toDart.map(_toDart).toList();
|
|
350
|
+
return _toJs(await body(dartArgs));
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
_registerTask(name, wrapper.toJS, _options(retry, timeoutSeconds, plan));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/// Runs [name] as a child task and waits for its result.
|
|
357
|
+
///
|
|
358
|
+
/// Each call becomes a separate task run on its own Render instance, so this
|
|
359
|
+
/// is how you fan work out. Safe to call after an `await`.
|
|
360
|
+
Future<Object?> callTask(String name, List<Object?> args) async {
|
|
361
|
+
final result = await _callTask(name, args.map(_toJs).toList().toJS).toDart;
|
|
362
|
+
return _toDart(result);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// Starts the task server. Call once, at the end of `main`.
|
|
366
|
+
void start() => _start();
|
|
367
|
+
|
|
368
|
+
JSObject? _options(Retry? retry, int? timeoutSeconds, TaskPlan? plan) {
|
|
369
|
+
if (retry == null && timeoutSeconds == null && plan == null) return null;
|
|
370
|
+
|
|
371
|
+
final options = JSObject();
|
|
372
|
+
if (retry != null) {
|
|
373
|
+
final r = JSObject();
|
|
374
|
+
r['maxRetries'] = retry.maxRetries.toJS;
|
|
375
|
+
if (retry.waitDurationMs != null) {
|
|
376
|
+
r['waitDurationMs'] = retry.waitDurationMs!.toJS;
|
|
377
|
+
}
|
|
378
|
+
if (retry.backoffScaling != null) {
|
|
379
|
+
r['backoffScaling'] = retry.backoffScaling!.toJS;
|
|
380
|
+
}
|
|
381
|
+
options['retry'] = r;
|
|
382
|
+
}
|
|
383
|
+
if (timeoutSeconds != null) options['timeoutSeconds'] = timeoutSeconds.toJS;
|
|
384
|
+
if (plan != null) options['plan'] = _planWire[plan]!.toJS;
|
|
385
|
+
return options;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Dart must never throw across the JS boundary.
|
|
389
|
+
///
|
|
390
|
+
/// A Dart exception converted by `Future.toJS` reaches Render as the opaque
|
|
391
|
+
/// "Dart exception thrown from converted Future...", with the real message
|
|
392
|
+
/// boxed where the SDK's `error.message` cannot see it. Returning an envelope
|
|
393
|
+
/// lets the JS side rethrow a genuine Error carrying the real text.
|
|
394
|
+
JSPromise<JSAny?> _guard(Future<JSAny?> Function() body) {
|
|
395
|
+
Future<JSAny?> wrapped() async {
|
|
396
|
+
final out = JSObject();
|
|
397
|
+
try {
|
|
398
|
+
out[r'$ok'] = await body();
|
|
399
|
+
} catch (e, stackTrace) {
|
|
400
|
+
out[r'$err'] = '$e\n$stackTrace'.toJS;
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return wrapped().toJS;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
JSAny? _toJs(Object? value) => switch (value) {
|
|
409
|
+
null => null,
|
|
410
|
+
final String v => v.toJS,
|
|
411
|
+
final bool v => v.toJS,
|
|
412
|
+
final int v => v.toJS,
|
|
413
|
+
final double v => v.toJS,
|
|
414
|
+
final List<Object?> v => v.map(_toJs).toList().toJS,
|
|
415
|
+
final Map<String, Object?> v => (() {
|
|
416
|
+
final o = JSObject();
|
|
417
|
+
v.forEach((key, val) => o[key] = _toJs(val));
|
|
418
|
+
return o;
|
|
419
|
+
})(),
|
|
420
|
+
_ => throw ArgumentError.value(
|
|
421
|
+
value,
|
|
422
|
+
'value',
|
|
423
|
+
'Task arguments and results must be JSON-serialisable. '
|
|
424
|
+
'${value.runtimeType} is not.',
|
|
425
|
+
),
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
Object? _toDart(JSAny? value) => value?.dartify();
|