@reboot-dev/reboot 0.29.4 → 0.30.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,4 @@
1
- import { errors_pb, IdempotencyOptions, protobuf_es, ScheduleOptions, tasks_pb } from "@reboot-dev/reboot-api";
1
+ import { errors_pb, IdempotencyOptions, protobuf_es, tasks_pb } from "@reboot-dev/reboot-api";
2
2
  import { z } from "zod/v4";
3
3
  import * as reboot_native from "./reboot_native.cjs";
4
4
  import { Application as ExpressApplication, NextFunction as ExpressNextFunction, Request as ExpressRequest, Response as ExpressResponse } from "express";
@@ -42,6 +42,8 @@ export declare class ExternalContext {
42
42
  generateIdempotentStateId(stateType: string, serviceName: string, method: string, idempotency: IdempotencyOptions): Promise<string>;
43
43
  }
44
44
  export declare function getContext(): Context;
45
+ export declare function isWithinUntil(): boolean;
46
+ export declare function isWithinLoop(): boolean;
45
47
  export declare function runWithContext<T>(context: Context, callback: () => T): Promise<T>;
46
48
  export declare class Context {
47
49
  #private;
@@ -52,7 +54,6 @@ export declare class Context {
52
54
  get auth(): Auth | null;
53
55
  get stateId(): string;
54
56
  get callerBearerToken(): string | null;
55
- get iteration(): number;
56
57
  get cookie(): string;
57
58
  get appInternal(): boolean;
58
59
  generateIdempotentStateId(stateType: string, serviceName: string, method: string, idempotency: IdempotencyOptions): Promise<any>;
@@ -70,9 +71,19 @@ export declare class TransactionContext extends Context {
70
71
  #private;
71
72
  constructor(external: any, cancelled: any);
72
73
  }
74
+ export type Interval = {
75
+ ms?: number;
76
+ secs?: number;
77
+ mins?: number;
78
+ hours?: number;
79
+ days?: number;
80
+ };
73
81
  export declare class WorkflowContext extends Context {
74
82
  #private;
75
83
  constructor(external: any, cancelled: any);
84
+ loop(alias: string, { interval }?: {
85
+ interval?: Interval;
86
+ }): AsyncGenerator<number, void, unknown>;
76
87
  }
77
88
  export declare function clearField(field: any, target: any): void;
78
89
  export declare function clearFields(target: any): void;
@@ -214,18 +225,50 @@ export declare namespace Application {
214
225
  post(path: Http.Path, ...handlers: Http.Handler[]): void;
215
226
  }
216
227
  }
217
- export declare class Loop {
218
- when: Date;
219
- constructor(options?: ScheduleOptions);
220
- }
221
- export declare function retry_reactively_until(context: WorkflowContext, condition: () => Promise<boolean>): Promise<void>;
222
- export declare function retry_reactively_until<T>(context: WorkflowContext, condition: () => Promise<false | Exclude<T, boolean>>): Promise<Exclude<T, boolean>>;
223
- export declare function atMostOnce(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
228
+ export declare function retryReactivelyUntil(context: WorkflowContext, condition: () => Promise<boolean>): Promise<void>;
229
+ export declare function retryReactivelyUntil<T>(context: WorkflowContext, condition: () => Promise<false | Exclude<T, boolean>>): Promise<Exclude<T, boolean>>;
230
+ export declare const ALWAYS: "ALWAYS";
231
+ export declare const PER_WORKFLOW: "PER_WORKFLOW";
232
+ export declare const PER_ITERATION: "PER_ITERATION";
233
+ export type How = "ALWAYS" | "PER_WORKFLOW" | "PER_ITERATION";
234
+ export type AtMostLeastOnceTupleType = [
235
+ string,
236
+ "PER_WORKFLOW" | "PER_ITERATION"
237
+ ];
238
+ export declare function atMostOnce(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<void>, options?: {
239
+ stringify?: undefined;
240
+ parse?: undefined;
241
+ validate?: undefined;
242
+ }): Promise<void>;
243
+ export declare function atMostOnce<T>(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<T>, options: {
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;
251
+ }): Promise<T>;
252
+ export declare function atMostOncePerWorkflow(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
253
+ stringify?: undefined;
254
+ parse?: undefined;
255
+ validate?: undefined;
256
+ }): Promise<void>;
257
+ export declare function atMostOncePerWorkflow<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
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;
265
+ }): Promise<T>;
266
+ export declare function atLeastOnce(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<void>, options?: {
224
267
  stringify?: undefined;
225
268
  parse?: undefined;
226
269
  validate?: undefined;
227
270
  }): Promise<void>;
