@reboot-dev/reboot 0.23.0 → 0.25.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
@@ -1,8 +1,9 @@
1
1
  import { errors_pb, IdempotencyOptions, protobuf_es, ScheduleOptions } from "@reboot-dev/reboot-api";
2
+ import express from "express";
2
3
  import * as reboot_native from "./reboot_native.cjs";
3
4
  export { reboot_native };
4
5
  export * from "./utils/index.js";
5
- type ApplicationConfig = {
6
+ type ApplicationRevision = {
6
7
  applicationId: string;
7
8
  };
8
9
  export declare class Reboot {
@@ -16,10 +17,9 @@ export declare class Reboot {
16
17
  }): ExternalContext;
17
18
  start(): Promise<void>;
18
19
  stop(): Promise<void>;
19
- up(servicers: any, options?: {
20
- tokenVerifier?: TokenVerifier;
20
+ up(application: Application, options?: {
21
21
  localEnvoy?: boolean;
22
- }): Promise<ApplicationConfig>;
22
+ }): Promise<ApplicationRevision>;
23
23
  down(): Promise<void>;
24
24
  url(): string;
25
25
  }
@@ -48,8 +48,10 @@ export declare class Context {
48
48
  static fromNativeExternal(external: any, kind: string, aborted: Promise<void>): ReaderContext | WriterContext | TransactionContext | WorkflowContext;
49
49
  get __external(): any;
50
50
  get auth(): Auth | null;
51
- get stateId(): any;
52
- get iteration(): any;
51
+ get stateId(): string;
52
+ get iteration(): number;
53
+ get cookie(): string;
54
+ get appInternal(): boolean;
53
55
  generateIdempotentStateId(stateType: string, serviceName: string, method: string, idempotency: IdempotencyOptions): Promise<any>;
54
56
  }
