@reboot-dev/reboot 0.33.1 → 0.35.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,4 +1,5 @@
1
1
  import { errors_pb, IdempotencyOptions, protobuf_es, tasks_pb } from "@reboot-dev/reboot-api";
2
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
3
  import { z } from "zod/v4";
3
4
  import * as reboot_native from "./reboot_native.cjs";
4
5
  import { Application as ExpressApplication, NextFunction as ExpressNextFunction, Request as ExpressRequest, Response as ExpressResponse } from "express";
@@ -52,30 +53,43 @@ export declare function runWithContext<T>(context: Context, callback: () => T):
52
53
  export declare class Context {
53
54
  #private;
54
55
  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;
56
+ readonly stateId: string;
57
+ readonly method: string;
58
+ readonly stateTypeName: string;
59
+ readonly callerBearerToken: string | null;
60
+ readonly cookie: string | null;
61
+ readonly appInternal: boolean;
62
+ readonly auth: Auth | null;
63
+ constructor({ external, stateId, method, stateTypeName, callerBearerToken, cookie, appInternal, auth, cancelled, }: {
64
+ external: any;
65
+ stateId: string;
66
+ method: string;
67
+ stateTypeName: string;
68
+ callerBearerToken: string | null;
69
+ cookie: string | null;
70
+ appInternal: boolean;
71
+ auth: Auth | null;
72
+ cancelled: Promise<void>;
73
+ });
74
+ static fromNativeExternal({ kind, ...options }: {
75
+ [x: string]: any;
76
+ kind: any;
77
+ }): ReaderContext | WriterContext | TransactionContext | WorkflowContext;
57
78
  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
79
  generateIdempotentStateId(stateType: string, serviceName: string, method: string, idempotency: IdempotencyOptions): Promise<any>;
66
80
  }
67
81
  export declare class ReaderContext extends Context {
68
82
  #private;
69
- constructor(external: any, cancelled: any);
83
+ constructor(options: any);
70
84
  }
71
85
  export declare class WriterContext extends Context {
72
86
  #private;
73
- constructor(external: any, cancelled: any);
87
+ constructor(options: any);
74
88
  set sync(sync: boolean);
75
89
  }
76
90
  export declare class TransactionContext extends Context {
77
91
  #private;
78
- constructor(external: any, cancelled: any);
92
+ constructor(options: any);
79
93
  }
