@reboot-dev/reboot 0.33.0 → 0.34.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
@@ -52,30 +52,43 @@ export declare function runWithContext<T>(context: Context, callback: () => T):
52
52
  export declare class Context {
53
53
  #private;
54
54
  readonly cancelled: Promise<void>;
55
- constructor(external: any, cancelled: Promise<void>);
56
- static fromNativeExternal(external: any, kind: string, cancelled: Promise<void>): ReaderContext | WriterContext | TransactionContext | WorkflowContext;
55
+ readonly stateId: string;
56
+ readonly method: string;
57
+ readonly stateTypeName: string;
58
+ readonly callerBearerToken: string;
59
+ readonly cookie: string;
60
+ readonly appInternal: boolean;
61
+ readonly auth: Auth | null;
62
+ constructor({ external, stateId, method, stateTypeName, callerBearerToken, cookie, appInternal, auth, cancelled, }: {
63
+ external: any;
64
+ stateId: string;
65
+ method: string;
66
+ stateTypeName: string;
67
+ callerBearerToken: string;
68
+ cookie: string;
69
+ appInternal: boolean;
70
+ auth: Auth | null;
71
+ cancelled: Promise<void>;
72
+ });
73
+ static fromNativeExternal({ kind, ...options }: {
74
+ [x: string]: any;
75
+ kind: any;
76
+ }): ReaderContext | WriterContext | TransactionContext | WorkflowContext;
57
77
  get __external(): any;
58
- get auth(): Auth | null;
59
- get stateId(): string;
60
- get stateTypeName(): string;
61
- get method(): string;
62
- get callerBearerToken(): string | null;
63
- get cookie(): string;
64
- get appInternal(): boolean;
65
78
  generateIdempotentStateId(stateType: string, serviceName: string, method: string, idempotency: IdempotencyOptions): Promise<any>;
66
79
  }
67
80
  export declare class ReaderContext extends Context {
68
81
  #private;
69
- constructor(external: any, cancelled: any);
82
+ constructor(options: any);
70
83
  }
71
84
  export declare class WriterContext extends Context {
72
85
  #private;
73
- constructor(external: any, cancelled: any);
86
+ constructor(options: any);
74
87
  set sync(sync: boolean);
75
88
  }
76
89
  export declare class TransactionContext extends Context {
77
90
  #private;
78
- constructor(external: any, cancelled: any);
91
+ constructor(options: any);
79
92
  }
