@u17g/deferrable 1.0.0 → 2.0.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/README.md CHANGED
@@ -59,9 +59,29 @@ await deferrable(async (defer): Promise<number> => {
59
59
  - **Result passed to deferred callbacks**: each deferred callback receives a tuple `[value, error]`:
60
60
  - `[value, undefined]` when the callback resolved
61
61
  - `[undefined, error]` when the callback threw / rejected
62
- - **Error propagation**: if the callback fails, deferred callbacks still run, then the original error is re-thrown.
62
+ - **Error propagation (main callback)**: if the callback fails, deferred callbacks still run; if none of them fails, the original error is thrown. (If a deferred callback fails, see below.)
63
63
  - **Awaited sequentially**: deferred callbacks are awaited one by one (no parallel execution).
64
- - **Deferred callback errors are ignored**: if a deferred callback throws/rejects, it is swallowed and does not affect the final result.
64
+ - **Deferred callback errors**: if a deferred callback throws/rejects, `deferrable` fails fast with that error (remaining deferred callbacks are not executed). If the main callback already failed, `deferrable` throws an `AggregateError` containing both errors.
65
+ - If you want **all** deferred callbacks to run even if one of them fails, catch errors inside the deferred callback itself.
66
+
67
+ ### Example: always run all deferred callbacks (catch inside defer)
68
+
69
+ ```ts
70
+ await deferrable(async (defer): Promise<void> => {
71
+ defer(async () => {
72
+ await cleanupA().catch((err) => {
73
+ // ignore / log
74
+ console.error("cleanupA failed:", err);
75
+ });
76
+ });
77
+
78
+ defer(async () => {
79
+ await cleanupB().catch((err) => {
80
+ console.error("cleanupB failed:", err);
81
+ });
82
+ });
83
+ });
84
+ ```
65
85
 
66
86
  ### Example: LIFO order
67
87
 
@@ -145,3 +165,10 @@ async function reserveInventory(_orderId: string): Promise<void> {
145
165
  }
146
166
  async function releaseInventory(_orderId: string): Promise<void> {}
147
167
  ```
168
+
169
+ ### Durable execution frameworks (Inngest / Temporal, etc.)
170
+
171
+ `deferrable` can be used with durable execution frameworks such as Inngest or Temporal.
172
+ Because deferred callbacks are **fail-fast**, a failure in cleanup/rollback will fail the run immediately, which helps avoid continuing a workflow in an inconsistent state.
173
+
174
+ If you want a particular cleanup to be best-effort (i.e. not fail the run), catch errors inside the deferred callback (e.g. `await cleanup().catch(...)`).
package/dist/index.cjs CHANGED
@@ -46,16 +46,22 @@ async function deferrable(fn) {
46
46
  stack.push(exec);
47
47
  };
48
48
  const result = await wrap(fn(defer));
49
+ const [value, mainError] = result;
49
50
  while (stack.length) {
50
51
  const exec = stack.pop();
51
52
  try {
52
53
  await exec(result);
53
54
  }
54
- catch { }
55
+ catch (error) {
56
+ if (mainError !== undefined) {
57
+ throw new AggregateError([mainError, error], "deferrable: multiple errors");
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+ if (mainError !== undefined) {
63
+ throw mainError;
55
64
  }
56
- const [value, error] = result;
57
- if (error !== undefined)
58
- throw error;
59
65
  return value;
60
66
  }
61
67
  ;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAyCA,gCAiBC;AAxDD,SAAS,EAAE,CAAI,KAAQ;IACrB,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AAC5B,CAAC;AAAA,CAAC;AAEF,SAAS,OAAO,CAAY,KAAQ;IAClC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;AAC5B,CAAC;AAAA,CAAC;AAEF,KAAK,UAAU,IAAI,CAAe,OAAmB;IACnD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC;QAC5B,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,OAAO,CAAC,KAAU,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACI,KAAK,UAAU,UAAU,CAC9B,EAAwI;IAExI,MAAM,KAAK,GAA8C,EAAE,CAAC;IAC5D,MAAM,KAAK,GAA6B,CAAC,IAAI,EAAE,EAAE;QAC/C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3C,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC,CAAC,CAAC;IACb,CAAC;IACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,CAAC;IAC9B,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,KAAK,CAAC;IACrC,OAAO,KAAU,CAAC;AACpB,CAAC;AAAA,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAqEA,gCAwBC;AA/DD,SAAS,EAAE,CAAI,KAAQ;IACrB,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AAC5B,CAAC;AAAA,CAAC;AAEF,SAAS,OAAO,CAAY,KAAQ;IAClC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;AAC5B,CAAC;AAAA,CAAC;AAEF,KAAK,UAAU,IAAI,CAAe,OAAmB;IACnD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC;QAC5B,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,OAAO,CAAC,KAAU,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACI,KAAK,UAAU,UAAU,CAC9B,EAAsC;IAEtC,MAAM,KAAK,GAA8C,EAAE,CAAC;IAC5D,MAAM,KAAK,GAA6B,CAAC,IAAI,EAAE,EAAE;QAC/C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3C,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC;IAClC,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,MAAM,IAAI,cAAc,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,6BAA6B,CAAC,CAAC;YAC9E,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,KAAU,CAAC;AACpB,CAAC;AAAA,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,28 @@
1
+ /**
2
+ * A deferred callback registered via `defer(...)`.
3
+ *
4
+ * It will be executed after the main callback finishes (either resolved or rejected),
5
+ * in **LIFO** order. If it throws/rejects, `deferrable` fails fast with that error.
6
+ */
7
+ export type DeferredCallback<T, E = Error> = (
8
+ /**
9
+ * The result of the main callback:
10
+ * - `[value, undefined]` on success
11
+ * - `[undefined, error]` on failure
12
+ */
13
+ result: [value: T, error: undefined] | [value: undefined, error: E]) => void | Promise<void>;
14
+ /**
15
+ * Register a deferred callback to be executed later (LIFO).
16
+ *
17
+ * @example
18
+ * await deferrable(async (defer): Promise<number> => {
19
+ * defer(async ([value, err]) => {
20
+ * // cleanup / rollback
21
+ * });
22
+ * return 42;
23
+ * });
24
+ */
25
+ export type Defer<T, E = Error> = (exec: DeferredCallback<T, E>) => void;
1
26
  /**
2
27
  * Deferrable execution.
3
28
  *
@@ -20,5 +45,5 @@
20
45
  * return "Hello, world!";
21
46
  * });
22
47
  */
23
- export declare function deferrable<T, E = Error>(fn: (defer: (exec: (result: [value: T, error: undefined] | [value: undefined, error: E]) => void | Promise<void>) => void) => Promise<T>): Promise<T>;
48
+ export declare function deferrable<T, E = Error>(fn: (defer: Defer<T, E>) => Promise<T>): Promise<T>;
24
49
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,EAC3C,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GACvI,OAAO,CAAC,CAAC,CAAC,CAeZ"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI;AAC3C;;;;GAIG;AACH,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,KAChE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;AAmBzE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,EAC3C,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACrC,OAAO,CAAC,CAAC,CAAC,CAsBZ"}
package/dist/index.js CHANGED
@@ -43,16 +43,22 @@ export async function deferrable(fn) {
43
43
  stack.push(exec);
44
44
  };
45
45
  const result = await wrap(fn(defer));
46
+ const [value, mainError] = result;
46
47
  while (stack.length) {
47
48
  const exec = stack.pop();
48
49
  try {
49
50
  await exec(result);
50
51
  }
51
- catch { }
52
+ catch (error) {
53
+ if (mainError !== undefined) {
54
+ throw new AggregateError([mainError, error], "deferrable: multiple errors");
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+ if (mainError !== undefined) {
60
+ throw mainError;
52
61
  }
53
- const [value, error] = result;
54
- if (error !== undefined)
55
- throw error;
56
62
  return value;
57
63
  }
58
64
  ;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,SAAS,EAAE,CAAI,KAAQ;IACrB,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AAC5B,CAAC;AAAA,CAAC;AAEF,SAAS,OAAO,CAAY,KAAQ;IAClC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;AAC5B,CAAC;AAAA,CAAC;AAEF,KAAK,UAAU,IAAI,CAAe,OAAmB;IACnD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC;QAC5B,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,OAAO,CAAC,KAAU,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,EAAwI;IAExI,MAAM,KAAK,GAA8C,EAAE,CAAC;IAC5D,MAAM,KAAK,GAA6B,CAAC,IAAI,EAAE,EAAE;QAC/C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3C,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC,CAAC,CAAC;IACb,CAAC;IACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,CAAC;IAC9B,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,KAAK,CAAC;IACrC,OAAO,KAAU,CAAC;AACpB,CAAC;AAAA,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA8BA,SAAS,EAAE,CAAI,KAAQ;IACrB,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AAC5B,CAAC;AAAA,CAAC;AAEF,SAAS,OAAO,CAAY,KAAQ;IAClC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;AAC5B,CAAC;AAAA,CAAC;AAEF,KAAK,UAAU,IAAI,CAAe,OAAmB;IACnD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC;QAC5B,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,OAAO,CAAC,KAAU,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,EAAsC;IAEtC,MAAM,KAAK,GAA8C,EAAE,CAAC;IAC5D,MAAM,KAAK,GAA6B,CAAC,IAAI,EAAE,EAAE;QAC/C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3C,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC;IAClC,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,MAAM,IAAI,cAAc,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,6BAA6B,CAAC,CAAC;YAC9E,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,KAAU,CAAC;AACpB,CAAC;AAAA,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@u17g/deferrable",
3
3
  "description": "A tiny defer utility for JavaScript/TypeScript: register cleanup/rollback callbacks that run in LIFO order. Works in any runtime (Node.js, Bun, Deno, browsers).",
4
- "version": "1.0.0",
4
+ "version": "2.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/u17g/deferrable.git"
package/src/index.ts CHANGED
@@ -1,5 +1,33 @@
1
1
  type Result<T, E = Error> = [value: T, error: undefined] | [value: undefined, error: E];
2
2
 
3
+ /**
4
+ * A deferred callback registered via `defer(...)`.
5
+ *
6
+ * It will be executed after the main callback finishes (either resolved or rejected),
7
+ * in **LIFO** order. If it throws/rejects, `deferrable` fails fast with that error.
8
+ */
9
+ export type DeferredCallback<T, E = Error> = (
10
+ /**
11
+ * The result of the main callback:
12
+ * - `[value, undefined]` on success
13
+ * - `[undefined, error]` on failure
14
+ */
15
+ result: [value: T, error: undefined] | [value: undefined, error: E],
16
+ ) => void | Promise<void>;
17
+
18
+ /**
19
+ * Register a deferred callback to be executed later (LIFO).
20
+ *
21
+ * @example
22
+ * await deferrable(async (defer): Promise<number> => {
23
+ * defer(async ([value, err]) => {
24
+ * // cleanup / rollback
25
+ * });
26
+ * return 42;
27
+ * });
28
+ */
29
+ export type Defer<T, E = Error> = (exec: DeferredCallback<T, E>) => void;
30
+
3
31
  function ok<T>(value: T): Result<T, never> {
4
32
  return [value, undefined];
5
33
  };
@@ -40,20 +68,27 @@ async function wrap<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>> {
40
68
  * });
41
69
  */
42
70
  export async function deferrable<T, E = Error>(
43
- fn: (defer: (exec: (result: [value: T, error: undefined] | [value: undefined, error: E]) => void | Promise<void>) => void) => Promise<T>,
71
+ fn: (defer: Defer<T, E>) => Promise<T>,
44
72
  ): Promise<T> {
45
73
  const stack: Parameters<Parameters<typeof fn>[0]>[0][] = [];
46
74
  const defer: Parameters<typeof fn>[0] = (exec) => {
47
75
  stack.push(exec);
48
76
  };
49
77
  const result = await wrap<T, E>(fn(defer));
78
+ const [value, mainError] = result;
50
79
  while (stack.length) {
51
80
  const exec = stack.pop()!;
52
81
  try {
53
82
  await exec(result);
54
- } catch { }
83
+ } catch (error) {
84
+ if (mainError !== undefined) {
85
+ throw new AggregateError([mainError, error], "deferrable: multiple errors");
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+ if (mainError !== undefined) {
91
+ throw mainError;
55
92
  }
56
- const [value, error] = result;
57
- if (error !== undefined) throw error;
58
93
  return value as T;
59
- };
94
+ };