80
94
  export type Interval = {
81
95
  ms?: number;
@@ -86,7 +100,7 @@ export type Interval = {
86
100
  };
87
101
  export declare class WorkflowContext extends Context {
88
102
  #private;
89
- constructor(external: any, cancelled: any);
103
+ constructor(options: any);
90
104
  loop(alias: string, { interval }?: {
91
105
  interval?: Interval;
92
106
  }): AsyncGenerator<number, void, unknown>;
@@ -140,7 +154,7 @@ export declare abstract class TokenVerifier {
140
154
  * `Auth` information if the token is valid, null otherwise.
141
155
  */
142
156
  abstract verifyToken(context: ReaderContext, token?: string): Promise<Auth | null>;
143
- _verifyToken(context: ReaderContext, token?: string): Promise<Uint8Array | null>;
157
+ _verifyToken(external: any, cancelled: Promise<void>, bytesCall: Uint8Array): Promise<Uint8Array | null>;
144
158
  }
145
159
  export type AuthorizerDecision = errors_pb.Unauthenticated | errors_pb.PermissionDenied | errors_pb.Ok;
146
160
  /**
@@ -164,7 +178,7 @@ export declare abstract class Authorizer<StateType, RequestTypes> {
164
178
  * `errors_pb.PermissionDenied()` otherwise.
165
179
  */
166
180
  abstract authorize(methodName: string, context: ReaderContext, state?: StateType, request?: RequestTypes): Promise<AuthorizerDecision>;
167
- _authorize?: (methodName: string, context: ReaderContext, bytesState?: Uint8Array, bytesRequest?: Uint8Array) => Promise<Uint8Array>;
181
+ _authorize?: (external: any, cancelled: Promise<void>, bytesCall: Uint8Array) => Promise<Uint8Array>;
168
182
  }
169
183
  export type AuthorizerCallable<StateType, RequestType> = (args: {
170
184
  context: ReaderContext;
@@ -232,7 +246,7 @@ export declare namespace Application {
232
246
  }
233
247
  }
234
248
  export declare function retryReactivelyUntil(context: WorkflowContext, condition: () => Promise<boolean>): Promise<void>;
235
- export declare function retryReactivelyUntil<T>(context: WorkflowContext, condition: () => Promise<false | Exclude<T, boolean>>): Promise<Exclude<T, boolean>>;
249
+ export declare function retryReactivelyUntil<T>(context: WorkflowContext, condition: () => Promise<false | T>): Promise<T>;
236
250
  export declare const ALWAYS: "ALWAYS";
237
251
  export declare const PER_WORKFLOW: "PER_WORKFLOW";
238
252
  export declare const PER_ITERATION: "PER_ITERATION";
@@ -242,99 +256,49 @@ export type AtMostLeastOnceTupleType = [
242
256
  "PER_WORKFLOW" | "PER_ITERATION"
243
257
  ];
244
258
  export declare function atMostOnce(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<void>, options?: {
245
- stringify?: undefined;
246
- parse?: undefined;
247
- validate?: undefined;
259
+ schema?: undefined;
248
260
  }): Promise<void>;
249
- export declare function atMostOnce<T>(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<T>, options: {
250
- stringify?: (result: T) => string;
251
- parse: (value: string) => T;
252
- validate?: undefined;
253
- } | {
254
- stringify?: (result: T) => string;
255
- parse?: undefined;
256
- validate: (result: T) => boolean;
257
- }): Promise<T>;
261
+ export declare function atMostOnce<Schema extends StandardSchemaV1>(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<StandardSchemaV1.InferInput<Schema>>, options: {
262
+ schema: Schema;
263
+ }): Promise<StandardSchemaV1.InferOutput<Schema>>;
258
264
  export declare function atMostOncePerWorkflow(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
259
- stringify?: undefined;
260
- parse?: undefined;
261
- validate?: undefined;
265
+ schema?: undefined;
262
266
  }): Promise<void>;
263
- export declare function atMostOncePerWorkflow<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
264
- stringify?: (result: T) => string;
265
- parse: (value: string) => T;
266
- validate?: undefined;
267
- } | {
268
- stringify?: (result: T) => string;
269
- parse?: undefined;
270
- validate: (result: T) => boolean;
271
- }): Promise<T>;
267
+ export declare function atMostOncePerWorkflow<Schema extends StandardSchemaV1>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<StandardSchemaV1.InferInput<Schema>>, options: {
268
+ schema: Schema;
269
+ }): Promise<StandardSchemaV1.InferOutput<Schema>>;
272
270
  export declare function atLeastOnce(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<void>, options?: {
273
- stringify?: undefined;
274
- parse?: undefined;
275
- validate?: undefined;
271
+ schema?: undefined;
276
272
  }): Promise<void>;
277
- export declare function atLeastOnce<T>(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<T>, options: {
278
- stringify?: (result: T) => string;
279
- parse: (value: string) => T;
280
- validate?: undefined;
281
- } | {
282
- stringify?: (result: T) => string;
283
- parse?: undefined;
284
- validate: (result: T) => boolean;
285
- }): Promise<T>;
273
+ export declare function atLeastOnce<Schema extends StandardSchemaV1>(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<StandardSchemaV1.InferInput<Schema>>, options: {
274
+ schema: Schema;
275
+ }): Promise<StandardSchemaV1.InferOutput<Schema>>;
286
276
  export declare function atLeastOncePerWorkflow(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
287
- stringify?: undefined;
288
- parse?: undefined;
289
- validate?: undefined;
277
+ schema?: undefined;
290
278
  }): Promise<void>;
291
- export declare function atLeastOncePerWorkflow<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
292
- stringify?: (result: T) => string;
293
- parse: (value: string) => T;
294
- validate?: undefined;
295
- } | {
296
- stringify?: (result: T) => string;
297
- parse?: undefined;
298
- validate: (result: T) => boolean;
299
- }): Promise<T>;
279
+ export declare function atLeastOncePerWorkflow<Schema extends StandardSchemaV1>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<StandardSchemaV1.InferInput<Schema>>, options: {
280
+ schema: Schema;
281
+ }): Promise<StandardSchemaV1.InferOutput<Schema>>;
300
282
  export type UntilTupleType = [
301
283
  string,
302
284
  "ALWAYS" | "PER_WORKFLOW" | "PER_ITERATION"
303
285
  ];
