@reboot-dev/reboot 0.18.2 → 0.19.0-alpha

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/package.json CHANGED
@@ -1,30 +1,35 @@
1
1
  {
2
2
  "dependencies": {
3
- "@types/node": "20.11.5",
4
- "@types/uuid": "^9.0.4",
5
- "uuid": "^9.0.1",
6
- "node-addon-api": "^7.0.0",
7
- "protobufjs": "^7.2.5",
8
- "chalk": "^4.1.2"
9
- },
10
- "peerDependencies": {
11
3
  "@bufbuild/protobuf": "1.3.2",
12
4
  "@bufbuild/protoplugin": "1.3.2",
13
5
  "@bufbuild/protoc-gen-es": "1.3.2",
14
- "@reboot-dev/reboot-api": "0.18.2",
15
- "typescript": ">=4.9.5"
6
+ "@reboot-dev/reboot-api": "0.19.0-alpha",
7
+ "chalk": "^4.1.2",
8
+ "node-addon-api": "^7.0.0",
9
+ "node-gyp": ">=10.2.0",
10
+ "uuid": "^9.0.1",
11
+ "which-pm-runs": "^1.1.0",
12
+ "extensionless": "^1.9.9",
13
+ "esbuild": "^0.24.0"
16
14
  },
17
15
  "type": "module",
18
16
  "name": "@reboot-dev/reboot",
19
- "version": "0.18.2",
17
+ "version": "0.19.0-alpha",
20
18
  "description": "npm package for Reboot",
21
19
  "scripts": {
22
20
  "postinstall": "rbt || exit 0",
23
21
  "test": "echo \"Error: no test specified\" && exit 1",
24
- "prepare": "tsc"
22
+ "prepack": "tsc"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": ">=4.9.5",
26
+ "@types/node": "20.11.5",
27
+ "@types/uuid": "^9.0.4"
25
28
  },
26
29
  "bin": {
27
- "rbt": "./rbt.js"
30
+ "rbt": "./rbt.js",
31
+ "rbt-esbuild": "./rbt-esbuild.js",
32
+ "protoc-gen-es": "./protoc-gen-es.js"
28
33
  },
29
34
  "engines": {
30
35
  "node": ">=18.0.0"
@@ -34,22 +39,27 @@
34
39
  "author": "reboot-dev",
35
40
  "license": "Apache-2.0",
36
41
  "files": [
42
+ "binding.gyp",
37
43
  "include",
38
44
  "internal",
39
- "utils",
40
- "binding.gyp",
41
45
  "package.json",
46
+ "protoc-gen-es.d.ts",
47
+ "protoc-gen-es.js",
48
+ "rbt.d.ts",
49
+ "rbt-esbuild.d.ts",
50
+ "rbt-esbuild.js",
51
+ "rbt.js",
52
+ "reboot.d.ts",
53
+ "reboot.js",
42
54
  "reboot_native.cc",
43
55
  "reboot_native.cjs",
44
56
  "reboot_native.d.ts",
45
- "reboot.js",
46
- "reboot.d.ts",
47
- "rbt.js",
48
- "rbt.d.ts",
49
- "venv.js",
57
+ "utils",
50
58
  "venv.d.ts",
59
+ "venv.js",
60
+ "version.d.ts",
51
61
  "version.js",
52
- "version.d.ts"
62
+ "workspaces.js"
53
63
  ],
54
64
  "exports": {
55
65
  "./package.json": "./package.json",
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "@bufbuild/protoc-gen-es/bin/protoc-gen-es";
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ // NOTE: Yarn doesn't add transitive binary dependencies to PATH!!!
3
+ // Thus this script which "proxies" to `protoc-gen-es`.
4
+ import "@bufbuild/protoc-gen-es/bin/protoc-gen-es";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare const BUNDLE_PATH: string;
package/rbt-esbuild.js ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import chalk from "chalk";
3
+ import esbuild from "esbuild";
4
+ import { writeFileSync } from "node:fs";
5
+ import * as path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { locateWorkspaces } from "./workspaces.js";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ // Expected usage: rbt-esbuild path/to/application.ts name
11
+ const args = process.argv.slice(2);
12
+ const application = args[0];
13
+ const name = args[1];
14
+ export const BUNDLE_PATH = path.join(__dirname, ".bundles", name);
15
+ let workspaces = [];
16
+ try {
17
+ workspaces = (await locateWorkspaces()).map(({ name }) => name);
18
+ }
19
+ catch (e) {
20
+ console.error(chalk.stderr.bold.red(`Failed to use your package manager to determine your workspaces (if any): ${e}`));
21
+ process.exit(-1);
22
+ }
23
+ const plugin = {
24
+ name: "rbt-esbuild",
25
+ setup(build) {
26
+ build.onResolve({ namespace: "file", filter: /.*/ }, (args) => {
27
+ // Do not mark as 'external' files starting with '.' or '/' because those
28
+ // are assumed to be local.
29
+ if (args.path.startsWith(".") || args.path.startsWith("/")) {
30
+ return null;
31
+ }
32
+ // Workspace modules are also local, not external.
33
+ for (const workspace of workspaces) {
34
+ if (args.path === workspace || args.path.startsWith(workspace + "/")) {
35
+ return null;
36
+ }
37
+ }
38
+ // Mark everything else external.
39
+ return { path: args.path, external: true };
40
+ });
41
+ },
42
+ };
43
+ esbuild
44
+ .build({
45
+ entryPoints: [application],
46
+ bundle: true,
47
+ platform: "node",
48
+ format: "esm",
49
+ metafile: true,
50
+ sourcemap: "inline",
51
+ banner: {
52
+ js: "/* eslint-disable */",
53
+ },
54
+ outfile: path.join(BUNDLE_PATH, "bundle.js"),
55
+ plugins: [plugin],
56
+ // This is only called by `rbt dev` and thus should only do module
57
+ // resolution for "development".
58
+ conditions: ["development"],
59
+ // TODO: support taking either `esbuild.config.js` or a more
60
+ // general `rbt.config.js` which would export a default object
61
+ // with options to pass on for specific esbuild, for example:
62
+ //
63
+ // export default {
64
+ // esbuild: {
65
+ // conditions: ["something"]
66
+ // }
67
+ // };
68
+ //
69
+ // We'll likely need to have users pass us the path to this file
70
+ // via some flag like
71
+ // `--esbuild-config=path/to/esbuild.config.js` or
72
+ // `--nodejs-config=path/to/rbt.config.js`.
73
+ //
74
+ // We could try to find the file but it's not obvious where to
75
+ // look. We could look for it based on `.rbtrc` or
76
+ // `--state-directory` (which ever was specified) but it's not
77
+ // obvious it should always be there, e.g., in the case of a
78
+ // monorepo with a `.rbtrc` that has "config" based flags.
79
+ //
80
+ // Once we have the file we can support overriding, for example:
81
+ //
82
+ // ...("conditions" in config.esbuild && { conditions: config.esbuild.conditions } || {}),
83
+ })
84
+ .then(async (result) => {
85
+ writeFileSync(path.join(BUNDLE_PATH, "meta.json"), JSON.stringify(result.metafile));
86
+ // Output the path where we bundled for `rbt dev` to consume.
87
+ console.log(BUNDLE_PATH);
88
+ process.exit(0);
89
+ })
90
+ .catch((error) => {
91
+ console.error(error instanceof Error ? error.message : error);
92
+ process.exit(-1);
93
+ });
package/rbt.js CHANGED
@@ -1,11 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "child_process";
3
3
  import * as path from "path";
4
+ import whichPMRuns from "which-pm-runs";
5
+ import { ensureYarnNodeLinker } from "./utils/index.js";
4
6
  import { ensurePythonVenv, VENV_EXEC_PATH } from "./venv.js";
7
+ async function ensureSupportedYarn() {
8
+ const packageManager = await whichPMRuns();
9
+ if (!packageManager) {
10
+ return;
11
+ }
12
+ if (packageManager.name === "yarn") {
13
+ ensureYarnNodeLinker();
14
+ }
15
+ }
16
+ function addExtensionlessToNodeOptions() {
17
+ // Add the `extensionless` loader to the `NODE_OPTIONS` env var so
18
+ // subsequent invocations of Node.js will use it. Depending on which
19
+ // version of Node.js we're using we have to add it differently.
20
+ const [major, minor] = process.versions.node.split(".").map(Number);
21
+ // Make sure that we can append to the `NODE_OPTIONS` env var.
22
+ process.env.NODE_OPTIONS =
23
+ (process.env.NODE_OPTIONS && process.env.NODE_OPTIONS + " ") || "";
24
+ // The `module.register()` function was added to Node.js in 20.6.0
25
+ // for the main release line and 18.19.0 for the LTS release line.
26
+ //
27
+ // If we have one of those two versions then we can pre import
28
+ // `extensionless/register` to use the `module.register()` function,
29
+ // otherwise we need to fall back to `--experimental-loader`.
30
+ if (major > 20 ||
31
+ (major == 20 && minor >= 6) ||
32
+ (major == 18 && minor >= 19)) {
33
+ process.env.NODE_OPTIONS += "--import=extensionless/register";
34
+ }
35
+ else {
36
+ process.env.NODE_OPTIONS += "--experimental-loader=extensionless";
37
+ }
38
+ }
5
39
  async function main() {
6
40
  ensurePythonVenv();
41
+ await ensureSupportedYarn();
7
42
  // Set env var to indicate that `rbt` is being invoked from Node.js.
8
43
  process.env.RBT_FROM_NODEJS = "true";
44
+ // Add extensionless loader.
45
+ addExtensionlessToNodeOptions();
9
46
  const rbt = spawnSync(`${path.join(VENV_EXEC_PATH, "rbt")} ${process.argv.slice(2).join(" ")}`, {
10
47
  stdio: [process.stdin, process.stdout, process.stderr],
11
48
  shell: true,
package/reboot.d.ts CHANGED
@@ -25,8 +25,7 @@ export declare class ExternalContext {
25
25
  #private;
26
26
  constructor(args: {
27
27
  name: string;
28
- gateway?: string;
29
- secureChannel?: boolean;
28
+ url?: string;
30
29
  bearerToken?: string;
31
30
  idempotencySeed?: string;
32
31
  idempotencyRequired?: boolean;
@@ -89,10 +88,10 @@ export type ServicerFactory = {
89
88
  */
90
89
  export declare class Auth {
91
90
  userId?: string;
92
- properties: unknown;
91
+ properties: protobuf_es.JsonValue;
93
92
  constructor(options?: {
94
93
  userId?: string;
95
- properties?: unknown;
94
+ properties?: protobuf_es.JsonValue;
96
95
  });
97
96
  toProtoBytes(): Uint8Array;
98
97
  static fromProtoBytes(bytes: Uint8Array): Auth;
@@ -151,9 +150,10 @@ export declare class AllowAllIfAuthenticated extends Authorizer<protobuf_es.Mess
151
150
  }
152
151
  export declare class Application {
153
152
  #private;
154
- constructor({ servicers, initialize, tokenVerifier, }: {
153
+ constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }: {
155
154
  servicers: ServicerFactory[];
156
155
  initialize?: (context: ExternalContext) => Promise<void>;
156
+ initializeBearerToken?: string;
157
157
  tokenVerifier?: TokenVerifier;
158
158
  });
159
159
  run(): Promise<any>;
package/reboot.js CHANGED
@@ -21,15 +21,34 @@ export { reboot_native };
21
21
  export * from "./utils/index.js";
22
22
  const startedInstances = [];
23
23
  if (process != null) {
24
- const processExitHandler = () => {
25
- if (startedInstances.length) {
26
- console.warn(`${startedInstances.length} Reboot instance(s) still running on exit. Did you forget to run \`rbt.stop()\`?`);
24
+ const checkForStartedInstances = () => {
25
+ if (startedInstances.length > 0) {
26
+ console.warn(`${startedInstances.length} Reboot instance(s) still running on exit which may keep your process from exiting. Did you forget to run \`rbt.stop()\`?`);
27
27
  }
28
- process.exit(0);
29
28
  };
30
- process.on("exit", processExitHandler);
31
- process.on("SIGTERM", processExitHandler);
32
- process.on("SIGINT", processExitHandler);
29
+ process.on("exit", (code) => {
30
+ checkForStartedInstances();
31
+ });
32
+ const checkIfNoOtherListenersAndIfSoExit = (signal, code) => {
33
+ // Check if there is only one listener for this signal which would
34
+ // imply just us and if so then we need to perform the _default_
35
+ // act of calling `process.exit()`.
36
+ if (process.listeners(signal).length === 1) {
37
+ process.exit(code);
38
+ }
39
+ };
40
+ process.on("SIGTERM", () => {
41
+ checkForStartedInstances();
42
+ // NOTE: empirical testing shows that a Node.js process that
43
+ // exits due to a SIGTERM will exit with a code of 143.
44
+ checkIfNoOtherListenersAndIfSoExit("SIGTERM", 143);
45
+ });
46
+ process.on("SIGINT", () => {
47
+ checkForStartedInstances();
48
+ // NOTE: empirical testing shows that a Node.js process that
49
+ // exits due to a SIGINT will exit with a code of 130.
50
+ checkIfNoOtherListenersAndIfSoExit("SIGINT", 130);
51
+ });
33
52
  }
34
53
  export class Reboot {
35
54
  constructor() {
@@ -76,7 +95,11 @@ export class ExternalContext {
76
95
  __classPrivateFieldSet(this, _ExternalContext_external, args.external, "f");
77
96
  }
78
97
  else {
79
- __classPrivateFieldSet(this, _ExternalContext_external, reboot_native.ExternalContext_constructor(args.name, args.gateway, args.secureChannel, args.bearerToken, args.idempotencySeed, args.idempotencyRequired, args.idempotencyRequiredReason), "f");
98
+ if (!args.url || !args.url.startsWith("http")) {
99
+ throw new Error("`ExternalContext` must be constructed with a `url` " +
100
+ "including an explicit 'http' or 'https' protocol");
101
+ }
102
+ __classPrivateFieldSet(this, _ExternalContext_external, reboot_native.ExternalContext_constructor(args.name, args.url, args.bearerToken, args.idempotencySeed, args.idempotencyRequired, args.idempotencyRequiredReason), "f");
80
103
  }
81
104
  }
82
105
  static fromNativeExternal(external) {
@@ -254,7 +277,7 @@ export class Auth {
254
277
  toProtoBytes() {
255
278
  const auth = new auth_pb.Auth({
256
279
  userId: this.userId,
257
- properties: this.properties,
280
+ properties: protobuf_es.Struct.fromJson(this.properties),
258
281
  });
259
282
  return auth.toBinary();
260
283
  }
@@ -301,7 +324,7 @@ export class AllowAllIfAuthenticated extends Authorizer {
301
324
  }
302
325
  }
303
326
  export class Application {
304
- constructor({ servicers, initialize, tokenVerifier, }) {
327
+ constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }) {
305
328
  _Application_external.set(this, void 0);
306
329
  __classPrivateFieldSet(this, _Application_external, reboot_native.Application_constructor(ExternalContext.fromNativeExternal, servicers, async (context) => {
307
330
  if (initialize !== undefined) {
@@ -317,7 +340,7 @@ export class Application {
317
340
  }
318
341
  }
319
342
  }
320
- }, tokenVerifier), "f");
343
+ }, initializeBearerToken, tokenVerifier), "f");
321
344
  }
322
345
  async run() {
323
346
  return await reboot_native.Application_run(__classPrivateFieldGet(this, _Application_external, "f"));
package/reboot_native.cc CHANGED
@@ -1177,13 +1177,11 @@ py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
1177
1177
  {Napi::Function::New(
1178
1178
  env,
1179
1179
  [py_future](const Napi::CallbackInfo& info) {
1180
- Napi::Object js_bytes_auth =
1181
- info[0].As<Napi::Object>();
1182
1180
  std::optional<std::string> bytes_auth;
1183
- if (!js_bytes_auth.IsNull()) {
1181
+ if (!info[0].IsNull()) {
1184
1182
  bytes_auth =
1185
1183
  uint8array_to_str(
1186
- js_bytes_auth.As<Napi::Uint8Array>());
1184
+ info[0].As<Napi::Uint8Array>());
1187
1185
  }
1188
1186
 
1189
1187
  adaptor->ScheduleCallbackOnPythonEventLoop(
@@ -1592,7 +1590,7 @@ Napi::Value Service_call(const Napi::CallbackInfo& info) {
1592
1590
 
1593
1591
  py::object py_task =
1594
1592
  py::module::import("reboot.nodejs.python")
1595
- .attr("create_task")(
1593
+ .attr("create_task_with_context")(
1596
1594
  py_service->attr(("_" + kind).c_str())(
1597
1595
  method,
1598
1596
  py_context,
@@ -1687,7 +1685,7 @@ Napi::Value Future_await(const Napi::CallbackInfo& info) {
1687
1685
  deferred = std::move(deferred)]() {
1688
1686
  py::object py_task =
1689
1687
  py::module::import("reboot.nodejs.python")
1690
- .attr("create_task")(
1688
+ .attr("create_task_with_context")(
1691
1689
  py_service->attr("_future_await")(
1692
1690
  method,
1693
1691
  py_context,
@@ -1738,14 +1736,9 @@ Napi::Value Future_await(const Napi::CallbackInfo& info) {
1738
1736
  Napi::Value ExternalContext_constructor(const Napi::CallbackInfo& info) {
1739
1737
  std::string name = info[0].As<Napi::String>().Utf8Value();
1740
1738
 
1741
- std::optional<std::string> gateway;
1739
+ std::optional<std::string> url;
1742
1740
  if (!info[1].IsUndefined()) {
1743
- gateway = info[1].As<Napi::String>().Utf8Value();
1744
- }
1745
-
1746
- std::optional<bool> secure_channel;
1747
- if (!info[2].IsUndefined()) {
1748
- secure_channel = info[2].As<Napi::Boolean>();
1741
+ url = info[1].As<Napi::String>().Utf8Value();
1749
1742
  }
1750
1743
 
1751
1744
  std::optional<std::string> bearer_token;
@@ -1776,8 +1769,7 @@ Napi::Value ExternalContext_constructor(const Napi::CallbackInfo& info) {
1776
1769
  adaptor->ScheduleCallbackOnPythonEventLoop(
1777
1770
  [&promise,
1778
1771
  &name,
1779
- &gateway,
1780
- &secure_channel,
1772
+ &url,
1781
1773
  &bearer_token,
1782
1774
  &idempotency_seed,
1783
1775
  &idempotency_required,
@@ -1806,8 +1798,7 @@ Napi::Value ExternalContext_constructor(const Napi::CallbackInfo& info) {
1806
1798
  promise.set_value(
1807
1799
  new py::object(py_external.attr("ExternalContext")(
1808
1800
  "name"_a = py::str(name),
1809
- "gateway"_a = convert_str(gateway),
1810
- "secure_channel"_a = convert_bool(secure_channel),
1801
+ "url"_a = convert_str(url),
1811
1802
  "bearer_token"_a = convert_str(bearer_token),
1812
1803
  "idempotency_seed"_a = convert_str(idempotency_seed),
1813
1804
  "idempotency_required"_a = py::bool_(idempotency_required),
@@ -1844,9 +1835,14 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
1844
1835
  auto js_initialize = NapiSafeFunctionReference(
1845
1836
  info[2].As<Napi::Function>());
1846
1837
 
1838
+ std::optional<std::string> initialize_bearer_token;
1839
+ if (!info[3].IsUndefined()) {
1840
+ initialize_bearer_token = info[3].As<Napi::String>().Utf8Value();
1841
+ }
1842
+
1847
1843
  std::optional<NapiSafeObjectReference> js_token_verifier;
1848
- if (info[3].IsObject()) {
1849
- js_token_verifier = NapiSafeReference(info[3].As<Napi::Object>());
1844
+ if (!info[4].IsUndefined()) {
1845
+ js_token_verifier = NapiSafeReference(info[4].As<Napi::Object>());
1850
1846
  }
1851
1847
 
1852
1848
  std::promise<py::object*> promise;
@@ -1854,6 +1850,7 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
1854
1850
  adaptor->ScheduleCallbackOnPythonEventLoop(
1855
1851
  [&promise,
1856
1852
  servicer_details = std::move(servicer_details),
1853
+ initialize_bearer_token = std::move(initialize_bearer_token),
1857
1854
  js_initialize = std::move(js_initialize),
1858
1855
  js_token_verifier,
1859
1856
  js_from_native_external = std::move(js_from_native_external)]() {
@@ -1950,8 +1947,10 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
1950
1947
  return py_future;
1951
1948
  });
1952
1949
 
1953
- py::object py_applications = py::module::import(
1954
- "reboot.aio.applications");
1950
+ py::object py_initialize_bearer_token = py::none();
1951
+ if (initialize_bearer_token.has_value()) {
1952
+ py_initialize_bearer_token = py::str(*initialize_bearer_token);
1953
+ }
1955
1954
 
1956
1955
  py::object py_token_verifier = py::none();
1957
1956
  if (js_token_verifier.has_value()) {
@@ -1959,10 +1958,14 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
1959
1958
  }
1960
1959
 
1961
1960
  promise.set_value(
1962
- new py::object(py_applications.attr("Application")(
1963
- "servicers"_a = py_servicers,
1964
- "initialize"_a = py_initialize,
1965
- "token_verifier"_a = py_token_verifier)));
1961
+ new py::object(
1962
+ py::module::import("reboot.aio.applications")
1963
+ .attr("Application")(
1964
+ "servicers"_a = py_servicers,
1965
+ "initialize"_a = py_initialize,
1966
+ "initialize_bearer_token"_a =
1967
+ py_initialize_bearer_token,
1968
+ "token_verifier"_a = py_token_verifier)));
1966
1969
  }
1967
1970
  // TODO(benh): improve error handling mechanism to force all
1968
1971
  // raised exceptions to be handled.
@@ -2023,9 +2026,10 @@ Napi::Value Application_run(const Napi::CallbackInfo& info) {
2023
2026
  py_application,
2024
2027
  deferred = std::move(deferred)]() {
2025
2028
  py::object py_task =
2026
- py::module::import("asyncio").attr("create_task")(
2027
- py_application->attr("run")(),
2028
- "name"_a = "Application.run() in nodejs");
2029
+ py::module::import("reboot.nodejs.python")
2030
+ .attr("create_task")(
2031
+ py_application->attr("run")(),
2032
+ "name"_a = "Application.run() in nodejs");
2029
2033
 
2030
2034
  py_task.attr("add_done_callback")(py::cpp_function(
2031
2035
  [deferred = std::move(deferred),
@@ -2048,9 +2052,16 @@ Napi::Value Application_run(const Napi::CallbackInfo& info) {
2048
2052
  if (!exception.has_value()) {
2049
2053
  deferred->Resolve(env.Null());
2050
2054
  } else {
2055
+ env.Global()
2056
+ .Get("process")
2057
+ .As<Napi::Object>()
2058
+ .Set("exitCode", 1);
2051
2059
  deferred->Reject(
2052
2060
  Napi::Error::New(env, *exception).Value());
2053
2061
  }
2062
+ // When `Application.run` exits, we unref our
2063
+ // thread_safe_function to cause the runtime to exit.
2064
+ adaptor->thread_safe_function.Unref(env);
2054
2065
  });
2055
2066
 
2056
2067
  delete py_task;
@@ -2347,7 +2358,7 @@ Napi::Value retry_reactively_until(const Napi::CallbackInfo& info) {
2347
2358
 
2348
2359
  py::object py_task =
2349
2360
  py::module::import("reboot.nodejs.python")
2350
- .attr("create_task")(
2361
+ .attr("create_task_with_context")(
2351
2362
  (py::module::import("reboot.aio.contexts")
2352
2363
  .attr("retry_reactively_until"))(
2353
2364
  py_context,
@@ -2483,7 +2494,7 @@ Napi::Value atLeastOrMostOnce(const Napi::CallbackInfo& info) {
2483
2494
 
2484
2495
  py::object py_task =
2485
2496
  py::module::import("reboot.nodejs.python")
2486
- .attr("create_task")(
2497
+ .attr("create_task_with_context")(
2487
2498
  (py::module::import("reboot.aio.memoize").attr("memoize"))(
2488
2499
  py::str(idempotency_alias),
2489
2500
  py_context,
@@ -2560,7 +2571,7 @@ Napi::Value Servicer_read(const Napi::CallbackInfo& info) {
2560
2571
  deferred = std::move(deferred)]() {
2561
2572
  py::object py_task =
2562
2573
  py::module::import("reboot.nodejs.python")
2563
- .attr("create_task")(
2574
+ .attr("create_task_with_context")(
2564
2575
  py_servicer->attr("_read")(py_context),
2565
2576
  py_context,
2566
2577
  "name"_a = "servicer._read(...) in nodejs");
@@ -2709,7 +2720,7 @@ Napi::Value Servicer_write(const Napi::CallbackInfo& info) {
2709
2720
 
2710
2721
  py::object py_task =
2711
2722
  py::module::import("reboot.nodejs.python")
2712
- .attr("create_task")(
2723
+ .attr("create_task_with_context")(
2713
2724
  py_servicer->attr("_write")(
2714
2725
  py_context,
2715
2726
  py_writer,
package/reboot_native.cjs CHANGED
@@ -7,7 +7,7 @@ const [major, minor, patch] = process.versions.node.split(".").map(Number);
7
7
 
8
8
  if (major < 18) {
9
9
  console.error(
10
- chalk.red(
10
+ chalk.stderr.bold.red(
11
11
  `Reboot requires nodejs version >=18 (found ${major}.${minor}.${patch})`
12
12
  )
13
13
  );
@@ -16,21 +16,25 @@ if (major < 18) {
16
16
 
17
17
  const reboot_native = { exports: {} };
18
18
 
19
- // Next.js has its own runtime environment that runs on top of Node. Thus,
20
- // __dirname isn't correctly defined in a Next.js app.
21
- // We special case for Next.js for this reason.
22
- // https://stackoverflow.com/questions/77589486/nextjs-cannot-find-dirname
23
- // TODO: find a more permanent solution for this.
24
- let node_modules_reboot_directory;
19
+ // If we are in a Next.js context, check that serverExternalPackages is
20
+ // correctly set. If not correctly set, Reboot backend TS code will not work
21
+ // with React Server Components inside Next.
25
22
  if (__dirname.includes(".next")) {
26
- node_modules_reboot_directory = path.join(
27
- process.cwd(),
28
- "node_modules",
29
- "@reboot-dev",
30
- "reboot"
23
+ throw new Error(
24
+ `For Next.js to work correctly with Reboot native code, an external
25
+ package must be added to 'next.config.ts':
26
+
27
+ In Next.js 14:
28
+ experimental: {
29
+ serverComponentsExternalPackages: ["@reboot-dev/reboot"],
30
+ },
31
+ ...
32
+
33
+ In Next.js 15
34
+ 'serverExternalPackages: ['@reboot-dev/reboot']',
35
+ ...
36
+ `
31
37
  );
32
- } else {
33
- node_modules_reboot_directory = __dirname;
34
38
  }
35
39
 
36
40
  // NOTE: as of Python 3.8 we _must_ load the Python library via
@@ -41,12 +45,7 @@ process.dlopen(
41
45
  reboot_native,
42
46
  // TODO(benh): consider using `require(bindings)` to properly locate
43
47
  // 'reboot_native.node'.
44
- path.join(
45
- node_modules_reboot_directory,
46
- "build",
47
- "Release",
48
- "reboot_native.node"
49
- ),
48
+ path.join(__dirname, "build", "Release", "reboot_native.node"),
50
49
  os.constants.dlopen.RTLD_NOW | os.constants.dlopen.RTLD_GLOBAL
51
50
  );
52
51
 
@@ -45,8 +45,7 @@ export namespace rbt_native {
45
45
  function Future_await(props: Future_awaitProps): string;
46
46
  function ExternalContext_constructor(
47
47
  name: string,
48
- gateway: string,
49
- secureChannel: boolean,
48
+ url: string,
50
49
  bearerToken: string,
51
50
  idempotencySeed: string,
52
51
  idempotencyRequired: boolean,
package/utils/index.d.ts CHANGED
@@ -1 +1,12 @@
1
1
  export * from "./errors.js";
2
+ export declare function parseVersion(version: string): [number, number, number];
3
+ export type Version = [number, number, number];
4
+ export declare function supportedVersion({ required: [requiredMajor, requiredMinor, requiredPatch], found: [foundMajor, foundMinor, foundPatch], }: {
5
+ required: Version;
6
+ found: Version;
7
+ }): boolean;
8
+ export declare function ensureYarnNodeLinker(): void;
9
+ /**
10
+ * Spawn a process with the given arguments, and return its stdout as a string.
11
+ */
12
+ export declare function spawnAndReturnStdout(command: string, args: string[]): string;
package/utils/index.js CHANGED
@@ -1 +1,47 @@
1
+ import chalk from "chalk";
2
+ import { spawnSync } from "child_process";
1
3
  export * from "./errors.js";
4
+ export function parseVersion(version) {
5
+ const match = version.trim().match(/(\d+\.\d+\.\d+)/);
6
+ if (!match) {
7
+ throw new Error(`Failed to parse '${version}' into a version`);
8
+ }
9
+ return version.split(".").map(Number);
10
+ }
11
+ export function supportedVersion({ required: [requiredMajor, requiredMinor, requiredPatch], found: [foundMajor, foundMinor, foundPatch], }) {
12
+ return (foundMajor > requiredMajor ||
13
+ (foundMajor === requiredMajor && foundMinor > requiredMinor) ||
14
+ (foundMajor === requiredMajor &&
15
+ foundMinor === requiredMinor &&
16
+ foundPatch >= requiredPatch));
17
+ }
18
+ export function ensureYarnNodeLinker() {
19
+ // Ensure that they aren't using Yarn Plug'n'Play which we don't yet
20
+ // support.
21
+ const nodeLinker = spawnAndReturnStdout("yarn", [
22
+ "config",
23
+ "get",
24
+ "nodeLinker",
25
+ ]);
26
+ if (nodeLinker.trim() !== "node-modules") {
27
+ console.error(chalk.stderr.bold.red("Yarn Plug'n'Play is not yet supported, you must use 'node-modules' as your 'nodeLinker'"));
28
+ process.exit(-1);
29
+ }
30
+ }
31
+ /**
32
+ * Spawn a process with the given arguments, and return its stdout as a string.
33
+ */
34
+ export function spawnAndReturnStdout(command, args) {
35
+ const result = spawnSync(command, args, {
36
+ stdio: ["ignore", "pipe", "inherit"],
37
+ });
38
+ if (result.error) {
39
+ throw new Error(`Failed to run '${command} ${args.join(" ")}': ${result.error}\n` +
40
+ `\n` +
41
+ `Please report this bug to the maintainers!`);
42
+ }
43
+ else if (result.status !== 0) {
44
+ throw new Error(`Running '${command} ${args.join(" ")}' exited with status ${result.status}. Please report this bug including the output of that command (if any) to the maintainers!`);
45
+ }
46
+ return result.stdout.toString();
47
+ }
package/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const REBOOT_VERSION = "0.18.2";
1
+ export declare const REBOOT_VERSION = "0.19.0-alpha";
package/version.js CHANGED
@@ -1 +1 @@
1
- export const REBOOT_VERSION = "0.18.2";
1
+ export const REBOOT_VERSION = "0.19.0-alpha";
package/workspaces.js ADDED
@@ -0,0 +1,120 @@
1
+ import whichPMRuns from "which-pm-runs";
2
+ import { parseVersion, spawnAndReturnStdout, supportedVersion, } from "./utils/index.js";
3
+ /**
4
+ * Returns package-manager Workspaces of which a project in the given directory
5
+ * is a member.
6
+ */
7
+ export async function locateWorkspaces() {
8
+ const packageManager = await whichPMRuns();
9
+ if (!packageManager) {
10
+ return null;
11
+ }
12
+ // NOTE: These methods will error if they fail to extract
13
+ // workspaces, based on the assumption that `whichPMRuns` will do a
14
+ // reasonable job of detecting a package manager, otherwise throws
15
+ // an exception.
16
+ switch (packageManager.name) {
17
+ case "yarn": {
18
+ return extractYarnWorkspaces();
19
+ }
20
+ case "npm": {
21
+ return extractNpmWorkspaces();
22
+ }
23
+ case "pnpm": {
24
+ return extractPnpmWorkspaces();
25
+ }
26
+ default: {
27
+ throw new Error(`Unsupported package manager '${packageManager}'`);
28
+ }
29
+ }
30
+ }
31
+ function extractYarnWorkspaces() {
32
+ // TODO: what is the actual minimum version we need?
33
+ const YARN_VERSION_REQUIRED = "4.0.0";
34
+ const required = parseVersion(YARN_VERSION_REQUIRED);
35
+ const found = parseVersion(spawnAndReturnStdout("yarn", ["--version"]));
36
+ if (!supportedVersion({ required, found })) {
37
+ throw new Error(`yarn version >=${YARN_VERSION_REQUIRED} is required, found ${found.join(".")}`);
38
+ }
39
+ const command = "yarn";
40
+ const args = ["workspaces", "list", "--json", "--recursive"];
41
+ // `yarn workspaces list --json` returns JSON lines, not complete
42
+ // JSON, so we have to do some extra parsing ourselves.
43
+ return spawnAndReturnStdout(command, args)
44
+ .trim()
45
+ .split("\n")
46
+ .map((line) => {
47
+ const { name } = JSON.parse(line.trim());
48
+ return { name };
49
+ });
50
+ }
51
+ function extractNpmWorkspaces() {
52
+ // We need >=8.16.0 which is when `npm query` was introduced.
53
+ const NPM_VERSION_REQUIRED = "8.16.0";
54
+ const required = parseVersion(NPM_VERSION_REQUIRED);
55
+ const found = parseVersion(spawnAndReturnStdout("npm", ["--version"]));
56
+ if (!supportedVersion({ required, found })) {
57
+ throw new Error(`npm version >=${NPM_VERSION_REQUIRED} is required, found ${found.join(".")}`);
58
+ }
59
+ const command = "npm";
60
+ const args = ["query", ".workspace"];
61
+ const workspaces = spawnAndParseJSON(command, args).map(({ name }) => ({
62
+ name,
63
+ }));
64
+ // Include the package name as it might be used and it is consistent
65
+ // with both `yarn` and `pnpm` which always include the top-level
66
+ // package name even if they aren't explicitly in `workspaces`.
67
+ const name = spawnAndParseJSON("npm", ["pkg", "get", "name"]);
68
+ // `npm pkg get name` returns an empty object if there is no
69
+ // top-level package name.
70
+ if (Object.keys(name).length === 0) {
71
+ return workspaces;
72
+ }
73
+ else if (typeof name !== "string") {
74
+ throw new Error(`Failed to get name of your package. Please report this issue to the maintainers!`);
75
+ }
76
+ return [{ name }, ...workspaces];
77
+ }
78
+ function extractPnpmWorkspaces() {
79
+ // TODO: is there a minimum version required?
80
+ const command = "pnpm";
81
+ const args = ["list", "--recursive", "--depth", "-1", "--only-projects"];
82
+ // `pnpm list --recursive --depth -1 --only-projects --json` emits something
83
+ // which is not quite valid JSON: closer to JSON lines, but not easily splittable.
84
+ //
85
+ // Instead, we parse the human-readable output -- a series of lines like:
86
+ // @reboot-pm/prosemirror@0.0.1 /Users/example/src/repo_root/project_dir (PRIVATE)
87
+ return spawnAndReturnStdout(command, args)
88
+ .trim()
89
+ .split("\n")
90
+ .map((line) => {
91
+ const parts = line.trim().split(" ");
92
+ if (parts.length !== 3) {
93
+ throw new Error(`Unexpected output from '${command} ${args.join(" ")}'. Please report the output of that command to the maintainers!`);
94
+ }
95
+ // Strip off the version from the name.
96
+ const lastAtSign = parts[0].lastIndexOf("@");
97
+ let name;
98
+ if (lastAtSign !== -1 && lastAtSign !== 0) {
99
+ name = parts[0].substring(0, lastAtSign);
100
+ }
101
+ else {
102
+ name = parts[0];
103
+ }
104
+ return { name };
105
+ });
106
+ }
107
+ /**
108
+ * Spawn a process with the given arguments, and parse its stdout as JSON.
109
+ *
110
+ * Raises an error if the process cannot be spawned, or if JSON cannot be parsed.
111
+ */
112
+ function spawnAndParseJSON(command, args) {
113
+ const stdout = spawnAndReturnStdout(command, args);
114
+ try {
115
+ return JSON.parse(stdout);
116
+ }
117
+ catch (error) {
118
+ throw new Error(`Failed to parse output of '${command} ${args.join(" ")}' as JSON: ${error}`);
119
+ }
120
+ }