228
- export declare function atMostOnce<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
271
+ export declare function atLeastOnce<T>(idempotencyAliasOrTuple: string | AtMostLeastOnceTupleType, context: WorkflowContext, callable: () => Promise<T>, options: {
229
272
  stringify?: (result: T) => string;
230
273
  parse: (value: string) => T;
231
274
  validate?: undefined;
@@ -234,12 +277,12 @@ export declare function atMostOnce<T>(idempotencyAlias: string, context: Workflo
234
277
  parse?: undefined;
235
278
  validate: (result: T) => boolean;
236
279
  }): Promise<T>;
237
- export declare function atLeastOnce(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
280
+ export declare function atLeastOncePerWorkflow(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<void>, options?: {
238
281
  stringify?: undefined;
239
282
  parse?: undefined;
240
283
  validate?: undefined;
241
284
  }): Promise<void>;
242
- export declare function atLeastOnce<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
285
+ export declare function atLeastOncePerWorkflow<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<T>, options: {
243
286
  stringify?: (result: T) => string;
244
287
  parse: (value: string) => T;
245
288
  validate?: undefined;
@@ -248,12 +291,30 @@ export declare function atLeastOnce<T>(idempotencyAlias: string, context: Workfl
248
291
  parse?: undefined;
249
292
  validate: (result: T) => boolean;
250
293
  }): Promise<T>;
251
- export declare function until(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<boolean>, options?: {
294
+ export type UntilTupleType = [
295
+ string,
296
+ "ALWAYS" | "PER_WORKFLOW" | "PER_ITERATION"
297
+ ];
298
+ export declare function until(idempotencyAliasOrTuple: string | UntilTupleType, context: WorkflowContext, callable: () => Promise<boolean>, options?: {
299
+ stringify?: undefined;
300
+ parse?: undefined;
301
+ validate?: undefined;
302
+ }): Promise<void>;
303
+ export declare function until<T>(idempotencyAliasOrTuple: string | UntilTupleType, context: WorkflowContext, callable: () => Promise<false | Exclude<T, boolean>>, options: {
304
+ stringify?: (result: T) => string;
305
+ parse: (value: string) => T;
306
+ validate?: undefined;
307
+ } | {
308
+ stringify?: (result: T) => string;
309
+ parse?: undefined;
310
+ validate: (result: T) => boolean;
311
+ }): Promise<Exclude<T, boolean>>;
312
+ export declare function untilPerWorkflow(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<boolean>, options?: {
252
313
  stringify?: undefined;
253
314
  parse?: undefined;
254
315
  validate?: undefined;
255
316
  }): Promise<void>;
256
- export declare function until<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<false | Exclude<T, boolean>>, options: {
317
+ export declare function untilPerWorkflow<T>(idempotencyAlias: string, context: WorkflowContext, callable: () => Promise<false | Exclude<T, boolean>>, options: {
257
318
  stringify?: (result: T) => string;
258
319
  parse: (value: string) => T;
259
320
  validate?: undefined;
package/index.js CHANGED
@@ -128,14 +128,32 @@ export class ExternalContext {
128
128
  _ExternalContext_external = new WeakMap();
129
129
  const contextStorage = new AsyncLocalStorage();
130
130
  export function getContext() {
131
- const context = contextStorage.getStore();
132
- if (!context) {
131
+ const store = contextStorage.getStore();
132
+ if (!store) {
133
133
  throw new Error("`getContext` may only be called within a `Servicer` method.");
134
134
  }
135
- return context;
135
+ return store.context;
136
+ }
137
+ export function isWithinUntil() {
138
+ const store = contextStorage.getStore();
139
+ if (!store) {
140
+ throw new Error("`isWithinUntil` may only be called within a `Servicer` method.");
141
+ }
142
+ return store.withinUntil;
143
+ }
144
+ export function isWithinLoop() {
145
+ const store = contextStorage.getStore();
146
+ if (!store) {
147
+ throw new Error("`isWithinLoop` may only be called within a `Servicer` method.");
148
+ }
149
+ return store.withinLoop;
136
150
  }
137
151
  export async function runWithContext(context, callback) {
138
- return await contextStorage.run(context, callback);
152
+ return await contextStorage.run({
153
+ context,
154
+ withinLoop: false,
155
+ withinUntil: false,
156
+ }, callback);
139
157
  }
140
158
  export class Context {
141
159
  constructor(external, cancelled) {
@@ -179,9 +197,6 @@ export class Context {
179
197
  get callerBearerToken() {
180
198
  return reboot_native.Context_callerBearerToken(__classPrivateFieldGet(this, _Context_external, "f"));
181
199
  }
182
- get iteration() {
183
- return reboot_native.Context_iteration(__classPrivateFieldGet(this, _Context_external, "f"));
184
- }
185
200
  get cookie() {
186
201
  return reboot_native.Context_cookie(__classPrivateFieldGet(this, _Context_external, "f"));
187
202
  }
@@ -243,6 +258,39 @@ export class WorkflowContext extends Context {
243
258
  // context type that structurally looks equivalent.
244
259
  _WorkflowContext_kind.set(this, "workflow");
245
260
  }
261
+ // TODO: implement workflow specific properties/methods.
262
+ async *loop(alias, { interval } = {}) {
263
+ const iterate = await reboot_native.WorkflowContext_loop(this.__external, alias);
264
+ const ms = (interval &&
265
+ (interval?.ms || 0) +
266
+ (interval?.secs * 1000 || 0) +
267
+ (interval?.mins * 60 * 1000 || 0) +
268
+ (interval?.hours * 60 * 60 * 1000 || 0) +
269
+ (interval?.days * 24 * 60 * 60 * 1000 || 0)) ||
270
+ 0;
271
+ const store = contextStorage.getStore();
272
+ assert(store !== undefined);
273
+ store.withinLoop = true;
274
+ let iteration = null;
275
+ try {
276
+ while (true) {
277
+ iteration = await iterate(true);
278
+ if (iteration === null) {
279
+ return;
280
+ }
281
+ yield iteration;
282
+ if (ms > 0) {
283
+ await new Promise((resolve) => setTimeout(resolve, ms));
284
+ }
285
+ }
286
+ }
287
+ finally {
288
+ if (iteration !== null) {
289
+ await iterate(false);
290
+ }
291
+ store.withinLoop = false;
292
+ }
293
+ }
246
294
  }
247
295
  _WorkflowContext_kind = new WeakMap();
248
296
  // Helper for clearing a specific field of a protobuf-es message.
@@ -613,14 +661,12 @@ _Application_servicers = new WeakMap(), _Application_tokenVerifier = new WeakMap
613
661
  _Http_express = new WeakMap(), _Http_createExternalContext = new WeakMap();
614
662
  Application.Http = Http;
615
663
  })(Application || (Application = {}));
616
- export class Loop {
617
- constructor(options) {
618
- this.when = options?.when || new Date();
619
- }
620
- }
621
- export async function retry_reactively_until(context, condition) {
664
+ export async function retryReactivelyUntil(context, condition) {
622
665
  let t = undefined;
623
- await reboot_native.retry_reactively_until(context.__external, async () => {
666
+ await reboot_native.retry_reactively_until(context.__external, AsyncLocalStorage.bind(async () => {
667
+ const store = contextStorage.getStore();
668
+ assert(store !== undefined);
669
+ store.withinUntil = true;
624
670
  try {
625
671
  const result = await condition();
626
672
  if (typeof result === "boolean") {
@@ -639,38 +685,62 @@ export async function retry_reactively_until(context, condition) {
639
685
  throw new Error(`${e}`);
640
686
  }
641
687
  }
642
- });
688
+ finally {
689
+ store.withinUntil = false;
690
+ }
691
+ }));
643
692
  return t;
644
693
  }
645
- async function memoize(idempotencyAlias, context, callable, { stringify = JSON.stringify, parse = JSON.parse, validate, atMostOnce, until = false, }) {
694
+ // NOTE: we're not using an enum because the values that can be used in
695
+ // `atMostOnce` and `atLeastOnce` are different than `until`.
696
+ export const ALWAYS = "ALWAYS";
697
+ export const PER_WORKFLOW = "PER_WORKFLOW";
698
+ export const PER_ITERATION = "PER_ITERATION";
699
+ async function memoize(idempotencyAliasOrTuple, context, callable, { stringify = JSON.stringify, parse = JSON.parse, validate, atMostOnce, until = false, }) {
646
700
  assert(stringify !== undefined);
647
701
  assert(parse !== undefined);
648
702
  assert(atMostOnce !== undefined);
649
- const result = await reboot_native.memoize(context.__external, idempotencyAlias, async () => {
650
- const t = await callable();
651
- if (t !== undefined) {
652
- if (validate === undefined) {
653
- // TODO: link to docs about why this is required, when those docs exist.
654
- throw new Error(`Result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' is not 'boolean' but 'validate' option is undefined`);
703
+ if (!(typeof idempotencyAliasOrTuple === "string" ||
704
+ (Array.isArray(idempotencyAliasOrTuple) &&
705
+ idempotencyAliasOrTuple.length === 2))) {
706
+ throw new TypeError(`Expecting either a 'string' or a tuple for first argument passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}'`);
707
+ }
708
+ const result = await reboot_native.memoize(context.__external, typeof idempotencyAliasOrTuple === "string"
709
+ ? [idempotencyAliasOrTuple, PER_ITERATION]
710
+ : idempotencyAliasOrTuple,
711
+ // Bind with async local storage so we can check things like
712
+ // `isWithinLoop`, etc.
713
+ AsyncLocalStorage.bind(async () => {
714
+ try {
715
+ const t = await callable();
716
+ if (t !== undefined) {
717
+ if (validate === undefined) {
718
+ // TODO: link to docs about why this is required, when those docs exist.
719
+ throw new Error(`Result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' is not 'boolean' but 'validate' option is undefined`);
720
+ }
721
+ else if (!validate(t)) {
722
+ throw new Error(`Calling 'validate' on result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' returned 'false'`);
723
+ }
724
+ // NOTE: to differentiate `callable` returning `void` (or
725
+ // explicitly `undefined`) from `stringify` returning an empty
726
+ // string we use `{ value: stringify(t) }`.
727
+ const result = { value: stringify(t) };
728
+ return JSON.stringify(result);
655
729
  }
656
- else if (!validate(t)) {
657
- throw new Error(`Calling 'validate' on result of function passed to '${atMostOnce ? "atMostOnce" : until ? "until" : "atLeastOnce"}' returned 'false'`);
730
+ // Fail early if the developer thinks that they have some value
731
+ // that they want to validate but we got `undefined`.
732
+ if (validate !== undefined) {
733
+ 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`)");
658
734
  }
659
- // NOTE: to differentiate `callable` returning `void` (or
660
- // explicitly `undefined`) from `stringify` returning an empty
661
- // string we use `{ value: stringify(t) }`.
662
- const result = { value: stringify(t) };
663
- return JSON.stringify(result);
664
- }
665
- // Fail early if the developer thinks that they have some value
666
- // that they want to validate but we got `undefined`.
667
- if (validate !== undefined) {
668
- 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`)");
669
- }
670
- // NOTE: using the empty string to represent a `callable`
671
- // returning `void` (or explicitly `undefined`).
672
- return "";
673
- }, atMostOnce);
735
+ // NOTE: using the empty string to represent a `callable`
736
+ // returning `void` (or explicitly `undefined`).
737
+ return "";
738
+ }
739
+ catch (e) {
740
+ console.warn(e);
741
+ throw e;
742
+ }
743
+ }), atMostOnce);
674
744
  // NOTE: we parse and validate `value` every time (even the first
675
745
  // time, even though we validate above). These semantics are the
676
746
  // same as Python (although Python uses the `type` keyword argument
@@ -692,9 +762,9 @@ async function memoize(idempotencyAlias, context, callable, { stringify = JSON.s
692
762
  // Otherwise `callable` must have returned void (or explicitly
693
763
  // `undefined`), fall through.
694
764
  }
695
- export async function atMostOnce(idempotencyAlias, context, callable, options = { validate: undefined }) {
765
+ export async function atMostOnce(idempotencyAliasOrTuple, context, callable, options = { validate: undefined }) {
696
766
  try {
697
- return await memoize(idempotencyAlias, context, callable, {
767
+ return await memoize(idempotencyAliasOrTuple, context, callable, {
698
768
  ...options,
699
769
  atMostOnce: true,
700
770
  });
@@ -706,25 +776,36 @@ export async function atMostOnce(idempotencyAlias, context, callable, options =
706
776
  throw e;
707
777
  }
708
778
  }
709
- export async function atLeastOnce(idempotencyAlias, context, callable, options = { validate: undefined }) {
710
- return memoize(idempotencyAlias, context, callable, {
779
+ export async function atMostOncePerWorkflow(idempotencyAlias, context, callable, options = { validate: undefined }) {
780
+ return atMostOnce([idempotencyAlias, PER_WORKFLOW], context, callable, options);
781
+ }
782
+ export async function atLeastOnce(idempotencyAliasOrTuple, context, callable, options = { validate: undefined }) {
783
+ return memoize(idempotencyAliasOrTuple, context, callable, {
711
784
  ...options,
712
785
  atMostOnce: false,
713
786
  });
714
787
  }
715
- export async function until(idempotencyAlias, context, callable, options = { validate: undefined }) {
788
+ export async function atLeastOncePerWorkflow(idempotencyAlias, context, callable, options = { validate: undefined }) {
789
+ return await atLeastOnce([idempotencyAlias, PER_WORKFLOW], context, callable, options);
790
+ }
791
+ export async function until(idempotencyAliasOrTuple, context, callable, options = { validate: undefined }) {
716
792
  // TODO(benh): figure out how to not use `as` type assertions here
717
793
  // to appease the TypeScript compiler which otherwise isn't happy
718
794
  // with passing on these types.
719
795
  const converge = () => {
720
- return retry_reactively_until(context, callable);
796
+ return retryReactivelyUntil(context, callable);
721
797
  };
722
- return memoize(idempotencyAlias, context, converge, {
798
+ // TODO: should we not memoize if passed `ALWAYS`? There still might
799
+ // be value in having a "paper trail" of what happened ...
800
+ return memoize(idempotencyAliasOrTuple, context, converge, {
723
801
  ...options,
724
802
  atMostOnce: false,
725
803
  until: true,
726
804
  });
727
805
  }
806
+ export async function untilPerWorkflow(idempotencyAlias, context, callable, options = { validate: undefined }) {
807
+ return await until([idempotencyAlias, PER_WORKFLOW], context, callable, options);
808
+ }
728
809
  const launchSubprocessConsensus = (base64_args) => {
729
810
  // Create a child process via `fork` (which does not mean `fork` as
730
811
  // in POSIX fork/clone) that uses the exact same module that was
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.29.4",
6
+ "@reboot-dev/reboot-api": "0.30.0",
7
7
  "chalk": "^4.1.2",
8
8
  "node-addon-api": "^7.0.0",
9
9
  "node-gyp": ">=10.2.0",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "type": "module",
20
20
  "name": "@reboot-dev/reboot",
21
- "version": "0.29.4",
21
+ "version": "0.30.0",
22
22
  "description": "npm package for Reboot",
23
23
  "scripts": {
24
24
  "preinstall": "node preinstall.cjs",
package/reboot_native.cc CHANGED
@@ -1533,7 +1533,7 @@ py::list make_py_servicers(
1533
1533
 
1534
1534
  // Include memoize servicers by default!
1535
1535
  py_servicers.attr("extend")(
1536
- py::module::import("reboot.aio.memoize").attr("servicers")());
1536
+ py::module::import("rebootdev.aio.memoize").attr("servicers")());
1537
1537
 
1538
1538
  return py_servicers;
1539
1539
  }
@@ -2401,33 +2401,6 @@ Napi::Value Context_callerBearerToken(
2401
2401
  }
2402
2402
 
2403
2403
 
2404
- Napi::Value Context_iteration(const Napi::CallbackInfo& info) {
2405
- Napi::External<py::object> js_external_context =
2406
- info[0].As<Napi::External<py::object>>();
2407
-
2408
- // CHECK(...CheckTypeTag(...));
2409
-
2410
- py::object* py_context = js_external_context.Data();
2411
-
2412
- std::optional<int> iteration = RunCallbackOnPythonEventLoop(
2413
- [py_context]() -> std::optional<int> {
2414
- py::object iteration = py_context->attr("iteration");
2415
-
2416
- if (iteration.is_none()) {
2417
- return std::nullopt;
2418
- } else {
2419
- return iteration.cast<int>();
2420
- }
2421
- });
2422
-
2423
- if (iteration.has_value()) {
2424
- return Napi::Number::New(info.Env(), *iteration);
2425
- } else {
2426
- return info.Env().Undefined();
2427
- }
2428
- }
2429
-
2430
-
2431
2404
  Napi::Value Context_cookie(const Napi::CallbackInfo& info) {
2432
2405
  Napi::External<py::object> js_external_context =
2433
2406
  info[0].As<Napi::External<py::object>>();
@@ -2493,6 +2466,11 @@ Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
2493
2466
  if (js_alias.IsString()) {
2494
2467
  alias = js_alias.As<Napi::String>().Utf8Value();
2495
2468
  }
2469
+ auto js_each_iteration = idempotency_options.Get("eachIteration");
2470
+ std::optional<bool> each_iteration;
2471
+ if (js_each_iteration.IsBoolean()) {
2472
+ each_iteration = js_each_iteration.As<Napi::Boolean>();
2473
+ }
2496
2474
 
2497
2475
  return NodePromiseFromPythonCallback(
2498
2476
  info.Env(),
@@ -2502,21 +2480,34 @@ Napi::Value Context_generateIdempotentStateId(const Napi::CallbackInfo& info) {
2502
2480
  service_name = std::move(service_name),
2503
2481
  method = std::move(method),
2504
2482
  key = std::move(key),
2505
- alias = std::move(alias)]() {
2506
- py::object py_key = py::none();
2507
- if (key.has_value()) {
2508
- py_key = py::cast(*key);
2509
- }
2510
- py::object py_alias = py::none();
2511
- if (alias.has_value()) {
2512
- py_alias = py::cast(*alias);
2513
- }
2514
- py::object py_idempotency_module = py::module::import(
2515
- "rebootdev.aio.idempotency");
2516
- py::object py_idempotency = py::object(
2517
- py_idempotency_module.attr("Idempotency")(
2518
- "key"_a = py_key,
2519
- "alias"_a = py_alias));
2483
+ alias = std::move(alias),
2484
+ each_iteration = std::move(each_iteration)]() {
2485
+ // Need to use `call_with_context` to ensure that we have
2486
+ // `py_context` as a valid asyncio context variable.
2487
+ py::object py_idempotency =
2488
+ py::module::import("rebootdev.nodejs.python")
2489
+ .attr("call_with_context")(
2490
+ py::cpp_function([&]() {
2491
+ py::object py_key = py::none();
2492
+ if (key.has_value()) {
2493
+ py_key = py::cast(*key);
2494
+ }
2495
+ py::object py_alias = py::none();
2496
+ if (alias.has_value()) {
2497
+ py_alias = py::cast(*alias);
2498
+ }
2499
+ py::object py_each_iteration = py::none();
2500
+ if (each_iteration.has_value()) {
2501
+ py_each_iteration = py::cast(*each_iteration);
2502
+ }
2503
+ return py::module::import("rebootdev.aio.contexts")
2504
+ .attr("Context")
2505
+ .attr("idempotency")(
2506
+ "key"_a = py_key,
2507
+ "alias"_a = py_alias,
2508
+ "each_iteration"_a = py_each_iteration);
2509
+ }),
2510
+ py_context);
2520
2511
 
2521
2512
  py::object py_generate_idempotent_state_id =
2522
2513
  py_context->attr("generate_idempotent_state_id");
@@ -2555,6 +2546,71 @@ Napi::Value WriterContext_set_sync(const Napi::CallbackInfo& info) {
2555
2546
  }
2556
2547
 
2557
2548
 
2549
+ Napi::Value WorkflowContext_loop(const Napi::CallbackInfo& info) {
2550
+ auto js_external_context = NapiSafeReference(
2551
+ info[0].As<Napi::External<py::object>>());
2552
+
2553
+ // CHECK(...CheckTypeTag(...));
2554
+
2555
+ py::object* py_context = js_external_context.Value(info.Env()).Data();
2556
+
2557
+ std::string alias = info[1].As<Napi::String>().Utf8Value();
2558
+
2559
+ return NodePromiseFromPythonTaskWithContext(
2560
+ info.Env(),
2561
+ "context.loop(...) in nodejs",
2562
+ js_external_context,
2563
+ [js_external_context, // Ensures `py_context` remains valid.
2564
+ py_context,
2565
+ alias = std::move(alias)]() {
2566
+ return py::module::import("rebootdev.nodejs.python")
2567
+ .attr("loop")(py_context, alias);
2568
+ },
2569
+ [](py::object py_iterate) {
2570
+ return new py::object(py_iterate);
2571
+ },
2572
+ [js_external_context](Napi::Env env, py::object* py_iterate) mutable {
2573
+ Napi::External<py::object> js_iterate_external =
2574
+ make_napi_external(env, py_iterate);
2575
+
2576
+ Napi::Function js_iterate =
2577
+ Napi::Function::New(
2578
+ env,
2579
+ [js_external_context,
2580
+ js_iterate_external = // Ensures `py_iterate` remains valid.
2581
+ NapiSafeReference(js_iterate_external),
2582
+ py_iterate](
2583
+ const Napi::CallbackInfo& info) mutable {
2584
+ bool more = info[0].As<Napi::Boolean>();
2585
+ return NodePromiseFromPythonTaskWithContext(
2586
+ info.Env(),
2587
+ "iterate(...) in nodejs",
2588
+ js_external_context,
2589
+ [js_iterate_external, py_iterate, more]() {
2590
+ return (*py_iterate)(more);
2591
+ },
2592
+ [](py::object py_iteration) -> std::optional<int> {
2593
+ if (!py_iteration.is_none()) {
2594
+ return py_iteration.cast<int>();
2595
+ } else {
2596
+ return std::nullopt;
2597
+ }
2598
+ },
2599
+ [](Napi::Env env,
2600
+ std::optional<int>&& iteration) -> Napi::Value {
2601
+ if (iteration.has_value()) {
2602
+ return Napi::Number::New(env, *iteration);
2603
+ } else {
2604
+ return env.Null();
2605
+ }
2606
+ });
2607
+ });
2608
+
2609
+ return js_iterate;
2610
+ });
2611
+ }
2612
+
2613
+
2558
2614
  Napi::Value retry_reactively_until(const Napi::CallbackInfo& info) {
2559
2615
  auto js_external_context = NapiSafeReference(
2560
2616
  info[0].As<Napi::External<py::object>>());
@@ -2605,7 +2661,11 @@ Napi::Value memoize(const Napi::CallbackInfo& info) {
2605
2661
 
2606
2662
  py::object* py_context = js_external_context.Value(info.Env()).Data();
2607
2663
 
2608
- std::string idempotency_alias = info[1].As<Napi::String>().Utf8Value();
2664
+ Napi::Object idempotency_tuple = info[1].As<Napi::Array>();
2665
+ auto js_alias = idempotency_tuple.Get((uint32_t) 0);
2666
+ std::string alias = js_alias.As<Napi::String>().Utf8Value();
2667
+ auto js_how = idempotency_tuple.Get((uint32_t) 1);
2668
+ std::string how = js_how.As<Napi::String>().Utf8Value();
2609
2669
 
2610
2670
  auto js_callable = NapiSafeFunctionReference(
2611
2671
  info[2].As<Napi::Function>());
@@ -2618,8 +2678,9 @@ Napi::Value memoize(const Napi::CallbackInfo& info) {
2618
2678
  js_external_context,
2619
2679
  [js_external_context, // Ensures `py_context` remains valid.
2620
2680
  py_context,
2681
+ alias = std::move(alias),
2682
+ how = std::move(how),
2621
2683
  js_callable = std::move(js_callable),
2622
- idempotency_alias = std::move(idempotency_alias),
2623
2684
  at_most_once]() {
2624
2685
  py::object py_callable = py::cpp_function(
2625
2686
  [js_callable = std::move(js_callable)]() mutable {
@@ -2637,9 +2698,9 @@ Napi::Value memoize(const Napi::CallbackInfo& info) {
2637
2698
  });
2638
2699
  });
2639
2700
 
2640
- return py::module::import("reboot.aio.memoize")
2701
+ return py::module::import("rebootdev.aio.memoize")
2641
2702
  .attr("memoize")(
2642
- py::str(idempotency_alias),
2703
+ py::make_tuple(alias, how),
2643
2704
  py_context,
2644
2705
  py_callable,
2645
2706
  "type_t"_a = py::eval("str"),
@@ -2667,6 +2728,8 @@ Napi::Value Servicer_read(const Napi::CallbackInfo& info) {
2667
2728
 
2668
2729
  // CHECK(...CheckTypeTag(...));
2669
2730
 
2731
+ std::string json_options = info[2].As<Napi::String>().Utf8Value();
2732
+
2670
2733
  py::object* py_context = js_external_context.Value(info.Env()).Data();
2671
2734
 
2672
2735
  return NodePromiseFromPythonTaskWithContext(
@@ -2676,8 +2739,9 @@ Napi::Value Servicer_read(const Napi::CallbackInfo& info) {
2676
2739
  [js_external_servicer, // Ensures `py_servicer` remains valid.
2677
2740
  py_servicer,
2678
2741
  js_external_context, // Ensures `py_context` remains valid.
2679
- py_context]() {
2680
- return py_servicer->attr("_read")(py_context);
2742
+ py_context,
2743
+ json_options = std::move(json_options)]() {
2744
+ return py_servicer->attr("_read")(py_context, json_options);
2681
2745
  },
2682
2746
  [](py::object py_json) {
2683
2747
  return py_json.cast<std::string>();
@@ -2815,10 +2879,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
2815
2879
  Napi::String::New(env, "Context_callerBearerToken"),
2816
2880
  Napi::Function::New<Context_callerBearerToken>(env));
2817
2881
 
2818
- exports.Set(
2819
- Napi::String::New(env, "Context_iteration"),
2820
- Napi::Function::New<Context_iteration>(env));
2821
-
2822
2882
  exports.Set(
2823
2883
  Napi::String::New(env, "Context_cookie"),
2824
2884
  Napi::Function::New<Context_cookie>(env));
@@ -2835,6 +2895,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
2835
2895
  Napi::String::New(env, "WriterContext_set_sync"),
2836
2896
  Napi::Function::New<WriterContext_set_sync>(env));
2837
2897
 
2898
+ exports.Set(
2899
+ Napi::String::New(env, "WorkflowContext_loop"),
2900
+ Napi::Function::New<WorkflowContext_loop>(env));
2901
+
2838
2902
  exports.Set(
2839
2903
  Napi::String::New(env, "retry_reactively_until"),
2840
2904
  Napi::Function::New<retry_reactively_until>(env));
package/reboot_native.cjs CHANGED
@@ -80,10 +80,10 @@ exports.Context_callerBearerToken =
80
80
  reboot_native.exports.Context_callerBearerToken;
81
81
  exports.Context_cookie = reboot_native.exports.Context_cookie;
82
82
  exports.Context_appInternal = reboot_native.exports.Context_appInternal;
83
- exports.Context_iteration = reboot_native.exports.Context_iteration;
84
83
  exports.Context_generateIdempotentStateId =
85
84
  reboot_native.exports.Context_generateIdempotentStateId;
86
85
  exports.WriterContext_set_sync = reboot_native.exports.WriterContext_set_sync;
86
+ exports.WorkflowContext_loop = reboot_native.exports.WorkflowContext_loop;
87
87
  exports.retry_reactively_until = reboot_native.exports.retry_reactively_until;
88
88
  exports.memoize = reboot_native.exports.memoize;
89
89
  exports.Servicer_read = reboot_native.exports.Servicer_read;
package/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const REBOOT_VERSION = "0.29.4";
1
+ export declare const REBOOT_VERSION = "0.30.0";
package/version.js CHANGED
@@ -1 +1 @@
1
- export const REBOOT_VERSION = "0.29.4";
1
+ export const REBOOT_VERSION = "0.30.0";