304
286
  export declare function until(idempotencyAliasOrTuple: string | UntilTupleType, context: WorkflowContext, callable: () => Promise<boolean>, options?: {
305
- stringify?: undefined;
306
- parse?: undefined;
307
- validate?: undefined;
287
+ schema?: undefined;
308
288
  }): Promise<void>;
309
- export declare function until<T>(idempotencyAliasOrTuple: string | UntilTupleType, context: WorkflowContext, callable: () => Promise<false | Exclude<T, boolean>>, options: {
310
- stringify?: (result: T) => string;
311
- parse: (value: string) => T;
312
- validate?: undefined;
313
- } | {
314
- stringify?: (result: T) => string;
315
- parse?: undefined;
316
- validate: (result: T) => boolean;
317
- }): Promise<Exclude<T, boolean>>;
289
+ export declare function until<Schema extends StandardSchemaV1>(idempotencyAliasOrTuple: string | UntilTupleType, context: WorkflowContext, callable: () => Promise<false | StandardSchemaV1.InferInput<Schema>>, options: {
290
+ schema: Schema;
291
+ }): Promise<StandardSchemaV1.InferOutput<Schema>>;
318
292
  export declare function untilPerWorkflow(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<boolean>, options?: {
319
- stringify?: undefined;
320
- parse?: undefined;
321
- validate?: undefined;
293
+ schema?: undefined;
322
294
  }): Promise<void>;
323
- export declare function untilPerWorkflow<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<false | Exclude<T, boolean>>, options: {
324
- stringify?: (result: T) => string;
325
- parse: (value: string) => T;
326
- validate?: undefined;
327
- } | {
328
- stringify?: (result: T) => string;
329
- parse?: undefined;
330
- validate: (result: T) => boolean;
331
- }): Promise<Exclude<T, boolean>>;
332
- export declare function untilChanges<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
333
- equals: (previous: T, current: T) => boolean;
334
- stringify?: (result: T) => string;
335
- parse?: (value: string) => T;
336
- validate?: (result: T) => boolean;
337
- }): Promise<T>;
295
+ export declare function untilPerWorkflow<Schema extends StandardSchemaV1>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<false | StandardSchemaV1.InferInput<Schema>>, options: {
296
+ schema: Schema;
297
+ }): Promise<StandardSchemaV1.InferOutput<Schema>>;
298
+ export declare function untilChanges<Schema extends StandardSchemaV1>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<StandardSchemaV1.InferInput<Schema>>, { equals, schema, }: {
299
+ equals: (previous: StandardSchemaV1.InferOutput<Schema>, current: StandardSchemaV1.InferOutput<Schema>) => boolean;
300
+ schema: Schema;
301
+ }): Promise<StandardSchemaV1.InferOutput<Schema>>;
338
302
  export declare const zod: {
339
303
  tasks: {
340
304
  TaskId: z.ZodCustom<protobuf_es.PartialMessage<tasks_pb.TaskId>, protobuf_es.PartialMessage<tasks_pb.TaskId>>;
package/index.js CHANGED
@@ -9,12 +9,12 @@ 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";
14
- import { strict as assert } from "assert";
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 { assert, auth_pb, errors_pb, nodejs_pb, parse, protobuf_es, stringify, tasks_pb, toCamelCase, } from "@reboot-dev/reboot-api";
15
14
  import { AsyncLocalStorage } from "node:async_hooks";
16
15
  import { fork } from "node:child_process";
17
16
  import { createRequire } from "node:module";
17
+ import toobusy from "toobusy-js";
18
18
  import { z } from "zod/v4";
19
19
  import * as reboot_native from "./reboot_native.cjs";
20
20
  import { ensureError } from "./utils/errors.js";
@@ -168,80 +168,52 @@ export async function runWithContext(context, callback) {
168
168
  }, callback);
169
169
  }