55
57
  export declare class ReaderContext extends Context {
@@ -117,8 +119,8 @@ export declare abstract class TokenVerifier {
117
119
  * Returns:
118
120
  * `Auth` information if the token is valid, null otherwise.
119
121
  */
120
- abstract verifyToken(context: ReaderContext, token: string): Promise<Auth | null>;
121
- _verifyToken(context: ReaderContext, token: string): Promise<Uint8Array | null>;
122
+ abstract verifyToken(context: ReaderContext, token?: string): Promise<Auth | null>;
123
+ _verifyToken(context: ReaderContext, token?: string): Promise<Uint8Array | null>;
122
124
  }
123
125
  export type AuthorizerDecision = errors_pb.Unauthenticated | errors_pb.PermissionDenied | errors_pb.Ok;
124
126
  /**
@@ -144,12 +146,38 @@ export declare abstract class Authorizer<StateType, RequestTypes> {
144
146
  abstract authorize(methodName: string, context: ReaderContext, state?: StateType, request?: RequestTypes): Promise<AuthorizerDecision>;
145
147
  _authorize?: (methodName: string, context: ReaderContext, bytesState?: Uint8Array, bytesRequest?: Uint8Array) => Promise<Uint8Array>;
146
148
  }
147
- /**
148
- * An authorizer that allows all requests, as long as the caller is authenticated.
149
- */
150
- export declare class AllowAllIfAuthenticated extends Authorizer<protobuf_es.Message, protobuf_es.Message> {
151
- authorize(methodName: string, context: ReaderContext, state?: protobuf_es.Message, request?: protobuf_es.Message): Promise<AuthorizerDecision>;
149
+ export type AuthorizerCallable<StateType, RequestType> = (args: {
150
+ context: ReaderContext;
151
+ state?: StateType;
152
+ request?: RequestType;
153
+ }) => Promise<AuthorizerDecision> | AuthorizerDecision;
154
+ export declare abstract class AuthorizerRule<StateType, RequestType> {
155
+ abstract execute(args: {
156
+ context: ReaderContext;
157
+ state?: StateType;
158
+ request?: RequestType;
159
+ }): Promise<AuthorizerDecision>;
152
160
  }
161
+ export declare function deny(): AuthorizerRule<protobuf_es.Message, protobuf_es.Message>;
162
+ export declare function allow(): AuthorizerRule<protobuf_es.Message, protobuf_es.Message>;
163
+ export declare function allowIf<StateType, RequestType>(args: {
164
+ all: AuthorizerCallable<StateType, RequestType>[];
165
+ any?: never;
166
+ }): AuthorizerRule<StateType, RequestType>;
167
+ export declare function allowIf<StateType, RequestType>(args: {
168
+ all?: never;
169
+ any: AuthorizerCallable<StateType, RequestType>[];
170
+ }): AuthorizerRule<StateType, RequestType>;
171
+ export declare function hasVerifiedToken({ context, }: {
172
+ context: ReaderContext;
173
+ state?: protobuf_es.Message;
174
+ request?: protobuf_es.Message;
175
+ }): errors_pb.Unauthenticated | errors_pb.Ok;
176
+ export declare function isAppInternal({ context, }: {
177
+ context: ReaderContext;
178
+ state?: protobuf_es.Message;
179
+ request?: protobuf_es.Message;
180
+ }): errors_pb.Unauthenticated | errors_pb.Ok;
153
181
  export declare class Application {
154
182
  #private;
155
183
  constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }: {
@@ -158,7 +186,30 @@ export declare class Application {
158
186
  initializeBearerToken?: string;
159
187
  tokenVerifier?: TokenVerifier;
160
188
  });
189
+ get servicers(): ServicerFactory[];
190
+ get tokenVerifier(): TokenVerifier;
191
+ get http(): Application.Http;
161
192
  run(): Promise<any>;
193
+ get __external(): any;
194
+ }
195
+ export declare namespace Application {
196
+ namespace Http {
197
+ type Path = string | RegExp | (string | RegExp)[];
198
+ type ReqResHandler = (context: ExternalContext, req: express.Request, res: express.Response) => any;
199
+ type ReqResNextHandler = (context: ExternalContext, req: express.Request, res: express.Response, next: express.NextFunction) => any;
200
+ type ErrReqResNextHandler = (context: ExternalContext, err: any, req: express.Request, res: express.Response, next: express.NextFunction) => any;
201
+ type Handler = ReqResHandler | ReqResNextHandler | ErrReqResNextHandler;
202
+ }
203
+ class Http {
204
+ #private;
205
+ constructor(express: express.Application, createExternalContext: (args: {
206
+ name: string;
207
+ bearerToken?: string;
208
+ }) => Promise<ExternalContext>);
209
+ private add;
210
+ get(path: Http.Path, ...handlers: Http.Handler[]): void;
211
+ post(path: Http.Path, ...handlers: Http.Handler[]): void;
212
+ }
162
213
  }
163
214
  /**
164
215
  * @deprecated testWithReboot is deprecated in favor of the manual start of Reboot in the test setup.
@@ -171,20 +222,44 @@ export declare class Loop {
171
222
  export declare function retry_reactively_until(context: WorkflowContext, condition: () => Promise<boolean>): Promise<void>;
172
223
  export declare function retry_reactively_until<T>(context: WorkflowContext, condition: () => Promise<false | Exclude<T, boolean>>): Promise<Exclude<T, boolean>>;
173
224
  export declare function atMostOnce(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
174
- parse: undefined;
225
+ stringify?: undefined;
226
+ parse?: undefined;
227
+ validate?: undefined;
175
228
  }): Promise<void>;
176
229
  export declare function atMostOnce<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
177
- parse: (value: any) => T;
230
+ stringify?: (result: T) => string;
231
+ parse: (value: string) => T;
232
+ validate?: undefined;
233
+ } | {
234
+ stringify?: (result: T) => string;
235
+ parse?: undefined;
236
+ validate: (result: T) => boolean;
178
237
  }): Promise<T>;
179
238
  export declare function atLeastOnce(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
180
- parse: undefined;
239
+ stringify?: undefined;
240
+ parse?: undefined;
241
+ validate?: undefined;
181
242
  }): Promise<void>;
182
243
  export declare function atLeastOnce<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
183
- parse: (value: any) => T;
244
+ stringify?: (result: T) => string;
245
+ parse: (value: string) => T;
246
+ validate?: undefined;
247
+ } | {
248
+ stringify?: (result: T) => string;
249
+ parse?: undefined;
250
+ validate: (result: T) => boolean;
184
251
  }): Promise<T>;
185
252
  export declare function until(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<boolean>, options?: {
186
- parse: undefined;
253
+ stringify?: undefined;
254
+ parse?: undefined;
255
+ validate?: undefined;
187
256
  }): Promise<void>;
188
257
  export declare function until<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<false | Exclude<T, boolean>>, options: {
189
- parse: (value: any) => T;
258
+ stringify?: (result: T) => string;
259
+ parse: (value: string) => T;
260
+ validate?: undefined;
261
+ } | {
262
+ stringify?: (result: T) => string;
263
+ parse?: undefined;
264
+ validate: (result: T) => boolean;
190
265
  }): Promise<Exclude<T, boolean>>;
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, _ReaderContext_kind, _WriterContext_kind, _TransactionContext_kind, _WorkflowContext_kind, _Application_external;
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
13
  import { auth_pb, errors_pb, protobuf_es, } from "@reboot-dev/reboot-api";
14
14
  import { strict as assert } from "assert";
15
+ import express from "express";
16
+ import { AsyncLocalStorage } from "node:async_hooks";
15
17
  import { fork } from "node:child_process";
16
18
  import { test } from "node:test";
17
- import { AsyncLocalStorage } from "node:async_hooks";
18
19
  import * as reboot_native from "./reboot_native.cjs";
19
20
  import { ensurePythonVenv } from "./venv.js";
20
21
  export { reboot_native };
@@ -49,6 +50,12 @@ if (process != null) {
49
50
  // exits due to a SIGINT will exit with a code of 130.
50
51
  checkIfNoOtherListenersAndIfSoExit("SIGINT", 130);
51
52
  });
53
+ process.on("unhandledRejection", (reason, promise) => {
54
+ // We install a slightly quieter unhandled-rejection handler because the
55
+ // native portion of Reboot renders useful error messages before raising.
56
+ console.error("Exiting:", reason);
57
+ checkIfNoOtherListenersAndIfSoExit("unhandledRejection", 1);
58
+ });
52
59
  }
53
60
  export class Reboot {
54
61
  constructor() {
@@ -78,10 +85,10 @@ export class Reboot {
78
85
  startedInstances.splice(indexOfTimestamp, 1);
79
86
  }
80
87
  }
81
- async up(servicers, options) {
88
+ async up(application, options) {
82
89
  // TODO(benh): determine module and file name so that we can
83
90
  // namespace if we have more than one implementation of servicers.
84
- return await reboot_native.Reboot_up(__classPrivateFieldGet(this, _Reboot_external, "f"), servicers, options?.tokenVerifier, options?.localEnvoy);
91
+ return await reboot_native.Reboot_up(__classPrivateFieldGet(this, _Reboot_external, "f"), application.__external, options?.localEnvoy);
85
92
  }
86
93
  async down() {
87
94
  await reboot_native.Reboot_down(__classPrivateFieldGet(this, _Reboot_external, "f"));
@@ -169,6 +176,12 @@ export class Context {
169
176
  get iteration() {
170
177
  return reboot_native.Context_iteration(__classPrivateFieldGet(this, _Context_external, "f"));
171
178
  }
179
+ get cookie() {
180
+ return reboot_native.Context_cookie(__classPrivateFieldGet(this, _Context_external, "f"));
181
+ }
182
+ get appInternal() {
183
+ return reboot_native.Context_appInternal(__classPrivateFieldGet(this, _Context_external, "f"));
184
+ }
172
185
  async generateIdempotentStateId(stateType, serviceName, method, idempotency) {
173
186
  return reboot_native.Context_generateIdempotentStateId(__classPrivateFieldGet(this, _Context_external, "f"), stateType, serviceName, method, idempotency);
174
187
  }
@@ -315,21 +328,173 @@ export class TokenVerifier {
315
328
  */
316
329
  export class Authorizer {
317
330
  }
318
- /**
319
- * An authorizer that allows all requests, as long as the caller is authenticated.
320
- */
321
- export class AllowAllIfAuthenticated extends Authorizer {
322
- async authorize(methodName, context, state, request) {
323
- if (!context.auth) {
324
- return new errors_pb.Unauthenticated();
331
+ export class AuthorizerRule {
332
+ }
333
+ export function deny() {
334
+ return new (class extends AuthorizerRule {
335
+ async execute(args) {
336
+ return new errors_pb.PermissionDenied();
337
+ }
338
+ })();
339
+ }
340
+ export function allow() {
341
+ return new (class extends AuthorizerRule {
342
+ async execute(args) {
343
+ return new errors_pb.Ok();
325
344
  }
345
+ })();
346
+ }
347
+ export function allowIf(args) {
348
+ const all = args.all;
349
+ const any = args.any;
350
+ if ((all === undefined && any === undefined) ||
351
+ (all !== undefined && any !== undefined)) {
352
+ throw new Error("Exactly one of `all` or `any` must be passed");
353
+ }
354
+ const callables = all ?? any;
355
+ return new (class extends AuthorizerRule {
356
+ async execute({ context, state, request }) {
357
+ // NOTE: we invoke each authorizer callable **one at a time**
358
+ // instead of concurrently so that:
359
+ //
360
+ // (1) We support dependency semantics for `all`, i.e.,
361
+ // callable's later can assume earlier callables did not
362
+ // return `Unauthenticated` or `PermissionDenied`.
363
+ //
364
+ // (2) We support short-circuiting`, i.e., cheaper authorizer
365
+ // callables can be listed first so more expensive ones
366
+ // aren't executed unless necessary.
367
+ //
368
+ // PLEASE KEEP SEMANTICS IN SYNC WITH PYTHON.
369
+ // Remember if we had any `PermissionDenied` for `any` so that
370
+ // we return that instead of `Unauthenticated`.
371
+ let denied = false;
372
+ for (const callable of callables) {
373
+ const decision = await callable({ context, state, request });
374
+ if (decision instanceof errors_pb.Ok) {
375
+ if (all !== undefined) {
376
+ // All callables must return `Ok`, keep checking.
377
+ continue;
378
+ }
379
+ else {
380
+ // Only needed one `Ok` and we got it, short-circuit.
381
+ return decision;
382
+ }
383
+ }
384
+ else if (decision instanceof errors_pb.Unauthenticated) {
385
+ if (all !== undefined) {
386
+ // All callables must return `Ok`, short-circuit.
387
+ return decision;
388
+ }
389
+ else {
390
+ // Just need one `Ok`, keep checking.
391
+ continue;
392
+ }
393
+ }
394
+ else if (decision instanceof errors_pb.PermissionDenied) {
395
+ if (all !== undefined) {
396
+ // All callables must return `Ok`, short-circuit.
397
+ return decision;
398
+ }
399
+ else {
400
+ // Remember that we got at least one `PermissionDenied` so
401
+ // we can return it later.
402
+ denied = true;
403
+ // Only need one `Ok`, keep checking.
404
+ continue;
405
+ }
406
+ }
407
+ }
408
+ // If this was `all`, then they must have all been `Ok`!
409
+ if (all !== undefined) {
410
+ // TODO: assert !denied
411
+ return new errors_pb.Ok();
412
+ }
413
+ // Must be `any`, check if we got a `PermissionDenied` otherwise
414
+ // return `Unauthenticated`.
415
+ if (denied) {
416
+ return new errors_pb.PermissionDenied();
417
+ }
418
+ else {
419
+ return new errors_pb.Unauthenticated();
420
+ }
421
+ }
422
+ })();
423
+ }
424
+ export function hasVerifiedToken({ context, }) {
425
+ if (!context.auth) {
426
+ return new errors_pb.Unauthenticated();
427
+ }
428
+ return new errors_pb.Ok();
429
+ }
430
+ export function isAppInternal({ context, }) {
431
+ if (context.appInternal) {
326
432
  return new errors_pb.Ok();
327
433
  }
434
+ return new errors_pb.Unauthenticated();
328
435
  }
329
436
  export class Application {
330
437
  constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }) {
438
+ _Application_servicers.set(this, void 0);
439
+ _Application_tokenVerifier.set(this, void 0);
440
+ _Application_express.set(this, void 0);
441
+ _Application_http.set(this, void 0);
442
+ _Application_servers.set(this, void 0);
443
+ _Application_createExternalContext.set(this, void 0);
331
444
  _Application_external.set(this, void 0);
332
- __classPrivateFieldSet(this, _Application_external, reboot_native.Application_constructor(ExternalContext.fromNativeExternal, servicers, async (context) => {
445
+ __classPrivateFieldSet(this, _Application_servicers, servicers, "f");
446
+ __classPrivateFieldSet(this, _Application_tokenVerifier, tokenVerifier, "f");
447
+ __classPrivateFieldSet(this, _Application_express, express(), "f");
448
+ // We assume that our users will want these middleware.
449
+ // TODO: expose `.use()` to allow users to add their own middleware.
450
+ __classPrivateFieldGet(this, _Application_express, "f").use(express.json());
451
+ __classPrivateFieldGet(this, _Application_express, "f").use(express.urlencoded({ extended: true }));
452
+ __classPrivateFieldSet(this, _Application_http, new Application.Http(__classPrivateFieldGet(this, _Application_express, "f"), async (args) => {
453
+ return await __classPrivateFieldGet(this, _Application_createExternalContext, "f").call(this, args);
454
+ }), "f");
455
+ __classPrivateFieldSet(this, _Application_servers, new Map(), "f");
456
+ __classPrivateFieldSet(this, _Application_external, reboot_native.Application_constructor(ExternalContext.fromNativeExternal, servicers, {
457
+ start: async (consensusId, port, createExternalContext) => {
458
+ // Store `createExternalContext` function before listening
459
+ // so we don't attempt to serve any traffic and try and use
460
+ // an `undefined` function.
461
+ __classPrivateFieldSet(this, _Application_createExternalContext, createExternalContext, "f");
462
+ let server;
463
+ [server, port] = await new Promise((resolve, reject) => {
464
+ const server = __classPrivateFieldGet(this, _Application_express, "f").listen(port ?? 0, (error) => {
465
+ if (error) {
466
+ reject(error);
467
+ }
468
+ else {
469
+ // We requested a port so we should have an
470
+ // `AddressInfo` not a string representing a file
471
+ // descriptor path for a pipe or socket, etc.
472
+ const address = server.address();
473
+ assert(typeof address !== "string");
474
+ resolve([server, address.port]);
475
+ }
476
+ });
477
+ });
478
+ __classPrivateFieldGet(this, _Application_servers, "f").set(consensusId, server);
479
+ return port;
480
+ },
481
+ stop: async (consensusId) => {
482
+ const server = __classPrivateFieldGet(this, _Application_servers, "f").get(consensusId);
483
+ if (server !== undefined) {
484
+ await new Promise((resolve, reject) => {
485
+ server.close((err) => {
486
+ if (err) {
487
+ reject(err);
488
+ }
489
+ else {
490
+ resolve();
491
+ }
492
+ });
493
+ });
494
+ __classPrivateFieldGet(this, _Application_servers, "f").delete(consensusId);
495
+ }
496
+ },
497
+ }, async (context) => {
333
498
  if (initialize !== undefined) {
334
499
  try {
335
500
  await initialize(context);
@@ -345,11 +510,98 @@ export class Application {
345
510
  }
346
511
  }, initializeBearerToken, tokenVerifier), "f");
347
512
  }
513
+ get servicers() {
514
+ return __classPrivateFieldGet(this, _Application_servicers, "f");
515
+ }
516
+ get tokenVerifier() {
517
+ return __classPrivateFieldGet(this, _Application_tokenVerifier, "f");
518
+ }
519
+ get http() {
520
+ return __classPrivateFieldGet(this, _Application_http, "f");
521
+ }
348
522
  async run() {
349
523
  return await reboot_native.Application_run(__classPrivateFieldGet(this, _Application_external, "f"));
350
524
  }
525
+ get __external() {
526
+ return __classPrivateFieldGet(this, _Application_external, "f");
527
+ }
351
528
  }
352
- _Application_external = new WeakMap();
529
+ _Application_servicers = new WeakMap(), _Application_tokenVerifier = new WeakMap(), _Application_express = new WeakMap(), _Application_http = new WeakMap(), _Application_servers = new WeakMap(), _Application_createExternalContext = new WeakMap(), _Application_external = new WeakMap();
530
+ (function (Application) {
531
+ var _Http_express, _Http_createExternalContext;
532
+ class Http {
533
+ constructor(express, createExternalContext) {
534
+ _Http_express.set(this, void 0);
535
+ _Http_createExternalContext.set(this, void 0);
536
+ __classPrivateFieldSet(this, _Http_express, express, "f");
537
+ __classPrivateFieldSet(this, _Http_createExternalContext, createExternalContext, "f");
538
+ }
539
+ add(method, path, handlers) {
540
+ const methods = {
541
+ GET: (path, ...handlers) => __classPrivateFieldGet(this, _Http_express, "f").get(path, ...handlers),
542
+ POST: (path, ...handlers) => __classPrivateFieldGet(this, _Http_express, "f").post(path, ...handlers),
543
+ };
544
+ const createExternalContext = (name, authorization) => {
545
+ let bearerToken = undefined;
546
+ if (authorization !== undefined) {
547
+ const parts = authorization.split(" ");
548
+ if (parts.length === 2 && parts[0].toLowerCase() === "bearer") {
549
+ bearerToken = parts[1];
550
+ }
551
+ }
552
+ return __classPrivateFieldGet(this, _Http_createExternalContext, "f").call(this, {
553
+ name,
554
+ bearerToken,
555
+ });
556
+ };
557
+ methods[method](path, ...handlers.map((handler) => {
558
+ // Express allows for three different kinds of handlers, a
559
+ // "normal" handler that takes two args, `req` and `res`, a
560
+ // "middlware" handler that takes three args, `req, `res`,
561
+ // and `next`, and an "error" handler that takes four args
562
+ // `err`, `req`, `res`, and `next`. To the best of our
563
+ // understanding they determine what arguments to pass to
564
+ // handlers based on how many arguments the handler takes,
565
+ // and so that is exactly what we do below as well, except
566
+ // accounting for an extra first arg for the
567
+ // `ExternalContext`.
568
+ switch (handler.length) {
569
+ // (context, req, res) => ...
570
+ case 3:
571
+ return async (req, res) => {
572
+ const context = await createExternalContext(`HTTP ${method} '${req.path}'`, req.header("Authorization"));
573
+ // @ts-ignore
574
+ handler(context, req, res);
575
+ };
576
+ // (context, req, res, next) => ...
577
+ case 4:
578
+ return async (req, res, next) => {
579
+ const context = await createExternalContext(`HTTP ${method} '${req.path}'`, req.header("Authorization"));
580
+ // @ts-ignore
581
+ handler(context, req, res, next);
582
+ };
583
+ // (context, err, req, res, next) => ...
584
+ case 5:
585
+ return async (err, req, res, next) => {
586
+ const context = await createExternalContext(`HTTP ${method} '${req.path}'`, req.header("Authorization"));
587
+ handler(context, err, req, res, next);
588
+ };
589
+ default:
590
+ throw new Error(`HTTP ${method} handler for '${path}' has unexpected ` +
591
+ `number of arguments`);
592
+ }
593
+ }));
594
+ }
595
+ get(path, ...handlers) {
596
+ this.add("GET", path, handlers);
597
+ }
598
+ post(path, ...handlers) {
599
+ this.add("POST", path, handlers);
600
+ }
601
+ }
602
+ _Http_express = new WeakMap(), _Http_createExternalContext = new WeakMap();
603
+ Application.Http = Http;
604
+ })(Application || (Application = {}));
353
605
  /**
354
606
  * @deprecated testWithReboot is deprecated in favor of the manual start of Reboot in the test setup.
355
607
  */
@@ -395,43 +647,55 @@ export async function retry_reactively_until(context, condition) {
395
647
  });
396
648
  return t;
397
649
  }
398
- async function atLeastOrMostOnce(idempotencyAlias, context, callable, options) {
399
- let t = undefined;
650
+ async function atLeastOrMostOnce(idempotencyAlias, context, callable, { stringify = JSON.stringify, parse = JSON.parse, validate, atMostOnce, }) {
651
+ assert(stringify !== undefined);
652
+ assert(parse !== undefined);
653
+ assert(atMostOnce !== undefined);
400
654
  const result = await reboot_native.atLeastOrMostOnce(context.__external, idempotencyAlias, async () => {
401
- t = await callable();
655
+ const t = await callable();
402
656
  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);
657
+ // NOTE: to differentiate `callable` returning `void` (or
658
+ // explicitly `undefined`) from `stringify` returning an empty
659
+ // string we use `{ value: stringify(t) }`.
660
+ const result = { value: stringify(t) };
661
+ return JSON.stringify(result);
662
+ }
663
+ // Fail early if the developer thinks that they have some value
664
+ // that they want to validate but we got `undefined`.
665
+ if (validate !== undefined) {
666
+ 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
667
  }
412
668
  // NOTE: using the empty string to represent a `callable`
413
- // returning void or explicitly `undefined`.
669
+ // returning `void` (or explicitly `undefined`).
414
670
  return "";
415
- }, options.atMostOnce);
416
- if (t !== undefined) {
417
- return t;
418
- }
671
+ }, atMostOnce);
672
+ // NOTE: we parse and validate `value` every time, even the first
673
+ // time, so as to catch bugs where the `value` returned from
674
+ // `callable` might not parse or be valid. We will have already
675
+ // persisted `result`, so in the event of a bug the developer will
676
+ // have to change the idempotency alias so that `callable` is
677
+ // re-executed. These semantics are the same as Python (although
678
+ // Python uses the `type` keyword argument instead of the
679
+ // `parse` and `validate` properties we use here).
419
680
  assert(result !== undefined);
420
681
  if (result !== "") {
421
- if (options.parse === undefined) {
422
- throw new Error("Required 'parse' property in 'options' is undefined");
682
+ const { value } = JSON.parse(result);
683
+ const t = parse(value);
684
+ if (parse !== JSON.parse) {
685
+ if (validate === undefined) {
686
+ // TODO: link to docs about why this is required, when those docs exist.
687
+ throw new Error("Missing `validate` property");
688
+ }
689
+ else if (!validate(t)) {
690
+ throw new Error("Failed to validate memoized result");
691
+ }
423
692
  }
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);
693
+ return t;
431
694
  }
432
- // Otherwise `callable` must return void (undefined), fall through.
695
+ // Otherwise `callable` must have returned void (or explicitly
696
+ // `undefined`), fall through.
433
697
  }
434
- export async function atMostOnce(idempotencyAlias, context, callable, options = { parse: undefined }) {
698
+ export async function atMostOnce(idempotencyAlias, context, callable, options = { validate: undefined }) {
435
699
  try {
436
700
  return await atLeastOrMostOnce(idempotencyAlias, context, callable, {
437
701
  ...options,
@@ -445,13 +709,13 @@ export async function atMostOnce(idempotencyAlias, context, callable, options =
445
709
  throw e;
446
710
  }
447
711
  }
448
- export async function atLeastOnce(idempotencyAlias, context, callable, options = { parse: undefined }) {
712
+ export async function atLeastOnce(idempotencyAlias, context, callable, options = { validate: undefined }) {
449
713
  return atLeastOrMostOnce(idempotencyAlias, context, callable, {
450
714
  ...options,
451
715
  atMostOnce: false,
452
716
  });
453
717
  }
454
- export async function until(idempotencyAlias, context, callable, options = { parse: undefined }) {
718
+ export async function until(idempotencyAlias, context, callable, options = { validate: undefined }) {
455
719
  // TODO(benh): figure out how to not use `as` type assertions here
456
720
  // to appease the TypeScript compiler which otherwise isn't happy
457
721
  // with passing on these types.
package/package.json CHANGED
@@ -3,18 +3,19 @@
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.23.0",
6
+ "@reboot-dev/reboot-api": "0.25.0",
7
7
  "chalk": "^4.1.2",
8
8
  "node-addon-api": "^7.0.0",
9
9
  "node-gyp": ">=10.2.0",
10
10
  "uuid": "^9.0.1",
11
11
  "which-pm-runs": "^1.1.0",
12
12
  "extensionless": "^1.9.9",
13
- "esbuild": "^0.24.0"
13
+ "esbuild": "^0.25.0",
14
+ "express": "^5.1.0"
14
15
  },
15
16
  "type": "module",
16
17
  "name": "@reboot-dev/reboot",
17
- "version": "0.23.0",
18
+ "version": "0.25.0",
18
19
  "description": "npm package for Reboot",
19
20
  "scripts": {
20
21
  "postinstall": "rbt || exit 0",
@@ -24,7 +25,8 @@
24
25
  "devDependencies": {
25
26
  "typescript": ">=4.9.5",
26
27
  "@types/node": "20.11.5",
27
- "@types/uuid": "^9.0.4"
28
+ "@types/uuid": "^9.0.4",
29
+ "@types/express": "^5.0.1"
28
30
  },
29
31
  "bin": {
30
32
  "rbt": "./rbt.js",
@@ -54,6 +56,7 @@
54
56
  "reboot_native.cc",
55
57
  "reboot_native.cjs",
56
58
  "reboot_native.d.ts",
59
+ "secrets",
57
60
  "utils",
58
61
  "venv.d.ts",
59
62
  "venv.js",
package/rbt.js CHANGED
@@ -27,13 +27,16 @@ function addExtensionlessToNodeOptions() {
27
27
  // If we have one of those two versions then we can pre import
28
28
  // `extensionless/register` to use the `module.register()` function,
29
29
  // otherwise we need to fall back to `--experimental-loader`.
30
- if (major > 20 ||
30
+ if (major >= 21 ||
31
31
  (major == 20 && minor >= 6) ||
32
32
  (major == 18 && minor >= 19)) {
33
33
  process.env.NODE_OPTIONS += "--import=extensionless/register";
34
34
  }
35
35
  else {
36
- process.env.NODE_OPTIONS += "--experimental-loader=extensionless";
36
+ throw new Error(`The current version of Node.js is not supported.
37
+ Supported versions are:
38
+ * greater than or equal to 20.6
39
+ * greater than or equal to 18.19 but less than 19`);
37
40
  }
38
41
  }
39
42
  async function main() {