@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 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: string): Promise<Auth | null>;
121
- _verifyToken(context: ReaderContext, token: string): Promise<Uint8Array | null>;
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
- parse: undefined;
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
- parse: (value: any) => T;
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
- parse: undefined;
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
- parse: (value: any) => T;
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
- parse: undefined;
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
- parse: (value: any) => T;
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, options) {
399
- let t = undefined;
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
- if (options.parse === undefined) {
404
- throw new Error("Required 'parse' property in 'options' is undefined");
405
- }
406
- // NOTE: we've decided not to stringify and parse `t` using
407
- // `options.parse` now to avoid the extra overhead, but it
408
- // might catch some bugs _before_ anything gets persisted and
409
- // users may prefer that tradeoff.
410
- return JSON.stringify(t);
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
- }, options.atMostOnce);
416
- if (t !== undefined) {
417
- return t;
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
- if (options.parse === undefined) {
422
- throw new Error("Required 'parse' property in 'options' is undefined");
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 options.parse(JSON.parse(result));
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 return void (undefined), fall through.
452
+ // Otherwise `callable` must have returned void (or explicitly
453
+ // `undefined`), fall through.
433
454
  }
434
- export async function atMostOnce(idempotencyAlias, context, callable, options = { parse: undefined }) {
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 = { parse: undefined }) {
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 = { parse: undefined }) {
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.22.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.22.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 { spawnSync } from "child_process";
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
- const rbt = spawnSync(`${path.join(VENV_EXEC_PATH, "rbt")} ${process.argv.slice(2).join(" ")}`, {
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
- if (rbt.error) {
51
- throw new Error(`Unable to execute 'rbt', please report this bug to the maintainers!\n${rbt.error}`);
52
- }
53
- process.exit(rbt.status);
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
- std::cerr
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
- std::cerr
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
- new Napi::Reference<T>(Napi::Persistent(std::move(t))),
512
- NapiReferenceDeleter()) {}
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
- new Napi::Reference<T>(std::move(reference)),
517
- NapiReferenceDeleter()) {}
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
- const std::string& token) {
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
- js_args.push_back(Napi::String::New(env, token));
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
- id,
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 Future_await(const Napi::CallbackInfo& info) {
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
- [js_external_service, // Ensures `py_service` remains valid.
1703
- py_service,
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
- py_service->attr("_future_await")(
1714
- method,
1715
- py_context,
1716
- json_task_id),
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 = ("servicer._future_await(\""
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, "Future_await"),
2867
- Napi::Function::New<Future_await>(env));
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.Future_await = reboot_native.exports.Future_await;
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;
@@ -31,9 +31,10 @@ interface Service_callProps {
31
31
  jsonRequest: string;
32
32
  }
33
33
 
34
- interface Future_awaitProps {
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 Future_await(props: Future_awaitProps): string;
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 {};
@@ -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.22.0";
1
+ export declare const REBOOT_VERSION = "0.24.0";
package/version.js CHANGED
@@ -1 +1 @@
1
- export const REBOOT_VERSION = "0.22.0";
1
+ export const REBOOT_VERSION = "0.24.0";