@vielzeug/ledger 3.0.0 → 26.10.1
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 +1 -1
- package/dist/compose.cjs +1 -1
- package/dist/compose.cjs.map +1 -1
- package/dist/compose.js +1 -42
- package/dist/compose.js.map +1 -1
- package/dist/errors.cjs +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -9
- package/dist/errors.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/ledger.cjs +1 -1
- package/dist/ledger.cjs.map +1 -1
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.iife.js +1 -1
- package/dist/ledger.iife.js.map +1 -1
- package/dist/ledger.js +1 -1
- package/dist/ledger.js.map +1 -1
- package/dist/types.d.ts +15 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -39,4 +39,4 @@ ledger.dispose();
|
|
|
39
39
|
|
|
40
40
|
## License
|
|
41
41
|
|
|
42
|
-
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu)
|
|
42
|
+
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu): part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
|
package/dist/compose.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function
|
|
1
|
+
function snapshotCommand(e){let{apply:t,label:n,meta:r,revert:i}=e;return{apply:t,label:n,meta:r,revert:i}}function compose(e,t){let n=e.map(snapshotCommand);return{apply:async e=>{let t=[];try{for(let r of n)await r.apply(e),t.push(r)}catch(n){let r=[];for(let n of[...t].reverse())try{await n.revert(e)}catch(e){r.push(e)}throw r.length>0?AggregateError([n,...r],`Command application and compensation failed`,{cause:n}):n}},label:t,revert:async e=>{let t=[];for(let r of[...n].reverse())try{await r.revert(e)}catch(e){t.push(e)}if(t.length>0)throw AggregateError(t,`Command reversion failed`)}}}exports.compose=compose;
|
|
2
2
|
//# sourceMappingURL=compose.cjs.map
|
package/dist/compose.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.cjs","names":[],"sources":["../src/compose.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n"],"mappings":"AAEA,SAAS,
|
|
1
|
+
{"version":3,"file":"compose.cjs","names":[],"sources":["../src/compose.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n"],"mappings":"AAEA,SAAS,gBAAuB,EAA6D,CAC3F,GAAM,CAAE,QAAO,QAAO,OAAM,UAAW,EAEvC,MAAO,CAAE,QAAO,QAAO,OAAM,QAAO,CACtC,CAcA,SAAgB,QACd,EACA,EAC0B,CAC1B,IAAM,EAAQ,EAAS,IAAI,eAAe,EAE1C,MAAO,CACL,MAAO,KAAO,IAA4B,CACxC,IAAM,EAAsC,CAAC,EAE7C,GAAI,CACF,IAAK,IAAM,KAAW,EACpB,MAAM,EAAQ,MAAM,CAAO,EAC3B,EAAQ,KAAK,CAAO,CAExB,OAAS,EAAO,CACd,IAAM,EAAkC,CAAC,EAEzC,IAAK,IAAM,IAAW,CAAC,GAAG,CAAO,CAAC,CAAC,QAAQ,EACzC,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAmB,CAC1B,EAAqB,KAAK,CAAiB,CAC7C,CASF,MANI,EAAqB,OAAS,EACtB,eAAe,CAAC,EAAO,GAAG,CAAoB,EAAG,8CAA+C,CACxG,MAAO,CACT,CAAC,EAGG,CACR,CACF,EACA,QACA,OAAQ,KAAO,IAA4B,CACzC,IAAM,EAAsB,CAAC,EAE7B,IAAK,IAAM,IAAW,CAAC,GAAG,CAAK,CAAC,CAAC,QAAQ,EACvC,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAO,CACd,EAAS,KAAK,CAAK,CACrB,CAGF,GAAI,EAAS,OAAS,EAAG,MAAU,eAAe,EAAU,0BAA0B,CACxF,CACF,CACF"}
|
package/dist/compose.js
CHANGED
|
@@ -1,43 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
function e(e) {
|
|
3
|
-
let { apply: t, label: n, meta: r, revert: i } = e;
|
|
4
|
-
return {
|
|
5
|
-
apply: t,
|
|
6
|
-
label: n,
|
|
7
|
-
meta: r,
|
|
8
|
-
revert: i
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
function t(t, n) {
|
|
12
|
-
let r = t.map(e);
|
|
13
|
-
return {
|
|
14
|
-
apply: async (e) => {
|
|
15
|
-
let t = [];
|
|
16
|
-
try {
|
|
17
|
-
for (let n of r) await n.apply(e), t.push(n);
|
|
18
|
-
} catch (n) {
|
|
19
|
-
let r = [];
|
|
20
|
-
for (let n of [...t].reverse()) try {
|
|
21
|
-
await n.revert(e);
|
|
22
|
-
} catch (e) {
|
|
23
|
-
r.push(e);
|
|
24
|
-
}
|
|
25
|
-
throw r.length > 0 ? AggregateError([n, ...r], "Command application and compensation failed", { cause: n }) : n;
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
label: n,
|
|
29
|
-
revert: async (e) => {
|
|
30
|
-
let t = [];
|
|
31
|
-
for (let n of [...r].reverse()) try {
|
|
32
|
-
await n.revert(e);
|
|
33
|
-
} catch (e) {
|
|
34
|
-
t.push(e);
|
|
35
|
-
}
|
|
36
|
-
if (t.length > 0) throw AggregateError(t, "Command reversion failed");
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
//#endregion
|
|
41
|
-
export { t as compose };
|
|
42
|
-
|
|
1
|
+
function snapshotCommand(e){let{apply:t,label:n,meta:r,revert:i}=e;return{apply:t,label:n,meta:r,revert:i}}function compose(e,t){let n=e.map(snapshotCommand);return{apply:async e=>{let t=[];try{for(let r of n)await r.apply(e),t.push(r)}catch(n){let r=[];for(let n of[...t].reverse())try{await n.revert(e)}catch(e){r.push(e)}throw r.length>0?AggregateError([n,...r],`Command application and compensation failed`,{cause:n}):n}},label:t,revert:async e=>{let t=[];for(let r of[...n].reverse())try{await r.revert(e)}catch(e){t.push(e)}if(t.length>0)throw AggregateError(t,`Command reversion failed`)}}}export{compose};
|
|
43
2
|
//# sourceMappingURL=compose.js.map
|
package/dist/compose.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.js","names":[],"sources":["../src/compose.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"compose.js","names":[],"sources":["../src/compose.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n"],"mappings":"AAEA,SAAS,gBAAuB,EAA6D,CAC3F,GAAM,CAAE,QAAO,QAAO,OAAM,UAAW,EAEvC,MAAO,CAAE,QAAO,QAAO,OAAM,QAAO,CACtC,CAcA,SAAgB,QACd,EACA,EAC0B,CAC1B,IAAM,EAAQ,EAAS,IAAI,eAAe,EAE1C,MAAO,CACL,MAAO,KAAO,IAA4B,CACxC,IAAM,EAAsC,CAAC,EAE7C,GAAI,CACF,IAAK,IAAM,KAAW,EACpB,MAAM,EAAQ,MAAM,CAAO,EAC3B,EAAQ,KAAK,CAAO,CAExB,OAAS,EAAO,CACd,IAAM,EAAkC,CAAC,EAEzC,IAAK,IAAM,IAAW,CAAC,GAAG,CAAO,CAAC,CAAC,QAAQ,EACzC,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAmB,CAC1B,EAAqB,KAAK,CAAiB,CAC7C,CASF,MANI,EAAqB,OAAS,EACtB,eAAe,CAAC,EAAO,GAAG,CAAoB,EAAG,8CAA+C,CACxG,MAAO,CACT,CAAC,EAGG,CACR,CACF,EACA,QACA,OAAQ,KAAO,IAA4B,CACzC,IAAM,EAAsB,CAAC,EAE7B,IAAK,IAAM,IAAW,CAAC,GAAG,CAAK,CAAC,CAAC,QAAQ,EACvC,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAO,CACd,EAAS,KAAK,CAAK,CACrB,CAGF,GAAI,EAAS,OAAS,EAAG,MAAU,eAAe,EAAU,0BAA0B,CACxF,CACF,CACF"}
|
package/dist/errors.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var LedgerError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},LedgerCancelledError=class extends LedgerError{},LedgerDisposedError=class extends LedgerError{},LedgerExecutionError=class extends LedgerError{},LedgerRollbackError=class extends LedgerError{},LedgerConfigError=class extends LedgerError{};exports.LedgerCancelledError=LedgerCancelledError,exports.LedgerConfigError=LedgerConfigError,exports.LedgerDisposedError=LedgerDisposedError,exports.LedgerError=LedgerError,exports.LedgerExecutionError=LedgerExecutionError,exports.LedgerRollbackError=LedgerRollbackError;
|
|
2
2
|
//# sourceMappingURL=errors.cjs.map
|
package/dist/errors.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class LedgerConfigError extends LedgerError {}\n"],"mappings":"AACA,IAAa,YAAb,cAAiC,KAAM,CACrC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAGa,qBAAb,cAA0C,WAAY,CAAC,EAG1C,oBAAb,cAAyC,WAAY,CAAC,EAGzC,qBAAb,cAA0C,WAAY,CAAC,EAG1C,oBAAb,cAAyC,WAAY,CAAC,EAGzC,kBAAb,cAAuC,WAAY,CAAC"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -15,6 +15,6 @@ export declare class LedgerExecutionError extends LedgerError {
|
|
|
15
15
|
export declare class LedgerRollbackError extends LedgerError {
|
|
16
16
|
}
|
|
17
17
|
/** Thrown when the ledger is constructed with invalid options. */
|
|
18
|
-
export declare class
|
|
18
|
+
export declare class LedgerConfigError extends LedgerError {
|
|
19
19
|
}
|
|
20
20
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY;CAKjD;AAED,uHAAuH;AACvH,qBAAa,oBAAqB,SAAQ,WAAW;CAAG;AAExD,oEAAoE;AACpE,qBAAa,mBAAoB,SAAQ,WAAW;CAAG;AAEvD,uGAAuG;AACvG,qBAAa,oBAAqB,SAAQ,WAAW;CAAG;AAExD,iIAAiI;AACjI,qBAAa,mBAAoB,SAAQ,WAAW;CAAG;AAEvD,kEAAkE;AAClE,qBAAa,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY;CAKjD;AAED,uHAAuH;AACvH,qBAAa,oBAAqB,SAAQ,WAAW;CAAG;AAExD,oEAAoE;AACpE,qBAAa,mBAAoB,SAAQ,WAAW;CAAG;AAEvD,uGAAuG;AACvG,qBAAa,oBAAqB,SAAQ,WAAW;CAAG;AAExD,iIAAiI;AACjI,qBAAa,mBAAoB,SAAQ,WAAW;CAAG;AAEvD,kEAAkE;AAClE,qBAAa,iBAAkB,SAAQ,WAAW;CAAG"}
|
package/dist/errors.js
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
var e = class extends Error {
|
|
3
|
-
constructor(e, t) {
|
|
4
|
-
super(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);
|
|
5
|
-
}
|
|
6
|
-
}, t = class extends e {}, n = class extends e {}, r = class extends e {}, i = class extends e {}, a = class extends e {};
|
|
7
|
-
//#endregion
|
|
8
|
-
export { t as LedgerCancelledError, a as LedgerConfigurationError, n as LedgerDisposedError, e as LedgerError, r as LedgerExecutionError, i as LedgerRollbackError };
|
|
9
|
-
|
|
1
|
+
var LedgerError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},LedgerCancelledError=class extends LedgerError{},LedgerDisposedError=class extends LedgerError{},LedgerExecutionError=class extends LedgerError{},LedgerRollbackError=class extends LedgerError{},LedgerConfigError=class extends LedgerError{};export{LedgerCancelledError,LedgerConfigError,LedgerDisposedError,LedgerError,LedgerExecutionError,LedgerRollbackError};
|
|
10
2
|
//# sourceMappingURL=errors.js.map
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class LedgerConfigError extends LedgerError {}\n"],"mappings":"AACA,IAAa,YAAb,cAAiC,KAAM,CACrC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAGa,qBAAb,cAA0C,WAAY,CAAC,EAG1C,oBAAb,cAAyC,WAAY,CAAC,EAGzC,qBAAb,cAA0C,WAAY,CAAC,EAG1C,oBAAb,cAAyC,WAAY,CAAC,EAGzC,kBAAb,cAAuC,WAAY,CAAC"}
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./compose.cjs"),t=require("./errors.cjs"),n=require("./ledger.cjs");exports.LedgerCancelledError=t.LedgerCancelledError,exports.
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./compose.cjs"),t=require("./errors.cjs"),n=require("./ledger.cjs");exports.LedgerCancelledError=t.LedgerCancelledError,exports.LedgerConfigError=t.LedgerConfigError,exports.LedgerDisposedError=t.LedgerDisposedError,exports.LedgerError=t.LedgerError,exports.LedgerExecutionError=t.LedgerExecutionError,exports.LedgerRollbackError=t.LedgerRollbackError,exports.compose=e.compose,exports.createLedger=n.createLedger;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { compose } from './compose';
|
|
2
|
-
export { LedgerCancelledError,
|
|
2
|
+
export { LedgerCancelledError, LedgerConfigError, LedgerDisposedError, LedgerError, LedgerExecutionError, LedgerRollbackError, } from './errors';
|
|
3
3
|
export { createLedger } from './ledger';
|
|
4
|
-
export type { CommandContext, HistoryEntry, Ledger, LedgerCallOptions, LedgerOptions,
|
|
4
|
+
export type { CommandContext, HistoryEntry, Ledger, LedgerCallOptions, LedgerOptions, LedgerState, ReversibleCommand, Subscribable, Unsubscribe, } from './types';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,oBAAoB,EACpB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,WAAW,EACX,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,cAAc,EACd,YAAY,EACZ,MAAM,EACN,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { LedgerCancelledError as t, LedgerConfigurationError as n, LedgerDisposedError as r, LedgerError as i, LedgerExecutionError as a, LedgerRollbackError as o } from "./errors.js";
|
|
3
|
-
import { createLedger as s } from "./ledger.js";
|
|
4
|
-
export { t as LedgerCancelledError, n as LedgerConfigurationError, r as LedgerDisposedError, i as LedgerError, a as LedgerExecutionError, o as LedgerRollbackError, e as compose, s as createLedger };
|
|
1
|
+
import{compose as e}from"./compose.js";import{LedgerCancelledError as t,LedgerConfigError as n,LedgerDisposedError as r,LedgerError as i,LedgerExecutionError as a,LedgerRollbackError as o}from"./errors.js";import{createLedger as s}from"./ledger.js";export{t as LedgerCancelledError,n as LedgerConfigError,r as LedgerDisposedError,i as LedgerError,a as LedgerExecutionError,o as LedgerRollbackError,e as compose,s as createLedger};
|
package/dist/ledger.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function snapshotCommand$1(e){let{apply:t,label:n,meta:r,revert:i}=e;return{apply:t,label:n,meta:r,revert:i}}function compose(e,t){let n=e.map(snapshotCommand$1);return{apply:async e=>{let t=[];try{for(let r of n)await r.apply(e),t.push(r)}catch(n){let r=[];for(let n of[...t].reverse())try{await n.revert(e)}catch(e){r.push(e)}throw r.length>0?AggregateError([n,...r],`Command application and compensation failed`,{cause:n}):n}},label:t,revert:async e=>{let t=[];for(let r of[...n].reverse())try{await r.revert(e)}catch(e){t.push(e)}if(t.length>0)throw AggregateError(t,`Command reversion failed`)}}}var LedgerError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},LedgerCancelledError=class extends LedgerError{},LedgerDisposedError=class extends LedgerError{},LedgerExecutionError=class extends LedgerError{},LedgerRollbackError=class extends LedgerError{},LedgerConfigError=class extends LedgerError{},StateStore=class{current;listeners=new Set;constructor(e){this.current=e}getSnapshot(){return this.current}set(e){this.current=e;for(let e of[...this.listeners])try{e()}catch(e){queueMicrotask(()=>{throw e})}}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}};function toMessage(e){return e instanceof Error?e.message:String(e)}function snapshotCommand(e){let{apply:t,label:n,meta:r,revert:i}=e;return{apply:t,entry:Object.freeze({label:n,meta:r}),revert:i}}function snapshotState(e){return Object.freeze({...e,redo:Object.freeze([...e.redo]),undo:Object.freeze([...e.undo])})}function operationError(e,t){return t?new LedgerDisposedError(`Cannot call ${e}() on a disposed ledger.`):new LedgerCancelledError(`${e}() was cancelled before it started.`)}function createLedger(e={}){let{maxHistory:t=100}=e;if(!Number.isSafeInteger(t)||t<0)throw new LedgerConfigError(`maxHistory must be a non-negative safe integer`);let n=new StateStore(snapshotState({queued:0,redo:[],running:0,undo:[]})),r=new WeakMap,i=new AbortController,a=new Set,o=new Set,s=!1,c=Promise.resolve();function updateState(e){if(n.set(snapshotState(e(n.getSnapshot()))),n.getSnapshot().queued===0&&n.getSnapshot().running===0){for(let e of a)e.resolve();a.clear()}}function settle(e,t){e.settled||(e.settled=!0,o.delete(e),e.cancel(),t===void 0?e.resolve():e.reject(t))}function enqueue(e,t,n){if(s)return Promise.reject(operationError(e,!0));let r=t?AbortSignal.any([t,i.signal]):i.signal;return new Promise((t,i)=>{let onAbort=()=>{a.started||a.settled||s||(updateState(e=>({...e,queued:e.queued-1})),settle(a,operationError(e,!1)))},a={cancel:()=>r.removeEventListener(`abort`,onAbort),reject:i,resolve:t,settled:!1,start:async()=>{if(!a.settled){if(s||r.aborted){updateState(e=>({...e,queued:e.queued-1})),settle(a,operationError(e,s));return}a.started=!0,updateState(e=>({...e,queued:e.queued-1,running:e.running+1}));try{await n({signal:r}),settle(a)}catch(e){settle(a,e)}finally{updateState(e=>({...e,running:e.running-1}))}}},started:!1};r.addEventListener(`abort`,onAbort,{once:!0}),o.add(a),updateState(e=>({...e,queued:e.queued+1})),c=c.then(a.start,a.start)})}async function runDo(e,n){try{await e.apply(n)}catch(e){throw n.signal.aborted?new LedgerCancelledError(`do() was cancelled while running.`,{cause:e}):new LedgerExecutionError(toMessage(e),{cause:e})}if(s||n.signal.aborted)throw new LedgerCancelledError(`do() was cancelled while running.`);updateState(n=>{if(t===0)return{...n,redo:[]};let r=[...n.undo,e.entry];return r.length>t&&r.shift(),{...n,redo:[],undo:r}}),r.set(e.entry,e)}async function runUndo(e){let t=n.getSnapshot().undo[n.getSnapshot().undo.length-1];if(!t)return;let i=r.get(t);if(!i)throw new LedgerError(`Undo history is corrupted.`);try{await i.revert(e)}catch(t){throw e.signal.aborted?new LedgerCancelledError(`undo() was cancelled while running.`,{cause:t}):new LedgerRollbackError(toMessage(t),{cause:t})}if(s||e.signal.aborted)throw new LedgerCancelledError(`undo() was cancelled while running.`);updateState(e=>({...e,redo:[...e.redo,t],undo:e.undo.slice(0,-1)}))}async function runRedo(e){let t=n.getSnapshot().redo[n.getSnapshot().redo.length-1];if(!t)return;let i=r.get(t);if(!i)throw new LedgerError(`Redo history is corrupted.`);try{await i.apply(e)}catch(t){throw e.signal.aborted?new LedgerCancelledError(`redo() was cancelled while running.`,{cause:t}):new LedgerExecutionError(toMessage(t),{cause:t})}if(s||e.signal.aborted)throw new LedgerCancelledError(`redo() was cancelled while running.`);updateState(e=>({...e,redo:e.redo.slice(0,-1),undo:[...e.undo,t]}))}return{clear(){return n.getSnapshot().undo.length===0&&n.getSnapshot().redo.length===0?s?Promise.reject(operationError(`clear`,!0)):Promise.resolve():enqueue(`clear`,void 0,async()=>{updateState(e=>({...e,redo:[],undo:[]}))})},get disposalSignal(){return i.signal},dispose(){if(s)return;s=!0,i.abort();let e=[...o].filter(e=>!e.started);for(let t of e)settle(t,operationError(`operation`,!0));for(let e of a)e.reject(operationError(`whenIdle`,!0));a.clear(),updateState(e=>({...e,queued:0,redo:[],undo:[]}))},get disposed(){return s},do(e,t){let n=snapshotCommand(e);return enqueue(`do`,t?.signal,e=>runDo(n,e))},record(e){if(s)throw new LedgerDisposedError(`Cannot call record() on a disposed ledger.`);let n=snapshotCommand(e);updateState(e=>{if(t===0)return{...e,redo:[]};let r=[...e.undo,n.entry];return r.length>t&&r.shift(),{...e,redo:[],undo:r}}),r.set(n.entry,n)},redo(e){return enqueue(`redo`,e?.signal,runRedo)},get state(){return n},[Symbol.dispose](){this.dispose()},undo(e){return enqueue(`undo`,e?.signal,runUndo)},whenIdle(){return n.getSnapshot().queued===0&&n.getSnapshot().running===0?s?Promise.reject(operationError(`whenIdle`,!0)):Promise.resolve():new Promise((e,t)=>a.add({reject:t,resolve:e}))}}}exports.LedgerCancelledError=LedgerCancelledError,exports.LedgerConfigError=LedgerConfigError,exports.LedgerDisposedError=LedgerDisposedError,exports.LedgerError=LedgerError,exports.LedgerExecutionError=LedgerExecutionError,exports.LedgerRollbackError=LedgerRollbackError,exports.compose=compose,exports.createLedger=createLedger;
|
|
2
2
|
//# sourceMappingURL=ledger.cjs.map
|
package/dist/ledger.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.cjs","names":[],"sources":["../src/compose.ts","../src/errors.ts","../src/ledger.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n","/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class LedgerConfigurationError extends LedgerError {}\n","import {\n LedgerCancelledError,\n LedgerConfigurationError,\n LedgerDisposedError,\n LedgerError,\n LedgerExecutionError,\n LedgerRollbackError,\n} from './errors';\nimport type {\n CommandContext,\n HistoryEntry,\n Ledger,\n LedgerCallOptions,\n LedgerOptions,\n LedgerReadable,\n LedgerState,\n ReversibleCommand,\n Unsubscribe,\n} from './types';\n\ntype StoredCommand<TMeta> = {\n apply: (context: CommandContext) => Promise<void> | void;\n entry: HistoryEntry<TMeta>;\n revert: (context: CommandContext) => Promise<void> | void;\n};\n\ntype Operation = {\n cancel: () => void;\n reject: (reason?: unknown) => void;\n resolve: () => void;\n settled: boolean;\n start: () => Promise<void>;\n started: boolean;\n};\n\nclass StateStore<T> implements LedgerReadable<T> {\n private currentValue: T;\n private readonly listeners = new Set<() => void>();\n\n constructor(initial: T) {\n this.currentValue = initial;\n }\n\n get value(): T {\n return this.currentValue;\n }\n\n peek(): T {\n return this.currentValue;\n }\n\n set(next: T): void {\n this.currentValue = next;\n\n for (const listener of [...this.listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n }\n\n subscribe(listener: () => void): Unsubscribe {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): StoredCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, entry: Object.freeze({ label, meta }), revert };\n}\n\nfunction snapshotState<TMeta>(state: LedgerState<TMeta>): LedgerState<TMeta> {\n return Object.freeze({\n ...state,\n redo: Object.freeze([...state.redo]),\n undo: Object.freeze([...state.undo]),\n });\n}\n\nfunction operationError(method: string, disposed: boolean): LedgerCancelledError | LedgerDisposedError {\n return disposed\n ? new LedgerDisposedError(`Cannot call ${method}() on a disposed ledger.`)\n : new LedgerCancelledError(`${method}() was cancelled before it started.`);\n}\n\n/**\n * Creates a serialized history of reversible commands.\n *\n * Commands are snapshotted when submitted. Queued commands cancelled before their queue turn do\n * not invoke user code. Active commands receive an abort signal and stop cooperatively.\n *\n * @example\n * const ledger = createLedger({ maxHistory: 50 });\n * await ledger.do({\n * apply: () => { item.name = next; },\n * revert: () => { item.name = previous; },\n * });\n * await ledger.undo();\n * await ledger.redo();\n * using ledger = createLedger();\n */\nexport function createLedger<TMeta = undefined>(options: LedgerOptions = {}): Ledger<TMeta> {\n const { maxHistory = 100 } = options;\n\n if (!Number.isSafeInteger(maxHistory) || maxHistory < 0) {\n throw new LedgerConfigurationError('maxHistory must be a non-negative safe integer');\n }\n\n const state = new StateStore<LedgerState<TMeta>>(\n snapshotState({ accepting: true, queued: 0, redo: [], running: 0, undo: [] }),\n );\n const commandStore = new WeakMap<HistoryEntry<TMeta>, StoredCommand<TMeta>>();\n const disposalController = new AbortController();\n const idleWaiters = new Set<() => void>();\n const operations = new Set<Operation>();\n let disposed = false;\n let queue = Promise.resolve();\n\n function updateState(update: (current: LedgerState<TMeta>) => LedgerState<TMeta>): void {\n state.set(snapshotState(update(state.value)));\n\n if (state.value.queued === 0 && state.value.running === 0) {\n for (const resolve of idleWaiters) resolve();\n idleWaiters.clear();\n }\n }\n\n function settle(operation: Operation, error?: unknown): void {\n if (operation.settled) return;\n\n operation.settled = true;\n operations.delete(operation);\n operation.cancel();\n\n if (error === undefined) operation.resolve();\n else operation.reject(error);\n }\n\n function enqueue(\n method: string,\n externalSignal: AbortSignal | undefined,\n task: (context: CommandContext) => Promise<void>,\n ): Promise<void> {\n if (disposed) return Promise.reject(operationError(method, true));\n\n const abortSignal = externalSignal\n ? AbortSignal.any([externalSignal, disposalController.signal])\n : disposalController.signal;\n\n return new Promise<void>((resolve, reject) => {\n const onAbort = (): void => {\n if (operation.started || operation.settled || disposed) return;\n\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, false));\n };\n\n const operation: Operation = {\n cancel: () => abortSignal.removeEventListener('abort', onAbort),\n reject,\n resolve,\n settled: false,\n start: async () => {\n if (operation.settled) return;\n\n if (disposed || abortSignal.aborted) {\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, disposed));\n\n return;\n }\n\n operation.started = true;\n updateState((current) => ({ ...current, queued: current.queued - 1, running: current.running + 1 }));\n\n try {\n await task({ signal: abortSignal });\n settle(operation);\n } catch (error) {\n settle(operation, error);\n } finally {\n updateState((current) => ({ ...current, running: current.running - 1 }));\n }\n },\n started: false,\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n operations.add(operation);\n updateState((current) => ({ ...current, queued: current.queued + 1 }));\n queue = queue.then(operation.start, operation.start);\n });\n }\n\n async function runDo(command: StoredCommand<TMeta>, context: CommandContext): Promise<void> {\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('do() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('do() was cancelled while running.');\n }\n\n updateState((current) => {\n if (maxHistory === 0) return { ...current, redo: [] };\n\n const undo = [...current.undo, command.entry];\n\n if (undo.length > maxHistory) undo.shift();\n\n return { ...current, redo: [], undo };\n });\n commandStore.set(command.entry, command);\n }\n\n async function runUndo(context: CommandContext): Promise<void> {\n const entry = state.value.undo[state.value.undo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Undo history is corrupted.');\n\n try {\n await command.revert(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('undo() was cancelled while running.', { cause: error })\n : new LedgerRollbackError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('undo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: [...current.redo, entry], undo: current.undo.slice(0, -1) }));\n }\n\n async function runRedo(context: CommandContext): Promise<void> {\n const entry = state.value.redo[state.value.redo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Redo history is corrupted.');\n\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('redo() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('redo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: current.redo.slice(0, -1), undo: [...current.undo, entry] }));\n }\n\n return {\n clear(): Promise<void> {\n return enqueue('clear', undefined, async () => {\n updateState((current) => ({ ...current, redo: [], undo: [] }));\n });\n },\n\n get disposalSignal(): AbortSignal {\n return disposalController.signal;\n },\n\n dispose(): void {\n if (disposed) return;\n\n disposed = true;\n disposalController.abort();\n\n const queued = [...operations].filter((operation) => !operation.started);\n\n for (const operation of queued) settle(operation, operationError('operation', true));\n\n updateState((current) => ({ ...current, accepting: false, queued: 0, redo: [], undo: [] }));\n },\n\n get disposed(): boolean {\n return disposed;\n },\n\n do(command: ReversibleCommand<TMeta>, callOptions?: LedgerCallOptions): Promise<void> {\n const snapshot = snapshotCommand(command);\n\n return enqueue('do', callOptions?.signal, (context) => runDo(snapshot, context));\n },\n\n redo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('redo', callOptions?.signal, runRedo);\n },\n\n get state() {\n return state;\n },\n\n [Symbol.dispose](): void {\n this.dispose();\n },\n\n undo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('undo', callOptions?.signal, runUndo);\n },\n\n whenIdle(): Promise<void> {\n if (state.value.queued === 0 && state.value.running === 0) return Promise.resolve();\n\n return new Promise((resolve) => idleWaiters.add(resolve));\n },\n };\n}\n"],"mappings":"mEAEA,SAAS,EAAuB,EAA6D,CAC3F,GAAM,CAAE,QAAO,QAAO,OAAM,UAAW,EAEvC,MAAO,CAAE,QAAO,QAAO,OAAM,QAAO,CACtC,CAcA,SAAgB,EACd,EACA,EAC0B,CAC1B,IAAM,EAAQ,EAAS,IAAI,CAAe,EAE1C,MAAO,CACL,MAAO,KAAO,IAA4B,CACxC,IAAM,EAAsC,CAAC,EAE7C,GAAI,CACF,IAAK,IAAM,KAAW,EACpB,MAAM,EAAQ,MAAM,CAAO,EAC3B,EAAQ,KAAK,CAAO,CAExB,OAAS,EAAO,CACd,IAAM,EAAkC,CAAC,EAEzC,IAAK,IAAM,IAAW,CAAC,GAAG,CAAO,CAAC,CAAC,QAAQ,EACzC,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAmB,CAC1B,EAAqB,KAAK,CAAiB,CAC7C,CASF,MANI,EAAqB,OAAS,EACtB,eAAe,CAAC,EAAO,GAAG,CAAoB,EAAG,8CAA+C,CACxG,MAAO,CACT,CAAC,EAGG,CACR,CACF,EACA,QACA,OAAQ,KAAO,IAA4B,CACzC,IAAM,EAAsB,CAAC,EAE7B,IAAK,IAAM,IAAW,CAAC,GAAG,CAAK,CAAC,CAAC,QAAQ,EACvC,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAO,CACd,EAAS,KAAK,CAAK,CACrB,CAGF,GAAI,EAAS,OAAS,EAAG,MAAU,eAAe,EAAU,0BAA0B,CACxF,CACF,CACF,CCrEA,IAAa,EAAb,cAAiC,KAAM,CACrC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAGa,EAAb,cAA0C,CAAY,CAAC,EAG1C,EAAb,cAAyC,CAAY,CAAC,EAGzC,EAAb,cAA0C,CAAY,CAAC,EAG1C,EAAb,cAAyC,CAAY,CAAC,EAGzC,EAAb,cAA8C,CAAY,CAAC,ECarD,EAAN,KAAiD,CAC/C,aACA,UAA6B,IAAI,IAEjC,YAAY,EAAY,CACtB,KAAK,aAAe,CACtB,CAEA,IAAI,OAAW,CACb,OAAO,KAAK,YACd,CAEA,MAAU,CACR,OAAO,KAAK,YACd,CAEA,IAAI,EAAe,CACjB,KAAK,aAAe,EAEpB,IAAK,IAAM,IAAY,CAAC,GAAG,KAAK,SAAS,EACvC,GAAI,CACF,EAAS,CACX,OAAS,EAAO,CACd,mBAAqB,CACnB,MAAM,CACR,CAAC,CACH,CAEJ,CAEA,UAAU,EAAmC,CAE3C,OADA,KAAK,UAAU,IAAI,CAAQ,MACd,KAAK,UAAU,OAAO,CAAQ,CAC7C,CACF,EAEA,SAAS,EAAU,EAAwB,CACzC,OAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAC9D,CAEA,SAAS,EAAuB,EAAyD,CACvF,GAAM,CAAE,QAAO,QAAO,OAAM,UAAW,EAEvC,MAAO,CAAE,QAAO,MAAO,OAAO,OAAO,CAAE,QAAO,MAAK,CAAC,EAAG,QAAO,CAChE,CAEA,SAAS,EAAqB,EAA+C,CAC3E,OAAO,OAAO,OAAO,CACnB,GAAG,EACH,KAAM,OAAO,OAAO,CAAC,GAAG,EAAM,IAAI,CAAC,EACnC,KAAM,OAAO,OAAO,CAAC,GAAG,EAAM,IAAI,CAAC,CACrC,CAAC,CACH,CAEA,SAAS,EAAe,EAAgB,EAA+D,CACrG,OAAO,EACH,IAAI,EAAoB,eAAe,EAAO,yBAAyB,EACvE,IAAI,EAAqB,GAAG,EAAO,oCAAoC,CAC7E,CAkBA,SAAgB,EAAgC,EAAyB,CAAC,EAAkB,CAC1F,GAAM,CAAE,aAAa,KAAQ,EAE7B,GAAI,CAAC,OAAO,cAAc,CAAU,GAAK,EAAa,EACpD,MAAM,IAAI,EAAyB,gDAAgD,EAGrF,IAAM,EAAQ,IAAI,EAChB,EAAc,CAAE,UAAW,GAAM,OAAQ,EAAG,KAAM,CAAC,EAAG,QAAS,EAAG,KAAM,CAAC,CAAE,CAAC,CAC9E,EACM,EAAe,IAAI,QACnB,EAAqB,IAAI,gBACzB,EAAc,IAAI,IAClB,EAAa,IAAI,IACnB,EAAW,GACX,EAAQ,QAAQ,QAAQ,EAE5B,SAAS,EAAY,EAAmE,CAGtF,GAFA,EAAM,IAAI,EAAc,EAAO,EAAM,KAAK,CAAC,CAAC,EAExC,EAAM,MAAM,SAAW,GAAK,EAAM,MAAM,UAAY,EAAG,CACzD,IAAK,IAAM,KAAW,EAAa,EAAQ,EAC3C,EAAY,MAAM,CACpB,CACF,CAEA,SAAS,EAAO,EAAsB,EAAuB,CACvD,EAAU,UAEd,EAAU,QAAU,GACpB,EAAW,OAAO,CAAS,EAC3B,EAAU,OAAO,EAEb,IAAU,IAAA,GAAW,EAAU,QAAQ,EACtC,EAAU,OAAO,CAAK,EAC7B,CAEA,SAAS,EACP,EACA,EACA,EACe,CACf,GAAI,EAAU,OAAO,QAAQ,OAAO,EAAe,EAAQ,EAAI,CAAC,EAEhE,IAAM,EAAc,EAChB,YAAY,IAAI,CAAC,EAAgB,EAAmB,MAAM,CAAC,EAC3D,EAAmB,OAEvB,OAAO,IAAI,SAAe,EAAS,IAAW,CAC5C,IAAM,MAAsB,CACtB,EAAU,SAAW,EAAU,SAAW,IAE9C,EAAa,IAAa,CAAE,GAAG,EAAS,OAAQ,EAAQ,OAAS,CAAE,EAAE,EACrE,EAAO,EAAW,EAAe,EAAQ,EAAK,CAAC,EACjD,EAEM,EAAuB,CAC3B,WAAc,EAAY,oBAAoB,QAAS,CAAO,EAC9D,SACA,UACA,QAAS,GACT,MAAO,SAAY,CACb,MAAU,QAEd,IAAI,GAAY,EAAY,QAAS,CACnC,EAAa,IAAa,CAAE,GAAG,EAAS,OAAQ,EAAQ,OAAS,CAAE,EAAE,EACrE,EAAO,EAAW,EAAe,EAAQ,CAAQ,CAAC,EAElD,MACF,CAEA,EAAU,QAAU,GACpB,EAAa,IAAa,CAAE,GAAG,EAAS,OAAQ,EAAQ,OAAS,EAAG,QAAS,EAAQ,QAAU,CAAE,EAAE,EAEnG,GAAI,CACF,MAAM,EAAK,CAAE,OAAQ,CAAY,CAAC,EAClC,EAAO,CAAS,CAClB,OAAS,EAAO,CACd,EAAO,EAAW,CAAK,CACzB,QAAU,CACR,EAAa,IAAa,CAAE,GAAG,EAAS,QAAS,EAAQ,QAAU,CAAE,EAAE,CACzE,CAZA,CAaF,EACA,QAAS,EACX,EAEA,EAAY,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,EAC7D,EAAW,IAAI,CAAS,EACxB,EAAa,IAAa,CAAE,GAAG,EAAS,OAAQ,EAAQ,OAAS,CAAE,EAAE,EACrE,EAAQ,EAAM,KAAK,EAAU,MAAO,EAAU,KAAK,CACrD,CAAC,CACH,CAEA,eAAe,EAAM,EAA+B,EAAwC,CAC1F,GAAI,CACF,MAAM,EAAQ,MAAM,CAAO,CAC7B,OAAS,EAAO,CACd,MAAM,EAAQ,OAAO,QACjB,IAAI,EAAqB,oCAAqC,CAAE,MAAO,CAAM,CAAC,EAC9E,IAAI,EAAqB,EAAU,CAAK,EAAG,CAAE,MAAO,CAAM,CAAC,CACjE,CAEA,GAAI,GAAY,EAAQ,OAAO,QAC7B,MAAM,IAAI,EAAqB,mCAAmC,EAGpE,EAAa,GAAY,CACvB,GAAI,IAAe,EAAG,MAAO,CAAE,GAAG,EAAS,KAAM,CAAC,CAAE,EAEpD,IAAM,EAAO,CAAC,GAAG,EAAQ,KAAM,EAAQ,KAAK,EAI5C,OAFI,EAAK,OAAS,GAAY,EAAK,MAAM,EAElC,CAAE,GAAG,EAAS,KAAM,CAAC,EAAG,MAAK,CACtC,CAAC,EACD,EAAa,IAAI,EAAQ,MAAO,CAAO,CACzC,CAEA,eAAe,EAAQ,EAAwC,CAC7D,IAAM,EAAQ,EAAM,MAAM,KAAK,EAAM,MAAM,KAAK,OAAS,GAEzD,GAAI,CAAC,EAAO,OAEZ,IAAM,EAAU,EAAa,IAAI,CAAK,EAEtC,GAAI,CAAC,EAAS,MAAM,IAAI,EAAY,4BAA4B,EAEhE,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAO,CACd,MAAM,EAAQ,OAAO,QACjB,IAAI,EAAqB,sCAAuC,CAAE,MAAO,CAAM,CAAC,EAChF,IAAI,EAAoB,EAAU,CAAK,EAAG,CAAE,MAAO,CAAM,CAAC,CAChE,CAEA,GAAI,GAAY,EAAQ,OAAO,QAC7B,MAAM,IAAI,EAAqB,qCAAqC,EAGtE,EAAa,IAAa,CAAE,GAAG,EAAS,KAAM,CAAC,GAAG,EAAQ,KAAM,CAAK,EAAG,KAAM,EAAQ,KAAK,MAAM,EAAG,EAAE,CAAE,EAAE,CAC5G,CAEA,eAAe,EAAQ,EAAwC,CAC7D,IAAM,EAAQ,EAAM,MAAM,KAAK,EAAM,MAAM,KAAK,OAAS,GAEzD,GAAI,CAAC,EAAO,OAEZ,IAAM,EAAU,EAAa,IAAI,CAAK,EAEtC,GAAI,CAAC,EAAS,MAAM,IAAI,EAAY,4BAA4B,EAEhE,GAAI,CACF,MAAM,EAAQ,MAAM,CAAO,CAC7B,OAAS,EAAO,CACd,MAAM,EAAQ,OAAO,QACjB,IAAI,EAAqB,sCAAuC,CAAE,MAAO,CAAM,CAAC,EAChF,IAAI,EAAqB,EAAU,CAAK,EAAG,CAAE,MAAO,CAAM,CAAC,CACjE,CAEA,GAAI,GAAY,EAAQ,OAAO,QAC7B,MAAM,IAAI,EAAqB,qCAAqC,EAGtE,EAAa,IAAa,CAAE,GAAG,EAAS,KAAM,EAAQ,KAAK,MAAM,EAAG,EAAE,EAAG,KAAM,CAAC,GAAG,EAAQ,KAAM,CAAK,CAAE,EAAE,CAC5G,CAEA,MAAO,CACL,OAAuB,CACrB,OAAO,EAAQ,QAAS,IAAA,GAAW,SAAY,CAC7C,EAAa,IAAa,CAAE,GAAG,EAAS,KAAM,CAAC,EAAG,KAAM,CAAC,CAAE,EAAE,CAC/D,CAAC,CACH,EAEA,IAAI,gBAA8B,CAChC,OAAO,EAAmB,MAC5B,EAEA,SAAgB,CACd,GAAI,EAAU,OAEd,EAAW,GACX,EAAmB,MAAM,EAEzB,IAAM,EAAS,CAAC,GAAG,CAAU,CAAC,CAAC,OAAQ,GAAc,CAAC,EAAU,OAAO,EAEvE,IAAK,IAAM,KAAa,EAAQ,EAAO,EAAW,EAAe,YAAa,EAAI,CAAC,EAEnF,EAAa,IAAa,CAAE,GAAG,EAAS,UAAW,GAAO,OAAQ,EAAG,KAAM,CAAC,EAAG,KAAM,CAAC,CAAE,EAAE,CAC5F,EAEA,IAAI,UAAoB,CACtB,OAAO,CACT,EAEA,GAAG,EAAmC,EAAgD,CACpF,IAAM,EAAW,EAAgB,CAAO,EAExC,OAAO,EAAQ,KAAM,GAAa,OAAS,GAAY,EAAM,EAAU,CAAO,CAAC,CACjF,EAEA,KAAK,EAAgD,CACnD,OAAO,EAAQ,OAAQ,GAAa,OAAQ,CAAO,CACrD,EAEA,IAAI,OAAQ,CACV,OAAO,CACT,EAEA,CAAC,OAAO,UAAiB,CACvB,KAAK,QAAQ,CACf,EAEA,KAAK,EAAgD,CACnD,OAAO,EAAQ,OAAQ,GAAa,OAAQ,CAAO,CACrD,EAEA,UAA0B,CAGxB,OAFI,EAAM,MAAM,SAAW,GAAK,EAAM,MAAM,UAAY,EAAU,QAAQ,QAAQ,EAE3E,IAAI,QAAS,GAAY,EAAY,IAAI,CAAO,CAAC,CAC1D,CACF,CACF"}
|
|
1
|
+
{"version":3,"file":"ledger.cjs","names":[],"sources":["../src/compose.ts","../src/errors.ts","../src/ledger.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n","/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class LedgerConfigError extends LedgerError {}\n","import {\n LedgerCancelledError,\n LedgerConfigError,\n LedgerDisposedError,\n LedgerError,\n LedgerExecutionError,\n LedgerRollbackError,\n} from './errors';\nimport type {\n CommandContext,\n HistoryEntry,\n Ledger,\n LedgerCallOptions,\n LedgerOptions,\n LedgerState,\n ReversibleCommand,\n Subscribable,\n Unsubscribe,\n} from './types';\n\ntype StoredCommand<TMeta> = {\n apply: (context: CommandContext) => Promise<void> | void;\n entry: HistoryEntry<TMeta>;\n revert: (context: CommandContext) => Promise<void> | void;\n};\n\ntype Operation = {\n cancel: () => void;\n reject: (reason?: unknown) => void;\n resolve: () => void;\n settled: boolean;\n start: () => Promise<void>;\n started: boolean;\n};\n\nclass StateStore<T> implements Subscribable<T> {\n private current: T;\n private readonly listeners = new Set<() => void>();\n\n constructor(initial: T) {\n this.current = initial;\n }\n\n getSnapshot(): T {\n return this.current;\n }\n\n set(next: T): void {\n this.current = next;\n\n for (const listener of [...this.listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n }\n\n subscribe(listener: () => void): Unsubscribe {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): StoredCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, entry: Object.freeze({ label, meta }), revert };\n}\n\nfunction snapshotState<TMeta>(state: LedgerState<TMeta>): LedgerState<TMeta> {\n return Object.freeze({\n ...state,\n redo: Object.freeze([...state.redo]),\n undo: Object.freeze([...state.undo]),\n });\n}\n\nfunction operationError(method: string, disposed: boolean): LedgerCancelledError | LedgerDisposedError {\n return disposed\n ? new LedgerDisposedError(`Cannot call ${method}() on a disposed ledger.`)\n : new LedgerCancelledError(`${method}() was cancelled before it started.`);\n}\n\n/**\n * Creates a serialized history of reversible commands.\n *\n * Commands are snapshotted when submitted. Queued commands cancelled before their queue turn do\n * not invoke user code. Active commands receive an abort signal and stop cooperatively.\n *\n * @example\n * const ledger = createLedger({ maxHistory: 50 });\n * await ledger.do({\n * apply: () => { item.name = next; },\n * revert: () => { item.name = previous; },\n * });\n * await ledger.undo();\n * await ledger.redo();\n * using ledger = createLedger();\n */\nexport function createLedger<TMeta = undefined>(options: LedgerOptions = {}): Ledger<TMeta> {\n const { maxHistory = 100 } = options;\n\n if (!Number.isSafeInteger(maxHistory) || maxHistory < 0) {\n throw new LedgerConfigError('maxHistory must be a non-negative safe integer');\n }\n\n const state = new StateStore<LedgerState<TMeta>>(snapshotState({ queued: 0, redo: [], running: 0, undo: [] }));\n const commandStore = new WeakMap<HistoryEntry<TMeta>, StoredCommand<TMeta>>();\n const disposalController = new AbortController();\n const idleWaiters = new Set<{ readonly reject: (reason?: unknown) => void; readonly resolve: () => void }>();\n const operations = new Set<Operation>();\n let disposed = false;\n let queue = Promise.resolve();\n\n function updateState(update: (current: LedgerState<TMeta>) => LedgerState<TMeta>): void {\n state.set(snapshotState(update(state.getSnapshot())));\n\n if (state.getSnapshot().queued === 0 && state.getSnapshot().running === 0) {\n for (const waiter of idleWaiters) waiter.resolve();\n idleWaiters.clear();\n }\n }\n\n function settle(operation: Operation, error?: unknown): void {\n if (operation.settled) return;\n\n operation.settled = true;\n operations.delete(operation);\n operation.cancel();\n\n if (error === undefined) operation.resolve();\n else operation.reject(error);\n }\n\n function enqueue(\n method: string,\n externalSignal: AbortSignal | undefined,\n task: (context: CommandContext) => Promise<void>,\n ): Promise<void> {\n if (disposed) return Promise.reject(operationError(method, true));\n\n const abortSignal = externalSignal\n ? AbortSignal.any([externalSignal, disposalController.signal])\n : disposalController.signal;\n\n return new Promise<void>((resolve, reject) => {\n const onAbort = (): void => {\n if (operation.started || operation.settled || disposed) return;\n\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, false));\n };\n\n const operation: Operation = {\n cancel: () => abortSignal.removeEventListener('abort', onAbort),\n reject,\n resolve,\n settled: false,\n start: async () => {\n if (operation.settled) return;\n\n if (disposed || abortSignal.aborted) {\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, disposed));\n\n return;\n }\n\n operation.started = true;\n updateState((current) => ({ ...current, queued: current.queued - 1, running: current.running + 1 }));\n\n try {\n await task({ signal: abortSignal });\n settle(operation);\n } catch (error) {\n settle(operation, error);\n } finally {\n updateState((current) => ({ ...current, running: current.running - 1 }));\n }\n },\n started: false,\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n operations.add(operation);\n updateState((current) => ({ ...current, queued: current.queued + 1 }));\n queue = queue.then(operation.start, operation.start);\n });\n }\n\n async function runDo(command: StoredCommand<TMeta>, context: CommandContext): Promise<void> {\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('do() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('do() was cancelled while running.');\n }\n\n updateState((current) => {\n if (maxHistory === 0) return { ...current, redo: [] };\n\n const undo = [...current.undo, command.entry];\n\n if (undo.length > maxHistory) undo.shift();\n\n return { ...current, redo: [], undo };\n });\n commandStore.set(command.entry, command);\n }\n\n async function runUndo(context: CommandContext): Promise<void> {\n const entry = state.getSnapshot().undo[state.getSnapshot().undo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Undo history is corrupted.');\n\n try {\n await command.revert(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('undo() was cancelled while running.', { cause: error })\n : new LedgerRollbackError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('undo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: [...current.redo, entry], undo: current.undo.slice(0, -1) }));\n }\n\n async function runRedo(context: CommandContext): Promise<void> {\n const entry = state.getSnapshot().redo[state.getSnapshot().redo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Redo history is corrupted.');\n\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('redo() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('redo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: current.redo.slice(0, -1), undo: [...current.undo, entry] }));\n }\n\n return {\n clear(): Promise<void> {\n if (state.getSnapshot().undo.length === 0 && state.getSnapshot().redo.length === 0) {\n return disposed ? Promise.reject(operationError('clear', true)) : Promise.resolve();\n }\n\n return enqueue('clear', undefined, async () => {\n updateState((current) => ({ ...current, redo: [], undo: [] }));\n });\n },\n\n get disposalSignal(): AbortSignal {\n return disposalController.signal;\n },\n\n dispose(): void {\n if (disposed) return;\n\n disposed = true;\n disposalController.abort();\n\n const queued = [...operations].filter((operation) => !operation.started);\n\n for (const operation of queued) settle(operation, operationError('operation', true));\n\n for (const waiter of idleWaiters) waiter.reject(operationError('whenIdle', true));\n idleWaiters.clear();\n\n updateState((current) => ({ ...current, queued: 0, redo: [], undo: [] }));\n },\n\n get disposed(): boolean {\n return disposed;\n },\n\n do(command: ReversibleCommand<TMeta>, callOptions?: LedgerCallOptions): Promise<void> {\n const snapshot = snapshotCommand(command);\n\n return enqueue('do', callOptions?.signal, (context) => runDo(snapshot, context));\n },\n\n record(command: ReversibleCommand<TMeta>): void {\n if (disposed) throw new LedgerDisposedError('Cannot call record() on a disposed ledger.');\n\n const snapshot = snapshotCommand(command);\n updateState((current) => {\n if (maxHistory === 0) return { ...current, redo: [] };\n\n const undo = [...current.undo, snapshot.entry];\n\n if (undo.length > maxHistory) undo.shift();\n\n return { ...current, redo: [], undo };\n });\n commandStore.set(snapshot.entry, snapshot);\n },\n\n redo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('redo', callOptions?.signal, runRedo);\n },\n\n get state() {\n return state;\n },\n\n [Symbol.dispose](): void {\n this.dispose();\n },\n\n undo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('undo', callOptions?.signal, runUndo);\n },\n\n whenIdle(): Promise<void> {\n if (state.getSnapshot().queued === 0 && state.getSnapshot().running === 0) {\n return disposed ? Promise.reject(operationError('whenIdle', true)) : Promise.resolve();\n }\n\n return new Promise<void>((resolve, reject) => idleWaiters.add({ reject, resolve }));\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/ledger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAGV,MAAM,EAEN,aAAa,EAKd,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAGV,MAAM,EAEN,aAAa,EAKd,MAAM,SAAS,CAAC;AAyEjB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,KAAK,GAAG,SAAS,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAAC,KAAK,CAAC,CAqP1F"}
|
package/dist/ledger.iife.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var Ledger=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function
|
|
1
|
+
var Ledger=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function snapshotCommand$1(e){let{apply:t,label:n,meta:r,revert:i}=e;return{apply:t,label:n,meta:r,revert:i}}function compose(e,t){let n=e.map(snapshotCommand$1);return{apply:async e=>{let t=[];try{for(let r of n)await r.apply(e),t.push(r)}catch(n){let r=[];for(let n of[...t].reverse())try{await n.revert(e)}catch(e){r.push(e)}throw r.length>0?AggregateError([n,...r],`Command application and compensation failed`,{cause:n}):n}},label:t,revert:async e=>{let t=[];for(let r of[...n].reverse())try{await r.revert(e)}catch(e){t.push(e)}if(t.length>0)throw AggregateError(t,`Command reversion failed`)}}}var LedgerError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},LedgerCancelledError=class extends LedgerError{},LedgerDisposedError=class extends LedgerError{},LedgerExecutionError=class extends LedgerError{},LedgerRollbackError=class extends LedgerError{},LedgerConfigError=class extends LedgerError{},StateStore=class{current;listeners=new Set;constructor(e){this.current=e}getSnapshot(){return this.current}set(e){this.current=e;for(let e of[...this.listeners])try{e()}catch(e){queueMicrotask(()=>{throw e})}}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}};function toMessage(e){return e instanceof Error?e.message:String(e)}function snapshotCommand(e){let{apply:t,label:n,meta:r,revert:i}=e;return{apply:t,entry:Object.freeze({label:n,meta:r}),revert:i}}function snapshotState(e){return Object.freeze({...e,redo:Object.freeze([...e.redo]),undo:Object.freeze([...e.undo])})}function operationError(e,t){return t?new LedgerDisposedError(`Cannot call ${e}() on a disposed ledger.`):new LedgerCancelledError(`${e}() was cancelled before it started.`)}function createLedger(e={}){let{maxHistory:t=100}=e;if(!Number.isSafeInteger(t)||t<0)throw new LedgerConfigError(`maxHistory must be a non-negative safe integer`);let n=new StateStore(snapshotState({queued:0,redo:[],running:0,undo:[]})),r=new WeakMap,i=new AbortController,a=new Set,o=new Set,s=!1,c=Promise.resolve();function updateState(e){if(n.set(snapshotState(e(n.getSnapshot()))),n.getSnapshot().queued===0&&n.getSnapshot().running===0){for(let e of a)e.resolve();a.clear()}}function settle(e,t){e.settled||(e.settled=!0,o.delete(e),e.cancel(),t===void 0?e.resolve():e.reject(t))}function enqueue(e,t,n){if(s)return Promise.reject(operationError(e,!0));let r=t?AbortSignal.any([t,i.signal]):i.signal;return new Promise((t,i)=>{let onAbort=()=>{a.started||a.settled||s||(updateState(e=>({...e,queued:e.queued-1})),settle(a,operationError(e,!1)))},a={cancel:()=>r.removeEventListener(`abort`,onAbort),reject:i,resolve:t,settled:!1,start:async()=>{if(!a.settled){if(s||r.aborted){updateState(e=>({...e,queued:e.queued-1})),settle(a,operationError(e,s));return}a.started=!0,updateState(e=>({...e,queued:e.queued-1,running:e.running+1}));try{await n({signal:r}),settle(a)}catch(e){settle(a,e)}finally{updateState(e=>({...e,running:e.running-1}))}}},started:!1};r.addEventListener(`abort`,onAbort,{once:!0}),o.add(a),updateState(e=>({...e,queued:e.queued+1})),c=c.then(a.start,a.start)})}async function runDo(e,n){try{await e.apply(n)}catch(e){throw n.signal.aborted?new LedgerCancelledError(`do() was cancelled while running.`,{cause:e}):new LedgerExecutionError(toMessage(e),{cause:e})}if(s||n.signal.aborted)throw new LedgerCancelledError(`do() was cancelled while running.`);updateState(n=>{if(t===0)return{...n,redo:[]};let r=[...n.undo,e.entry];return r.length>t&&r.shift(),{...n,redo:[],undo:r}}),r.set(e.entry,e)}async function runUndo(e){let t=n.getSnapshot().undo[n.getSnapshot().undo.length-1];if(!t)return;let i=r.get(t);if(!i)throw new LedgerError(`Undo history is corrupted.`);try{await i.revert(e)}catch(t){throw e.signal.aborted?new LedgerCancelledError(`undo() was cancelled while running.`,{cause:t}):new LedgerRollbackError(toMessage(t),{cause:t})}if(s||e.signal.aborted)throw new LedgerCancelledError(`undo() was cancelled while running.`);updateState(e=>({...e,redo:[...e.redo,t],undo:e.undo.slice(0,-1)}))}async function runRedo(e){let t=n.getSnapshot().redo[n.getSnapshot().redo.length-1];if(!t)return;let i=r.get(t);if(!i)throw new LedgerError(`Redo history is corrupted.`);try{await i.apply(e)}catch(t){throw e.signal.aborted?new LedgerCancelledError(`redo() was cancelled while running.`,{cause:t}):new LedgerExecutionError(toMessage(t),{cause:t})}if(s||e.signal.aborted)throw new LedgerCancelledError(`redo() was cancelled while running.`);updateState(e=>({...e,redo:e.redo.slice(0,-1),undo:[...e.undo,t]}))}return{clear(){return n.getSnapshot().undo.length===0&&n.getSnapshot().redo.length===0?s?Promise.reject(operationError(`clear`,!0)):Promise.resolve():enqueue(`clear`,void 0,async()=>{updateState(e=>({...e,redo:[],undo:[]}))})},get disposalSignal(){return i.signal},dispose(){if(s)return;s=!0,i.abort();let e=[...o].filter(e=>!e.started);for(let t of e)settle(t,operationError(`operation`,!0));for(let e of a)e.reject(operationError(`whenIdle`,!0));a.clear(),updateState(e=>({...e,queued:0,redo:[],undo:[]}))},get disposed(){return s},do(e,t){let n=snapshotCommand(e);return enqueue(`do`,t?.signal,e=>runDo(n,e))},record(e){if(s)throw new LedgerDisposedError(`Cannot call record() on a disposed ledger.`);let n=snapshotCommand(e);updateState(e=>{if(t===0)return{...e,redo:[]};let r=[...e.undo,n.entry];return r.length>t&&r.shift(),{...e,redo:[],undo:r}}),r.set(n.entry,n)},redo(e){return enqueue(`redo`,e?.signal,runRedo)},get state(){return n},[Symbol.dispose](){this.dispose()},undo(e){return enqueue(`undo`,e?.signal,runUndo)},whenIdle(){return n.getSnapshot().queued===0&&n.getSnapshot().running===0?s?Promise.reject(operationError(`whenIdle`,!0)):Promise.resolve():new Promise((e,t)=>a.add({reject:t,resolve:e}))}}}return e.LedgerCancelledError=LedgerCancelledError,e.LedgerConfigError=LedgerConfigError,e.LedgerDisposedError=LedgerDisposedError,e.LedgerError=LedgerError,e.LedgerExecutionError=LedgerExecutionError,e.LedgerRollbackError=LedgerRollbackError,e.compose=compose,e.createLedger=createLedger,e})({});
|
|
2
2
|
//# sourceMappingURL=ledger.iife.js.map
|
package/dist/ledger.iife.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.iife.js","names":[],"sources":["../src/compose.ts","../src/errors.ts","../src/ledger.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n","/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class LedgerConfigurationError extends LedgerError {}\n","import {\n LedgerCancelledError,\n LedgerConfigurationError,\n LedgerDisposedError,\n LedgerError,\n LedgerExecutionError,\n LedgerRollbackError,\n} from './errors';\nimport type {\n CommandContext,\n HistoryEntry,\n Ledger,\n LedgerCallOptions,\n LedgerOptions,\n LedgerReadable,\n LedgerState,\n ReversibleCommand,\n Unsubscribe,\n} from './types';\n\ntype StoredCommand<TMeta> = {\n apply: (context: CommandContext) => Promise<void> | void;\n entry: HistoryEntry<TMeta>;\n revert: (context: CommandContext) => Promise<void> | void;\n};\n\ntype Operation = {\n cancel: () => void;\n reject: (reason?: unknown) => void;\n resolve: () => void;\n settled: boolean;\n start: () => Promise<void>;\n started: boolean;\n};\n\nclass StateStore<T> implements LedgerReadable<T> {\n private currentValue: T;\n private readonly listeners = new Set<() => void>();\n\n constructor(initial: T) {\n this.currentValue = initial;\n }\n\n get value(): T {\n return this.currentValue;\n }\n\n peek(): T {\n return this.currentValue;\n }\n\n set(next: T): void {\n this.currentValue = next;\n\n for (const listener of [...this.listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n }\n\n subscribe(listener: () => void): Unsubscribe {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): StoredCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, entry: Object.freeze({ label, meta }), revert };\n}\n\nfunction snapshotState<TMeta>(state: LedgerState<TMeta>): LedgerState<TMeta> {\n return Object.freeze({\n ...state,\n redo: Object.freeze([...state.redo]),\n undo: Object.freeze([...state.undo]),\n });\n}\n\nfunction operationError(method: string, disposed: boolean): LedgerCancelledError | LedgerDisposedError {\n return disposed\n ? new LedgerDisposedError(`Cannot call ${method}() on a disposed ledger.`)\n : new LedgerCancelledError(`${method}() was cancelled before it started.`);\n}\n\n/**\n * Creates a serialized history of reversible commands.\n *\n * Commands are snapshotted when submitted. Queued commands cancelled before their queue turn do\n * not invoke user code. Active commands receive an abort signal and stop cooperatively.\n *\n * @example\n * const ledger = createLedger({ maxHistory: 50 });\n * await ledger.do({\n * apply: () => { item.name = next; },\n * revert: () => { item.name = previous; },\n * });\n * await ledger.undo();\n * await ledger.redo();\n * using ledger = createLedger();\n */\nexport function createLedger<TMeta = undefined>(options: LedgerOptions = {}): Ledger<TMeta> {\n const { maxHistory = 100 } = options;\n\n if (!Number.isSafeInteger(maxHistory) || maxHistory < 0) {\n throw new LedgerConfigurationError('maxHistory must be a non-negative safe integer');\n }\n\n const state = new StateStore<LedgerState<TMeta>>(\n snapshotState({ accepting: true, queued: 0, redo: [], running: 0, undo: [] }),\n );\n const commandStore = new WeakMap<HistoryEntry<TMeta>, StoredCommand<TMeta>>();\n const disposalController = new AbortController();\n const idleWaiters = new Set<() => void>();\n const operations = new Set<Operation>();\n let disposed = false;\n let queue = Promise.resolve();\n\n function updateState(update: (current: LedgerState<TMeta>) => LedgerState<TMeta>): void {\n state.set(snapshotState(update(state.value)));\n\n if (state.value.queued === 0 && state.value.running === 0) {\n for (const resolve of idleWaiters) resolve();\n idleWaiters.clear();\n }\n }\n\n function settle(operation: Operation, error?: unknown): void {\n if (operation.settled) return;\n\n operation.settled = true;\n operations.delete(operation);\n operation.cancel();\n\n if (error === undefined) operation.resolve();\n else operation.reject(error);\n }\n\n function enqueue(\n method: string,\n externalSignal: AbortSignal | undefined,\n task: (context: CommandContext) => Promise<void>,\n ): Promise<void> {\n if (disposed) return Promise.reject(operationError(method, true));\n\n const abortSignal = externalSignal\n ? AbortSignal.any([externalSignal, disposalController.signal])\n : disposalController.signal;\n\n return new Promise<void>((resolve, reject) => {\n const onAbort = (): void => {\n if (operation.started || operation.settled || disposed) return;\n\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, false));\n };\n\n const operation: Operation = {\n cancel: () => abortSignal.removeEventListener('abort', onAbort),\n reject,\n resolve,\n settled: false,\n start: async () => {\n if (operation.settled) return;\n\n if (disposed || abortSignal.aborted) {\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, disposed));\n\n return;\n }\n\n operation.started = true;\n updateState((current) => ({ ...current, queued: current.queued - 1, running: current.running + 1 }));\n\n try {\n await task({ signal: abortSignal });\n settle(operation);\n } catch (error) {\n settle(operation, error);\n } finally {\n updateState((current) => ({ ...current, running: current.running - 1 }));\n }\n },\n started: false,\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n operations.add(operation);\n updateState((current) => ({ ...current, queued: current.queued + 1 }));\n queue = queue.then(operation.start, operation.start);\n });\n }\n\n async function runDo(command: StoredCommand<TMeta>, context: CommandContext): Promise<void> {\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('do() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('do() was cancelled while running.');\n }\n\n updateState((current) => {\n if (maxHistory === 0) return { ...current, redo: [] };\n\n const undo = [...current.undo, command.entry];\n\n if (undo.length > maxHistory) undo.shift();\n\n return { ...current, redo: [], undo };\n });\n commandStore.set(command.entry, command);\n }\n\n async function runUndo(context: CommandContext): Promise<void> {\n const entry = state.value.undo[state.value.undo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Undo history is corrupted.');\n\n try {\n await command.revert(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('undo() was cancelled while running.', { cause: error })\n : new LedgerRollbackError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('undo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: [...current.redo, entry], undo: current.undo.slice(0, -1) }));\n }\n\n async function runRedo(context: CommandContext): Promise<void> {\n const entry = state.value.redo[state.value.redo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Redo history is corrupted.');\n\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('redo() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('redo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: current.redo.slice(0, -1), undo: [...current.undo, entry] }));\n }\n\n return {\n clear(): Promise<void> {\n return enqueue('clear', undefined, async () => {\n updateState((current) => ({ ...current, redo: [], undo: [] }));\n });\n },\n\n get disposalSignal(): AbortSignal {\n return disposalController.signal;\n },\n\n dispose(): void {\n if (disposed) return;\n\n disposed = true;\n disposalController.abort();\n\n const queued = [...operations].filter((operation) => !operation.started);\n\n for (const operation of queued) settle(operation, operationError('operation', true));\n\n updateState((current) => ({ ...current, accepting: false, queued: 0, redo: [], undo: [] }));\n },\n\n get disposed(): boolean {\n return disposed;\n },\n\n do(command: ReversibleCommand<TMeta>, callOptions?: LedgerCallOptions): Promise<void> {\n const snapshot = snapshotCommand(command);\n\n return enqueue('do', callOptions?.signal, (context) => runDo(snapshot, context));\n },\n\n redo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('redo', callOptions?.signal, runRedo);\n },\n\n get state() {\n return state;\n },\n\n [Symbol.dispose](): void {\n this.dispose();\n },\n\n undo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('undo', callOptions?.signal, runUndo);\n },\n\n whenIdle(): Promise<void> {\n if (state.value.queued === 0 && state.value.running === 0) return Promise.resolve();\n\n return new Promise((resolve) => idleWaiters.add(resolve));\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ledger.iife.js","names":[],"sources":["../src/compose.ts","../src/errors.ts","../src/ledger.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n","/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class LedgerConfigError extends LedgerError {}\n","import {\n LedgerCancelledError,\n LedgerConfigError,\n LedgerDisposedError,\n LedgerError,\n LedgerExecutionError,\n LedgerRollbackError,\n} from './errors';\nimport type {\n CommandContext,\n HistoryEntry,\n Ledger,\n LedgerCallOptions,\n LedgerOptions,\n LedgerState,\n ReversibleCommand,\n Subscribable,\n Unsubscribe,\n} from './types';\n\ntype StoredCommand<TMeta> = {\n apply: (context: CommandContext) => Promise<void> | void;\n entry: HistoryEntry<TMeta>;\n revert: (context: CommandContext) => Promise<void> | void;\n};\n\ntype Operation = {\n cancel: () => void;\n reject: (reason?: unknown) => void;\n resolve: () => void;\n settled: boolean;\n start: () => Promise<void>;\n started: boolean;\n};\n\nclass StateStore<T> implements Subscribable<T> {\n private current: T;\n private readonly listeners = new Set<() => void>();\n\n constructor(initial: T) {\n this.current = initial;\n }\n\n getSnapshot(): T {\n return this.current;\n }\n\n set(next: T): void {\n this.current = next;\n\n for (const listener of [...this.listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n }\n\n subscribe(listener: () => void): Unsubscribe {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): StoredCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, entry: Object.freeze({ label, meta }), revert };\n}\n\nfunction snapshotState<TMeta>(state: LedgerState<TMeta>): LedgerState<TMeta> {\n return Object.freeze({\n ...state,\n redo: Object.freeze([...state.redo]),\n undo: Object.freeze([...state.undo]),\n });\n}\n\nfunction operationError(method: string, disposed: boolean): LedgerCancelledError | LedgerDisposedError {\n return disposed\n ? new LedgerDisposedError(`Cannot call ${method}() on a disposed ledger.`)\n : new LedgerCancelledError(`${method}() was cancelled before it started.`);\n}\n\n/**\n * Creates a serialized history of reversible commands.\n *\n * Commands are snapshotted when submitted. Queued commands cancelled before their queue turn do\n * not invoke user code. Active commands receive an abort signal and stop cooperatively.\n *\n * @example\n * const ledger = createLedger({ maxHistory: 50 });\n * await ledger.do({\n * apply: () => { item.name = next; },\n * revert: () => { item.name = previous; },\n * });\n * await ledger.undo();\n * await ledger.redo();\n * using ledger = createLedger();\n */\nexport function createLedger<TMeta = undefined>(options: LedgerOptions = {}): Ledger<TMeta> {\n const { maxHistory = 100 } = options;\n\n if (!Number.isSafeInteger(maxHistory) || maxHistory < 0) {\n throw new LedgerConfigError('maxHistory must be a non-negative safe integer');\n }\n\n const state = new StateStore<LedgerState<TMeta>>(snapshotState({ queued: 0, redo: [], running: 0, undo: [] }));\n const commandStore = new WeakMap<HistoryEntry<TMeta>, StoredCommand<TMeta>>();\n const disposalController = new AbortController();\n const idleWaiters = new Set<{ readonly reject: (reason?: unknown) => void; readonly resolve: () => void }>();\n const operations = new Set<Operation>();\n let disposed = false;\n let queue = Promise.resolve();\n\n function updateState(update: (current: LedgerState<TMeta>) => LedgerState<TMeta>): void {\n state.set(snapshotState(update(state.getSnapshot())));\n\n if (state.getSnapshot().queued === 0 && state.getSnapshot().running === 0) {\n for (const waiter of idleWaiters) waiter.resolve();\n idleWaiters.clear();\n }\n }\n\n function settle(operation: Operation, error?: unknown): void {\n if (operation.settled) return;\n\n operation.settled = true;\n operations.delete(operation);\n operation.cancel();\n\n if (error === undefined) operation.resolve();\n else operation.reject(error);\n }\n\n function enqueue(\n method: string,\n externalSignal: AbortSignal | undefined,\n task: (context: CommandContext) => Promise<void>,\n ): Promise<void> {\n if (disposed) return Promise.reject(operationError(method, true));\n\n const abortSignal = externalSignal\n ? AbortSignal.any([externalSignal, disposalController.signal])\n : disposalController.signal;\n\n return new Promise<void>((resolve, reject) => {\n const onAbort = (): void => {\n if (operation.started || operation.settled || disposed) return;\n\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, false));\n };\n\n const operation: Operation = {\n cancel: () => abortSignal.removeEventListener('abort', onAbort),\n reject,\n resolve,\n settled: false,\n start: async () => {\n if (operation.settled) return;\n\n if (disposed || abortSignal.aborted) {\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, disposed));\n\n return;\n }\n\n operation.started = true;\n updateState((current) => ({ ...current, queued: current.queued - 1, running: current.running + 1 }));\n\n try {\n await task({ signal: abortSignal });\n settle(operation);\n } catch (error) {\n settle(operation, error);\n } finally {\n updateState((current) => ({ ...current, running: current.running - 1 }));\n }\n },\n started: false,\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n operations.add(operation);\n updateState((current) => ({ ...current, queued: current.queued + 1 }));\n queue = queue.then(operation.start, operation.start);\n });\n }\n\n async function runDo(command: StoredCommand<TMeta>, context: CommandContext): Promise<void> {\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('do() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('do() was cancelled while running.');\n }\n\n updateState((current) => {\n if (maxHistory === 0) return { ...current, redo: [] };\n\n const undo = [...current.undo, command.entry];\n\n if (undo.length > maxHistory) undo.shift();\n\n return { ...current, redo: [], undo };\n });\n commandStore.set(command.entry, command);\n }\n\n async function runUndo(context: CommandContext): Promise<void> {\n const entry = state.getSnapshot().undo[state.getSnapshot().undo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Undo history is corrupted.');\n\n try {\n await command.revert(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('undo() was cancelled while running.', { cause: error })\n : new LedgerRollbackError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('undo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: [...current.redo, entry], undo: current.undo.slice(0, -1) }));\n }\n\n async function runRedo(context: CommandContext): Promise<void> {\n const entry = state.getSnapshot().redo[state.getSnapshot().redo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Redo history is corrupted.');\n\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('redo() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('redo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: current.redo.slice(0, -1), undo: [...current.undo, entry] }));\n }\n\n return {\n clear(): Promise<void> {\n if (state.getSnapshot().undo.length === 0 && state.getSnapshot().redo.length === 0) {\n return disposed ? Promise.reject(operationError('clear', true)) : Promise.resolve();\n }\n\n return enqueue('clear', undefined, async () => {\n updateState((current) => ({ ...current, redo: [], undo: [] }));\n });\n },\n\n get disposalSignal(): AbortSignal {\n return disposalController.signal;\n },\n\n dispose(): void {\n if (disposed) return;\n\n disposed = true;\n disposalController.abort();\n\n const queued = [...operations].filter((operation) => !operation.started);\n\n for (const operation of queued) settle(operation, operationError('operation', true));\n\n for (const waiter of idleWaiters) waiter.reject(operationError('whenIdle', true));\n idleWaiters.clear();\n\n updateState((current) => ({ ...current, queued: 0, redo: [], undo: [] }));\n },\n\n get disposed(): boolean {\n return disposed;\n },\n\n do(command: ReversibleCommand<TMeta>, callOptions?: LedgerCallOptions): Promise<void> {\n const snapshot = snapshotCommand(command);\n\n return enqueue('do', callOptions?.signal, (context) => runDo(snapshot, context));\n },\n\n record(command: ReversibleCommand<TMeta>): void {\n if (disposed) throw new LedgerDisposedError('Cannot call record() on a disposed ledger.');\n\n const snapshot = snapshotCommand(command);\n updateState((current) => {\n if (maxHistory === 0) return { ...current, redo: [] };\n\n const undo = [...current.undo, snapshot.entry];\n\n if (undo.length > maxHistory) undo.shift();\n\n return { ...current, redo: [], undo };\n });\n commandStore.set(snapshot.entry, snapshot);\n },\n\n redo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('redo', callOptions?.signal, runRedo);\n },\n\n get state() {\n return state;\n },\n\n [Symbol.dispose](): void {\n this.dispose();\n },\n\n undo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('undo', callOptions?.signal, runUndo);\n },\n\n whenIdle(): Promise<void> {\n if (state.getSnapshot().queued === 0 && state.getSnapshot().running === 0) {\n return disposed ? Promise.reject(operationError('whenIdle', true)) : Promise.resolve();\n }\n\n return new Promise<void>((resolve, reject) => idleWaiters.add({ reject, resolve }));\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/ledger.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function
|
|
1
|
+
function snapshotCommand$1(e){let{apply:t,label:n,meta:r,revert:i}=e;return{apply:t,label:n,meta:r,revert:i}}function compose(e,t){let n=e.map(snapshotCommand$1);return{apply:async e=>{let t=[];try{for(let r of n)await r.apply(e),t.push(r)}catch(n){let r=[];for(let n of[...t].reverse())try{await n.revert(e)}catch(e){r.push(e)}throw r.length>0?AggregateError([n,...r],`Command application and compensation failed`,{cause:n}):n}},label:t,revert:async e=>{let t=[];for(let r of[...n].reverse())try{await r.revert(e)}catch(e){t.push(e)}if(t.length>0)throw AggregateError(t,`Command reversion failed`)}}}var LedgerError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},LedgerCancelledError=class extends LedgerError{},LedgerDisposedError=class extends LedgerError{},LedgerExecutionError=class extends LedgerError{},LedgerRollbackError=class extends LedgerError{},LedgerConfigError=class extends LedgerError{},StateStore=class{current;listeners=new Set;constructor(e){this.current=e}getSnapshot(){return this.current}set(e){this.current=e;for(let e of[...this.listeners])try{e()}catch(e){queueMicrotask(()=>{throw e})}}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}};function toMessage(e){return e instanceof Error?e.message:String(e)}function snapshotCommand(e){let{apply:t,label:n,meta:r,revert:i}=e;return{apply:t,entry:Object.freeze({label:n,meta:r}),revert:i}}function snapshotState(e){return Object.freeze({...e,redo:Object.freeze([...e.redo]),undo:Object.freeze([...e.undo])})}function operationError(e,t){return t?new LedgerDisposedError(`Cannot call ${e}() on a disposed ledger.`):new LedgerCancelledError(`${e}() was cancelled before it started.`)}function createLedger(e={}){let{maxHistory:t=100}=e;if(!Number.isSafeInteger(t)||t<0)throw new LedgerConfigError(`maxHistory must be a non-negative safe integer`);let n=new StateStore(snapshotState({queued:0,redo:[],running:0,undo:[]})),r=new WeakMap,i=new AbortController,a=new Set,o=new Set,s=!1,c=Promise.resolve();function updateState(e){if(n.set(snapshotState(e(n.getSnapshot()))),n.getSnapshot().queued===0&&n.getSnapshot().running===0){for(let e of a)e.resolve();a.clear()}}function settle(e,t){e.settled||(e.settled=!0,o.delete(e),e.cancel(),t===void 0?e.resolve():e.reject(t))}function enqueue(e,t,n){if(s)return Promise.reject(operationError(e,!0));let r=t?AbortSignal.any([t,i.signal]):i.signal;return new Promise((t,i)=>{let onAbort=()=>{a.started||a.settled||s||(updateState(e=>({...e,queued:e.queued-1})),settle(a,operationError(e,!1)))},a={cancel:()=>r.removeEventListener(`abort`,onAbort),reject:i,resolve:t,settled:!1,start:async()=>{if(!a.settled){if(s||r.aborted){updateState(e=>({...e,queued:e.queued-1})),settle(a,operationError(e,s));return}a.started=!0,updateState(e=>({...e,queued:e.queued-1,running:e.running+1}));try{await n({signal:r}),settle(a)}catch(e){settle(a,e)}finally{updateState(e=>({...e,running:e.running-1}))}}},started:!1};r.addEventListener(`abort`,onAbort,{once:!0}),o.add(a),updateState(e=>({...e,queued:e.queued+1})),c=c.then(a.start,a.start)})}async function runDo(e,n){try{await e.apply(n)}catch(e){throw n.signal.aborted?new LedgerCancelledError(`do() was cancelled while running.`,{cause:e}):new LedgerExecutionError(toMessage(e),{cause:e})}if(s||n.signal.aborted)throw new LedgerCancelledError(`do() was cancelled while running.`);updateState(n=>{if(t===0)return{...n,redo:[]};let r=[...n.undo,e.entry];return r.length>t&&r.shift(),{...n,redo:[],undo:r}}),r.set(e.entry,e)}async function runUndo(e){let t=n.getSnapshot().undo[n.getSnapshot().undo.length-1];if(!t)return;let i=r.get(t);if(!i)throw new LedgerError(`Undo history is corrupted.`);try{await i.revert(e)}catch(t){throw e.signal.aborted?new LedgerCancelledError(`undo() was cancelled while running.`,{cause:t}):new LedgerRollbackError(toMessage(t),{cause:t})}if(s||e.signal.aborted)throw new LedgerCancelledError(`undo() was cancelled while running.`);updateState(e=>({...e,redo:[...e.redo,t],undo:e.undo.slice(0,-1)}))}async function runRedo(e){let t=n.getSnapshot().redo[n.getSnapshot().redo.length-1];if(!t)return;let i=r.get(t);if(!i)throw new LedgerError(`Redo history is corrupted.`);try{await i.apply(e)}catch(t){throw e.signal.aborted?new LedgerCancelledError(`redo() was cancelled while running.`,{cause:t}):new LedgerExecutionError(toMessage(t),{cause:t})}if(s||e.signal.aborted)throw new LedgerCancelledError(`redo() was cancelled while running.`);updateState(e=>({...e,redo:e.redo.slice(0,-1),undo:[...e.undo,t]}))}return{clear(){return n.getSnapshot().undo.length===0&&n.getSnapshot().redo.length===0?s?Promise.reject(operationError(`clear`,!0)):Promise.resolve():enqueue(`clear`,void 0,async()=>{updateState(e=>({...e,redo:[],undo:[]}))})},get disposalSignal(){return i.signal},dispose(){if(s)return;s=!0,i.abort();let e=[...o].filter(e=>!e.started);for(let t of e)settle(t,operationError(`operation`,!0));for(let e of a)e.reject(operationError(`whenIdle`,!0));a.clear(),updateState(e=>({...e,queued:0,redo:[],undo:[]}))},get disposed(){return s},do(e,t){let n=snapshotCommand(e);return enqueue(`do`,t?.signal,e=>runDo(n,e))},record(e){if(s)throw new LedgerDisposedError(`Cannot call record() on a disposed ledger.`);let n=snapshotCommand(e);updateState(e=>{if(t===0)return{...e,redo:[]};let r=[...e.undo,n.entry];return r.length>t&&r.shift(),{...e,redo:[],undo:r}}),r.set(n.entry,n)},redo(e){return enqueue(`redo`,e?.signal,runRedo)},get state(){return n},[Symbol.dispose](){this.dispose()},undo(e){return enqueue(`undo`,e?.signal,runUndo)},whenIdle(){return n.getSnapshot().queued===0&&n.getSnapshot().running===0?s?Promise.reject(operationError(`whenIdle`,!0)):Promise.resolve():new Promise((e,t)=>a.add({reject:t,resolve:e}))}}}export{LedgerCancelledError,LedgerConfigError,LedgerDisposedError,LedgerError,LedgerExecutionError,LedgerRollbackError,compose,createLedger};
|
|
2
2
|
//# sourceMappingURL=ledger.js.map
|
package/dist/ledger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.js","names":[],"sources":["../src/compose.ts","../src/errors.ts","../src/ledger.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n","/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class LedgerConfigurationError extends LedgerError {}\n","import {\n LedgerCancelledError,\n LedgerConfigurationError,\n LedgerDisposedError,\n LedgerError,\n LedgerExecutionError,\n LedgerRollbackError,\n} from './errors';\nimport type {\n CommandContext,\n HistoryEntry,\n Ledger,\n LedgerCallOptions,\n LedgerOptions,\n LedgerReadable,\n LedgerState,\n ReversibleCommand,\n Unsubscribe,\n} from './types';\n\ntype StoredCommand<TMeta> = {\n apply: (context: CommandContext) => Promise<void> | void;\n entry: HistoryEntry<TMeta>;\n revert: (context: CommandContext) => Promise<void> | void;\n};\n\ntype Operation = {\n cancel: () => void;\n reject: (reason?: unknown) => void;\n resolve: () => void;\n settled: boolean;\n start: () => Promise<void>;\n started: boolean;\n};\n\nclass StateStore<T> implements LedgerReadable<T> {\n private currentValue: T;\n private readonly listeners = new Set<() => void>();\n\n constructor(initial: T) {\n this.currentValue = initial;\n }\n\n get value(): T {\n return this.currentValue;\n }\n\n peek(): T {\n return this.currentValue;\n }\n\n set(next: T): void {\n this.currentValue = next;\n\n for (const listener of [...this.listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n }\n\n subscribe(listener: () => void): Unsubscribe {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): StoredCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, entry: Object.freeze({ label, meta }), revert };\n}\n\nfunction snapshotState<TMeta>(state: LedgerState<TMeta>): LedgerState<TMeta> {\n return Object.freeze({\n ...state,\n redo: Object.freeze([...state.redo]),\n undo: Object.freeze([...state.undo]),\n });\n}\n\nfunction operationError(method: string, disposed: boolean): LedgerCancelledError | LedgerDisposedError {\n return disposed\n ? new LedgerDisposedError(`Cannot call ${method}() on a disposed ledger.`)\n : new LedgerCancelledError(`${method}() was cancelled before it started.`);\n}\n\n/**\n * Creates a serialized history of reversible commands.\n *\n * Commands are snapshotted when submitted. Queued commands cancelled before their queue turn do\n * not invoke user code. Active commands receive an abort signal and stop cooperatively.\n *\n * @example\n * const ledger = createLedger({ maxHistory: 50 });\n * await ledger.do({\n * apply: () => { item.name = next; },\n * revert: () => { item.name = previous; },\n * });\n * await ledger.undo();\n * await ledger.redo();\n * using ledger = createLedger();\n */\nexport function createLedger<TMeta = undefined>(options: LedgerOptions = {}): Ledger<TMeta> {\n const { maxHistory = 100 } = options;\n\n if (!Number.isSafeInteger(maxHistory) || maxHistory < 0) {\n throw new LedgerConfigurationError('maxHistory must be a non-negative safe integer');\n }\n\n const state = new StateStore<LedgerState<TMeta>>(\n snapshotState({ accepting: true, queued: 0, redo: [], running: 0, undo: [] }),\n );\n const commandStore = new WeakMap<HistoryEntry<TMeta>, StoredCommand<TMeta>>();\n const disposalController = new AbortController();\n const idleWaiters = new Set<() => void>();\n const operations = new Set<Operation>();\n let disposed = false;\n let queue = Promise.resolve();\n\n function updateState(update: (current: LedgerState<TMeta>) => LedgerState<TMeta>): void {\n state.set(snapshotState(update(state.value)));\n\n if (state.value.queued === 0 && state.value.running === 0) {\n for (const resolve of idleWaiters) resolve();\n idleWaiters.clear();\n }\n }\n\n function settle(operation: Operation, error?: unknown): void {\n if (operation.settled) return;\n\n operation.settled = true;\n operations.delete(operation);\n operation.cancel();\n\n if (error === undefined) operation.resolve();\n else operation.reject(error);\n }\n\n function enqueue(\n method: string,\n externalSignal: AbortSignal | undefined,\n task: (context: CommandContext) => Promise<void>,\n ): Promise<void> {\n if (disposed) return Promise.reject(operationError(method, true));\n\n const abortSignal = externalSignal\n ? AbortSignal.any([externalSignal, disposalController.signal])\n : disposalController.signal;\n\n return new Promise<void>((resolve, reject) => {\n const onAbort = (): void => {\n if (operation.started || operation.settled || disposed) return;\n\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, false));\n };\n\n const operation: Operation = {\n cancel: () => abortSignal.removeEventListener('abort', onAbort),\n reject,\n resolve,\n settled: false,\n start: async () => {\n if (operation.settled) return;\n\n if (disposed || abortSignal.aborted) {\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, disposed));\n\n return;\n }\n\n operation.started = true;\n updateState((current) => ({ ...current, queued: current.queued - 1, running: current.running + 1 }));\n\n try {\n await task({ signal: abortSignal });\n settle(operation);\n } catch (error) {\n settle(operation, error);\n } finally {\n updateState((current) => ({ ...current, running: current.running - 1 }));\n }\n },\n started: false,\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n operations.add(operation);\n updateState((current) => ({ ...current, queued: current.queued + 1 }));\n queue = queue.then(operation.start, operation.start);\n });\n }\n\n async function runDo(command: StoredCommand<TMeta>, context: CommandContext): Promise<void> {\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('do() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('do() was cancelled while running.');\n }\n\n updateState((current) => {\n if (maxHistory === 0) return { ...current, redo: [] };\n\n const undo = [...current.undo, command.entry];\n\n if (undo.length > maxHistory) undo.shift();\n\n return { ...current, redo: [], undo };\n });\n commandStore.set(command.entry, command);\n }\n\n async function runUndo(context: CommandContext): Promise<void> {\n const entry = state.value.undo[state.value.undo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Undo history is corrupted.');\n\n try {\n await command.revert(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('undo() was cancelled while running.', { cause: error })\n : new LedgerRollbackError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('undo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: [...current.redo, entry], undo: current.undo.slice(0, -1) }));\n }\n\n async function runRedo(context: CommandContext): Promise<void> {\n const entry = state.value.redo[state.value.redo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Redo history is corrupted.');\n\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('redo() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('redo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: current.redo.slice(0, -1), undo: [...current.undo, entry] }));\n }\n\n return {\n clear(): Promise<void> {\n return enqueue('clear', undefined, async () => {\n updateState((current) => ({ ...current, redo: [], undo: [] }));\n });\n },\n\n get disposalSignal(): AbortSignal {\n return disposalController.signal;\n },\n\n dispose(): void {\n if (disposed) return;\n\n disposed = true;\n disposalController.abort();\n\n const queued = [...operations].filter((operation) => !operation.started);\n\n for (const operation of queued) settle(operation, operationError('operation', true));\n\n updateState((current) => ({ ...current, accepting: false, queued: 0, redo: [], undo: [] }));\n },\n\n get disposed(): boolean {\n return disposed;\n },\n\n do(command: ReversibleCommand<TMeta>, callOptions?: LedgerCallOptions): Promise<void> {\n const snapshot = snapshotCommand(command);\n\n return enqueue('do', callOptions?.signal, (context) => runDo(snapshot, context));\n },\n\n redo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('redo', callOptions?.signal, runRedo);\n },\n\n get state() {\n return state;\n },\n\n [Symbol.dispose](): void {\n this.dispose();\n },\n\n undo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('undo', callOptions?.signal, runUndo);\n },\n\n whenIdle(): Promise<void> {\n if (state.value.queued === 0 && state.value.running === 0) return Promise.resolve();\n\n return new Promise((resolve) => idleWaiters.add(resolve));\n },\n };\n}\n"],"mappings":"AAEA,SAAS,EAAuB,EAA6D,CAC3F,GAAM,CAAE,QAAO,QAAO,OAAM,UAAW,EAEvC,MAAO,CAAE,QAAO,QAAO,OAAM,QAAO,CACtC,CAcA,SAAgB,EACd,EACA,EAC0B,CAC1B,IAAM,EAAQ,EAAS,IAAI,CAAe,EAE1C,MAAO,CACL,MAAO,KAAO,IAA4B,CACxC,IAAM,EAAsC,CAAC,EAE7C,GAAI,CACF,IAAK,IAAM,KAAW,EACpB,MAAM,EAAQ,MAAM,CAAO,EAC3B,EAAQ,KAAK,CAAO,CAExB,OAAS,EAAO,CACd,IAAM,EAAkC,CAAC,EAEzC,IAAK,IAAM,IAAW,CAAC,GAAG,CAAO,CAAC,CAAC,QAAQ,EACzC,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAmB,CAC1B,EAAqB,KAAK,CAAiB,CAC7C,CASF,MANI,EAAqB,OAAS,EACtB,eAAe,CAAC,EAAO,GAAG,CAAoB,EAAG,8CAA+C,CACxG,MAAO,CACT,CAAC,EAGG,CACR,CACF,EACA,QACA,OAAQ,KAAO,IAA4B,CACzC,IAAM,EAAsB,CAAC,EAE7B,IAAK,IAAM,IAAW,CAAC,GAAG,CAAK,CAAC,CAAC,QAAQ,EACvC,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAO,CACd,EAAS,KAAK,CAAK,CACrB,CAGF,GAAI,EAAS,OAAS,EAAG,MAAU,eAAe,EAAU,0BAA0B,CACxF,CACF,CACF,CCrEA,IAAa,EAAb,cAAiC,KAAM,CACrC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAGa,EAAb,cAA0C,CAAY,CAAC,EAG1C,EAAb,cAAyC,CAAY,CAAC,EAGzC,EAAb,cAA0C,CAAY,CAAC,EAG1C,EAAb,cAAyC,CAAY,CAAC,EAGzC,EAAb,cAA8C,CAAY,CAAC,ECarD,EAAN,KAAiD,CAC/C,aACA,UAA6B,IAAI,IAEjC,YAAY,EAAY,CACtB,KAAK,aAAe,CACtB,CAEA,IAAI,OAAW,CACb,OAAO,KAAK,YACd,CAEA,MAAU,CACR,OAAO,KAAK,YACd,CAEA,IAAI,EAAe,CACjB,KAAK,aAAe,EAEpB,IAAK,IAAM,IAAY,CAAC,GAAG,KAAK,SAAS,EACvC,GAAI,CACF,EAAS,CACX,OAAS,EAAO,CACd,mBAAqB,CACnB,MAAM,CACR,CAAC,CACH,CAEJ,CAEA,UAAU,EAAmC,CAE3C,OADA,KAAK,UAAU,IAAI,CAAQ,MACd,KAAK,UAAU,OAAO,CAAQ,CAC7C,CACF,EAEA,SAAS,EAAU,EAAwB,CACzC,OAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAC9D,CAEA,SAAS,EAAuB,EAAyD,CACvF,GAAM,CAAE,QAAO,QAAO,OAAM,UAAW,EAEvC,MAAO,CAAE,QAAO,MAAO,OAAO,OAAO,CAAE,QAAO,MAAK,CAAC,EAAG,QAAO,CAChE,CAEA,SAAS,EAAqB,EAA+C,CAC3E,OAAO,OAAO,OAAO,CACnB,GAAG,EACH,KAAM,OAAO,OAAO,CAAC,GAAG,EAAM,IAAI,CAAC,EACnC,KAAM,OAAO,OAAO,CAAC,GAAG,EAAM,IAAI,CAAC,CACrC,CAAC,CACH,CAEA,SAAS,EAAe,EAAgB,EAA+D,CACrG,OAAO,EACH,IAAI,EAAoB,eAAe,EAAO,yBAAyB,EACvE,IAAI,EAAqB,GAAG,EAAO,oCAAoC,CAC7E,CAkBA,SAAgB,EAAgC,EAAyB,CAAC,EAAkB,CAC1F,GAAM,CAAE,aAAa,KAAQ,EAE7B,GAAI,CAAC,OAAO,cAAc,CAAU,GAAK,EAAa,EACpD,MAAM,IAAI,EAAyB,gDAAgD,EAGrF,IAAM,EAAQ,IAAI,EAChB,EAAc,CAAE,UAAW,GAAM,OAAQ,EAAG,KAAM,CAAC,EAAG,QAAS,EAAG,KAAM,CAAC,CAAE,CAAC,CAC9E,EACM,EAAe,IAAI,QACnB,EAAqB,IAAI,gBACzB,EAAc,IAAI,IAClB,EAAa,IAAI,IACnB,EAAW,GACX,EAAQ,QAAQ,QAAQ,EAE5B,SAAS,EAAY,EAAmE,CAGtF,GAFA,EAAM,IAAI,EAAc,EAAO,EAAM,KAAK,CAAC,CAAC,EAExC,EAAM,MAAM,SAAW,GAAK,EAAM,MAAM,UAAY,EAAG,CACzD,IAAK,IAAM,KAAW,EAAa,EAAQ,EAC3C,EAAY,MAAM,CACpB,CACF,CAEA,SAAS,EAAO,EAAsB,EAAuB,CACvD,EAAU,UAEd,EAAU,QAAU,GACpB,EAAW,OAAO,CAAS,EAC3B,EAAU,OAAO,EAEb,IAAU,IAAA,GAAW,EAAU,QAAQ,EACtC,EAAU,OAAO,CAAK,EAC7B,CAEA,SAAS,EACP,EACA,EACA,EACe,CACf,GAAI,EAAU,OAAO,QAAQ,OAAO,EAAe,EAAQ,EAAI,CAAC,EAEhE,IAAM,EAAc,EAChB,YAAY,IAAI,CAAC,EAAgB,EAAmB,MAAM,CAAC,EAC3D,EAAmB,OAEvB,OAAO,IAAI,SAAe,EAAS,IAAW,CAC5C,IAAM,MAAsB,CACtB,EAAU,SAAW,EAAU,SAAW,IAE9C,EAAa,IAAa,CAAE,GAAG,EAAS,OAAQ,EAAQ,OAAS,CAAE,EAAE,EACrE,EAAO,EAAW,EAAe,EAAQ,EAAK,CAAC,EACjD,EAEM,EAAuB,CAC3B,WAAc,EAAY,oBAAoB,QAAS,CAAO,EAC9D,SACA,UACA,QAAS,GACT,MAAO,SAAY,CACb,MAAU,QAEd,IAAI,GAAY,EAAY,QAAS,CACnC,EAAa,IAAa,CAAE,GAAG,EAAS,OAAQ,EAAQ,OAAS,CAAE,EAAE,EACrE,EAAO,EAAW,EAAe,EAAQ,CAAQ,CAAC,EAElD,MACF,CAEA,EAAU,QAAU,GACpB,EAAa,IAAa,CAAE,GAAG,EAAS,OAAQ,EAAQ,OAAS,EAAG,QAAS,EAAQ,QAAU,CAAE,EAAE,EAEnG,GAAI,CACF,MAAM,EAAK,CAAE,OAAQ,CAAY,CAAC,EAClC,EAAO,CAAS,CAClB,OAAS,EAAO,CACd,EAAO,EAAW,CAAK,CACzB,QAAU,CACR,EAAa,IAAa,CAAE,GAAG,EAAS,QAAS,EAAQ,QAAU,CAAE,EAAE,CACzE,CAZA,CAaF,EACA,QAAS,EACX,EAEA,EAAY,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,EAC7D,EAAW,IAAI,CAAS,EACxB,EAAa,IAAa,CAAE,GAAG,EAAS,OAAQ,EAAQ,OAAS,CAAE,EAAE,EACrE,EAAQ,EAAM,KAAK,EAAU,MAAO,EAAU,KAAK,CACrD,CAAC,CACH,CAEA,eAAe,EAAM,EAA+B,EAAwC,CAC1F,GAAI,CACF,MAAM,EAAQ,MAAM,CAAO,CAC7B,OAAS,EAAO,CACd,MAAM,EAAQ,OAAO,QACjB,IAAI,EAAqB,oCAAqC,CAAE,MAAO,CAAM,CAAC,EAC9E,IAAI,EAAqB,EAAU,CAAK,EAAG,CAAE,MAAO,CAAM,CAAC,CACjE,CAEA,GAAI,GAAY,EAAQ,OAAO,QAC7B,MAAM,IAAI,EAAqB,mCAAmC,EAGpE,EAAa,GAAY,CACvB,GAAI,IAAe,EAAG,MAAO,CAAE,GAAG,EAAS,KAAM,CAAC,CAAE,EAEpD,IAAM,EAAO,CAAC,GAAG,EAAQ,KAAM,EAAQ,KAAK,EAI5C,OAFI,EAAK,OAAS,GAAY,EAAK,MAAM,EAElC,CAAE,GAAG,EAAS,KAAM,CAAC,EAAG,MAAK,CACtC,CAAC,EACD,EAAa,IAAI,EAAQ,MAAO,CAAO,CACzC,CAEA,eAAe,EAAQ,EAAwC,CAC7D,IAAM,EAAQ,EAAM,MAAM,KAAK,EAAM,MAAM,KAAK,OAAS,GAEzD,GAAI,CAAC,EAAO,OAEZ,IAAM,EAAU,EAAa,IAAI,CAAK,EAEtC,GAAI,CAAC,EAAS,MAAM,IAAI,EAAY,4BAA4B,EAEhE,GAAI,CACF,MAAM,EAAQ,OAAO,CAAO,CAC9B,OAAS,EAAO,CACd,MAAM,EAAQ,OAAO,QACjB,IAAI,EAAqB,sCAAuC,CAAE,MAAO,CAAM,CAAC,EAChF,IAAI,EAAoB,EAAU,CAAK,EAAG,CAAE,MAAO,CAAM,CAAC,CAChE,CAEA,GAAI,GAAY,EAAQ,OAAO,QAC7B,MAAM,IAAI,EAAqB,qCAAqC,EAGtE,EAAa,IAAa,CAAE,GAAG,EAAS,KAAM,CAAC,GAAG,EAAQ,KAAM,CAAK,EAAG,KAAM,EAAQ,KAAK,MAAM,EAAG,EAAE,CAAE,EAAE,CAC5G,CAEA,eAAe,EAAQ,EAAwC,CAC7D,IAAM,EAAQ,EAAM,MAAM,KAAK,EAAM,MAAM,KAAK,OAAS,GAEzD,GAAI,CAAC,EAAO,OAEZ,IAAM,EAAU,EAAa,IAAI,CAAK,EAEtC,GAAI,CAAC,EAAS,MAAM,IAAI,EAAY,4BAA4B,EAEhE,GAAI,CACF,MAAM,EAAQ,MAAM,CAAO,CAC7B,OAAS,EAAO,CACd,MAAM,EAAQ,OAAO,QACjB,IAAI,EAAqB,sCAAuC,CAAE,MAAO,CAAM,CAAC,EAChF,IAAI,EAAqB,EAAU,CAAK,EAAG,CAAE,MAAO,CAAM,CAAC,CACjE,CAEA,GAAI,GAAY,EAAQ,OAAO,QAC7B,MAAM,IAAI,EAAqB,qCAAqC,EAGtE,EAAa,IAAa,CAAE,GAAG,EAAS,KAAM,EAAQ,KAAK,MAAM,EAAG,EAAE,EAAG,KAAM,CAAC,GAAG,EAAQ,KAAM,CAAK,CAAE,EAAE,CAC5G,CAEA,MAAO,CACL,OAAuB,CACrB,OAAO,EAAQ,QAAS,IAAA,GAAW,SAAY,CAC7C,EAAa,IAAa,CAAE,GAAG,EAAS,KAAM,CAAC,EAAG,KAAM,CAAC,CAAE,EAAE,CAC/D,CAAC,CACH,EAEA,IAAI,gBAA8B,CAChC,OAAO,EAAmB,MAC5B,EAEA,SAAgB,CACd,GAAI,EAAU,OAEd,EAAW,GACX,EAAmB,MAAM,EAEzB,IAAM,EAAS,CAAC,GAAG,CAAU,CAAC,CAAC,OAAQ,GAAc,CAAC,EAAU,OAAO,EAEvE,IAAK,IAAM,KAAa,EAAQ,EAAO,EAAW,EAAe,YAAa,EAAI,CAAC,EAEnF,EAAa,IAAa,CAAE,GAAG,EAAS,UAAW,GAAO,OAAQ,EAAG,KAAM,CAAC,EAAG,KAAM,CAAC,CAAE,EAAE,CAC5F,EAEA,IAAI,UAAoB,CACtB,OAAO,CACT,EAEA,GAAG,EAAmC,EAAgD,CACpF,IAAM,EAAW,EAAgB,CAAO,EAExC,OAAO,EAAQ,KAAM,GAAa,OAAS,GAAY,EAAM,EAAU,CAAO,CAAC,CACjF,EAEA,KAAK,EAAgD,CACnD,OAAO,EAAQ,OAAQ,GAAa,OAAQ,CAAO,CACrD,EAEA,IAAI,OAAQ,CACV,OAAO,CACT,EAEA,CAAC,OAAO,UAAiB,CACvB,KAAK,QAAQ,CACf,EAEA,KAAK,EAAgD,CACnD,OAAO,EAAQ,OAAQ,GAAa,OAAQ,CAAO,CACrD,EAEA,UAA0B,CAGxB,OAFI,EAAM,MAAM,SAAW,GAAK,EAAM,MAAM,UAAY,EAAU,QAAQ,QAAQ,EAE3E,IAAI,QAAS,GAAY,EAAY,IAAI,CAAO,CAAC,CAC1D,CACF,CACF"}
|
|
1
|
+
{"version":3,"file":"ledger.js","names":[],"sources":["../src/compose.ts","../src/errors.ts","../src/ledger.ts"],"sourcesContent":["import type { CommandContext, ReversibleCommand } from './types';\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): ReversibleCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, label, meta, revert };\n}\n\n/**\n * Composes reversible commands into one reversible command.\n *\n * `apply` runs each child in order. `revert` runs children in reverse order.\n * A failed child application compensates completed children before rethrowing.\n *\n * @example\n * await ledger.do(compose([\n * { apply: () => { node.x = newX; }, revert: () => { node.x = oldX; } },\n * { apply: () => { node.y = newY; }, revert: () => { node.y = oldY; } },\n * ], 'Move node'));\n */\nexport function compose<TMeta = undefined>(\n commands: readonly ReversibleCommand<TMeta>[],\n label?: string,\n): ReversibleCommand<TMeta> {\n const steps = commands.map(snapshotCommand);\n\n return {\n apply: async (context: CommandContext) => {\n const applied: ReversibleCommand<TMeta>[] = [];\n\n try {\n for (const command of steps) {\n await command.apply(context);\n applied.push(command);\n }\n } catch (error) {\n const compensationFailures: unknown[] = [];\n\n for (const command of [...applied].reverse()) {\n try {\n await command.revert(context);\n } catch (compensationError) {\n compensationFailures.push(compensationError);\n }\n }\n\n if (compensationFailures.length > 0) {\n throw new AggregateError([error, ...compensationFailures], 'Command application and compensation failed', {\n cause: error,\n });\n }\n\n throw error;\n }\n },\n label,\n revert: async (context: CommandContext) => {\n const failures: unknown[] = [];\n\n for (const command of [...steps].reverse()) {\n try {\n await command.revert(context);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) throw new AggregateError(failures, 'Command reversion failed');\n },\n };\n}\n","/** Base class for all ledger errors. Use `instanceof LedgerError` to catch any ledger-originated error. */\nexport class LedgerError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown when a queued operation is cancelled before user code starts, or an active operation cooperatively stops. */\nexport class LedgerCancelledError extends LedgerError {}\n\n/** Thrown when a method is called on a disposed ledger instance. */\nexport class LedgerDisposedError extends LedgerError {}\n\n/** Thrown when a command's `apply()` function throws. The original error is available via `.cause`. */\nexport class LedgerExecutionError extends LedgerError {}\n\n/** Thrown when a command's `revert()` function throws during an undo operation. The original error is available via `.cause`. */\nexport class LedgerRollbackError extends LedgerError {}\n\n/** Thrown when the ledger is constructed with invalid options. */\nexport class LedgerConfigError extends LedgerError {}\n","import {\n LedgerCancelledError,\n LedgerConfigError,\n LedgerDisposedError,\n LedgerError,\n LedgerExecutionError,\n LedgerRollbackError,\n} from './errors';\nimport type {\n CommandContext,\n HistoryEntry,\n Ledger,\n LedgerCallOptions,\n LedgerOptions,\n LedgerState,\n ReversibleCommand,\n Subscribable,\n Unsubscribe,\n} from './types';\n\ntype StoredCommand<TMeta> = {\n apply: (context: CommandContext) => Promise<void> | void;\n entry: HistoryEntry<TMeta>;\n revert: (context: CommandContext) => Promise<void> | void;\n};\n\ntype Operation = {\n cancel: () => void;\n reject: (reason?: unknown) => void;\n resolve: () => void;\n settled: boolean;\n start: () => Promise<void>;\n started: boolean;\n};\n\nclass StateStore<T> implements Subscribable<T> {\n private current: T;\n private readonly listeners = new Set<() => void>();\n\n constructor(initial: T) {\n this.current = initial;\n }\n\n getSnapshot(): T {\n return this.current;\n }\n\n set(next: T): void {\n this.current = next;\n\n for (const listener of [...this.listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n }\n\n subscribe(listener: () => void): Unsubscribe {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction snapshotCommand<TMeta>(command: ReversibleCommand<TMeta>): StoredCommand<TMeta> {\n const { apply, label, meta, revert } = command;\n\n return { apply, entry: Object.freeze({ label, meta }), revert };\n}\n\nfunction snapshotState<TMeta>(state: LedgerState<TMeta>): LedgerState<TMeta> {\n return Object.freeze({\n ...state,\n redo: Object.freeze([...state.redo]),\n undo: Object.freeze([...state.undo]),\n });\n}\n\nfunction operationError(method: string, disposed: boolean): LedgerCancelledError | LedgerDisposedError {\n return disposed\n ? new LedgerDisposedError(`Cannot call ${method}() on a disposed ledger.`)\n : new LedgerCancelledError(`${method}() was cancelled before it started.`);\n}\n\n/**\n * Creates a serialized history of reversible commands.\n *\n * Commands are snapshotted when submitted. Queued commands cancelled before their queue turn do\n * not invoke user code. Active commands receive an abort signal and stop cooperatively.\n *\n * @example\n * const ledger = createLedger({ maxHistory: 50 });\n * await ledger.do({\n * apply: () => { item.name = next; },\n * revert: () => { item.name = previous; },\n * });\n * await ledger.undo();\n * await ledger.redo();\n * using ledger = createLedger();\n */\nexport function createLedger<TMeta = undefined>(options: LedgerOptions = {}): Ledger<TMeta> {\n const { maxHistory = 100 } = options;\n\n if (!Number.isSafeInteger(maxHistory) || maxHistory < 0) {\n throw new LedgerConfigError('maxHistory must be a non-negative safe integer');\n }\n\n const state = new StateStore<LedgerState<TMeta>>(snapshotState({ queued: 0, redo: [], running: 0, undo: [] }));\n const commandStore = new WeakMap<HistoryEntry<TMeta>, StoredCommand<TMeta>>();\n const disposalController = new AbortController();\n const idleWaiters = new Set<{ readonly reject: (reason?: unknown) => void; readonly resolve: () => void }>();\n const operations = new Set<Operation>();\n let disposed = false;\n let queue = Promise.resolve();\n\n function updateState(update: (current: LedgerState<TMeta>) => LedgerState<TMeta>): void {\n state.set(snapshotState(update(state.getSnapshot())));\n\n if (state.getSnapshot().queued === 0 && state.getSnapshot().running === 0) {\n for (const waiter of idleWaiters) waiter.resolve();\n idleWaiters.clear();\n }\n }\n\n function settle(operation: Operation, error?: unknown): void {\n if (operation.settled) return;\n\n operation.settled = true;\n operations.delete(operation);\n operation.cancel();\n\n if (error === undefined) operation.resolve();\n else operation.reject(error);\n }\n\n function enqueue(\n method: string,\n externalSignal: AbortSignal | undefined,\n task: (context: CommandContext) => Promise<void>,\n ): Promise<void> {\n if (disposed) return Promise.reject(operationError(method, true));\n\n const abortSignal = externalSignal\n ? AbortSignal.any([externalSignal, disposalController.signal])\n : disposalController.signal;\n\n return new Promise<void>((resolve, reject) => {\n const onAbort = (): void => {\n if (operation.started || operation.settled || disposed) return;\n\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, false));\n };\n\n const operation: Operation = {\n cancel: () => abortSignal.removeEventListener('abort', onAbort),\n reject,\n resolve,\n settled: false,\n start: async () => {\n if (operation.settled) return;\n\n if (disposed || abortSignal.aborted) {\n updateState((current) => ({ ...current, queued: current.queued - 1 }));\n settle(operation, operationError(method, disposed));\n\n return;\n }\n\n operation.started = true;\n updateState((current) => ({ ...current, queued: current.queued - 1, running: current.running + 1 }));\n\n try {\n await task({ signal: abortSignal });\n settle(operation);\n } catch (error) {\n settle(operation, error);\n } finally {\n updateState((current) => ({ ...current, running: current.running - 1 }));\n }\n },\n started: false,\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n operations.add(operation);\n updateState((current) => ({ ...current, queued: current.queued + 1 }));\n queue = queue.then(operation.start, operation.start);\n });\n }\n\n async function runDo(command: StoredCommand<TMeta>, context: CommandContext): Promise<void> {\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('do() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('do() was cancelled while running.');\n }\n\n updateState((current) => {\n if (maxHistory === 0) return { ...current, redo: [] };\n\n const undo = [...current.undo, command.entry];\n\n if (undo.length > maxHistory) undo.shift();\n\n return { ...current, redo: [], undo };\n });\n commandStore.set(command.entry, command);\n }\n\n async function runUndo(context: CommandContext): Promise<void> {\n const entry = state.getSnapshot().undo[state.getSnapshot().undo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Undo history is corrupted.');\n\n try {\n await command.revert(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('undo() was cancelled while running.', { cause: error })\n : new LedgerRollbackError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('undo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: [...current.redo, entry], undo: current.undo.slice(0, -1) }));\n }\n\n async function runRedo(context: CommandContext): Promise<void> {\n const entry = state.getSnapshot().redo[state.getSnapshot().redo.length - 1];\n\n if (!entry) return;\n\n const command = commandStore.get(entry);\n\n if (!command) throw new LedgerError('Redo history is corrupted.');\n\n try {\n await command.apply(context);\n } catch (error) {\n throw context.signal.aborted\n ? new LedgerCancelledError('redo() was cancelled while running.', { cause: error })\n : new LedgerExecutionError(toMessage(error), { cause: error });\n }\n\n if (disposed || context.signal.aborted) {\n throw new LedgerCancelledError('redo() was cancelled while running.');\n }\n\n updateState((current) => ({ ...current, redo: current.redo.slice(0, -1), undo: [...current.undo, entry] }));\n }\n\n return {\n clear(): Promise<void> {\n if (state.getSnapshot().undo.length === 0 && state.getSnapshot().redo.length === 0) {\n return disposed ? Promise.reject(operationError('clear', true)) : Promise.resolve();\n }\n\n return enqueue('clear', undefined, async () => {\n updateState((current) => ({ ...current, redo: [], undo: [] }));\n });\n },\n\n get disposalSignal(): AbortSignal {\n return disposalController.signal;\n },\n\n dispose(): void {\n if (disposed) return;\n\n disposed = true;\n disposalController.abort();\n\n const queued = [...operations].filter((operation) => !operation.started);\n\n for (const operation of queued) settle(operation, operationError('operation', true));\n\n for (const waiter of idleWaiters) waiter.reject(operationError('whenIdle', true));\n idleWaiters.clear();\n\n updateState((current) => ({ ...current, queued: 0, redo: [], undo: [] }));\n },\n\n get disposed(): boolean {\n return disposed;\n },\n\n do(command: ReversibleCommand<TMeta>, callOptions?: LedgerCallOptions): Promise<void> {\n const snapshot = snapshotCommand(command);\n\n return enqueue('do', callOptions?.signal, (context) => runDo(snapshot, context));\n },\n\n record(command: ReversibleCommand<TMeta>): void {\n if (disposed) throw new LedgerDisposedError('Cannot call record() on a disposed ledger.');\n\n const snapshot = snapshotCommand(command);\n updateState((current) => {\n if (maxHistory === 0) return { ...current, redo: [] };\n\n const undo = [...current.undo, snapshot.entry];\n\n if (undo.length > maxHistory) undo.shift();\n\n return { ...current, redo: [], undo };\n });\n commandStore.set(snapshot.entry, snapshot);\n },\n\n redo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('redo', callOptions?.signal, runRedo);\n },\n\n get state() {\n return state;\n },\n\n [Symbol.dispose](): void {\n this.dispose();\n },\n\n undo(callOptions?: LedgerCallOptions): Promise<void> {\n return enqueue('undo', callOptions?.signal, runUndo);\n },\n\n whenIdle(): Promise<void> {\n if (state.getSnapshot().queued === 0 && state.getSnapshot().running === 0) {\n return disposed ? Promise.reject(operationError('whenIdle', true)) : Promise.resolve();\n }\n\n return new Promise<void>((resolve, reject) => idleWaiters.add({ reject, resolve }));\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export type Unsubscribe = () => void;
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Read-only observable contract: `getSnapshot()` reads current state,
|
|
4
|
+
* `subscribe()` registers a change listener. Structural by design; matches
|
|
5
|
+
* `Subscribable` in `@vielzeug/arsenal`.
|
|
6
|
+
*/
|
|
7
|
+
export interface Subscribable<T> {
|
|
8
|
+
getSnapshot(): T;
|
|
4
9
|
subscribe(listener: () => void): Unsubscribe;
|
|
5
|
-
readonly value: T;
|
|
6
10
|
}
|
|
7
11
|
export interface CommandContext {
|
|
8
12
|
readonly signal: AbortSignal;
|
|
@@ -24,7 +28,6 @@ export interface LedgerOptions {
|
|
|
24
28
|
maxHistory?: number;
|
|
25
29
|
}
|
|
26
30
|
export interface LedgerState<TMeta = undefined> {
|
|
27
|
-
readonly accepting: boolean;
|
|
28
31
|
readonly queued: number;
|
|
29
32
|
readonly redo: readonly HistoryEntry<TMeta>[];
|
|
30
33
|
readonly running: number;
|
|
@@ -36,8 +39,15 @@ export interface Ledger<TMeta = undefined> {
|
|
|
36
39
|
dispose(): void;
|
|
37
40
|
readonly disposed: boolean;
|
|
38
41
|
do(command: ReversibleCommand<TMeta>, options?: LedgerCallOptions): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Appends an already-executed command to the history without running it through the queue.
|
|
44
|
+
* For stores that execute synchronously and own their writes: `undo()` calls the recorded
|
|
45
|
+
* `revert`, `redo()` calls the recorded `apply`, so `apply` must be able to re-apply the
|
|
46
|
+
* effect. Synchronous by design: the work happened before `record`, there is nothing to run.
|
|
47
|
+
*/
|
|
48
|
+
record(command: ReversibleCommand<TMeta>): void;
|
|
39
49
|
redo(options?: LedgerCallOptions): Promise<void>;
|
|
40
|
-
readonly state:
|
|
50
|
+
readonly state: Subscribable<LedgerState<TMeta>>;
|
|
41
51
|
undo(options?: LedgerCallOptions): Promise<void>;
|
|
42
52
|
whenIdle(): Promise<void>;
|
|
43
53
|
[Symbol.dispose](): void;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,WAAW,IAAI,CAAC,CAAC;IACjB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW,CAAC;CAC9C;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB,CAAC,KAAK,GAAG,SAAS;IAClD,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACpE;AAED,MAAM,WAAW,YAAY,CAAC,KAAK,GAAG,SAAS;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,SAAS;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;CAC/C;AAED,MAAM,WAAW,MAAM,CAAC,KAAK,GAAG,SAAS;IACvC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC;IACrC,OAAO,IAAI,IAAI,CAAC;IAChB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF;;;;;OAKG;IACH,MAAM,CAAC,OAAO,EAAE,iBAAiB,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IACjD,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vielzeug/ledger",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Serialized reversible command history with atomic state and cancellation ownership",
|
|
3
|
+
"version": "26.10.1",
|
|
4
|
+
"description": "Serialized reversible command history with atomic framework-neutral state and cancellation ownership",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/helmuthdu/vielzeug",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"test": "vitest"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/node": "^26.
|
|
42
|
+
"@types/node": "^26.6.2",
|
|
43
43
|
"typescript": "^6.0.3",
|
|
44
|
-
"vite": "^8.
|
|
45
|
-
"vitest": "^5.0.
|
|
44
|
+
"vite": "^8.3.0",
|
|
45
|
+
"vitest": "^5.0.1"
|
|
46
46
|
}
|
|
47
47
|
}
|