@reboot-dev/reboot 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +33 -8
- package/index.js +50 -29
- package/package.json +4 -2
- package/rbt.js +17 -6
- package/reboot_native.cc +95 -40
- package/reboot_native.cjs +2 -1
- package/reboot_native.d.ts +4 -3
- package/secrets/index.d.ts +37 -0
- package/secrets/index.js +96 -0
- package/venv.js +5 -0
- package/version.d.ts +1 -1
- package/version.js +1 -1
package/index.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export declare class Context {
|
|
|
50
50
|
get auth(): Auth | null;
|
|
51
51
|
get stateId(): any;
|
|
52
52
|
get iteration(): any;
|
|
53
|
+
get cookie(): any;
|
|
53
54
|
generateIdempotentStateId(stateType: string, serviceName: string, method: string, idempotency: IdempotencyOptions): Promise<any>;
|
|
54
55
|
}
|
|
55
56
|
export declare class ReaderContext extends Context {
|
|
@@ -117,8 +118,8 @@ export declare abstract class TokenVerifier {
|
|
|
117
118
|
* Returns:
|
|
118
119
|
* `Auth` information if the token is valid, null otherwise.
|
|
119
120
|
*/
|
|
120
|
-
abstract verifyToken(context: ReaderContext, token
|
|
121
|
-
_verifyToken(context: ReaderContext, token
|
|
121
|
+
abstract verifyToken(context: ReaderContext, token?: string): Promise<Auth | null>;
|
|
122
|
+
_verifyToken(context: ReaderContext, token?: string): Promise<Uint8Array | null>;
|
|
122
123
|
}
|
|
123
124
|
export type AuthorizerDecision = errors_pb.Unauthenticated | errors_pb.PermissionDenied | errors_pb.Ok;
|
|
124
125
|
/**
|
|
@@ -171,20 +172,44 @@ export declare class Loop {
|
|
|
171
172
|
export declare function retry_reactively_until(context: WorkflowContext, condition: () => Promise<boolean>): Promise<void>;
|
|
172
173
|
export declare function retry_reactively_until<T>(context: WorkflowContext, condition: () => Promise<false | Exclude<T, boolean>>): Promise<Exclude<T, boolean>>;
|
|
173
174
|
export declare function atMostOnce(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
|
|
174
|
-
|
|
175
|
+
stringify?: undefined;
|
|
176
|
+
parse?: undefined;
|
|
177
|
+
validate?: undefined;
|
|
175
178
|
}): Promise<void>;
|
|
176
179
|
export declare function atMostOnce<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
|
|
177
|
-
|
|
180
|
+
stringify?: (result: T) => string;
|
|
181
|
+
parse: (value: string) => T;
|
|
182
|
+
validate?: undefined;
|
|
183
|
+
} | {
|
|
184
|
+
stringify?: (result: T) => string;
|
|
185
|
+
parse?: undefined;
|
|
186
|
+
validate: (result: T) => boolean;
|
|
178
187
|
}): Promise<T>;
|
|
179
188
|
export declare function atLeastOnce(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
|
|
180
|
-
|
|
189
|
+
stringify?: undefined;
|
|
190
|
+
parse?: undefined;
|
|
191
|
+
validate?: undefined;
|
|
181
192
|
}): Promise<void>;
|
|
182
193
|
export declare function atLeastOnce<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
|
|
183
|
-
|
|
194
|
+
stringify?: (result: T) => string;
|
|
195
|
+
parse: (value: string) => T;
|
|
196
|
+
validate?: undefined;
|
|
197
|
+
} | {
|
|
198
|
+
stringify?: (result: T) => string;
|
|
199
|
+
parse?: undefined;
|
|
200
|
+
validate: (result: T) => boolean;
|
|
184
201
|
}): Promise<T>;
|
|
185
202
|
export declare function until(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<boolean>, options?: {
|
|
186
|
-
|
|
203
|
+
stringify?: undefined;
|
|
204
|
+
parse?: undefined;
|
|
205
|
+
validate?: undefined;
|
|
187
206
|
}): Promise<void>;
|
|
188
207
|
export declare function until<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<false | Exclude<T, boolean>>, options: {
|
|
189
|
-
|
|
208
|
+
stringify?: (result: T) => string;
|
|
209
|
+
parse: (value: string) => T;
|
|
210
|
+
validate?: undefined;
|
|
211
|
+
} | {
|
|
212
|
+
stringify?: (result: T) => string;
|
|
213
|
+
parse?: undefined;
|
|
214
|
+
validate: (result: T) => boolean;
|
|
190
215
|
}): Promise<Exclude<T, boolean>>;
|
package/index.js
CHANGED
|
@@ -49,6 +49,12 @@ if (process != null) {
|
|
|
49
49
|
// exits due to a SIGINT will exit with a code of 130.
|
|
50
50
|
checkIfNoOtherListenersAndIfSoExit("SIGINT", 130);
|
|
51
51
|
});
|
|
52
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
53
|
+
// We install a slightly quieter unhandled-rejection handler because the
|
|
54
|
+
// native portion of Reboot renders useful error messages before raising.
|
|
55
|
+
console.error("Exiting:", reason);
|
|
56
|
+
checkIfNoOtherListenersAndIfSoExit("unhandledRejection", 1);
|
|
57
|
+
});
|
|
52
58
|
}
|
|
53
59
|
export class Reboot {
|
|
54
60
|
constructor() {
|
|
@@ -169,6 +175,9 @@ export class Context {
|
|
|
169
175
|
get iteration() {
|
|
170
176
|
return reboot_native.Context_iteration(__classPrivateFieldGet(this, _Context_external, "f"));
|
|
171
177
|
}
|
|
178
|
+
get cookie() {
|
|
179
|
+
return reboot_native.Context_cookie(__classPrivateFieldGet(this, _Context_external, "f"));
|
|
180
|
+
}
|
|
172
181
|
async generateIdempotentStateId(stateType, serviceName, method, idempotency) {
|
|
173
182
|
return reboot_native.Context_generateIdempotentStateId(__classPrivateFieldGet(this, _Context_external, "f"), stateType, serviceName, method, idempotency);
|
|
174
183
|
}
|
|
@@ -395,43 +404,55 @@ export async function retry_reactively_until(context, condition) {
|
|
|
395
404
|
});
|
|
396
405
|
return t;
|
|
397
406
|
}
|
|
398
|
-
async function atLeastOrMostOnce(idempotencyAlias, context, callable,
|
|
399
|
-
|
|
407
|
+
async function atLeastOrMostOnce(idempotencyAlias, context, callable, { stringify = JSON.stringify, parse = JSON.parse, validate, atMostOnce, }) {
|
|
408
|
+
assert(stringify !== undefined);
|
|
409
|
+
assert(parse !== undefined);
|
|
410
|
+
assert(atMostOnce !== undefined);
|
|
400
411
|
const result = await reboot_native.atLeastOrMostOnce(context.__external, idempotencyAlias, async () => {
|
|
401
|
-
t = await callable();
|
|
412
|
+
const t = await callable();
|
|
402
413
|
if (t !== undefined) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
414
|
+
// NOTE: to differentiate `callable` returning `void` (or
|
|
415
|
+
// explicitly `undefined`) from `stringify` returning an empty
|
|
416
|
+
// string we use `{ value: stringify(t) }`.
|
|
417
|
+
const result = { value: stringify(t) };
|
|
418
|
+
return JSON.stringify(result);
|
|
419
|
+
}
|
|
420
|
+
// Fail early if the developer thinks that they have some value
|
|
421
|
+
// that they want to validate but we got `undefined`.
|
|
422
|
+
if (validate !== undefined) {
|
|
423
|
+
throw new Error("Not expecting `validate` as you are returning `void` (or explicitly `undefined`); did you mean to return a value (or if you want to explicitly return the absence of a value use `null`)");
|
|
411
424
|
}
|
|
412
425
|
// NOTE: using the empty string to represent a `callable`
|
|
413
|
-
// returning void or explicitly `undefined
|
|
426
|
+
// returning `void` (or explicitly `undefined`).
|
|
414
427
|
return "";
|
|
415
|
-
},
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
428
|
+
}, atMostOnce);
|
|
429
|
+
// NOTE: we parse and validate `value` every time, even the first
|
|
430
|
+
// time, so as to catch bugs where the `value` returned from
|
|
431
|
+
// `callable` might not parse or be valid. We will have already
|
|
432
|
+
// persisted `result`, so in the event of a bug the developer will
|
|
433
|
+
// have to change the idempotency alias so that `callable` is
|
|
434
|
+
// re-executed. These semantics are the same as Python (although
|
|
435
|
+
// Python uses the `type` keyword argument instead of the
|
|
436
|
+
// `parse` and `validate` properties we use here).
|
|
419
437
|
assert(result !== undefined);
|
|
420
438
|
if (result !== "") {
|
|
421
|
-
|
|
422
|
-
|
|
439
|
+
const { value } = JSON.parse(result);
|
|
440
|
+
const t = parse(value);
|
|
441
|
+
if (parse !== JSON.parse) {
|
|
442
|
+
if (validate === undefined) {
|
|
443
|
+
// TODO: link to docs about why this is required, when those docs exist.
|
|
444
|
+
throw new Error("Missing `validate` property");
|
|
445
|
+
}
|
|
446
|
+
else if (!validate(t)) {
|
|
447
|
+
throw new Error("Failed to validate memoized result");
|
|
448
|
+
}
|
|
423
449
|
}
|
|
424
|
-
return
|
|
425
|
-
}
|
|
426
|
-
assert(result === "");
|
|
427
|
-
// Let end user decide what they want to do with `undefined` if
|
|
428
|
-
// they specify `options.parse`.
|
|
429
|
-
if (options.parse !== undefined) {
|
|
430
|
-
return options.parse(undefined);
|
|
450
|
+
return t;
|
|
431
451
|
}
|
|
432
|
-
// Otherwise `callable` must
|
|
452
|
+
// Otherwise `callable` must have returned void (or explicitly
|
|
453
|
+
// `undefined`), fall through.
|
|
433
454
|
}
|
|
434
|
-
export async function atMostOnce(idempotencyAlias, context, callable, options = {
|
|
455
|
+
export async function atMostOnce(idempotencyAlias, context, callable, options = { validate: undefined }) {
|
|
435
456
|
try {
|
|
436
457
|
return await atLeastOrMostOnce(idempotencyAlias, context, callable, {
|
|
437
458
|
...options,
|
|
@@ -445,13 +466,13 @@ export async function atMostOnce(idempotencyAlias, context, callable, options =
|
|
|
445
466
|
throw e;
|
|
446
467
|
}
|
|
447
468
|
}
|
|
448
|
-
export async function atLeastOnce(idempotencyAlias, context, callable, options = {
|
|
469
|
+
export async function atLeastOnce(idempotencyAlias, context, callable, options = { validate: undefined }) {
|
|
449
470
|
return atLeastOrMostOnce(idempotencyAlias, context, callable, {
|
|
450
471
|
...options,
|
|
451
472
|
atMostOnce: false,
|
|
452
473
|
});
|
|
453
474
|
}
|
|
454
|
-
export async function until(idempotencyAlias, context, callable, options = {
|
|
475
|
+
export async function until(idempotencyAlias, context, callable, options = { validate: undefined }) {
|
|
455
476
|
// TODO(benh): figure out how to not use `as` type assertions here
|
|
456
477
|
// to appease the TypeScript compiler which otherwise isn't happy
|
|
457
478
|
// with passing on these types.
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"@bufbuild/protobuf": "1.3.2",
|
|
4
4
|
"@bufbuild/protoplugin": "1.3.2",
|
|
5
5
|
"@bufbuild/protoc-gen-es": "1.3.2",
|
|
6
|
-
"@reboot-dev/reboot-api": "0.
|
|
6
|
+
"@reboot-dev/reboot-api": "0.24.0",
|
|
7
7
|
"chalk": "^4.1.2",
|
|
8
8
|
"node-addon-api": "^7.0.0",
|
|
9
9
|
"node-gyp": ">=10.2.0",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"name": "@reboot-dev/reboot",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.24.0",
|
|
18
18
|
"description": "npm package for Reboot",
|
|
19
19
|
"scripts": {
|
|
20
20
|
"postinstall": "rbt || exit 0",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"reboot_native.cc",
|
|
55
55
|
"reboot_native.cjs",
|
|
56
56
|
"reboot_native.d.ts",
|
|
57
|
+
"secrets",
|
|
57
58
|
"utils",
|
|
58
59
|
"venv.d.ts",
|
|
59
60
|
"venv.js",
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"exports": {
|
|
65
66
|
"./package.json": "./package.json",
|
|
66
67
|
".": "./index.js",
|
|
68
|
+
"./secrets": "./secrets/index.js",
|
|
67
69
|
"./utils": "./utils/index.js"
|
|
68
70
|
}
|
|
69
71
|
}
|
package/rbt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import whichPMRuns from "which-pm-runs";
|
|
5
5
|
import { ensureYarnNodeLinker } from "./utils/index.js";
|
|
@@ -43,14 +43,25 @@ async function main() {
|
|
|
43
43
|
process.env.RBT_FROM_NODEJS = "true";
|
|
44
44
|
// Add extensionless loader.
|
|
45
45
|
addExtensionlessToNodeOptions();
|
|
46
|
-
|
|
46
|
+
// Using 'spawn' instead of 'spawnSync' to avoid blocking the event loop for
|
|
47
|
+
// signal handling.
|
|
48
|
+
const rbt = spawn(`${path.join(VENV_EXEC_PATH, "rbt")} ${process.argv.slice(2).join(" ")}`, {
|
|
47
49
|
stdio: [process.stdin, process.stdout, process.stderr],
|
|
48
50
|
shell: true,
|
|
49
51
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
process.on("SIGINT", () => {
|
|
53
|
+
// Make sure to kill the child process before exiting.
|
|
54
|
+
rbt.once("exit", (code) => {
|
|
55
|
+
process.exit(code);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
rbt.on("exit", (code) => {
|
|
59
|
+
// If the child process exits with a non-zero code, exit with the same code.
|
|
60
|
+
process.exit(code ?? 1);
|
|
61
|
+
});
|
|
62
|
+
rbt.on("error", (error) => {
|
|
63
|
+
throw new Error(`Unable to execute 'rbt', please report this bug to the maintainers!\n${error}`);
|
|
64
|
+
});
|
|
54
65
|
}
|
|
55
66
|
main().catch((error) => {
|
|
56
67
|
console.error(error instanceof Error ? error.message : error);
|
package/reboot_native.cc
CHANGED
|
@@ -32,6 +32,40 @@ struct PythonNodeAdaptor {
|
|
|
32
32
|
// static instance of `PythonNodeAdaptor`.
|
|
33
33
|
void Initialize(Napi::Env& env, const Napi::Function& js_callback);
|
|
34
34
|
|
|
35
|
+
static void HandleException(const std::exception& e) {
|
|
36
|
+
// First determine if the exception was thrown from Python or C++.
|
|
37
|
+
if (const py::error_already_set* e_py =
|
|
38
|
+
dynamic_cast<const py::error_already_set*>(&e)) {
|
|
39
|
+
// This is a Python exception. Is it an `InputError`?
|
|
40
|
+
if (e_py->matches(py::module::import(
|
|
41
|
+
"reboot.controller.exceptions")
|
|
42
|
+
.attr("InputError"))) {
|
|
43
|
+
// This is an InputError, which means it reports a mistake in the input
|
|
44
|
+
// provided by the developer. We want to print _only_ the user-friendly
|
|
45
|
+
// error message in the exception, without intimidating stack traces.
|
|
46
|
+
//
|
|
47
|
+
// Calling `e_py->what()` would produce a stack trace, so we get only
|
|
48
|
+
// the user-friendly message by stringifying in Python-land instead.
|
|
49
|
+
std::string what = py::str(e_py->value());
|
|
50
|
+
std::cerr << what << std::endl;
|
|
51
|
+
} else {
|
|
52
|
+
// This is an internal error from the Python library. Request that the
|
|
53
|
+
// developer reports the issue, and give them the full stack trace to
|
|
54
|
+
// help diagnose the problem.
|
|
55
|
+
std::cerr << "Unexpected library exception: " << e_py->what()
|
|
56
|
+
<< std::endl
|
|
57
|
+
<< "Please report this bug to the maintainers!"
|
|
58
|
+
<< std::endl;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// This is a C++ exception; something went wrong in the C++ code. Request
|
|
62
|
+
// that the developer reports the issue.
|
|
63
|
+
std::cerr
|
|
64
|
+
<< "Unexpected adapter exception: " << e.what() << std::endl
|
|
65
|
+
<< "Please report this bug to the maintainers!" << std::endl;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
35
69
|
template <typename F>
|
|
36
70
|
void ScheduleCallbackOnPythonEventLoop(F&& f) {
|
|
37
71
|
auto function = [f = std::forward<F>(f)]() mutable {
|
|
@@ -54,11 +88,7 @@ struct PythonNodeAdaptor {
|
|
|
54
88
|
try {
|
|
55
89
|
f(env);
|
|
56
90
|
} catch (const std::exception& e) {
|
|
57
|
-
|
|
58
|
-
<< "Unexpected exception: " << e.what()
|
|
59
|
-
<< "\n"
|
|
60
|
-
<< "Please report this bug to the maintainers!"
|
|
61
|
-
<< std::endl;
|
|
91
|
+
PythonNodeAdaptor::HandleException(e);
|
|
62
92
|
}
|
|
63
93
|
});
|
|
64
94
|
|
|
@@ -208,12 +238,8 @@ void PythonNodeAdaptor::Initialize(
|
|
|
208
238
|
py::cpp_function([function = std::move(function)]() {
|
|
209
239
|
try {
|
|
210
240
|
function();
|
|
211
|
-
} catch (std::exception& e) {
|
|
212
|
-
|
|
213
|
-
<< "Unexpected exception: " << e.what()
|
|
214
|
-
<< "\n"
|
|
215
|
-
<< "Please report this bug to the maintainers!"
|
|
216
|
-
<< std::endl;
|
|
241
|
+
} catch (const std::exception& e) {
|
|
242
|
+
PythonNodeAdaptor::HandleException(e);
|
|
217
243
|
}
|
|
218
244
|
}));
|
|
219
245
|
}
|
|
@@ -508,13 +534,13 @@ class NapiSafeReference {
|
|
|
508
534
|
public:
|
|
509
535
|
NapiSafeReference(T t)
|
|
510
536
|
: _reference(
|
|
511
|
-
|
|
512
|
-
|
|
537
|
+
new Napi::Reference<T>(Napi::Persistent(std::move(t))),
|
|
538
|
+
NapiReferenceDeleter()) {}
|
|
513
539
|
|
|
514
540
|
NapiSafeReference(Napi::Reference<T>&& reference)
|
|
515
541
|
: _reference(
|
|
516
|
-
|
|
517
|
-
|
|
542
|
+
new Napi::Reference<T>(std::move(reference)),
|
|
543
|
+
NapiReferenceDeleter()) {}
|
|
518
544
|
|
|
519
545
|
// Helper for getting the value of the reference. We require a
|
|
520
546
|
// `Napi::Env` to ensure we only try and get the value from within a
|
|
@@ -1130,7 +1156,7 @@ py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
|
|
|
1130
1156
|
[](py::object self,
|
|
1131
1157
|
py::object py_reader_context,
|
|
1132
1158
|
py::object py_aborted,
|
|
1133
|
-
|
|
1159
|
+
py::object token) {
|
|
1134
1160
|
py::object py_future =
|
|
1135
1161
|
py::module::import("asyncio").attr("Future")();
|
|
1136
1162
|
|
|
@@ -1142,7 +1168,7 @@ py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
|
|
|
1142
1168
|
[js_token_verifier_reference,
|
|
1143
1169
|
py_reader_context = new py::object(py_reader_context),
|
|
1144
1170
|
py_aborted = new py::object(py_aborted),
|
|
1145
|
-
token,
|
|
1171
|
+
token = new py::object(token),
|
|
1146
1172
|
py_future = new py::object(py_future)](Napi::Env env) {
|
|
1147
1173
|
std::vector<Napi::Value> js_args;
|
|
1148
1174
|
|
|
@@ -1153,7 +1179,12 @@ py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
|
|
|
1153
1179
|
py_aborted,
|
|
1154
1180
|
"reader"));
|
|
1155
1181
|
|
|
1156
|
-
|
|
1182
|
+
if (token->is_none()) {
|
|
1183
|
+
js_args.push_back(env.Undefined());
|
|
1184
|
+
} else {
|
|
1185
|
+
js_args.push_back(
|
|
1186
|
+
Napi::String::New(env, std::string(py::str(*token))));
|
|
1187
|
+
}
|
|
1157
1188
|
|
|
1158
1189
|
Napi::Object js_token_verifier =
|
|
1159
1190
|
js_token_verifier_reference->Value(env);
|
|
@@ -1522,7 +1553,9 @@ Napi::Value Service_constructor(const Napi::CallbackInfo& info) {
|
|
|
1522
1553
|
.attr("_Schedule");
|
|
1523
1554
|
promise.set_value(
|
|
1524
1555
|
new py::object(py_module.attr(node_adaptor.c_str())(
|
|
1525
|
-
|
|
1556
|
+
// The call will stay within the same application.
|
|
1557
|
+
"application_id"_a = py::none(),
|
|
1558
|
+
"state_id"_a = id,
|
|
1526
1559
|
"schedule_type"_a = py_schedule_type)));
|
|
1527
1560
|
});
|
|
1528
1561
|
|
|
@@ -1666,19 +1699,9 @@ Napi::Value Service_call(const Napi::CallbackInfo& info) {
|
|
|
1666
1699
|
}
|
|
1667
1700
|
|
|
1668
1701
|
|
|
1669
|
-
Napi::Value
|
|
1702
|
+
Napi::Value Task_await(const Napi::CallbackInfo& info) {
|
|
1670
1703
|
Napi::Object js_args = info[0].As<Napi::Object>();
|
|
1671
1704
|
|
|
1672
|
-
// NOTE: we immediately get a safe reference to the `Napi::External`
|
|
1673
|
-
// so that Node will not garbage collect it and the `py::object*` we
|
|
1674
|
-
// get out of it will remain valid.
|
|
1675
|
-
auto js_external_service = NapiSafeReference(
|
|
1676
|
-
js_args.Get("external").As<Napi::External<py::object>>());
|
|
1677
|
-
|
|
1678
|
-
// CHECK(js_external_service.CheckTypeTag(...));
|
|
1679
|
-
|
|
1680
|
-
py::object* py_service = js_external_service.Value(info.Env()).Data();
|
|
1681
|
-
|
|
1682
1705
|
// NOTE: we immediately get a safe reference to the `Napi::External`
|
|
1683
1706
|
// so that Node will not garbage collect it and the `py::object*` we
|
|
1684
1707
|
// get out of it will remain valid.
|
|
@@ -1690,6 +1713,10 @@ Napi::Value Future_await(const Napi::CallbackInfo& info) {
|
|
|
1690
1713
|
py::object* py_context =
|
|
1691
1714
|
js_external_context.Value(info.Env()).Data();
|
|
1692
1715
|
|
|
1716
|
+
std::string rbt_module = js_args.Get("rbtModule").As<Napi::String>();
|
|
1717
|
+
|
|
1718
|
+
std::string state_name = js_args.Get("stateName").As<Napi::String>();
|
|
1719
|
+
|
|
1693
1720
|
std::string method = js_args.Get("method").As<Napi::String>();
|
|
1694
1721
|
|
|
1695
1722
|
std::string json_task_id = js_args.Get("jsonTaskId").As<Napi::String>();
|
|
@@ -1699,9 +1726,9 @@ Napi::Value Future_await(const Napi::CallbackInfo& info) {
|
|
|
1699
1726
|
auto promise = deferred->Promise();
|
|
1700
1727
|
|
|
1701
1728
|
adaptor->ScheduleCallbackOnPythonEventLoop(
|
|
1702
|
-
[
|
|
1703
|
-
|
|
1704
|
-
method,
|
|
1729
|
+
[rbt_module = std::move(rbt_module),
|
|
1730
|
+
state_name = std::move(state_name),
|
|
1731
|
+
method = std::move(method),
|
|
1705
1732
|
// Ensures `py_context` remains valid.
|
|
1706
1733
|
js_external_context,
|
|
1707
1734
|
py_context,
|
|
@@ -1710,12 +1737,16 @@ Napi::Value Future_await(const Napi::CallbackInfo& info) {
|
|
|
1710
1737
|
py::object py_task =
|
|
1711
1738
|
py::module::import("reboot.nodejs.python")
|
|
1712
1739
|
.attr("create_task_with_context")(
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1740
|
+
py::module::import("reboot.nodejs.python")
|
|
1741
|
+
.attr("task_await")(
|
|
1742
|
+
py_context,
|
|
1743
|
+
py::module::import(rbt_module.c_str())
|
|
1744
|
+
.attr(state_name.c_str()),
|
|
1745
|
+
method,
|
|
1746
|
+
json_task_id),
|
|
1717
1747
|
py_context,
|
|
1718
|
-
"name"_a = ("
|
|
1748
|
+
"name"_a = ("reboot.nodejs.python.task_await(\""
|
|
1749
|
+
+ state_name + "\", \""
|
|
1719
1750
|
+ method + "\", ...) in nodejs")
|
|
1720
1751
|
.c_str());
|
|
1721
1752
|
py_task.attr("add_done_callback")(py::cpp_function(
|
|
@@ -2172,6 +2203,26 @@ Napi::Value Context_iteration(const Napi::CallbackInfo& info) {
|
|
|
2172
2203
|
}
|
|
2173
2204
|
|
|
2174
2205
|
|
|
2206
|
+
Napi::Value Context_cookie(const Napi::CallbackInfo& info) {
|
|
2207
|
+
Napi::External<py::object> js_external_context =
|
|
2208
|
+
info[0].As<Napi::External<py::object>>();
|
|
2209
|
+
|
|
2210
|
+
// CHECK(...CheckTypeTag(...));
|
|
2211
|
+
|
|
2212
|
+
py::object* py_context = js_external_context.Data();
|
|
2213
|
+
|
|
2214
|
+
std::promise<std::string> promise;
|
|
2215
|
+
|
|
2216
|
+
adaptor->ScheduleCallbackOnPythonEventLoop(
|
|
2217
|
+
[py_context, &promise]() {
|
|
2218
|
+
py::str cookie = py_context->attr("cookie");
|
|
2219
|
+
promise.set_value(std::string(cookie));
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
return Napi::String::New(info.Env(), promise.get_future().get());
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
|
|
2175
2226
|
Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
|
|
2176
2227
|
// NOTE: we immediately get a safe reference to the `Napi::External`
|
|
2177
2228
|
// so that Node will not garbage collect it and the `py::object*` we
|
|
@@ -2846,6 +2897,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
2846
2897
|
Napi::String::New(env, "Context_iteration"),
|
|
2847
2898
|
Napi::Function::New<Context_iteration>(env));
|
|
2848
2899
|
|
|
2900
|
+
exports.Set(
|
|
2901
|
+
Napi::String::New(env, "Context_cookie"),
|
|
2902
|
+
Napi::Function::New<Context_cookie>(env));
|
|
2903
|
+
|
|
2849
2904
|
exports.Set(
|
|
2850
2905
|
Napi::String::New(env, "Context_generateIdempotentStateId"),
|
|
2851
2906
|
Napi::Function::New<Context_generateIdempotentStateId>(env));
|
|
@@ -2863,8 +2918,8 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
2863
2918
|
Napi::Function::New<atLeastOrMostOnce>(env));
|
|
2864
2919
|
|
|
2865
2920
|
exports.Set(
|
|
2866
|
-
Napi::String::New(env, "
|
|
2867
|
-
Napi::Function::New<
|
|
2921
|
+
Napi::String::New(env, "Task_await"),
|
|
2922
|
+
Napi::Function::New<Task_await>(env));
|
|
2868
2923
|
|
|
2869
2924
|
exports.Set(
|
|
2870
2925
|
Napi::String::New(env, "ExternalContext_constructor"),
|
package/reboot_native.cjs
CHANGED
|
@@ -52,7 +52,7 @@ process.dlopen(
|
|
|
52
52
|
exports.python3Path = reboot_native.exports.python3Path;
|
|
53
53
|
exports.Service_constructor = reboot_native.exports.Service_constructor;
|
|
54
54
|
exports.Service_call = reboot_native.exports.Service_call;
|
|
55
|
-
exports.
|
|
55
|
+
exports.Task_await = reboot_native.exports.Task_await;
|
|
56
56
|
exports.ExternalContext_constructor =
|
|
57
57
|
reboot_native.exports.ExternalContext_constructor;
|
|
58
58
|
exports.Application_constructor = reboot_native.exports.Application_constructor;
|
|
@@ -67,6 +67,7 @@ exports.Reboot_down = reboot_native.exports.Reboot_down;
|
|
|
67
67
|
exports.Reboot_url = reboot_native.exports.Reboot_url;
|
|
68
68
|
exports.Context_auth = reboot_native.exports.Context_auth;
|
|
69
69
|
exports.Context_stateId = reboot_native.exports.Context_stateId;
|
|
70
|
+
exports.Context_cookie = reboot_native.exports.Context_cookie;
|
|
70
71
|
exports.Context_iteration = reboot_native.exports.Context_iteration;
|
|
71
72
|
exports.Context_generateIdempotentStateId =
|
|
72
73
|
reboot_native.exports.Context_generateIdempotentStateId;
|
package/reboot_native.d.ts
CHANGED
|
@@ -31,9 +31,10 @@ interface Service_callProps {
|
|
|
31
31
|
jsonRequest: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
interface
|
|
35
|
-
external: NapiExternal;
|
|
34
|
+
interface Task_awaitProps {
|
|
36
35
|
context: Context | ExternalContext;
|
|
36
|
+
rbtModule: string;
|
|
37
|
+
stateName: string;
|
|
37
38
|
method: string;
|
|
38
39
|
jsonTaskId: string;
|
|
39
40
|
}
|
|
@@ -41,7 +42,7 @@ interface Future_awaitProps {
|
|
|
41
42
|
export namespace rbt_native {
|
|
42
43
|
function Service_constructor(props: Service_constructorProps): NapiExternal;
|
|
43
44
|
function Service_call(props: Service_callProps): string;
|
|
44
|
-
function
|
|
45
|
+
function Task_await(props: Task_awaitProps): string;
|
|
45
46
|
function ExternalContext_constructor(
|
|
46
47
|
name: string,
|
|
47
48
|
url: string,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const ENVVAR_RBT_SECRETS_DIRECTORY = "RBT_SECRETS_DIRECTORY";
|
|
2
|
+
declare abstract class SecretSource {
|
|
3
|
+
abstract get(secretName: string): Promise<Buffer>;
|
|
4
|
+
}
|
|
5
|
+
export declare class Secrets {
|
|
6
|
+
private static _staticSecretSource;
|
|
7
|
+
private _secretCache;
|
|
8
|
+
private _secretSource;
|
|
9
|
+
constructor();
|
|
10
|
+
static setSecretSource(secretSource: SecretSource | null): void;
|
|
11
|
+
get secretSource(): SecretSource;
|
|
12
|
+
get(secretName: string, { ttlSecs }?: {
|
|
13
|
+
ttlSecs?: number;
|
|
14
|
+
}): Promise<Buffer>;
|
|
15
|
+
}
|
|
16
|
+
export declare class SecretNotFoundException extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
export declare class DirectorySecretSource extends SecretSource {
|
|
20
|
+
directory: string;
|
|
21
|
+
constructor(directory: string);
|
|
22
|
+
get(secretName: string): Promise<Buffer>;
|
|
23
|
+
}
|
|
24
|
+
export declare class EnvironmentSecretSource extends SecretSource {
|
|
25
|
+
ENVIRONMENT_VARIABLE_PREFIX: string;
|
|
26
|
+
get(secretName: string): Promise<Buffer>;
|
|
27
|
+
}
|
|
28
|
+
export declare class MockSecretSource extends SecretSource {
|
|
29
|
+
secrets: {
|
|
30
|
+
[key: string]: Buffer;
|
|
31
|
+
};
|
|
32
|
+
constructor(secrets: {
|
|
33
|
+
[key: string]: Buffer;
|
|
34
|
+
});
|
|
35
|
+
get(secretName: string): Promise<Buffer>;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
package/secrets/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
export const ENVVAR_RBT_SECRETS_DIRECTORY = "RBT_SECRETS_DIRECTORY";
|
|
4
|
+
class SecretSource {
|
|
5
|
+
}
|
|
6
|
+
export class Secrets {
|
|
7
|
+
constructor() {
|
|
8
|
+
this._secretCache = {};
|
|
9
|
+
if (Secrets._staticSecretSource) {
|
|
10
|
+
this._secretSource = Secrets._staticSecretSource;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const secretsDirectory = process.env[ENVVAR_RBT_SECRETS_DIRECTORY];
|
|
14
|
+
if (secretsDirectory) {
|
|
15
|
+
this._secretSource = new DirectorySecretSource(path.resolve(secretsDirectory));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this._secretSource = new EnvironmentSecretSource();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
static setSecretSource(secretSource) {
|
|
22
|
+
Secrets._staticSecretSource = secretSource;
|
|
23
|
+
}
|
|
24
|
+
get secretSource() {
|
|
25
|
+
return this._secretSource;
|
|
26
|
+
}
|
|
27
|
+
async get(secretName, { ttlSecs = 15.0 } = {}) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const cachedSecret = this._secretCache[secretName];
|
|
30
|
+
if (cachedSecret && cachedSecret.cachedAt + ttlSecs > now) {
|
|
31
|
+
return cachedSecret.value;
|
|
32
|
+
}
|
|
33
|
+
const value = await this._secretSource.get(secretName);
|
|
34
|
+
this._secretCache[secretName] = new _CachedSecret(value, now);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
Secrets._staticSecretSource = null;
|
|
39
|
+
export class SecretNotFoundException extends Error {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "SecretNotFoundException";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export class DirectorySecretSource extends SecretSource {
|
|
46
|
+
constructor(directory) {
|
|
47
|
+
super();
|
|
48
|
+
this.directory = directory;
|
|
49
|
+
}
|
|
50
|
+
async get(secretName) {
|
|
51
|
+
const secretPath = path.join(this.directory, secretName);
|
|
52
|
+
try {
|
|
53
|
+
return await fs.readFile(secretPath);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === "ENOENT") {
|
|
57
|
+
throw new SecretNotFoundException(`No secret is stored for secretName=${secretName} (at \`${secretPath}\`).`);
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export class EnvironmentSecretSource extends SecretSource {
|
|
64
|
+
constructor() {
|
|
65
|
+
super(...arguments);
|
|
66
|
+
this.ENVIRONMENT_VARIABLE_PREFIX = "RBT_SECRET_";
|
|
67
|
+
}
|
|
68
|
+
async get(secretName) {
|
|
69
|
+
const environmentVariableName = `${this.ENVIRONMENT_VARIABLE_PREFIX}${secretName.toUpperCase().replace(/-/g, "_")}`;
|
|
70
|
+
const value = process.env[environmentVariableName];
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
throw new SecretNotFoundException(`No environment variable was set for secretName=${secretName}; ` +
|
|
73
|
+
`expected \`${environmentVariableName}\` to be set`);
|
|
74
|
+
}
|
|
75
|
+
return Buffer.from(value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export class MockSecretSource extends SecretSource {
|
|
79
|
+
constructor(secrets) {
|
|
80
|
+
super();
|
|
81
|
+
this.secrets = secrets;
|
|
82
|
+
}
|
|
83
|
+
async get(secretName) {
|
|
84
|
+
const value = this.secrets[secretName];
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
throw new SecretNotFoundException(`No mock secret was stored for secretName=${secretName}.`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
class _CachedSecret {
|
|
92
|
+
constructor(value, cachedAt) {
|
|
93
|
+
this.value = value;
|
|
94
|
+
this.cachedAt = cachedAt;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/venv.js
CHANGED
|
@@ -45,6 +45,11 @@ function pipInstallReboot() {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
export function ensurePythonVenv() {
|
|
48
|
+
// If the virtual environment is already activated, do not attempt to
|
|
49
|
+
// re-create it.
|
|
50
|
+
if (process.env.VIRTUAL_ENV === VENV_PATH) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
48
53
|
// Ensure that the Docker base Reboot image version and the Reboot
|
|
49
54
|
// version are in sync.
|
|
50
55
|
if (process.env.REBOOT_BASE_IMAGE_VERSION) {
|
package/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const REBOOT_VERSION = "0.
|
|
1
|
+
export declare const REBOOT_VERSION = "0.24.0";
|
package/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const REBOOT_VERSION = "0.
|
|
1
|
+
export const REBOOT_VERSION = "0.24.0";
|