80
93
  export type Interval = {
81
94
  ms?: number;
@@ -86,7 +99,7 @@ export type Interval = {
86
99
  };
87
100
  export declare class WorkflowContext extends Context {
88
101
  #private;
89
- constructor(external: any, cancelled: any);
102
+ constructor(options: any);
90
103
  loop(alias: string, { interval }?: {
91
104
  interval?: Interval;
92
105
  }): AsyncGenerator<number, void, unknown>;
@@ -140,7 +153,7 @@ export declare abstract class TokenVerifier {
140
153
  * `Auth` information if the token is valid, null otherwise.
141
154
  */
142
155
  abstract verifyToken(context: ReaderContext, token?: string): Promise<Auth | null>;
143
- _verifyToken(context: ReaderContext, token?: string): Promise<Uint8Array | null>;
156
+ _verifyToken(external: any, cancelled: Promise<void>, bytesCall: Uint8Array): Promise<Uint8Array | null>;
144
157
  }
145
158
  export type AuthorizerDecision = errors_pb.Unauthenticated | errors_pb.PermissionDenied | errors_pb.Ok;
146
159
  /**
@@ -164,7 +177,7 @@ export declare abstract class Authorizer<StateType, RequestTypes> {
164
177
  * `errors_pb.PermissionDenied()` otherwise.
165
178
  */
166
179
  abstract authorize(methodName: string, context: ReaderContext, state?: StateType, request?: RequestTypes): Promise<AuthorizerDecision>;
167
- _authorize?: (methodName: string, context: ReaderContext, bytesState?: Uint8Array, bytesRequest?: Uint8Array) => Promise<Uint8Array>;
180
+ _authorize?: (external: any, cancelled: Promise<void>, bytesCall: Uint8Array) => Promise<Uint8Array>;
168
181
  }
169
182
  export type AuthorizerCallable<StateType, RequestType> = (args: {
170
183
  context: ReaderContext;
package/index.js CHANGED
@@ -9,12 +9,13 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _Reboot_external, _ExternalContext_external, _a, _Context_external, _Context_isInternalConstructing, _Context_stateId, _Context_method, _Context_stateTypeName, _ReaderContext_kind, _WriterContext_kind, _TransactionContext_kind, _WorkflowContext_kind, _Application_servicers, _Application_tokenVerifier, _Application_express, _Application_http, _Application_servers, _Application_createExternalContext, _Application_external;
13
- import { auth_pb, errors_pb, protobuf_es, toCamelCase, tasks_pb, } from "@reboot-dev/reboot-api";
12
+ var _Reboot_external, _ExternalContext_external, _a, _Context_external, _Context_isInternalConstructing, _ReaderContext_kind, _WriterContext_kind, _TransactionContext_kind, _WorkflowContext_kind, _Application_servicers, _Application_tokenVerifier, _Application_express, _Application_http, _Application_servers, _Application_createExternalContext, _Application_external;
13
+ import { auth_pb, errors_pb, nodejs_pb, protobuf_es, tasks_pb, toCamelCase, } from "@reboot-dev/reboot-api";
14
14
  import { strict as assert } from "assert";
15
15
  import { AsyncLocalStorage } from "node:async_hooks";
16
16
  import { fork } from "node:child_process";
17
17
  import { createRequire } from "node:module";
18
+ import toobusy from "toobusy-js";
18
19
  import { z } from "zod/v4";
19
20
  import * as reboot_native from "./reboot_native.cjs";
20
21
  import { ensureError } from "./utils/errors.js";
@@ -168,80 +169,52 @@ export async function runWithContext(context, callback) {
168
169
  }, callback);
169
170
  }
170
171
  export class Context {
171
- constructor(external, cancelled) {
172
+ constructor({ external, stateId, method, stateTypeName, callerBearerToken, cookie, appInternal, auth, cancelled, }) {
172
173
  _Context_external.set(this, void 0);
173
- _Context_stateId.set(this, void 0);
174
- _Context_method.set(this, void 0);
175
- _Context_stateTypeName.set(this, void 0);
176
174
  if (!__classPrivateFieldGet(_a, _a, "f", _Context_isInternalConstructing)) {
177
175
  throw new TypeError("Context is not publicly constructable");
178
176
  }
179
177
  __classPrivateFieldSet(_a, _a, false, "f", _Context_isInternalConstructing);
180
178
  __classPrivateFieldSet(this, _Context_external, external, "f");
179
+ this.stateId = stateId;
180
+ // Since we are passing a 'method' from Python, it will contain the
181
+ // Python-style method name, so we have to convert it there.
182
+ this.method = toCamelCase(method);
183
+ this.stateTypeName = stateTypeName;
184
+ this.callerBearerToken = callerBearerToken;
185
+ this.cookie = cookie;
186
+ this.appInternal = appInternal;
187
+ this.auth = auth;
181
188
  this.cancelled = cancelled;
182
189
  }
183
- static fromNativeExternal(external, kind, cancelled) {
190
+ static fromNativeExternal({ kind, ...options }) {
184
191
  __classPrivateFieldSet(_a, _a, true, "f", _Context_isInternalConstructing);
185
192
  if (kind === "reader") {
186
- return new ReaderContext(external, cancelled);
193
+ return new ReaderContext(options);
187
194
  }
188
195
  else if (kind === "writer") {
189
- return new WriterContext(external, cancelled);
196
+ return new WriterContext(options);
190
197
  }
191
198
  else if (kind === "transaction") {
192
- return new TransactionContext(external, cancelled);
199
+ return new TransactionContext(options);
193
200
  }
194
201
  else if (kind === "workflow") {
195
- return new WorkflowContext(external, cancelled);
202
+ return new WorkflowContext(options);
196
203
  }
197
204
  throw new Error("Unknown method kind");
198
205
  }
199
206
  get __external() {
200
207
  return __classPrivateFieldGet(this, _Context_external, "f");
201
208
  }
202
- get auth() {
203
- const authBytes = reboot_native.Context_auth(__classPrivateFieldGet(this, _Context_external, "f"));
204
- if (authBytes) {
205
- return Auth.fromProtoBytes(authBytes);
206
- }
207
- return null;
208
- }
209
- get stateId() {
210
- if (__classPrivateFieldGet(this, _Context_stateId, "f") === undefined) {
211
- __classPrivateFieldSet(this, _Context_stateId, reboot_native.Context_stateId(__classPrivateFieldGet(this, _Context_external, "f")), "f");
212
- }
213
- return __classPrivateFieldGet(this, _Context_stateId, "f");
214
- }
215
- get stateTypeName() {
216
- if (__classPrivateFieldGet(this, _Context_stateTypeName, "f") === undefined) {
217
- __classPrivateFieldSet(this, _Context_stateTypeName, reboot_native.Context_stateTypeName(__classPrivateFieldGet(this, _Context_external, "f")), "f");
218
- }
219
- return __classPrivateFieldGet(this, _Context_stateTypeName, "f");
220
- }
221
- get method() {
222
- if (__classPrivateFieldGet(this, _Context_method, "f") === undefined) {
223
- __classPrivateFieldSet(this, _Context_method, toCamelCase(reboot_native.Context_method(__classPrivateFieldGet(this, _Context_external, "f"))), "f");
224
- }
225
- return __classPrivateFieldGet(this, _Context_method, "f");
226
- }
227
- get callerBearerToken() {
228
- return reboot_native.Context_callerBearerToken(__classPrivateFieldGet(this, _Context_external, "f"));
229
- }
230
- get cookie() {
231
- return reboot_native.Context_cookie(__classPrivateFieldGet(this, _Context_external, "f"));
232
- }
233
- get appInternal() {
234
- return reboot_native.Context_appInternal(__classPrivateFieldGet(this, _Context_external, "f"));
235
- }
236
209
  async generateIdempotentStateId(stateType, serviceName, method, idempotency) {
237
210
  return reboot_native.Context_generateIdempotentStateId(__classPrivateFieldGet(this, _Context_external, "f"), stateType, serviceName, method, idempotency);
238
211
  }
239
212
  }
240
- _a = Context, _Context_external = new WeakMap(), _Context_stateId = new WeakMap(), _Context_method = new WeakMap(), _Context_stateTypeName = new WeakMap();
213
+ _a = Context, _Context_external = new WeakMap();
241
214
  _Context_isInternalConstructing = { value: false };
242
215
  export class ReaderContext extends Context {
243
- constructor(external, cancelled) {
244
- super(external, cancelled);
216
+ constructor(options) {
217
+ super(options);
245
218
  // Property helps `tsc` not treat a `ReaderContext` as any other
246
219
  // context type that structurally looks equivalent.
247
220
  _ReaderContext_kind.set(this, "reader");
@@ -249,8 +222,8 @@ export class ReaderContext extends Context {
249
222
  }
250
223
  _ReaderContext_kind = new WeakMap();
251
224
  export class WriterContext extends Context {
252
- constructor(external, cancelled) {
253
- super(external, cancelled);
225
+ constructor(options) {
226
+ super(options);
254
227
  // Property helps `tsc` not treat a `WriterContext` as any other
255
228
  // context type that structurally looks equivalent.
256
229
  _WriterContext_kind.set(this, "writer");
@@ -268,13 +241,14 @@ export class WriterContext extends Context {
268
241
  // 3. The writer is _not_ within a transaction.
269
242
  // 4. The writer is _not_ running as a task.
270
243
  set sync(sync) {
244
+ // TODO: optimize this so that we don't block the event loop.
271
245
  reboot_native.WriterContext_set_sync(this.__external, sync);
272
246
  }
273
247
  }
274
248
  _WriterContext_kind = new WeakMap();
275
249
  export class TransactionContext extends Context {
276
- constructor(external, cancelled) {
277
- super(external, cancelled);
250
+ constructor(options) {
251
+ super(options);
278
252
  // Property helps `tsc` not treat a `TransactionContext` as any other
279
253
  // context type that structurally looks equivalent.
280
254
  _TransactionContext_kind.set(this, "transaction");
@@ -282,8 +256,8 @@ export class TransactionContext extends Context {
282
256
  }
283
257
  _TransactionContext_kind = new WeakMap();
284
258
  export class WorkflowContext extends Context {
285
- constructor(external, cancelled) {
286
- super(external, cancelled);
259
+ constructor(options) {
260
+ super(options);
287
261
  // Property helps `tsc` not treat a `WorkflowContext` as any other
288
262
  // context type that structurally looks equivalent.
289
263
  _WorkflowContext_kind.set(this, "workflow");
@@ -398,8 +372,21 @@ export class Auth {
398
372
  * Bearer` token when passed and optionally extract token metadata.
399
373
  */
400
374
  export class TokenVerifier {
401
- async _verifyToken(context, token) {
402
- const auth = await this.verifyToken(context, token);
375
+ async _verifyToken(external, cancelled, bytesCall) {
376
+ const call = nodejs_pb.VerifyTokenCall.fromBinary(bytesCall);
377
+ const context = Context.fromNativeExternal({
378
+ external,
379
+ kind: "reader",
380
+ stateId: call.context.stateId,
381
+ method: call.context.method,
382
+ stateTypeName: call.context.stateTypeName,
383
+ callerBearerToken: call.context.callerBearerToken,
384
+ cookie: call.context.cookie,
385
+ appInternal: call.context.appInternal,
386
+ auth: null,
387
+ cancelled,
388
+ });
389
+ const auth = await this.verifyToken(context, call.token);
403
390
  if (!auth) {
404
391
  return null;
405
392
  }
@@ -617,6 +604,14 @@ export class Application {
617
604
  return __classPrivateFieldGet(this, _Application_http, "f");
618
605
  }
619
606
  async run() {
607
+ if (process.env.REBOOT_ENABLE_EVENT_LOOP_LAG_MONITORING === "true") {
608
+ toobusy.onLag((lag) => {
609
+ console.log(`Node.js event loop lag detected! Latency: ${lag}ms.
610
+ This may indicate a blocking operation on the main thread
611
+ (e.g., CPU-intensive task). If you are not running such tasks,
612
+ please report this issue to the maintainers.`);
613
+ });
614
+ }
620
615
  return await reboot_native.Application_run(__classPrivateFieldGet(this, _Application_external, "f"));
621
616
  }
622
617
  get __external() {
@@ -897,7 +892,7 @@ const callback = (...args) => {
897
892
  console.log(new Date(), ...args);
898
893
  };
899
894
  ensurePythonVenv();
900
- reboot_native.initialize(callback, Context.fromNativeExternal, launchSubprocessConsensus);
895
+ reboot_native.initialize(callback, launchSubprocessConsensus);
901
896
  // TODO: move these into @reboot-dev/reboot-api and also generate them
902
897
  // via plugin, perhaps via a new plugin which emits the Zod schemas
903
898
  // for all protobuf messages, which might be easier after we've moved
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.33.0",
6
+ "@reboot-dev/reboot-api": "0.34.0",
7
7
  "chalk": "^4.1.2",
8
8
  "node-addon-api": "^7.0.0",
9
9
  "node-gyp": ">=10.2.0",
@@ -14,11 +14,12 @@
14
14
  "express": "^5.1.0",
15
15
  "@scarf/scarf": "1.4.0",
16
16
  "tsx": "^4.19.2",
17
- "zod": "^3.25.51"
17
+ "zod": "^3.25.51",
18
+ "toobusy-js": "^0.5.1"
18
19
  },
19
20
  "type": "module",
20
21
  "name": "@reboot-dev/reboot",
21
- "version": "0.33.0",
22
+ "version": "0.34.0",
22
23
  "description": "npm package for Reboot",
23
24
  "scripts": {
24
25
  "preinstall": "node preinstall.cjs",
package/reboot_native.cc CHANGED
@@ -2,6 +2,10 @@
2
2
  #include <pybind11/pybind11.h>
3
3
  #include <pybind11/stl.h>
4
4
 
5
+ #if __linux__
6
+ #include <sys/eventfd.h>
7
+ #endif
8
+
5
9
  #include <atomic>
6
10
  #include <future>
7
11
  #include <iostream>
@@ -21,8 +25,53 @@ using namespace pybind11::literals;
21
25
  namespace py = pybind11;
22
26
 
23
27
 
28
+ // Helper when debugging, left for future optimization work.
29
+ struct Timer {
30
+ Timer(const char* name)
31
+ : name_(name) {
32
+ start_ = std::chrono::high_resolution_clock::now();
33
+ }
34
+
35
+ void elapsed(const char* step = nullptr) const {
36
+ auto end = std::chrono::high_resolution_clock::now();
37
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
38
+ end - start_);
39
+ std::stringstream ss;
40
+ ss << name_
41
+ << (step != nullptr ? " at " + std::string(step) : std::string(""))
42
+ << " took " << duration.count() << "ms"
43
+ << std::endl;
44
+ std::cout << ss.str();
45
+ }
46
+
47
+ std::string name_;
48
+ std::chrono::time_point<std::chrono::high_resolution_clock> start_;
49
+ };
50
+
51
+
52
+ void _RunNodeFunctions(
53
+ Napi::Env env,
54
+ Napi::Function /* callback */,
55
+ void* /* context */,
56
+ void* /* data */);
57
+
58
+
24
59
  struct PythonNodeAdaptor {
25
- PythonNodeAdaptor() {}
60
+ PythonNodeAdaptor() {
61
+ #if __linux__
62
+ read_fd_ = write_fd_ = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
63
+ #else
64
+ int pipe_fds[2];
65
+ if (pipe(pipe_fds) == -1) {
66
+ perror("pipe(...)");
67
+ abort();
68
+ }
69
+
70
+ // TODO: make nonblocking and close on exec.
71
+ read_fd_ = pipe_fds[0];
72
+ write_fd_ = pipe_fds[1];
73
+ #endif
74
+ }
26
75
 
27
76
  ~PythonNodeAdaptor() {
28
77
  // We must join because thread captures `this`.
@@ -68,20 +117,41 @@ struct PythonNodeAdaptor {
68
117
  }
69
118
 
70
119
  template <typename F>
71
- void ScheduleCallbackOnPythonEventLoop(F&& f) {
72
- auto function = [f = std::forward<F>(f)]() mutable {
73
- f();
74
- };
120
+ void ScheduleCallbackOnPythonEventLoop(F&& f, bool high_priority = true) {
121
+ bool signal_fd = false;
75
122
 
76
123
  {
77
124
  std::lock_guard<std::mutex> lock(mutex);
78
- python_functions.emplace_back(std::move(function));
125
+ python_functions.emplace_back(std::move(f));
126
+ if (high_priority && should_signal_fd_) {
127
+ signal_fd = true;
128
+ should_signal_fd_ = false;
129
+ }
130
+ }
131
+
132
+ if (signal_fd) {
133
+ uint64_t one = 1;
134
+ // Writing to this file descriptor communicates to Python
135
+ // (see `public/rebootdev/nodejs/python.py`) that
136
+ // `run_functions()` should be called (while holding the Python
137
+ // GIL). In `Initialize()` we've set up `run_functions()` to run
138
+ // the `python_functions` we've just added our callback to.
139
+ if (write(write_fd_, &one, sizeof(one)) < 0) {
140
+ perror("write(...)");
141
+ abort();
142
+ }
79
143
  }
80
- python_functions_not_empty.notify_one();
81
144
  }
82
145
 
83
146
  template <typename F>
84
- void ScheduleCallbackOnNodeEventLoop(F&& f) {
147
+ void ScheduleCallbackOnPythonEventLoopLowPriority(F&& f) {
148
+ return ScheduleCallbackOnPythonEventLoop(
149
+ std::move(f),
150
+ /* high_priority = */ false);
151
+ }
152
+
153
+ template <typename F>
154
+ void ScheduleCallbackOnNodeEventLoop(F&& f, bool high_priority = true) {
85
155
  // It is possible that Node.js is shutting down and has already
86
156
  // called the `thread_safe_function` finalizer and trying to use
87
157
  // `thread_safe_function` will raise SIGABRT or SIGSEGV.
@@ -99,36 +169,67 @@ struct PythonNodeAdaptor {
99
169
  return;
100
170
  }
101
171
 
102
- napi_status status = thread_safe_function.BlockingCall(
103
- [f = std::forward<F>(f)](
104
- Napi::Env env,
105
- Napi::Function /* js_callback */) mutable {
106
- try {
107
- f(env);
108
- } catch (const std::exception& e) {
109
- PythonNodeAdaptor::HandleException(e);
110
- }
111
- });
172
+ bool call_thread_safe_function = false;
112
173
 
113
- // TODO: handle each of the possible values for `status`:
114
- //
115
- // napi_ok: the call was successfully added to the queue.
116
- //
117
- // napi_queue_full: the queue was full when trying to call in a
118
- // non-blocking method.
119
- //
120
- // napi_closing: the thread-safe function is aborted and cannot
121
- // accept more calls.
122
- //
123
- // napi_invalid_arg: the thread-safe function is closed.
124
- //
125
- // napi_generic_failure: a generic error occurred when attempting
126
- //
174
+ {
175
+ std::lock_guard<std::mutex> lock(node_functions_mutex_);
176
+ node_functions_.emplace_back(std::move(f));
177
+ if (high_priority && should_call_thread_safe_function_) {
178
+ call_thread_safe_function = true;
179
+ should_call_thread_safe_function_ = false;
180
+ }
181
+ }
182
+
183
+ if (call_thread_safe_function) {
184
+ napi_status status = thread_safe_function.BlockingCall();
185
+
186
+ // TODO: handle each of the possible values for `status`:
187
+ //
188
+ // napi_ok: the call was successfully added to the queue.
189
+ //
190
+ // napi_queue_full: the queue was full when trying to call in a
191
+ // non-blocking method.
192
+ //
193
+ // napi_closing: the thread-safe function is aborted and cannot
194
+ // accept more calls.
195
+ //
196
+ // napi_invalid_arg: the thread-safe function is closed.
197
+ //
198
+ // napi_generic_failure: a generic error occurred when attempting
199
+ //
200
+
201
+ if (status != napi_ok) {
202
+ Napi::Error::Fatal(
203
+ "ThreadEntry",
204
+ "Napi::ThreadSafeNapi::Function.BlockingCall() failed");
205
+ }
206
+ }
207
+ }
208
+
209
+ template <typename F>
210
+ void ScheduleCallbackOnNodeEventLoopLowPriority(F&& f) {
211
+ return ScheduleCallbackOnNodeEventLoop(
212
+ std::move(f),
213
+ /* high_priority = */ false);
214
+ }
127
215
 
128
- if (status != napi_ok) {
129
- Napi::Error::Fatal(
130
- "ThreadEntry",
131
- "Napi::ThreadSafeNapi::Function.BlockingCall() failed");
216
+ void RunNodeFunctions(Napi::Env& env) {
217
+ std::list<std::function<void(Napi::Env&)>> functions;
218
+ {
219
+ std::lock_guard<std::mutex> lock(node_functions_mutex_);
220
+ functions = std::move(node_functions_);
221
+ should_call_thread_safe_function_ = true;
222
+ }
223
+ for (auto&& function : functions) {
224
+ try {
225
+ function(env);
226
+ } catch (const std::exception& e) {
227
+ std::cerr
228
+ << "Unexpected exception: " << e.what()
229
+ << "\n"
230
+ << "Please report this bug to the maintainers!"
231
+ << std::endl;
232
+ }
132
233
  }
133
234
  }
134
235
 
@@ -162,23 +263,29 @@ struct PythonNodeAdaptor {
162
263
  // into Python or expecting calls from Python.
163
264
  std::atomic<int> references{0};
164
265
 
165
- Napi::ThreadSafeFunction thread_safe_function;
266
+ Napi::TypedThreadSafeFunction<
267
+ void,
268
+ void,
269
+ _RunNodeFunctions>
270
+ thread_safe_function;
271
+ std::mutex node_functions_mutex_;
272
+ std::list<std::function<void(Napi::Env&)>> node_functions_;
273
+ bool should_call_thread_safe_function_ = true;
166
274
 
167
275
  bool thread_safe_function_finalized = false;
168
276
 
169
277
  std::thread thread;
170
278
  std::mutex mutex;
171
- std::condition_variable python_functions_not_empty;
172
279
  std::list<std::function<void()>> python_functions;
280
+ int read_fd_ = -1;
281
+ int write_fd_ = -1;
282
+ bool should_signal_fd_ = true;
173
283
  };
174
284
 
175
285
 
176
286
  static PythonNodeAdaptor* adaptor = new PythonNodeAdaptor();
177
287
 
178
288
  // References to nodejs functions so we don't have to look them up.
179
- static Napi::FunctionReference* js_Context_fromNativeExternal =
180
- new Napi::FunctionReference();
181
-
182
289
  static Napi::FunctionReference* js_launchSubprocessConsensus =
183
290
  new Napi::FunctionReference();
184
291
 
@@ -186,7 +293,7 @@ static Napi::FunctionReference* js_launchSubprocessConsensus =
186
293
  struct NapiReferenceDeleter {
187
294
  template <typename T>
188
295
  void operator()(Napi::Reference<T>* reference) {
189
- adaptor->ScheduleCallbackOnNodeEventLoop(
296
+ adaptor->ScheduleCallbackOnNodeEventLoopLowPriority(
190
297
  [reference](Napi::Env) {
191
298
  delete reference;
192
299
  });
@@ -246,7 +353,7 @@ Napi::External<py::object> make_napi_external(
246
353
  env,
247
354
  py_object,
248
355
  [](Napi::Env, py::object* py_object) {
249
- adaptor->ScheduleCallbackOnPythonEventLoop(
356
+ adaptor->ScheduleCallbackOnPythonEventLoopLowPriority(
250
357
  [py_object]() {
251
358
  delete py_object;
252
359
  });
@@ -260,18 +367,30 @@ Napi::External<py::object> make_napi_external(
260
367
  }
261
368
 
262
369
 
370
+ void _RunNodeFunctions(
371
+ Napi::Env env,
372
+ Napi::Function /* callback */,
373
+ void* /* context */,
374
+ void* /* data */) {
375
+ adaptor->RunNodeFunctions(env);
376
+ }
377
+
378
+
263
379
  void PythonNodeAdaptor::Initialize(
264
380
  Napi::Env& env,
265
381
  const Napi::Function& js_callback) {
266
- thread_safe_function = Napi::ThreadSafeFunction::New(
382
+ thread_safe_function = Napi::TypedThreadSafeFunction<
383
+ void,
384
+ void,
385
+ _RunNodeFunctions>::New(
267
386
  /* Environment: */ env,
268
387
  /* JS callback: */ js_callback,
269
388
  /* Resource name: */ "reboot_native",
270
389
  /* Max queue size (0 = unlimited): */ 0,
271
390
  /* Initial thread count: */ 1,
272
- /* Context: */ this,
391
+ /* Context: */ (void*) nullptr,
273
392
  // Finalizer:
274
- [this](Napi::Env env, void*, PythonNodeAdaptor*) {
393
+ [this](Napi::Env env, void*, void*) {
275
394
  // Set that we've been finalized so that we don't use
276
395
  // `thread_safe_function` again, see comment in
277
396
  // `ScheduleCallbackOnNodeEventLoop`.
@@ -345,35 +464,35 @@ void PythonNodeAdaptor::Initialize(
345
464
  return py_future;
346
465
  });
347
466
 
348
- py::object event_loop_thread = module.attr("EventLoopThread")();
467
+ module.attr("run_functions") = py::cpp_function(
468
+ [this]() {
469
+ std::list<std::function<void()>> functions;
470
+ std::vector<std::pair<py::object*, std::string>> futures;
471
+
472
+ {
473
+ std::unique_lock<std::mutex> lock(mutex);
474
+ functions = std::move(python_functions);
475
+ should_signal_fd_ = true;
476
+ }
477
+
478
+ for (auto&& function : functions) {
479
+ try {
480
+ function();
481
+ } catch (std::exception& e) {
482
+ PythonNodeAdaptor::HandleException(e);
483
+ }
484
+ }
485
+ });
486
+
487
+ py::object event_loop_thread = module.attr("EventLoopThread")(
488
+ read_fd_);
349
489
 
350
490
  py::gil_scoped_release release;
351
491
 
352
492
  while (true) {
353
- std::function<void()> function;
354
-
355
- {
356
- std::unique_lock<std::mutex> lock(mutex);
357
- python_functions_not_empty.wait(
358
- lock,
359
- [this] { return !python_functions.empty(); });
360
-
361
- function = std::move(python_functions.front());
362
- python_functions.pop_front();
363
- }
364
-
365
- {
366
- py::gil_scoped_acquire acquire;
367
-
368
- event_loop_thread.attr("run_callback_on_event_loop")(
369
- py::cpp_function([function = std::move(function)]() {
370
- try {
371
- function();
372
- } catch (const std::exception& e) {
373
- PythonNodeAdaptor::HandleException(e);
374
- }
375
- }));
376
- }
493
+ std::this_thread::sleep_until(
494
+ std::chrono::time_point<std::chrono::system_clock>::max());
495
+ continue;
377
496
  }
378
497
  } catch (const std::exception& e) {
379
498
  std::cout << e.what() << std::endl;
@@ -968,12 +1087,8 @@ void Initialize(const Napi::CallbackInfo& info) {
968
1087
  std::call_once(initialize_once, [&]() {
969
1088
  Napi::Env env = info.Env();
970
1089
 
971
- js_Context_fromNativeExternal->Reset(
972
- info[1].As<Napi::Function>(),
973
- /* refcount = */ 1);
974
-
975
1090
  js_launchSubprocessConsensus->Reset(
976
- info[2].As<Napi::Function>(),
1091
+ info[1].As<Napi::Function>(),
977
1092
  /* refcount = */ 1);
978
1093
 
979
1094
  // NOTE: must initialize after storing above nodejs functions.
@@ -1133,15 +1248,10 @@ Napi::Value Reboot_createExternalContext(const Napi::CallbackInfo& info) {
1133
1248
 
1134
1249
 
1135
1250
  // NOTE: must be called within _Node_.
1136
- Napi::Object make_js_context(
1251
+ Napi::Promise make_js_cancelled(
1137
1252
  Napi::Env& env,
1138
- py::object* py_context,
1139
- py::object* py_cancelled,
1140
- const std::string& kind) {
1141
- Napi::External<py::object> js_external_context =
1142
- make_napi_external(env, py_context);
1143
-
1144
- Napi::Promise js_cancelled = NodePromiseFromPythonFuture(
1253
+ py::object* py_cancelled) {
1254
+ return NodePromiseFromPythonFuture(
1145
1255
  env,
1146
1256
  [py_cancelled]() {
1147
1257
  // After returning we won't need `py_cancelled` anymore, so we
@@ -1161,13 +1271,6 @@ Napi::Object make_js_context(
1161
1271
  // this lambda or the one above.
1162
1272
  return env.Undefined();
1163
1273
  });
1164
-
1165
- return js_Context_fromNativeExternal
1166
- ->Call(
1167
- {js_external_context,
1168
- Napi::String::New(env, kind),
1169
- js_cancelled})
1170
- .As<Napi::Object>();
1171
1274
  }
1172
1275
 
1173
1276
 
@@ -1191,11 +1294,9 @@ py::object make_py_authorizer(NapiSafeObjectReference js_authorizer) {
1191
1294
  // Trampolines us from Python through C++ into Node.
1192
1295
  attributes["_authorize"] = py::cpp_function(
1193
1296
  [](py::object self,
1194
- const std::string& method_name,
1195
1297
  py::object py_reader_context,
1196
1298
  py::object py_cancelled,
1197
- std::optional<std::string> bytes_state,
1198
- std::optional<std::string> bytes_request) {
1299
+ std::string bytes_call) {
1199
1300
  NapiSafeObjectReference* js_authorizer_reference =
1200
1301
  self.attr("_js_authorizer")
1201
1302
  .cast<NapiSafeObjectReference*>();
@@ -1204,30 +1305,21 @@ py::object make_py_authorizer(NapiSafeObjectReference js_authorizer) {
1204
1305
  [js_authorizer_reference,
1205
1306
  py_reader_context = new py::object(py_reader_context),
1206
1307
  py_cancelled = new py::object(py_cancelled),
1207
- method_name,
1208
- bytes_state = std::move(bytes_state),
1209
- bytes_request = std::move(bytes_request)](Napi::Env env) {
1308
+ bytes_call = std::move(bytes_call)](Napi::Env env) {
1210
1309
  std::vector<Napi::Value> js_args;
1211
1310
 
1212
- Napi::Object js_context = make_js_context(
1213
- env,
1214
- py_reader_context,
1215
- py_cancelled,
1216
- "reader");
1311
+ Napi::External<py::object> js_external_context =
1312
+ make_napi_external(env, py_reader_context);
1217
1313
 
1218
- Napi::Object js_authorizer = js_authorizer_reference->Value(env);
1314
+ js_args.push_back(js_external_context);
1315
+
1316
+ Napi::Promise js_cancelled = make_js_cancelled(env, py_cancelled);
1219
1317
 
1220
- Napi::Value js_bytes_request = bytes_request.has_value()
1221
- ? str_to_uint8array(env, *bytes_request)
1222
- : env.Undefined();
1223
- Napi::Value js_bytes_state = bytes_state.has_value()
1224
- ? str_to_uint8array(env, *bytes_state)
1225
- : env.Undefined();
1318
+ js_args.push_back(js_cancelled);
1226
1319
 
1227
- js_args.push_back(Napi::String::New(env, method_name));
1228
- js_args.push_back(js_context);
1229
- js_args.push_back(js_bytes_state);
1230
- js_args.push_back(js_bytes_request);
1320
+ js_args.push_back(str_to_uint8array(env, bytes_call));
1321
+
1322
+ Napi::Object js_authorizer = js_authorizer_reference->Value(env);
1231
1323
 
1232
1324
  return js_authorizer.Get("_authorize")
1233
1325
  .As<Napi::Function>()
@@ -1243,11 +1335,9 @@ py::object make_py_authorizer(NapiSafeObjectReference js_authorizer) {
1243
1335
  });
1244
1336
  },
1245
1337
  py::name("_authorize"),
1246
- py::arg("method_name"),
1247
1338
  py::arg("context"),
1248
1339
  py::arg("cancelled"),
1249
- py::arg("state"),
1250
- py::arg("request"),
1340
+ py::arg("bytes_call"),
1251
1341
  py::is_method(py::none()));
1252
1342
 
1253
1343
  // Now define our subclass.
@@ -1412,41 +1502,41 @@ py::object make_py_user_servicer(
1412
1502
  // calls.
1413
1503
  attributes["_trampoline"] = py::cpp_function(
1414
1504
  [](NapiSafeObjectReference& js_servicer_reference,
1415
- const std::string& kind,
1416
- const std::string& method,
1417
1505
  py::object py_context,
1418
1506
  py::object py_cancelled,
1419
- const std::string& json_state,
1420
- const std::string& json_request) {
1507
+ std::string bytes_call) {
1421
1508
  return PythonFutureFromNodePromise(
1422
1509
  [&js_servicer_reference,
1423
- kind,
1424
- method,
1425
1510
  py_context = new py::object(py_context),
1426
1511
  py_cancelled = new py::object(py_cancelled),
1427
- json_state,
1428
- json_request](Napi::Env env) {
1512
+ bytes_call = std::move(bytes_call)](Napi::Env env) {
1429
1513
  std::vector<Napi::Value> js_args;
1430
1514
 
1431
- Napi::Object js_context =
1432
- make_js_context(env, py_context, py_cancelled, kind);
1515
+ Napi::External<py::object> js_external_context =
1516
+ make_napi_external(env, py_context);
1517
+
1518
+ js_args.push_back(js_external_context);
1433
1519
 
1434
- js_args.push_back(js_context);
1520
+ Napi::Promise js_cancelled = make_js_cancelled(env, py_cancelled);
1435
1521
 
1436
- js_args.push_back(Napi::String::New(env, json_state));
1437
- js_args.push_back(Napi::String::New(env, json_request));
1522
+ js_args.push_back(js_cancelled);
1523
+
1524
+ js_args.push_back(str_to_uint8array(env, bytes_call));
1438
1525
 
1439
1526
  Napi::Object js_servicer =
1440
1527
  js_servicer_reference.Value(env);
1441
1528
 
1442
1529
  return js_servicer
1443
- .Get("_" + method)
1530
+ .Get("__dispatch")
1444
1531
  .As<Napi::Function>()
1445
1532
  .Call(js_servicer, js_args)
1446
1533
  .As<Napi::Object>();
1447
1534
  },
1448
1535
  [](Napi::Env env, Napi::Value value) {
1449
- return std::string(value.As<Napi::String>().Utf8Value());
1536
+ return uint8array_to_str(value.As<Napi::Uint8Array>());
1537
+ },
1538
+ [](std::string&& bytes_result) -> py::object {
1539
+ return py::bytes(bytes_result);
1450
1540
  });
1451
1541
  });
1452
1542
 
@@ -1562,19 +1652,11 @@ py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
1562
1652
  [](py::object self,
1563
1653
  py::object py_reader_context,
1564
1654
  py::object py_cancelled,
1565
- py::object py_token) {
1655
+ std::string bytes_call) {
1566
1656
  NapiSafeObjectReference* js_token_verifier_reference =
1567
1657
  self.attr("_js_token_verifier")
1568
1658
  .cast<NapiSafeObjectReference*>();
1569
1659
 
1570
- // Converting to 'py::str' involves Python runtime access.
1571
- // Perform this conversion here, on the Python thread, before entering
1572
- // Node.
1573
- std::optional<std::string> token;
1574
- if (!py_token.is_none()) {
1575
- token = py::str(py_token);
1576
- }
1577
-
1578
1660
  return PythonFutureFromNodePromise(
1579
1661
  [js_token_verifier_reference,
1580
1662
  // We allocate 'py_cancelled' and 'py_reader_context' with 'new
@@ -1583,21 +1665,19 @@ py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
1583
1665
  // thread, see 'make_js_context'.
1584
1666
  py_reader_context = new py::object(py_reader_context),
1585
1667
  py_cancelled = new py::object(py_cancelled),
1586
- token = std::move(token)](Napi::Env env) {
1668
+ bytes_call = std::move(bytes_call)](Napi::Env env) {
1587
1669
  std::vector<Napi::Value> js_args;
1588
1670
 
1589
- js_args.push_back(
1590
- make_js_context(
1591
- env,
1592
- py_reader_context,
1593
- py_cancelled,
1594
- "reader"));
1671
+ Napi::External<py::object> js_external_context =
1672
+ make_napi_external(env, py_reader_context);
1595
1673
 
1596
- if (token.has_value()) {
1597
- js_args.push_back(Napi::String::New(env, *token));
1598
- } else {
1599
- js_args.push_back(env.Null());
1600
- }
1674
+ js_args.push_back(js_external_context);
1675
+
1676
+ Napi::Promise js_cancelled = make_js_cancelled(env, py_cancelled);
1677
+
1678
+ js_args.push_back(js_cancelled);
1679
+
1680
+ js_args.push_back(str_to_uint8array(env, bytes_call));
1601
1681
 
1602
1682
  Napi::Object js_token_verifier =
1603
1683
  js_token_verifier_reference->Value(env);
@@ -1626,7 +1706,7 @@ py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
1626
1706
  py::name("_verify_token"),
1627
1707
  py::arg("context"),
1628
1708
  py::arg("cancelled"),
1629
- py::arg("py_token"),
1709
+ py::arg("bytes_call"),
1630
1710
  py::is_method(py::none()));
1631
1711
 
1632
1712
  py::object py_parent_class =
@@ -2331,151 +2411,6 @@ Napi::Value Application_run(const Napi::CallbackInfo& info) {
2331
2411
  }
2332
2412
 
2333
2413
 
2334
- Napi::Value Context_auth(const Napi::CallbackInfo& info) {
2335
- Napi::External<py::object> js_external_context =
2336
- info[0].As<Napi::External<py::object>>();
2337
-
2338
- // CHECK(...CheckTypeTag(...));
2339
-
2340
- py::object* py_context = js_external_context.Data();
2341
-
2342
- std::optional<std::string> auth_bytes = RunCallbackOnPythonEventLoop(
2343
- [py_context]() -> std::optional<std::string> {
2344
- py::object py_auth = py_context->attr("auth");
2345
-
2346
- if (py_auth.is_none()) {
2347
- return std::nullopt;
2348
- } else {
2349
- std::string auth_bytes = py::bytes(py_auth.attr("to_proto_bytes")());
2350
- return auth_bytes;
2351
- }
2352
- });
2353
-
2354
- if (auth_bytes.has_value()) {
2355
- Napi::Env env = info.Env();
2356
- return str_to_uint8array(env, *auth_bytes);
2357
- } else {
2358
- return info.Env().Null();
2359
- }
2360
- }
2361
-
2362
-
2363
- Napi::Value Context_stateId(const Napi::CallbackInfo& info) {
2364
- Napi::External<py::object> js_external_context =
2365
- info[0].As<Napi::External<py::object>>();
2366
-
2367
- // CHECK(...CheckTypeTag(...));
2368
-
2369
- py::object* py_context = js_external_context.Data();
2370
-
2371
- std::string state_id = RunCallbackOnPythonEventLoop(
2372
- [py_context]() {
2373
- py::str state_id = py_context->attr("state_id");
2374
- return std::string(state_id);
2375
- });
2376
-
2377
- return Napi::String::New(info.Env(), state_id);
2378
- }
2379
-
2380
-
2381
- Napi::Value Context_stateTypeName(const Napi::CallbackInfo& info) {
2382
- Napi::External<py::object> js_external_context =
2383
- info[0].As<Napi::External<py::object>>();
2384
-
2385
- // CHECK(...CheckTypeTag(...));
2386
-
2387
- py::object* py_context = js_external_context.Data();
2388
-
2389
- std::string state_type_name = RunCallbackOnPythonEventLoop(
2390
- [py_context]() {
2391
- py::str state_type_name = py_context->attr("state_type_name");
2392
- return std::string(state_type_name);
2393
- });
2394
-
2395
- return Napi::String::New(info.Env(), state_type_name);
2396
- }
2397
-
2398
-
2399
- Napi::Value Context_method(const Napi::CallbackInfo& info) {
2400
- Napi::External<py::object> js_external_context =
2401
- info[0].As<Napi::External<py::object>>();
2402
-
2403
- // CHECK(...CheckTypeTag(...));
2404
-
2405
- py::object* py_context = js_external_context.Data();
2406
-
2407
- std::string method = RunCallbackOnPythonEventLoop(
2408
- [py_context]() {
2409
- py::str method = py_context->attr("method");
2410
- return std::string(method);
2411
- });
2412
-
2413
- return Napi::String::New(info.Env(), method);
2414
- }
2415
-
2416
-
2417
- Napi::Value Context_callerBearerToken(
2418
- const Napi::CallbackInfo& info) {
2419
- Napi::External<py::object> js_external_context =
2420
- info[0].As<Napi::External<py::object>>();
2421
-
2422
- // CHECK(...CheckTypeTag(...));
2423
-
2424
- py::object* py_context = js_external_context.Data();
2425
-
2426
- std::optional<std::string> caller_bearer_token = RunCallbackOnPythonEventLoop(
2427
- [py_context]() -> std::optional<std::string> {
2428
- py::object caller_bearer_token = py_context->attr(
2429
- "caller_bearer_token");
2430
- if (caller_bearer_token.is_none()) {
2431
- return std::nullopt;
2432
- } else {
2433
- return std::string(py::str(caller_bearer_token));
2434
- }
2435
- });
2436
- if (caller_bearer_token.has_value()) {
2437
- return Napi::String::New(info.Env(), *caller_bearer_token);
2438
- } else {
2439
- return info.Env().Null();
2440
- }
2441
- }
2442
-
2443
-
2444
- Napi::Value Context_cookie(const Napi::CallbackInfo& info) {
2445
- Napi::External<py::object> js_external_context =
2446
- info[0].As<Napi::External<py::object>>();
2447
-
2448
- // CHECK(...CheckTypeTag(...));
2449
-
2450
- py::object* py_context = js_external_context.Data();
2451
-
2452
- std::string cookie = RunCallbackOnPythonEventLoop(
2453
- [py_context]() {
2454
- py::str cookie = py_context->attr("cookie");
2455
- return std::string(cookie);
2456
- });
2457
-
2458
- return Napi::String::New(info.Env(), cookie);
2459
- }
2460
-
2461
-
2462
- Napi::Value Context_appInternal(const Napi::CallbackInfo& info) {
2463
- Napi::External<py::object> js_external_context =
2464
- info[0].As<Napi::External<py::object>>();
2465
-
2466
- // CHECK(...CheckTypeTag(...));
2467
-
2468
- py::object* py_context = js_external_context.Data();
2469
-
2470
- bool app_internal = RunCallbackOnPythonEventLoop(
2471
- [py_context]() {
2472
- return py_context->attr("app_internal").cast<bool>();
2473
- });
2474
-
2475
- return Napi::Boolean::New(info.Env(), app_internal);
2476
- }
2477
-
2478
-
2479
2414
  Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
2480
2415
  // NOTE: we immediately get a safe reference to the `Napi::External`
2481
2416
  // so that Node will not garbage collect it and the `py::object*` we
@@ -2911,34 +2846,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
2911
2846
  Napi::String::New(env, "Service_call"),
2912
2847
  Napi::Function::New<Service_call>(env));
2913
2848
 
2914
- exports.Set(
2915
- Napi::String::New(env, "Context_auth"),
2916
- Napi::Function::New<Context_auth>(env));
2917
-
2918
- exports.Set(
2919
- Napi::String::New(env, "Context_stateId"),
2920
- Napi::Function::New<Context_stateId>(env));
2921
-
2922
- exports.Set(
2923
- Napi::String::New(env, "Context_stateTypeName"),
2924
- Napi::Function::New<Context_stateTypeName>(env));
2925
-
2926
- exports.Set(
2927
- Napi::String::New(env, "Context_method"),
2928
- Napi::Function::New<Context_method>(env));
2929
-
2930
- exports.Set(
2931
- Napi::String::New(env, "Context_callerBearerToken"),
2932
- Napi::Function::New<Context_callerBearerToken>(env));
2933
-
2934
- exports.Set(
2935
- Napi::String::New(env, "Context_cookie"),
2936
- Napi::Function::New<Context_cookie>(env));
2937
-
2938
- exports.Set(
2939
- Napi::String::New(env, "Context_appInternal"),
2940
- Napi::Function::New<Context_appInternal>(env));
2941
-
2942
2849
  exports.Set(
2943
2850
  Napi::String::New(env, "Context_generateIdempotentStateId"),
2944
2851
  Napi::Function::New<Context_generateIdempotentStateId>(env));
package/reboot_native.cjs CHANGED
@@ -74,14 +74,6 @@ exports.Reboot_stop = reboot_native.exports.Reboot_stop;
74
74
  exports.Reboot_up = reboot_native.exports.Reboot_up;
75
75
  exports.Reboot_down = reboot_native.exports.Reboot_down;
76
76
  exports.Reboot_url = reboot_native.exports.Reboot_url;
77
- exports.Context_auth = reboot_native.exports.Context_auth;
78
- exports.Context_stateId = reboot_native.exports.Context_stateId;
79
- exports.Context_stateTypeName = reboot_native.exports.Context_stateTypeName;
80
- exports.Context_method = reboot_native.exports.Context_method;
81
- exports.Context_callerBearerToken =
82
- reboot_native.exports.Context_callerBearerToken;
83
- exports.Context_cookie = reboot_native.exports.Context_cookie;
84
- exports.Context_appInternal = reboot_native.exports.Context_appInternal;
85
77
  exports.Context_generateIdempotentStateId =
86
78
  reboot_native.exports.Context_generateIdempotentStateId;
87
79
  exports.WriterContext_set_sync = reboot_native.exports.WriterContext_set_sync;
package/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const REBOOT_VERSION = "0.33.0";
1
+ export declare const REBOOT_VERSION = "0.34.0";
package/version.js CHANGED
@@ -1 +1 @@
1
- export const REBOOT_VERSION = "0.33.0";
1
+ export const REBOOT_VERSION = "0.34.0";