170
170
  export class Context {
171
- constructor(external, cancelled) {
171
+ constructor({ external, stateId, method, stateTypeName, callerBearerToken, cookie, appInternal, auth, cancelled, }) {
172
172
  _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
173
  if (!__classPrivateFieldGet(_a, _a, "f", _Context_isInternalConstructing)) {
177
174
  throw new TypeError("Context is not publicly constructable");
178
175
  }
179
176
  __classPrivateFieldSet(_a, _a, false, "f", _Context_isInternalConstructing);
180
177
  __classPrivateFieldSet(this, _Context_external, external, "f");
178
+ this.stateId = stateId;
179
+ // Since we are passing a 'method' from Python, it will contain the
180
+ // Python-style method name, so we have to convert it there.
181
+ this.method = toCamelCase(method);
182
+ this.stateTypeName = stateTypeName;
183
+ this.callerBearerToken = callerBearerToken;
184
+ this.cookie = cookie;
185
+ this.appInternal = appInternal;
186
+ this.auth = auth;
181
187
  this.cancelled = cancelled;
182
188
  }
183
- static fromNativeExternal(external, kind, cancelled) {
189
+ static fromNativeExternal({ kind, ...options }) {
184
190
  __classPrivateFieldSet(_a, _a, true, "f", _Context_isInternalConstructing);
185
191
  if (kind === "reader") {
186
- return new ReaderContext(external, cancelled);
192
+ return new ReaderContext(options);
187
193
  }
188
194
  else if (kind === "writer") {
189
- return new WriterContext(external, cancelled);
195
+ return new WriterContext(options);
190
196
  }
191
197
  else if (kind === "transaction") {
192
- return new TransactionContext(external, cancelled);
198
+ return new TransactionContext(options);
193
199
  }
194
200
  else if (kind === "workflow") {
195
- return new WorkflowContext(external, cancelled);
201
+ return new WorkflowContext(options);
196
202
  }
197
203
  throw new Error("Unknown method kind");
198
204
  }
199
205
  get __external() {
200
206
  return __classPrivateFieldGet(this, _Context_external, "f");
201
207
  }
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
208
  async generateIdempotentStateId(stateType, serviceName, method, idempotency) {
237
209
  return reboot_native.Context_generateIdempotentStateId(__classPrivateFieldGet(this, _Context_external, "f"), stateType, serviceName, method, idempotency);
238
210
  }
239
211
  }
240
- _a = Context, _Context_external = new WeakMap(), _Context_stateId = new WeakMap(), _Context_method = new WeakMap(), _Context_stateTypeName = new WeakMap();
212
+ _a = Context, _Context_external = new WeakMap();
241
213
  _Context_isInternalConstructing = { value: false };
242
214
  export class ReaderContext extends Context {
243
- constructor(external, cancelled) {
244
- super(external, cancelled);
215
+ constructor(options) {
216
+ super(options);
245
217
  // Property helps `tsc` not treat a `ReaderContext` as any other
246
218
  // context type that structurally looks equivalent.
247
219
  _ReaderContext_kind.set(this, "reader");
@@ -249,8 +221,8 @@ export class ReaderContext extends Context {
249
221
  }
250
222
  _ReaderContext_kind = new WeakMap();
251
223
  export class WriterContext extends Context {
252
- constructor(external, cancelled) {
253
- super(external, cancelled);
224
+ constructor(options) {
225
+ super(options);
254
226
  // Property helps `tsc` not treat a `WriterContext` as any other
255
227
  // context type that structurally looks equivalent.
256
228
  _WriterContext_kind.set(this, "writer");
@@ -268,13 +240,14 @@ export class WriterContext extends Context {
268
240
  // 3. The writer is _not_ within a transaction.
269
241
  // 4. The writer is _not_ running as a task.
270
242
  set sync(sync) {
243
+ // TODO: optimize this so that we don't block the event loop.
271
244
  reboot_native.WriterContext_set_sync(this.__external, sync);
272
245
  }
273
246
  }
274
247
  _WriterContext_kind = new WeakMap();
275
248
  export class TransactionContext extends Context {
276
- constructor(external, cancelled) {
277
- super(external, cancelled);
249
+ constructor(options) {
250
+ super(options);
278
251
  // Property helps `tsc` not treat a `TransactionContext` as any other
279
252
  // context type that structurally looks equivalent.
280
253
  _TransactionContext_kind.set(this, "transaction");
@@ -282,8 +255,8 @@ export class TransactionContext extends Context {
282
255
  }
283
256
  _TransactionContext_kind = new WeakMap();
284
257
  export class WorkflowContext extends Context {
285
- constructor(external, cancelled) {
286
- super(external, cancelled);
258
+ constructor(options) {
259
+ super(options);
287
260
  // Property helps `tsc` not treat a `WorkflowContext` as any other
288
261
  // context type that structurally looks equivalent.
289
262
  _WorkflowContext_kind.set(this, "workflow");
@@ -398,8 +371,21 @@ export class Auth {
398
371
  * Bearer` token when passed and optionally extract token metadata.
399
372
  */
400
373
  export class TokenVerifier {
401
- async _verifyToken(context, token) {
402
- const auth = await this.verifyToken(context, token);
374
+ async _verifyToken(external, cancelled, bytesCall) {
375
+ const call = nodejs_pb.VerifyTokenCall.fromBinary(bytesCall);
376
+ const context = Context.fromNativeExternal({
377
+ external,
378
+ kind: "reader",
379
+ stateId: call.context.stateId,
380
+ method: call.context.method,
381
+ stateTypeName: call.context.stateTypeName,
382
+ callerBearerToken: call.context.callerBearerToken,
383
+ cookie: call.context.cookie,
384
+ appInternal: call.context.appInternal,
385
+ auth: null,
386
+ cancelled,
387
+ });
388
+ const auth = await this.verifyToken(context, call.token);
403
389
  if (!auth) {
404
390
  return null;
405
391
  }
@@ -617,6 +603,14 @@ export class Application {
617
603
  return __classPrivateFieldGet(this, _Application_http, "f");
618
604
  }
619
605
  async run() {
606
+ if (process.env.REBOOT_ENABLE_EVENT_LOOP_LAG_MONITORING === "true") {
607
+ toobusy.onLag((lag) => {
608
+ console.log(`Node.js event loop lag detected! Latency: ${lag}ms.
609
+ This may indicate a blocking operation on the main thread
610
+ (e.g., CPU-intensive task). If you are not running such tasks,
611
+ please report this issue to the maintainers.`);
612
+ });
613
+ }
620
614
  return await reboot_native.Application_run(__classPrivateFieldGet(this, _Application_external, "f"));
621
615
  }
622
616
  get __external() {
@@ -731,9 +725,7 @@ export async function retryReactivelyUntil(context, condition) {
731
725
  export const ALWAYS = "ALWAYS";
732
726
  export const PER_WORKFLOW = "PER_WORKFLOW";
733
727
  export const PER_ITERATION = "PER_ITERATION";
734
- async function memoize(idempotencyAliasOrTuple, context, callable, { stringify = JSON.stringify, parse = JSON.parse, validate, atMostOnce, until = false, }) {
735
- assert(stringify !== undefined);
736
- assert(parse !== undefined);
728
+ async function memoize(idempotencyAliasOrTuple, context, callable, { schema, atMostOnce, until = false, }) {
737
729
  assert(atMostOnce !== undefined);
738
730
  if (!(typeof idempotencyAliasOrTuple === "string" ||
739
731
  (Array.isArray(idempotencyAliasOrTuple) &&
@@ -749,27 +741,38 @@ async function memoize(idempotencyAliasOrTuple, context, callable, { stringify =
749
741
  try {
750
742
  const t = await callable();
751
743
  if (t !== undefined) {
752
- if (validate === undefined) {
753
- // TODO: link to docs about why this is required, when those docs exist.
754
- throw new Error(`Result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' is not 'boolean' but 'validate' option is undefined`);
744
+ // Fail early if the developer forgot to pass `schema`.
745
+ if (schema === undefined) {
746
+ throw new Error(`Expecting 'schema' as you are returning a value from the function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}'`);
747
+ }
748
+ let validate = schema["~standard"].validate(t);
749
+ if (validate instanceof Promise) {
750
+ validate = await validate;
755
751
  }
756
- else if (!validate(t)) {
757
- throw new Error(`Calling 'validate' on result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' returned 'false'`);
752
+ // If the `issues` field exists, the validation failed.
753
+ if (validate.issues) {
754
+ throw new Error(`Failed to validate result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}': ${JSON.stringify(validate.issues, null, 2)}`);
758
755
  }
756
+ // We use `stringify` from `@reboot-dev/reboot-api` because
757
+ // it can handle `BigInt` and `Uint8Array` which are common
758
+ // types from protobuf.
759
+ //
759
760
  // NOTE: to differentiate `callable` returning `void` (or
760
- // explicitly `undefined`) from `stringify` returning an empty
761
- // string we use `{ value: stringify(t) }`.
762
- const result = { value: stringify(t) };
763
- return JSON.stringify(result);
761
+ // explicitly `undefined`) from `stringify` returning
762
+ // an empty string we use `{ value: t }`.
763
+ const result = { value: t };
764
+ return stringify(result);
764
765
  }
765
- // Fail early if the developer thinks that they have some value
766
- // that they want to validate but we got `undefined`.
767
- if (validate !== undefined) {
768
- 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`)");
766
+ else {
767
+ // Fail early if the developer thinks that they have some
768
+ // value that they want to validate but we got `undefined`.
769
+ if (schema !== undefined) {
770
+ throw new Error(`Not expecting 'schema' as you are returning 'void' (or explicitly 'undefined') from the function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' ; did you mean to return a value (or if you want to explicitly return the absence of a value use 'null')`);
771
+ }
772
+ // NOTE: using the empty string to represent a `callable`
773
+ // returning `void` (or explicitly `undefined`).
774
+ return "";
769
775
  }
770
- // NOTE: using the empty string to represent a `callable`
771
- // returning `void` (or explicitly `undefined`).
772
- return "";
773
776
  }
774
777
  catch (e) {
775
778
  const error = ensureError(e);
@@ -787,22 +790,27 @@ async function memoize(idempotencyAliasOrTuple, context, callable, { stringify =
787
790
  // instead of the `parse` and `validate` properties we use here).
788
791
  assert(result !== undefined);
789
792
  if (result !== "") {
790
- const { value } = JSON.parse(result);
791
- const t = parse(value);
792
- if (parse !== JSON.parse) {
793
- if (validate === undefined) {
794
- throw new Error(`Result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' is not 'boolean' but 'validate' option is undefined`);
795
- }
796
- else if (!validate(t)) {
797
- throw new Error(`Calling 'validate' on result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' returned 'false'`);
798
- }
793
+ // We use `parse` from `@reboot-dev/reboot-api` because it can
794
+ // handle `BigInt` and `Uint8Array` which are common types from
795
+ // protobuf.
796
+ const { value } = parse(result);
797
+ if (schema === undefined) {
798
+ throw new Error(`Expecting 'schema' as we have already memoized a result for the function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' has the code been updated to remove a previously existing 'schema'`);
799
+ }
800
+ let validate = schema["~standard"].validate(value);
801
+ if (validate instanceof Promise) {
802
+ validate = await validate;
803
+ }
804
+ // If the `issues` field exists, the validation failed.
805
+ if (validate.issues) {
806
+ throw new Error(`Failed to validate result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}': ${JSON.stringify(validate.issues, null, 2)}`);
799
807
  }
800
- return t;
808
+ return validate.value;
801
809
  }
802
810
  // Otherwise `callable` must have returned void (or explicitly
803
811
  // `undefined`), fall through.
804
812
  }
805
- export async function atMostOnce(idempotencyAliasOrTuple, context, callable, options = { validate: undefined }) {
813
+ export async function atMostOnce(idempotencyAliasOrTuple, context, callable, options = {}) {
806
814
  try {
807
815
  return await memoize(idempotencyAliasOrTuple, context, callable, {
808
816
  ...options,
@@ -816,19 +824,19 @@ export async function atMostOnce(idempotencyAliasOrTuple, context, callable, opt
816
824
  throw e;
817
825
  }
818
826
  }
819
- export async function atMostOncePerWorkflow(idempotencyAlias, context, callable, options = { validate: undefined }) {
827
+ export async function atMostOncePerWorkflow(idempotencyAlias, context, callable, options = {}) {
820
828
  return atMostOnce([idempotencyAlias, PER_WORKFLOW], context, callable, options);
821
829
  }
822
- export async function atLeastOnce(idempotencyAliasOrTuple, context, callable, options = { validate: undefined }) {
830
+ export async function atLeastOnce(idempotencyAliasOrTuple, context, callable, options = {}) {
823
831
  return memoize(idempotencyAliasOrTuple, context, callable, {
824
832
  ...options,
825
833
  atMostOnce: false,
826
834
  });
827
835
  }
828
- export async function atLeastOncePerWorkflow(idempotencyAlias, context, callable, options = { validate: undefined }) {
836
+ export async function atLeastOncePerWorkflow(idempotencyAlias, context, callable, options = {}) {
829
837
  return await atLeastOnce([idempotencyAlias, PER_WORKFLOW], context, callable, options);
830
838
  }
831
- export async function until(idempotencyAliasOrTuple, context, callable, options = { validate: undefined }) {
839
+ export async function until(idempotencyAliasOrTuple, context, callable, options = {}) {
832
840
  // TODO(benh): figure out how to not use `as` type assertions here
833
841
  // to appease the TypeScript compiler which otherwise isn't happy
834
842
  // with passing on these types.
@@ -843,26 +851,25 @@ export async function until(idempotencyAliasOrTuple, context, callable, options
843
851
  until: true,
844
852
  });
845
853
  }
846
- export async function untilPerWorkflow(idempotencyAlias, context, callable, options = { validate: undefined }) {
854
+ export async function untilPerWorkflow(idempotencyAlias, context, callable, options = {}) {
847
855
  return await until([idempotencyAlias, PER_WORKFLOW], context, callable, options);
848
856
  }
849
- export async function untilChanges(idempotencyAlias, context, callable, options) {
857
+ export async function untilChanges(idempotencyAlias, context, callable, { equals, schema, }) {
850
858
  const iteration = getLoopIteration();
851
859
  if (iteration === undefined) {
852
860
  throw new Error("Waiting for changes must be done _within_ a control loop");
853
861
  }
854
- if (options.equals === undefined) {
862
+ if (equals === undefined) {
855
863
  // TODO: don't make `equals` required, instead use one of the
856
864
  // various libraries that does deep equality.
857
865
  throw new Error("Missing 'equals' option");
858
866
  }
859
- const { equals, ...optionsWithoutEquals } = options;
860
867
  let previous = null;
861
868
  if (iteration > 0) {
862
869
  // Get the previous memoized result!
863
870
  previous = (await untilPerWorkflow(`${idempotencyAlias} #${iteration - 1}`, context, (async () => {
864
871
  throw new Error(`Missing memoized value for '${idempotencyAlias}'`);
865
- }), optionsWithoutEquals));
872
+ }), { schema }));
866
873
  }
867
874
  // Wait until previous result does not equal current result.
868
875
  return (await untilPerWorkflow(`${idempotencyAlias} #${iteration}`, context, (async () => {
@@ -875,7 +882,7 @@ export async function untilChanges(idempotencyAlias, context, callable, options)
875
882
  return current;
876
883
  }
877
884
  return false;
878
- }), optionsWithoutEquals));
885
+ }), { schema }));
879
886
  }
880
887
  const launchSubprocessConsensus = (base64_args) => {
881
888
  // Create a child process via `fork` (which does not mean `fork` as
@@ -897,7 +904,7 @@ const callback = (...args) => {
897
904
  console.log(new Date(), ...args);
898
905
  };
899
906
  ensurePythonVenv();
900
- reboot_native.initialize(callback, Context.fromNativeExternal, launchSubprocessConsensus);
907
+ reboot_native.initialize(callback, launchSubprocessConsensus);
901
908
  // TODO: move these into @reboot-dev/reboot-api and also generate them
902
909
  // via plugin, perhaps via a new plugin which emits the Zod schemas
903
910
  // 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.1",
6
+ "@reboot-dev/reboot-api": "0.35.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,13 @@
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",
19
+ "@standard-schema/spec": "1.0.0"
18
20
  },
19
21
  "type": "module",
20
22
  "name": "@reboot-dev/reboot",
21
- "version": "0.33.1",
23
+ "version": "0.35.0",
22
24
  "description": "npm package for Reboot",
23
25
  "scripts": {
24
26
  "preinstall": "node preinstall.cjs",