@metamask/keyring-sdk 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.2.0]
11
+
12
+ ### Added
13
+
14
+ - Add `AtomicKeyring` support ([#640](https://github.com/MetaMask/accounts/pull/640))
15
+ - Atomic keyrings run long-running operations inside their own read-write lock, entirely outside the controller mutex (only the `update` method will be guarded by the `KeyringController`'s vault mutex).
16
+ - Add `AtomicUpdater` type, callback forwarded by the `KeyringController` during register phase.
17
+ - Add `isAtomicKeyring` helper to identify atomic keyrings.
18
+ - Add `ReadWriteLock` primitive with parallel readers, exclusive writers ([#640](https://github.com/MetaMask/accounts/pull/640))
19
+ - Also support priority mode: `'read'` or `'write'`.
20
+
10
21
  ## [4.1.0]
11
22
 
12
23
  ### Added
@@ -141,7 +152,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
141
152
 
142
153
  - Initial release, extracted from `@metamask/keyring-api` ([#478](https://github.com/MetaMask/accounts/pull/478), [#482](https://github.com/MetaMask/accounts/pull/482))
143
154
 
144
- [Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@4.1.0...HEAD
155
+ [Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@4.2.0...HEAD
156
+ [4.2.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@4.1.0...@metamask/keyring-sdk@4.2.0
145
157
  [4.1.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@4.0.0...@metamask/keyring-sdk@4.1.0
146
158
  [4.0.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@3.1.0...@metamask/keyring-sdk@4.0.0
147
159
  [3.1.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@3.0.0...@metamask/keyring-sdk@3.1.0
package/README.md CHANGED
@@ -42,6 +42,97 @@ export class MyKeyringV2 extends EthKeyringWrapper<MyLegacyKeyring> {
42
42
  }
43
43
  ```
44
44
 
45
+ ## Atomic keyrings
46
+
47
+ Most keyrings are in-memory: their operations complete in microseconds, so the
48
+ `KeyringController` can safely wrap every operation in a controller-wide mutex.
49
+ That model breaks down for keyrings whose operations are long-running network
50
+ protocols (for example, MPC ceremonies): holding the mutex for the ceremony
51
+ freezes every other controller operation, and multi-step remote/local
52
+ transitions cannot be made crash-safe.
53
+
54
+ `AtomicKeyring` inverts the lock ownership for such keyrings. The keyring
55
+ serializes its own operations with an internal read-write lock, runs long work
56
+ entirely outside the controller mutex, and calls back into the controller —
57
+ through the injected `update` callback — only when it has new state to persist.
58
+ The controller holds its mutex just for the sub-millisecond commit window.
59
+
60
+ ```ts
61
+ import {
62
+ AtomicKeyring,
63
+ isAtomicKeyring,
64
+ type AtomicUpdater,
65
+ } from '@metamask/keyring-sdk';
66
+
67
+ class ExampleAtomicKeyring extends AtomicKeyring {
68
+ #state: 'uninitialized' | 'staged' | 'ready' = 'uninitialized';
69
+
70
+ constructor(updater: AtomicUpdater) {
71
+ super({ updater });
72
+ }
73
+
74
+ // Snapshot reads stay lock-free: the controller calls them while holding
75
+ // its own mutex.
76
+ async serialize(): Promise<Json> {
77
+ return { state: this.#state };
78
+ }
79
+
80
+ async createAccount(): Promise<void> {
81
+ // Long-running work runs inside the write lock, outside the controller
82
+ // mutex. Readers and other writers queue behind it.
83
+ await this.withWriteLock(async () => {
84
+ const next = await this.#runCeremony(); // compute and validate — may fail freely
85
+
86
+ // Checkpoint 1: stage the pre-computed state.
87
+ await this.update(() => {
88
+ this.#state = 'staged';
89
+ });
90
+
91
+ await this.#activateRemote(); // remote commit
92
+
93
+ // Checkpoint 2: promote to ready.
94
+ await this.update(() => {
95
+ this.#state = 'ready';
96
+ });
97
+ });
98
+ }
99
+ }
100
+
101
+ // Brand-based detection — never `instanceof`, which fails across duplicate
102
+ // copies of the package.
103
+ if (isAtomicKeyring(value)) {
104
+ /* dispatch without holding the controller mutex */
105
+ }
106
+ ```
107
+
108
+ The contract, enforced by design rather than caller discipline:
109
+
110
+ - **Actions are pure assignments.** Compute and validate the next state
111
+ _before_ calling `update`. A throwing action is a keyring bug.
112
+ - **No nested `update` calls.** A nested call is rejected with an explicit
113
+ error, never a deadlock.
114
+ - **Snapshot reads stay lock-free.** `getAccounts` and `serialize` are called
115
+ by the controller while it holds its mutex (and by `update` while the keyring
116
+ holds its write lock), so they must return immediately from the current
117
+ state.
118
+ - **`init` stays cheap.** The controller invokes `init` under its lock, at
119
+ construction and vault-restore time. Long work belongs in write operations.
120
+ - **No rollback.** On the atomic dispatch path a failed operation keeps every
121
+ committed checkpoint, so the next write operation advances the state machine
122
+ instead of re-running the ceremony.
123
+
124
+ A crash between the checkpoints leaves a persisted, recoverable intermediate
125
+ state; the next write operation advances the state machine (`staged` →
126
+ `ready`) without re-running the ceremony.
127
+
128
+ `ReadWriteLock` is also exported on its own, for keyrings that want the
129
+ read-write lock without the base class: parallel readers, exclusive writers,
130
+ first-in-first-out grant order, and a new reader always queues behind a
131
+ pending writer — so a steady stream of reads cannot starve a long write. For
132
+ read-heavy workloads, `{ priority: 'read' }` lets new readers join active
133
+ readers even while a writer is merely queued, at the cost that continuous
134
+ reads can starve a queued writer.
135
+
45
136
  ## Contributing
46
137
 
47
138
  This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/accounts#readme).
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _AtomicKeyring_updater, _AtomicKeyring_rwLock, _AtomicKeyring_updating;
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.isAtomicKeyring = exports.AtomicKeyring = void 0;
16
+ const read_write_lock_1 = require("./read-write-lock.cjs");
17
+ /**
18
+ * Brand used to mark `AtomicKeyring` instances. Registered on the global
19
+ * symbol registry, so the brand survives duplicate copies of this package —
20
+ * where `instanceof` would fail — and `isAtomicKeyring` keeps working.
21
+ */
22
+ const ATOMIC_KEYRING_BRAND = Symbol.for('@metamask/keyring-sdk/atomic-keyring');
23
+ /**
24
+ * Base class for keyrings that serialize their own operations and call back
25
+ * into the controller only to persist state.
26
+ *
27
+ * Long-running work runs inside the lock helpers, entirely outside the
28
+ * controller mutex; state is committed through `update`.
29
+ *
30
+ * Subclasses implementing an `init` lifecycle hook must keep it cheap —
31
+ * in-memory work only. The controller invokes `init` under its lock, at
32
+ * construction and vault-restore time; an `init` that performs long work,
33
+ * or calls `update` (which needs that same, non-reentrant lock), would
34
+ * block or deadlock the controller. Long work belongs in write operations,
35
+ * which the controller dispatches without holding its lock.
36
+ *
37
+ * Snapshot reads the controller performs itself — `getAccounts` and
38
+ * `serialize` — must stay lock-free in subclasses: the controller calls
39
+ * them while holding its own mutex, and the updater calls `serialize` while
40
+ * the keyring is inside a write operation. They must return immediately
41
+ * from the current state.
42
+ */
43
+ class AtomicKeyring {
44
+ /**
45
+ * The updater is required at construction — no unbound state, ever.
46
+ *
47
+ * @param options - Constructor options.
48
+ * @param options.updater - The controller's vault-write callback, which
49
+ * this keyring invokes through `update` to commit state.
50
+ */
51
+ constructor(options) {
52
+ _AtomicKeyring_updater.set(this, void 0);
53
+ _AtomicKeyring_rwLock.set(this, new read_write_lock_1.ReadWriteLock());
54
+ _AtomicKeyring_updating.set(this, false);
55
+ __classPrivateFieldSet(this, _AtomicKeyring_updater, options.updater, "f");
56
+ this[ATOMIC_KEYRING_BRAND] = true;
57
+ }
58
+ /**
59
+ * Commit a state mutation on behalf of this keyring. Injects `this` into
60
+ * the updater, so the controller can verify registration.
61
+ *
62
+ * Actions must be pure assignments of pre-computed next state: compute
63
+ * and validate before calling `update`. A throwing action is a keyring
64
+ * bug, and is rethrown as such. Nested `update` calls are rejected with
65
+ * an error, never a deadlock.
66
+ *
67
+ * Call `update` only from within a write-locked operation, so commits
68
+ * cannot interleave with other operations on this keyring.
69
+ *
70
+ * @param action - The state mutation to commit. Must be a pure assignment
71
+ * of pre-computed next state.
72
+ * @throws If called while another `update` call is in progress, or if the
73
+ * action or updater throws.
74
+ */
75
+ async update(action) {
76
+ if (__classPrivateFieldGet(this, _AtomicKeyring_updating, "f")) {
77
+ throw new Error('AtomicKeyring - Nested update() calls are not allowed. The update() action must not call update() again.');
78
+ }
79
+ __classPrivateFieldSet(this, _AtomicKeyring_updating, true, "f");
80
+ try {
81
+ await __classPrivateFieldGet(this, _AtomicKeyring_updater, "f").call(this, this, action);
82
+ }
83
+ finally {
84
+ __classPrivateFieldSet(this, _AtomicKeyring_updating, false, "f");
85
+ }
86
+ }
87
+ /**
88
+ * Run a read operation — parallel with other readers, queued behind
89
+ * writers. Only for operations that can tolerate waiting behind a write
90
+ * section.
91
+ *
92
+ * @param fn - The operation to run while holding the read lock.
93
+ * @returns Whatever `fn` resolves to.
94
+ */
95
+ async withReadLock(fn) {
96
+ return __classPrivateFieldGet(this, _AtomicKeyring_rwLock, "f").withReadLock(fn);
97
+ }
98
+ /**
99
+ * Run a write operation — exclusive with readers and other writers.
100
+ * Long-running network work belongs here, outside the controller mutex.
101
+ *
102
+ * @param fn - The operation to run while holding the write lock.
103
+ * @returns Whatever `fn` resolves to.
104
+ */
105
+ async withWriteLock(fn) {
106
+ return __classPrivateFieldGet(this, _AtomicKeyring_rwLock, "f").withWriteLock(fn);
107
+ }
108
+ }
109
+ exports.AtomicKeyring = AtomicKeyring;
110
+ _AtomicKeyring_updater = new WeakMap(), _AtomicKeyring_rwLock = new WeakMap(), _AtomicKeyring_updating = new WeakMap();
111
+ /**
112
+ * Whether the value is an `AtomicKeyring`.
113
+ *
114
+ * Brand-based — a global-registry symbol set by the constructor — so it
115
+ * works across duplicate copies of the package, where `instanceof` fails.
116
+ *
117
+ * @param value - The value to check.
118
+ * @returns Whether the value is an `AtomicKeyring` instance.
119
+ */
120
+ function isAtomicKeyring(value) {
121
+ return (typeof value === 'object' &&
122
+ value !== null &&
123
+ value[ATOMIC_KEYRING_BRAND] === true);
124
+ }
125
+ exports.isAtomicKeyring = isAtomicKeyring;
126
+ //# sourceMappingURL=atomic-keyring.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-keyring.cjs","sourceRoot":"","sources":["../src/atomic-keyring.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2DAAkD;AAiBlD;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAsB,aAAa;IAOjC;;;;;;OAMG;IACH,YAAY,OAAmC;QAbtC,yCAAwB;QAExB,gCAAyB,IAAI,+BAAa,EAAE,EAAC;QAEtD,kCAAqB,KAAK,EAAC;QAUzB,uBAAA,IAAI,0BAAY,OAAO,CAAC,OAAO,MAAA,CAAC;QAC/B,IAAqC,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC;IACtE,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACO,KAAK,CAAC,MAAM,CAAC,MAAkC;QACvD,IAAI,uBAAA,IAAI,+BAAU,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G,CAAC;QACJ,CAAC;QAED,uBAAA,IAAI,2BAAa,IAAI,MAAA,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,uBAAA,IAAI,8BAAS,MAAb,IAAI,EAAU,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,CAAC;gBAAS,CAAC;YACT,uBAAA,IAAI,2BAAa,KAAK,MAAA,CAAC;QACzB,CAAC;IACH,CAAC;IAED;;;;;;;OAOG;IACO,KAAK,CAAC,YAAY,CAC1B,EAAyB;QAEzB,OAAO,uBAAA,IAAI,6BAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACvC,CAAC;IAED;;;;;;OAMG;IACO,KAAK,CAAC,aAAa,CAC3B,EAAyB;QAEzB,OAAO,uBAAA,IAAI,6BAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IACxC,CAAC;CACF;AA7ED,sCA6EC;;AAED;;;;;;;;GAQG;AACH,SAAgB,eAAe,CAAC,KAAc;IAC5C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAAsC,CAAC,oBAAoB,CAAC,KAAK,IAAI,CACvE,CAAC;AACJ,CAAC;AAND,0CAMC","sourcesContent":["import { ReadWriteLock } from './read-write-lock';\n\n/**\n * A vault-write callback injected into an `AtomicKeyring` at construction,\n * via the keyring builder context.\n *\n * Stateless: the calling keyring is the first parameter, so a single\n * function serves every keyring. Acquires the controller lock, verifies the\n * keyring is still registered, runs `action()`, writes the vault if and only\n * if the serialized state changed, then releases — a sub-millisecond commit\n * window.\n */\nexport type AtomicUpdater = (\n keyring: AtomicKeyring,\n action: () => void | Promise<void>,\n) => Promise<void>;\n\n/**\n * Brand used to mark `AtomicKeyring` instances. Registered on the global\n * symbol registry, so the brand survives duplicate copies of this package —\n * where `instanceof` would fail — and `isAtomicKeyring` keeps working.\n */\nconst ATOMIC_KEYRING_BRAND = Symbol.for('@metamask/keyring-sdk/atomic-keyring');\n\n/**\n * Base class for keyrings that serialize their own operations and call back\n * into the controller only to persist state.\n *\n * Long-running work runs inside the lock helpers, entirely outside the\n * controller mutex; state is committed through `update`.\n *\n * Subclasses implementing an `init` lifecycle hook must keep it cheap —\n * in-memory work only. The controller invokes `init` under its lock, at\n * construction and vault-restore time; an `init` that performs long work,\n * or calls `update` (which needs that same, non-reentrant lock), would\n * block or deadlock the controller. Long work belongs in write operations,\n * which the controller dispatches without holding its lock.\n *\n * Snapshot reads the controller performs itself — `getAccounts` and\n * `serialize` — must stay lock-free in subclasses: the controller calls\n * them while holding its own mutex, and the updater calls `serialize` while\n * the keyring is inside a write operation. They must return immediately\n * from the current state.\n */\nexport abstract class AtomicKeyring {\n readonly #updater: AtomicUpdater;\n\n readonly #rwLock: ReadWriteLock = new ReadWriteLock();\n\n #updating: boolean = false;\n\n /**\n * The updater is required at construction — no unbound state, ever.\n *\n * @param options - Constructor options.\n * @param options.updater - The controller's vault-write callback, which\n * this keyring invokes through `update` to commit state.\n */\n constructor(options: { updater: AtomicUpdater }) {\n this.#updater = options.updater;\n (this as Record<PropertyKey, unknown>)[ATOMIC_KEYRING_BRAND] = true;\n }\n\n /**\n * Commit a state mutation on behalf of this keyring. Injects `this` into\n * the updater, so the controller can verify registration.\n *\n * Actions must be pure assignments of pre-computed next state: compute\n * and validate before calling `update`. A throwing action is a keyring\n * bug, and is rethrown as such. Nested `update` calls are rejected with\n * an error, never a deadlock.\n *\n * Call `update` only from within a write-locked operation, so commits\n * cannot interleave with other operations on this keyring.\n *\n * @param action - The state mutation to commit. Must be a pure assignment\n * of pre-computed next state.\n * @throws If called while another `update` call is in progress, or if the\n * action or updater throws.\n */\n protected async update(action: () => void | Promise<void>): Promise<void> {\n if (this.#updating) {\n throw new Error(\n 'AtomicKeyring - Nested update() calls are not allowed. The update() action must not call update() again.',\n );\n }\n\n this.#updating = true;\n try {\n await this.#updater(this, action);\n } finally {\n this.#updating = false;\n }\n }\n\n /**\n * Run a read operation — parallel with other readers, queued behind\n * writers. Only for operations that can tolerate waiting behind a write\n * section.\n *\n * @param fn - The operation to run while holding the read lock.\n * @returns Whatever `fn` resolves to.\n */\n protected async withReadLock<Result>(\n fn: () => Promise<Result>,\n ): Promise<Result> {\n return this.#rwLock.withReadLock(fn);\n }\n\n /**\n * Run a write operation — exclusive with readers and other writers.\n * Long-running network work belongs here, outside the controller mutex.\n *\n * @param fn - The operation to run while holding the write lock.\n * @returns Whatever `fn` resolves to.\n */\n protected async withWriteLock<Result>(\n fn: () => Promise<Result>,\n ): Promise<Result> {\n return this.#rwLock.withWriteLock(fn);\n }\n}\n\n/**\n * Whether the value is an `AtomicKeyring`.\n *\n * Brand-based — a global-registry symbol set by the constructor — so it\n * works across duplicate copies of the package, where `instanceof` fails.\n *\n * @param value - The value to check.\n * @returns Whether the value is an `AtomicKeyring` instance.\n */\nexport function isAtomicKeyring(value: unknown): value is AtomicKeyring {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<PropertyKey, unknown>)[ATOMIC_KEYRING_BRAND] === true\n );\n}\n"]}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A vault-write callback injected into an `AtomicKeyring` at construction,
3
+ * via the keyring builder context.
4
+ *
5
+ * Stateless: the calling keyring is the first parameter, so a single
6
+ * function serves every keyring. Acquires the controller lock, verifies the
7
+ * keyring is still registered, runs `action()`, writes the vault if and only
8
+ * if the serialized state changed, then releases — a sub-millisecond commit
9
+ * window.
10
+ */
11
+ export type AtomicUpdater = (keyring: AtomicKeyring, action: () => void | Promise<void>) => Promise<void>;
12
+ /**
13
+ * Base class for keyrings that serialize their own operations and call back
14
+ * into the controller only to persist state.
15
+ *
16
+ * Long-running work runs inside the lock helpers, entirely outside the
17
+ * controller mutex; state is committed through `update`.
18
+ *
19
+ * Subclasses implementing an `init` lifecycle hook must keep it cheap —
20
+ * in-memory work only. The controller invokes `init` under its lock, at
21
+ * construction and vault-restore time; an `init` that performs long work,
22
+ * or calls `update` (which needs that same, non-reentrant lock), would
23
+ * block or deadlock the controller. Long work belongs in write operations,
24
+ * which the controller dispatches without holding its lock.
25
+ *
26
+ * Snapshot reads the controller performs itself — `getAccounts` and
27
+ * `serialize` — must stay lock-free in subclasses: the controller calls
28
+ * them while holding its own mutex, and the updater calls `serialize` while
29
+ * the keyring is inside a write operation. They must return immediately
30
+ * from the current state.
31
+ */
32
+ export declare abstract class AtomicKeyring {
33
+ #private;
34
+ /**
35
+ * The updater is required at construction — no unbound state, ever.
36
+ *
37
+ * @param options - Constructor options.
38
+ * @param options.updater - The controller's vault-write callback, which
39
+ * this keyring invokes through `update` to commit state.
40
+ */
41
+ constructor(options: {
42
+ updater: AtomicUpdater;
43
+ });
44
+ /**
45
+ * Commit a state mutation on behalf of this keyring. Injects `this` into
46
+ * the updater, so the controller can verify registration.
47
+ *
48
+ * Actions must be pure assignments of pre-computed next state: compute
49
+ * and validate before calling `update`. A throwing action is a keyring
50
+ * bug, and is rethrown as such. Nested `update` calls are rejected with
51
+ * an error, never a deadlock.
52
+ *
53
+ * Call `update` only from within a write-locked operation, so commits
54
+ * cannot interleave with other operations on this keyring.
55
+ *
56
+ * @param action - The state mutation to commit. Must be a pure assignment
57
+ * of pre-computed next state.
58
+ * @throws If called while another `update` call is in progress, or if the
59
+ * action or updater throws.
60
+ */
61
+ protected update(action: () => void | Promise<void>): Promise<void>;
62
+ /**
63
+ * Run a read operation — parallel with other readers, queued behind
64
+ * writers. Only for operations that can tolerate waiting behind a write
65
+ * section.
66
+ *
67
+ * @param fn - The operation to run while holding the read lock.
68
+ * @returns Whatever `fn` resolves to.
69
+ */
70
+ protected withReadLock<Result>(fn: () => Promise<Result>): Promise<Result>;
71
+ /**
72
+ * Run a write operation — exclusive with readers and other writers.
73
+ * Long-running network work belongs here, outside the controller mutex.
74
+ *
75
+ * @param fn - The operation to run while holding the write lock.
76
+ * @returns Whatever `fn` resolves to.
77
+ */
78
+ protected withWriteLock<Result>(fn: () => Promise<Result>): Promise<Result>;
79
+ }
80
+ /**
81
+ * Whether the value is an `AtomicKeyring`.
82
+ *
83
+ * Brand-based — a global-registry symbol set by the constructor — so it
84
+ * works across duplicate copies of the package, where `instanceof` fails.
85
+ *
86
+ * @param value - The value to check.
87
+ * @returns Whether the value is an `AtomicKeyring` instance.
88
+ */
89
+ export declare function isAtomicKeyring(value: unknown): value is AtomicKeyring;
90
+ //# sourceMappingURL=atomic-keyring.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-keyring.d.cts","sourceRoot":"","sources":["../src/atomic-keyring.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAC/B,OAAO,CAAC,IAAI,CAAC,CAAC;AASnB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,8BAAsB,aAAa;;IAOjC;;;;;;OAMG;gBACS,OAAO,EAAE;QAAE,OAAO,EAAE,aAAa,CAAA;KAAE;IAK/C;;;;;;;;;;;;;;;;OAgBG;cACa,MAAM,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAezE;;;;;;;OAOG;cACa,YAAY,CAAC,MAAM,EACjC,EAAE,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GACxB,OAAO,CAAC,MAAM,CAAC;IAIlB;;;;;;OAMG;cACa,aAAa,CAAC,MAAM,EAClC,EAAE,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GACxB,OAAO,CAAC,MAAM,CAAC;CAGnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAMtE"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A vault-write callback injected into an `AtomicKeyring` at construction,
3
+ * via the keyring builder context.
4
+ *
5
+ * Stateless: the calling keyring is the first parameter, so a single
6
+ * function serves every keyring. Acquires the controller lock, verifies the
7
+ * keyring is still registered, runs `action()`, writes the vault if and only
8
+ * if the serialized state changed, then releases — a sub-millisecond commit
9
+ * window.
10
+ */
11
+ export type AtomicUpdater = (keyring: AtomicKeyring, action: () => void | Promise<void>) => Promise<void>;
12
+ /**
13
+ * Base class for keyrings that serialize their own operations and call back
14
+ * into the controller only to persist state.
15
+ *
16
+ * Long-running work runs inside the lock helpers, entirely outside the
17
+ * controller mutex; state is committed through `update`.
18
+ *
19
+ * Subclasses implementing an `init` lifecycle hook must keep it cheap —
20
+ * in-memory work only. The controller invokes `init` under its lock, at
21
+ * construction and vault-restore time; an `init` that performs long work,
22
+ * or calls `update` (which needs that same, non-reentrant lock), would
23
+ * block or deadlock the controller. Long work belongs in write operations,
24
+ * which the controller dispatches without holding its lock.
25
+ *
26
+ * Snapshot reads the controller performs itself — `getAccounts` and
27
+ * `serialize` — must stay lock-free in subclasses: the controller calls
28
+ * them while holding its own mutex, and the updater calls `serialize` while
29
+ * the keyring is inside a write operation. They must return immediately
30
+ * from the current state.
31
+ */
32
+ export declare abstract class AtomicKeyring {
33
+ #private;
34
+ /**
35
+ * The updater is required at construction — no unbound state, ever.
36
+ *
37
+ * @param options - Constructor options.
38
+ * @param options.updater - The controller's vault-write callback, which
39
+ * this keyring invokes through `update` to commit state.
40
+ */
41
+ constructor(options: {
42
+ updater: AtomicUpdater;
43
+ });
44
+ /**
45
+ * Commit a state mutation on behalf of this keyring. Injects `this` into
46
+ * the updater, so the controller can verify registration.
47
+ *
48
+ * Actions must be pure assignments of pre-computed next state: compute
49
+ * and validate before calling `update`. A throwing action is a keyring
50
+ * bug, and is rethrown as such. Nested `update` calls are rejected with
51
+ * an error, never a deadlock.
52
+ *
53
+ * Call `update` only from within a write-locked operation, so commits
54
+ * cannot interleave with other operations on this keyring.
55
+ *
56
+ * @param action - The state mutation to commit. Must be a pure assignment
57
+ * of pre-computed next state.
58
+ * @throws If called while another `update` call is in progress, or if the
59
+ * action or updater throws.
60
+ */
61
+ protected update(action: () => void | Promise<void>): Promise<void>;
62
+ /**
63
+ * Run a read operation — parallel with other readers, queued behind
64
+ * writers. Only for operations that can tolerate waiting behind a write
65
+ * section.
66
+ *
67
+ * @param fn - The operation to run while holding the read lock.
68
+ * @returns Whatever `fn` resolves to.
69
+ */
70
+ protected withReadLock<Result>(fn: () => Promise<Result>): Promise<Result>;
71
+ /**
72
+ * Run a write operation — exclusive with readers and other writers.
73
+ * Long-running network work belongs here, outside the controller mutex.
74
+ *
75
+ * @param fn - The operation to run while holding the write lock.
76
+ * @returns Whatever `fn` resolves to.
77
+ */
78
+ protected withWriteLock<Result>(fn: () => Promise<Result>): Promise<Result>;
79
+ }
80
+ /**
81
+ * Whether the value is an `AtomicKeyring`.
82
+ *
83
+ * Brand-based — a global-registry symbol set by the constructor — so it
84
+ * works across duplicate copies of the package, where `instanceof` fails.
85
+ *
86
+ * @param value - The value to check.
87
+ * @returns Whether the value is an `AtomicKeyring` instance.
88
+ */
89
+ export declare function isAtomicKeyring(value: unknown): value is AtomicKeyring;
90
+ //# sourceMappingURL=atomic-keyring.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-keyring.d.mts","sourceRoot":"","sources":["../src/atomic-keyring.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAC/B,OAAO,CAAC,IAAI,CAAC,CAAC;AASnB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,8BAAsB,aAAa;;IAOjC;;;;;;OAMG;gBACS,OAAO,EAAE;QAAE,OAAO,EAAE,aAAa,CAAA;KAAE;IAK/C;;;;;;;;;;;;;;;;OAgBG;cACa,MAAM,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAezE;;;;;;;OAOG;cACa,YAAY,CAAC,MAAM,EACjC,EAAE,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GACxB,OAAO,CAAC,MAAM,CAAC;IAIlB;;;;;;OAMG;cACa,aAAa,CAAC,MAAM,EAClC,EAAE,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GACxB,OAAO,CAAC,MAAM,CAAC;CAGnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAMtE"}
@@ -0,0 +1,121 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _AtomicKeyring_updater, _AtomicKeyring_rwLock, _AtomicKeyring_updating;
13
+ import { ReadWriteLock } from "./read-write-lock.mjs";
14
+ /**
15
+ * Brand used to mark `AtomicKeyring` instances. Registered on the global
16
+ * symbol registry, so the brand survives duplicate copies of this package —
17
+ * where `instanceof` would fail — and `isAtomicKeyring` keeps working.
18
+ */
19
+ const ATOMIC_KEYRING_BRAND = Symbol.for('@metamask/keyring-sdk/atomic-keyring');
20
+ /**
21
+ * Base class for keyrings that serialize their own operations and call back
22
+ * into the controller only to persist state.
23
+ *
24
+ * Long-running work runs inside the lock helpers, entirely outside the
25
+ * controller mutex; state is committed through `update`.
26
+ *
27
+ * Subclasses implementing an `init` lifecycle hook must keep it cheap —
28
+ * in-memory work only. The controller invokes `init` under its lock, at
29
+ * construction and vault-restore time; an `init` that performs long work,
30
+ * or calls `update` (which needs that same, non-reentrant lock), would
31
+ * block or deadlock the controller. Long work belongs in write operations,
32
+ * which the controller dispatches without holding its lock.
33
+ *
34
+ * Snapshot reads the controller performs itself — `getAccounts` and
35
+ * `serialize` — must stay lock-free in subclasses: the controller calls
36
+ * them while holding its own mutex, and the updater calls `serialize` while
37
+ * the keyring is inside a write operation. They must return immediately
38
+ * from the current state.
39
+ */
40
+ export class AtomicKeyring {
41
+ /**
42
+ * The updater is required at construction — no unbound state, ever.
43
+ *
44
+ * @param options - Constructor options.
45
+ * @param options.updater - The controller's vault-write callback, which
46
+ * this keyring invokes through `update` to commit state.
47
+ */
48
+ constructor(options) {
49
+ _AtomicKeyring_updater.set(this, void 0);
50
+ _AtomicKeyring_rwLock.set(this, new ReadWriteLock());
51
+ _AtomicKeyring_updating.set(this, false);
52
+ __classPrivateFieldSet(this, _AtomicKeyring_updater, options.updater, "f");
53
+ this[ATOMIC_KEYRING_BRAND] = true;
54
+ }
55
+ /**
56
+ * Commit a state mutation on behalf of this keyring. Injects `this` into
57
+ * the updater, so the controller can verify registration.
58
+ *
59
+ * Actions must be pure assignments of pre-computed next state: compute
60
+ * and validate before calling `update`. A throwing action is a keyring
61
+ * bug, and is rethrown as such. Nested `update` calls are rejected with
62
+ * an error, never a deadlock.
63
+ *
64
+ * Call `update` only from within a write-locked operation, so commits
65
+ * cannot interleave with other operations on this keyring.
66
+ *
67
+ * @param action - The state mutation to commit. Must be a pure assignment
68
+ * of pre-computed next state.
69
+ * @throws If called while another `update` call is in progress, or if the
70
+ * action or updater throws.
71
+ */
72
+ async update(action) {
73
+ if (__classPrivateFieldGet(this, _AtomicKeyring_updating, "f")) {
74
+ throw new Error('AtomicKeyring - Nested update() calls are not allowed. The update() action must not call update() again.');
75
+ }
76
+ __classPrivateFieldSet(this, _AtomicKeyring_updating, true, "f");
77
+ try {
78
+ await __classPrivateFieldGet(this, _AtomicKeyring_updater, "f").call(this, this, action);
79
+ }
80
+ finally {
81
+ __classPrivateFieldSet(this, _AtomicKeyring_updating, false, "f");
82
+ }
83
+ }
84
+ /**
85
+ * Run a read operation — parallel with other readers, queued behind
86
+ * writers. Only for operations that can tolerate waiting behind a write
87
+ * section.
88
+ *
89
+ * @param fn - The operation to run while holding the read lock.
90
+ * @returns Whatever `fn` resolves to.
91
+ */
92
+ async withReadLock(fn) {
93
+ return __classPrivateFieldGet(this, _AtomicKeyring_rwLock, "f").withReadLock(fn);
94
+ }
95
+ /**
96
+ * Run a write operation — exclusive with readers and other writers.
97
+ * Long-running network work belongs here, outside the controller mutex.
98
+ *
99
+ * @param fn - The operation to run while holding the write lock.
100
+ * @returns Whatever `fn` resolves to.
101
+ */
102
+ async withWriteLock(fn) {
103
+ return __classPrivateFieldGet(this, _AtomicKeyring_rwLock, "f").withWriteLock(fn);
104
+ }
105
+ }
106
+ _AtomicKeyring_updater = new WeakMap(), _AtomicKeyring_rwLock = new WeakMap(), _AtomicKeyring_updating = new WeakMap();
107
+ /**
108
+ * Whether the value is an `AtomicKeyring`.
109
+ *
110
+ * Brand-based — a global-registry symbol set by the constructor — so it
111
+ * works across duplicate copies of the package, where `instanceof` fails.
112
+ *
113
+ * @param value - The value to check.
114
+ * @returns Whether the value is an `AtomicKeyring` instance.
115
+ */
116
+ export function isAtomicKeyring(value) {
117
+ return (typeof value === 'object' &&
118
+ value !== null &&
119
+ value[ATOMIC_KEYRING_BRAND] === true);
120
+ }
121
+ //# sourceMappingURL=atomic-keyring.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-keyring.mjs","sourceRoot":"","sources":["../src/atomic-keyring.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,aAAa,EAAE,8BAA0B;AAiBlD;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAgB,aAAa;IAOjC;;;;;;OAMG;IACH,YAAY,OAAmC;QAbtC,yCAAwB;QAExB,gCAAyB,IAAI,aAAa,EAAE,EAAC;QAEtD,kCAAqB,KAAK,EAAC;QAUzB,uBAAA,IAAI,0BAAY,OAAO,CAAC,OAAO,MAAA,CAAC;QAC/B,IAAqC,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC;IACtE,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACO,KAAK,CAAC,MAAM,CAAC,MAAkC;QACvD,IAAI,uBAAA,IAAI,+BAAU,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G,CAAC;QACJ,CAAC;QAED,uBAAA,IAAI,2BAAa,IAAI,MAAA,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,uBAAA,IAAI,8BAAS,MAAb,IAAI,EAAU,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,CAAC;gBAAS,CAAC;YACT,uBAAA,IAAI,2BAAa,KAAK,MAAA,CAAC;QACzB,CAAC;IACH,CAAC;IAED;;;;;;;OAOG;IACO,KAAK,CAAC,YAAY,CAC1B,EAAyB;QAEzB,OAAO,uBAAA,IAAI,6BAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACvC,CAAC;IAED;;;;;;OAMG;IACO,KAAK,CAAC,aAAa,CAC3B,EAAyB;QAEzB,OAAO,uBAAA,IAAI,6BAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IACxC,CAAC;CACF;;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAAsC,CAAC,oBAAoB,CAAC,KAAK,IAAI,CACvE,CAAC;AACJ,CAAC","sourcesContent":["import { ReadWriteLock } from './read-write-lock';\n\n/**\n * A vault-write callback injected into an `AtomicKeyring` at construction,\n * via the keyring builder context.\n *\n * Stateless: the calling keyring is the first parameter, so a single\n * function serves every keyring. Acquires the controller lock, verifies the\n * keyring is still registered, runs `action()`, writes the vault if and only\n * if the serialized state changed, then releases — a sub-millisecond commit\n * window.\n */\nexport type AtomicUpdater = (\n keyring: AtomicKeyring,\n action: () => void | Promise<void>,\n) => Promise<void>;\n\n/**\n * Brand used to mark `AtomicKeyring` instances. Registered on the global\n * symbol registry, so the brand survives duplicate copies of this package —\n * where `instanceof` would fail — and `isAtomicKeyring` keeps working.\n */\nconst ATOMIC_KEYRING_BRAND = Symbol.for('@metamask/keyring-sdk/atomic-keyring');\n\n/**\n * Base class for keyrings that serialize their own operations and call back\n * into the controller only to persist state.\n *\n * Long-running work runs inside the lock helpers, entirely outside the\n * controller mutex; state is committed through `update`.\n *\n * Subclasses implementing an `init` lifecycle hook must keep it cheap —\n * in-memory work only. The controller invokes `init` under its lock, at\n * construction and vault-restore time; an `init` that performs long work,\n * or calls `update` (which needs that same, non-reentrant lock), would\n * block or deadlock the controller. Long work belongs in write operations,\n * which the controller dispatches without holding its lock.\n *\n * Snapshot reads the controller performs itself — `getAccounts` and\n * `serialize` — must stay lock-free in subclasses: the controller calls\n * them while holding its own mutex, and the updater calls `serialize` while\n * the keyring is inside a write operation. They must return immediately\n * from the current state.\n */\nexport abstract class AtomicKeyring {\n readonly #updater: AtomicUpdater;\n\n readonly #rwLock: ReadWriteLock = new ReadWriteLock();\n\n #updating: boolean = false;\n\n /**\n * The updater is required at construction — no unbound state, ever.\n *\n * @param options - Constructor options.\n * @param options.updater - The controller's vault-write callback, which\n * this keyring invokes through `update` to commit state.\n */\n constructor(options: { updater: AtomicUpdater }) {\n this.#updater = options.updater;\n (this as Record<PropertyKey, unknown>)[ATOMIC_KEYRING_BRAND] = true;\n }\n\n /**\n * Commit a state mutation on behalf of this keyring. Injects `this` into\n * the updater, so the controller can verify registration.\n *\n * Actions must be pure assignments of pre-computed next state: compute\n * and validate before calling `update`. A throwing action is a keyring\n * bug, and is rethrown as such. Nested `update` calls are rejected with\n * an error, never a deadlock.\n *\n * Call `update` only from within a write-locked operation, so commits\n * cannot interleave with other operations on this keyring.\n *\n * @param action - The state mutation to commit. Must be a pure assignment\n * of pre-computed next state.\n * @throws If called while another `update` call is in progress, or if the\n * action or updater throws.\n */\n protected async update(action: () => void | Promise<void>): Promise<void> {\n if (this.#updating) {\n throw new Error(\n 'AtomicKeyring - Nested update() calls are not allowed. The update() action must not call update() again.',\n );\n }\n\n this.#updating = true;\n try {\n await this.#updater(this, action);\n } finally {\n this.#updating = false;\n }\n }\n\n /**\n * Run a read operation — parallel with other readers, queued behind\n * writers. Only for operations that can tolerate waiting behind a write\n * section.\n *\n * @param fn - The operation to run while holding the read lock.\n * @returns Whatever `fn` resolves to.\n */\n protected async withReadLock<Result>(\n fn: () => Promise<Result>,\n ): Promise<Result> {\n return this.#rwLock.withReadLock(fn);\n }\n\n /**\n * Run a write operation — exclusive with readers and other writers.\n * Long-running network work belongs here, outside the controller mutex.\n *\n * @param fn - The operation to run while holding the write lock.\n * @returns Whatever `fn` resolves to.\n */\n protected async withWriteLock<Result>(\n fn: () => Promise<Result>,\n ): Promise<Result> {\n return this.#rwLock.withWriteLock(fn);\n }\n}\n\n/**\n * Whether the value is an `AtomicKeyring`.\n *\n * Brand-based — a global-registry symbol set by the constructor — so it\n * works across duplicate copies of the package, where `instanceof` fails.\n *\n * @param value - The value to check.\n * @returns Whether the value is an `AtomicKeyring` instance.\n */\nexport function isAtomicKeyring(value: unknown): value is AtomicKeyring {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<PropertyKey, unknown>)[ATOMIC_KEYRING_BRAND] === true\n );\n}\n"]}
package/dist/index.cjs CHANGED
@@ -14,9 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./atomic-keyring.cjs"), exports);
17
18
  __exportStar(require("./keyring-account-registry.cjs"), exports);
18
19
  __exportStar(require("./migration.cjs"), exports);
19
20
  __exportStar(require("./mnemonic.cjs"), exports);
20
21
  __exportStar(require("./entropy.cjs"), exports);
22
+ __exportStar(require("./read-write-lock.cjs"), exports);
21
23
  __exportStar(require("./eth/index.cjs"), exports);
22
24
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iEAA2C;AAC3C,kDAA4B;AAC5B,iDAA2B;AAC3B,gDAA0B;AAC1B,kDAAsB","sourcesContent":["export * from './keyring-account-registry';\nexport * from './migration';\nexport * from './mnemonic';\nexport * from './entropy';\nexport * from './eth';\n"]}
1
+ {"version":3,"file":"index.cjs","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,uDAAiC;AACjC,iEAA2C;AAC3C,kDAA4B;AAC5B,iDAA2B;AAC3B,gDAA0B;AAC1B,wDAAkC;AAClC,kDAAsB","sourcesContent":["export * from './atomic-keyring';\nexport * from './keyring-account-registry';\nexport * from './migration';\nexport * from './mnemonic';\nexport * from './entropy';\nexport * from './read-write-lock';\nexport * from './eth';\n"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,8 @@
1
+ export * from "./atomic-keyring.cjs";
1
2
  export * from "./keyring-account-registry.cjs";
2
3
  export * from "./migration.cjs";
3
4
  export * from "./mnemonic.cjs";
4
5
  export * from "./entropy.cjs";
6
+ export * from "./read-write-lock.cjs";
5
7
  export * from "./eth/index.cjs";
6
8
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+CAA2C;AAC3C,gCAA4B;AAC5B,+BAA2B;AAC3B,8BAA0B;AAC1B,gCAAsB"}
1
+ {"version":3,"file":"index.d.cts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qCAAiC;AACjC,+CAA2C;AAC3C,gCAA4B;AAC5B,+BAA2B;AAC3B,8BAA0B;AAC1B,sCAAkC;AAClC,gCAAsB"}
package/dist/index.d.mts CHANGED
@@ -1,6 +1,8 @@
1
+ export * from "./atomic-keyring.mjs";
1
2
  export * from "./keyring-account-registry.mjs";
2
3
  export * from "./migration.mjs";
3
4
  export * from "./mnemonic.mjs";
4
5
  export * from "./entropy.mjs";
6
+ export * from "./read-write-lock.mjs";
5
7
  export * from "./eth/index.mjs";
6
8
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+CAA2C;AAC3C,gCAA4B;AAC5B,+BAA2B;AAC3B,8BAA0B;AAC1B,gCAAsB"}
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qCAAiC;AACjC,+CAA2C;AAC3C,gCAA4B;AAC5B,+BAA2B;AAC3B,8BAA0B;AAC1B,sCAAkC;AAClC,gCAAsB"}
package/dist/index.mjs CHANGED
@@ -1,6 +1,8 @@
1
+ export * from "./atomic-keyring.mjs";
1
2
  export * from "./keyring-account-registry.mjs";
2
3
  export * from "./migration.mjs";
3
4
  export * from "./mnemonic.mjs";
4
5
  export * from "./entropy.mjs";
6
+ export * from "./read-write-lock.mjs";
5
7
  export * from "./eth/index.mjs";
6
8
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+CAA2C;AAC3C,gCAA4B;AAC5B,+BAA2B;AAC3B,8BAA0B;AAC1B,gCAAsB","sourcesContent":["export * from './keyring-account-registry';\nexport * from './migration';\nexport * from './mnemonic';\nexport * from './entropy';\nexport * from './eth';\n"]}
1
+ {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qCAAiC;AACjC,+CAA2C;AAC3C,gCAA4B;AAC5B,+BAA2B;AAC3B,8BAA0B;AAC1B,sCAAkC;AAClC,gCAAsB","sourcesContent":["export * from './atomic-keyring';\nexport * from './keyring-account-registry';\nexport * from './migration';\nexport * from './mnemonic';\nexport * from './entropy';\nexport * from './read-write-lock';\nexport * from './eth';\n"]}
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _ReadWriteLock_instances, _ReadWriteLock_readerCount, _ReadWriteLock_writerActive, _ReadWriteLock_waiters, _ReadWriteLock_priority, _ReadWriteLock_withLock, _ReadWriteLock_hasWaiters, _ReadWriteLock_isReading, _ReadWriteLock_isWriting, _ReadWriteLock_acquire, _ReadWriteLock_grant, _ReadWriteLock_release, _ReadWriteLock_drain;
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ReadWriteLock = void 0;
16
+ /**
17
+ * A read-write lock: multiple readers may hold the lock simultaneously,
18
+ * while writers hold it exclusively.
19
+ *
20
+ * Grant order is first-in-first-out, and the lock is write-preferring by
21
+ * default: a pending writer blocks new readers, and consecutive queued
22
+ * readers are granted together once the lock turns over to them. Set the
23
+ * `priority` option to `'read'` to let new readers join active readers even
24
+ * while a writer is queued — better read latency, at the cost that
25
+ * continuous reads can starve a queued writer. The lock is always released
26
+ * when the callback settles, whether it resolves or throws.
27
+ */
28
+ class ReadWriteLock {
29
+ /**
30
+ * @param options - Lock options.
31
+ * @param options.priority - Which mode takes priority when both are
32
+ * contended. Defaults to `'write'`.
33
+ */
34
+ constructor(options = {}) {
35
+ _ReadWriteLock_instances.add(this);
36
+ _ReadWriteLock_readerCount.set(this, 0);
37
+ _ReadWriteLock_writerActive.set(this, false);
38
+ _ReadWriteLock_waiters.set(this, []);
39
+ _ReadWriteLock_priority.set(this, void 0);
40
+ __classPrivateFieldSet(this, _ReadWriteLock_priority, options.priority ?? 'write', "f");
41
+ }
42
+ /**
43
+ * Run a read operation — parallel with other readers, exclusive with
44
+ * writers. New readers queue behind any pending writer.
45
+ *
46
+ * @param fn - The operation to run while holding the read lock.
47
+ * @returns Whatever `fn` resolves to.
48
+ */
49
+ async withReadLock(fn) {
50
+ return __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_withLock).call(this, 'read', fn);
51
+ }
52
+ /**
53
+ * Run a write operation — exclusive with readers and other writers.
54
+ *
55
+ * @param fn - The operation to run while holding the write lock.
56
+ * @returns Whatever `fn` resolves to.
57
+ */
58
+ async withWriteLock(fn) {
59
+ return __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_withLock).call(this, 'write', fn);
60
+ }
61
+ }
62
+ exports.ReadWriteLock = ReadWriteLock;
63
+ _ReadWriteLock_readerCount = new WeakMap(), _ReadWriteLock_writerActive = new WeakMap(), _ReadWriteLock_waiters = new WeakMap(), _ReadWriteLock_priority = new WeakMap(), _ReadWriteLock_instances = new WeakSet(), _ReadWriteLock_withLock =
64
+ /**
65
+ * Acquire the lock in the given mode, run the operation, and release the
66
+ * lock — including on a throw, so the lock can never leak.
67
+ *
68
+ * @param mode - The mode in which to hold the lock.
69
+ * @param fn - The operation to run while holding the lock.
70
+ * @returns Whatever `fn` resolves to.
71
+ */
72
+ async function _ReadWriteLock_withLock(mode, fn) {
73
+ await __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_acquire).call(this, mode);
74
+ try {
75
+ return await fn();
76
+ }
77
+ finally {
78
+ __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_release).call(this, mode);
79
+ }
80
+ }, _ReadWriteLock_hasWaiters = function _ReadWriteLock_hasWaiters() {
81
+ return __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").length > 0;
82
+ }, _ReadWriteLock_isReading = function _ReadWriteLock_isReading() {
83
+ return __classPrivateFieldGet(this, _ReadWriteLock_readerCount, "f") > 0;
84
+ }, _ReadWriteLock_isWriting = function _ReadWriteLock_isWriting() {
85
+ return __classPrivateFieldGet(this, _ReadWriteLock_writerActive, "f");
86
+ }, _ReadWriteLock_acquire =
87
+ /**
88
+ * Acquire the lock in the given mode, queuing when it cannot be granted
89
+ * immediately.
90
+ *
91
+ * @param mode - The mode to acquire.
92
+ * @returns A promise that resolves once the lock is held.
93
+ */
94
+ async function _ReadWriteLock_acquire(mode) {
95
+ return new Promise((resolve) => {
96
+ // Writing is exclusive, so we cannot acquire the lock if a writer is active.
97
+ const blockedByWriter = __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_isWriting).call(this);
98
+ // Writing is exclusive with readers, so we cannot acquire the lock if any reader is active.
99
+ const blockedByReaders = mode === 'write' && __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_isReading).call(this);
100
+ // We cannot acquire the lock for reading if there are waiters, unless priority is 'read'.
101
+ const blockedByWriterPriority = mode === 'read' && __classPrivateFieldGet(this, _ReadWriteLock_priority, "f") === 'write' && __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_hasWaiters).call(this); // We might have pending write requests in the queue.
102
+ const acquire = !(blockedByWriter ||
103
+ blockedByReaders ||
104
+ blockedByWriterPriority);
105
+ if (acquire) {
106
+ __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_grant).call(this, mode);
107
+ resolve();
108
+ }
109
+ else {
110
+ // We cannot grant the lock immediately, so we queue the waiter.
111
+ __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").push({ mode, resolve });
112
+ }
113
+ });
114
+ }, _ReadWriteLock_grant = function _ReadWriteLock_grant(mode) {
115
+ if (mode === 'read') {
116
+ __classPrivateFieldSet(this, _ReadWriteLock_readerCount, __classPrivateFieldGet(this, _ReadWriteLock_readerCount, "f") + 1, "f");
117
+ }
118
+ else {
119
+ __classPrivateFieldSet(this, _ReadWriteLock_writerActive, true, "f");
120
+ }
121
+ }, _ReadWriteLock_release = function _ReadWriteLock_release(mode) {
122
+ if (mode === 'read') {
123
+ __classPrivateFieldSet(this, _ReadWriteLock_readerCount, __classPrivateFieldGet(this, _ReadWriteLock_readerCount, "f") - 1, "f");
124
+ }
125
+ else {
126
+ __classPrivateFieldSet(this, _ReadWriteLock_writerActive, false, "f");
127
+ }
128
+ __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_drain).call(this);
129
+ }, _ReadWriteLock_drain = function _ReadWriteLock_drain() {
130
+ let waiter = __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").shift();
131
+ while (waiter !== undefined) {
132
+ // Writing is exclusive, so we cannot acquire the lock if a writer is active.
133
+ const blockedByWriter = __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_isWriting).call(this);
134
+ // Reading blocks a writer, so we cannot acquire the lock for writing if any reader is active.
135
+ const blockedByReaders = waiter.mode === 'write' && __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_isReading).call(this);
136
+ const acquire = !(blockedByWriter || blockedByReaders);
137
+ if (acquire) {
138
+ // Grant the front waiter and continue with the next one, so consecutive queued readers are granted together.
139
+ __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_grant).call(this, waiter.mode);
140
+ waiter.resolve();
141
+ waiter = __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").shift();
142
+ }
143
+ else {
144
+ // The waiter is blocked by an active holder; put it back at the front and stop.
145
+ __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").unshift(waiter);
146
+ return;
147
+ }
148
+ }
149
+ };
150
+ //# sourceMappingURL=read-write-lock.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-write-lock.cjs","sourceRoot":"","sources":["../src/read-write-lock.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A mode in which the lock can be held: shared (`'read'`) or exclusive\n * (`'write'`).\n */\ntype LockMode = 'read' | 'write';\n\n/**\n * A queued lock-acquisition request.\n */\ntype LockWaiter = {\n /** The mode being acquired. */\n mode: LockMode;\n /** Resolves once the lock has been granted. */\n resolve: () => void;\n};\n\n/**\n * Which mode a {@link ReadWriteLock} prioritizes when both are contended.\n */\nexport type ReadWriteLockPriority = 'read' | 'write';\n\n/**\n * Options for constructing a {@link ReadWriteLock}.\n */\nexport type ReadWriteLockOptions = {\n /**\n * Which mode takes priority when both are contended. Defaults to\n * `'write'`.\n *\n * - `'write'`: a pending writer blocks new readers, so a steady stream of\n * reads cannot starve a long write.\n * - `'read'`: new readers join active readers even while a writer is\n * queued, for better read latency — at the cost that a continuous stream\n * of reads can starve a queued writer.\n */\n priority?: ReadWriteLockPriority;\n};\n\n/**\n * A read-write lock: multiple readers may hold the lock simultaneously,\n * while writers hold it exclusively.\n *\n * Grant order is first-in-first-out, and the lock is write-preferring by\n * default: a pending writer blocks new readers, and consecutive queued\n * readers are granted together once the lock turns over to them. Set the\n * `priority` option to `'read'` to let new readers join active readers even\n * while a writer is queued — better read latency, at the cost that\n * continuous reads can starve a queued writer. The lock is always released\n * when the callback settles, whether it resolves or throws.\n */\nexport class ReadWriteLock {\n #readerCount: number = 0;\n\n #writerActive: boolean = false;\n\n readonly #waiters: LockWaiter[] = [];\n\n readonly #priority: ReadWriteLockPriority;\n\n /**\n * @param options - Lock options.\n * @param options.priority - Which mode takes priority when both are\n * contended. Defaults to `'write'`.\n */\n constructor(options: ReadWriteLockOptions = {}) {\n this.#priority = options.priority ?? 'write';\n }\n\n /**\n * Run a read operation — parallel with other readers, exclusive with\n * writers. New readers queue behind any pending writer.\n *\n * @param fn - The operation to run while holding the read lock.\n * @returns Whatever `fn` resolves to.\n */\n async withReadLock<Result>(fn: () => Promise<Result>): Promise<Result> {\n return this.#withLock('read', fn);\n }\n\n /**\n * Run a write operation — exclusive with readers and other writers.\n *\n * @param fn - The operation to run while holding the write lock.\n * @returns Whatever `fn` resolves to.\n */\n async withWriteLock<Result>(fn: () => Promise<Result>): Promise<Result> {\n return this.#withLock('write', fn);\n }\n\n /**\n * Acquire the lock in the given mode, run the operation, and release the\n * lock — including on a throw, so the lock can never leak.\n *\n * @param mode - The mode in which to hold the lock.\n * @param fn - The operation to run while holding the lock.\n * @returns Whatever `fn` resolves to.\n */\n async #withLock<Result>(\n mode: LockMode,\n fn: () => Promise<Result>,\n ): Promise<Result> {\n await this.#acquire(mode);\n try {\n return await fn();\n } finally {\n this.#release(mode);\n }\n }\n\n /**\n * Whether any acquisition request is queued, waiting for the lock to turn\n * over to it. A queued waiter always blocks newcomers, which keeps the\n * grant order first-in-first-out.\n *\n * @returns Whether any acquisition request is queued.\n */\n #hasWaiters(): boolean {\n return this.#waiters.length > 0;\n }\n\n /**\n * Whether the lock is currently held for reading. Readers hold the lock\n * in parallel, so this does not mean an exclusive hold.\n *\n * @returns Whether any reader is active.\n */\n #isReading(): boolean {\n return this.#readerCount > 0;\n }\n\n /**\n * Whether the lock is currently held for writing. A writer holds the lock\n * exclusively: while writing, no other holder — reader or writer — can\n * be active.\n *\n * @returns Whether a writer is active.\n */\n #isWriting(): boolean {\n return this.#writerActive;\n }\n\n /**\n * Acquire the lock in the given mode, queuing when it cannot be granted\n * immediately.\n *\n * @param mode - The mode to acquire.\n * @returns A promise that resolves once the lock is held.\n */\n async #acquire(mode: LockMode): Promise<void> {\n return new Promise<void>((resolve) => {\n // Writing is exclusive, so we cannot acquire the lock if a writer is active.\n const blockedByWriter = this.#isWriting();\n // Writing is exclusive with readers, so we cannot acquire the lock if any reader is active.\n const blockedByReaders = mode === 'write' && this.#isReading();\n // We cannot acquire the lock for reading if there are waiters, unless priority is 'read'.\n const blockedByWriterPriority =\n mode === 'read' && this.#priority === 'write' && this.#hasWaiters(); // We might have pending write requests in the queue.\n\n const acquire = !(\n blockedByWriter ||\n blockedByReaders ||\n blockedByWriterPriority\n );\n\n if (acquire) {\n this.#grant(mode);\n resolve();\n } else {\n // We cannot grant the lock immediately, so we queue the waiter.\n this.#waiters.push({ mode, resolve });\n }\n });\n }\n\n /**\n * Grant the lock in the given mode. Only called when the grant is legal.\n *\n * @param mode - The mode to grant.\n */\n #grant(mode: LockMode): void {\n if (mode === 'read') {\n this.#readerCount += 1;\n } else {\n this.#writerActive = true;\n }\n }\n\n /**\n * Release the lock in the given mode, then grant queued waiters in order.\n *\n * @param mode - The mode being released.\n */\n #release(mode: LockMode): void {\n if (mode === 'read') {\n this.#readerCount -= 1;\n } else {\n this.#writerActive = false;\n }\n this.#drain();\n }\n\n /**\n * Grant queued waiters, in order, while the front of the queue can be\n * granted. Consecutive queued readers are granted together; a granted\n * writer blocks everything behind it. Unlike a newcomer, the front\n * waiter is never blocked by queued waiters — everyone behind it\n * arrived later — so only active holders block a grant here.\n */\n #drain(): void {\n let waiter: LockWaiter | undefined = this.#waiters.shift();\n while (waiter !== undefined) {\n // Writing is exclusive, so we cannot acquire the lock if a writer is active.\n const blockedByWriter = this.#isWriting();\n // Reading blocks a writer, so we cannot acquire the lock for writing if any reader is active.\n const blockedByReaders = waiter.mode === 'write' && this.#isReading();\n\n const acquire = !(blockedByWriter || blockedByReaders);\n\n if (acquire) {\n // Grant the front waiter and continue with the next one, so consecutive queued readers are granted together.\n this.#grant(waiter.mode);\n waiter.resolve();\n waiter = this.#waiters.shift();\n } else {\n // The waiter is blocked by an active holder; put it back at the front and stop.\n this.#waiters.unshift(waiter);\n return;\n }\n }\n }\n}\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Which mode a {@link ReadWriteLock} prioritizes when both are contended.
3
+ */
4
+ export type ReadWriteLockPriority = 'read' | 'write';
5
+ /**
6
+ * Options for constructing a {@link ReadWriteLock}.
7
+ */
8
+ export type ReadWriteLockOptions = {
9
+ /**
10
+ * Which mode takes priority when both are contended. Defaults to
11
+ * `'write'`.
12
+ *
13
+ * - `'write'`: a pending writer blocks new readers, so a steady stream of
14
+ * reads cannot starve a long write.
15
+ * - `'read'`: new readers join active readers even while a writer is
16
+ * queued, for better read latency — at the cost that a continuous stream
17
+ * of reads can starve a queued writer.
18
+ */
19
+ priority?: ReadWriteLockPriority;
20
+ };
21
+ /**
22
+ * A read-write lock: multiple readers may hold the lock simultaneously,
23
+ * while writers hold it exclusively.
24
+ *
25
+ * Grant order is first-in-first-out, and the lock is write-preferring by
26
+ * default: a pending writer blocks new readers, and consecutive queued
27
+ * readers are granted together once the lock turns over to them. Set the
28
+ * `priority` option to `'read'` to let new readers join active readers even
29
+ * while a writer is queued — better read latency, at the cost that
30
+ * continuous reads can starve a queued writer. The lock is always released
31
+ * when the callback settles, whether it resolves or throws.
32
+ */
33
+ export declare class ReadWriteLock {
34
+ #private;
35
+ /**
36
+ * @param options - Lock options.
37
+ * @param options.priority - Which mode takes priority when both are
38
+ * contended. Defaults to `'write'`.
39
+ */
40
+ constructor(options?: ReadWriteLockOptions);
41
+ /**
42
+ * Run a read operation — parallel with other readers, exclusive with
43
+ * writers. New readers queue behind any pending writer.
44
+ *
45
+ * @param fn - The operation to run while holding the read lock.
46
+ * @returns Whatever `fn` resolves to.
47
+ */
48
+ withReadLock<Result>(fn: () => Promise<Result>): Promise<Result>;
49
+ /**
50
+ * Run a write operation — exclusive with readers and other writers.
51
+ *
52
+ * @param fn - The operation to run while holding the write lock.
53
+ * @returns Whatever `fn` resolves to.
54
+ */
55
+ withWriteLock<Result>(fn: () => Promise<Result>): Promise<Result>;
56
+ }
57
+ //# sourceMappingURL=read-write-lock.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-write-lock.d.cts","sourceRoot":"","sources":["../src/read-write-lock.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,CAAC;AAErD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,qBAAa,aAAa;;IASxB;;;;OAIG;gBACS,OAAO,GAAE,oBAAyB;IAI9C;;;;;;OAMG;IACG,YAAY,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAItE;;;;;OAKG;IACG,aAAa,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;CAiJxE"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Which mode a {@link ReadWriteLock} prioritizes when both are contended.
3
+ */
4
+ export type ReadWriteLockPriority = 'read' | 'write';
5
+ /**
6
+ * Options for constructing a {@link ReadWriteLock}.
7
+ */
8
+ export type ReadWriteLockOptions = {
9
+ /**
10
+ * Which mode takes priority when both are contended. Defaults to
11
+ * `'write'`.
12
+ *
13
+ * - `'write'`: a pending writer blocks new readers, so a steady stream of
14
+ * reads cannot starve a long write.
15
+ * - `'read'`: new readers join active readers even while a writer is
16
+ * queued, for better read latency — at the cost that a continuous stream
17
+ * of reads can starve a queued writer.
18
+ */
19
+ priority?: ReadWriteLockPriority;
20
+ };
21
+ /**
22
+ * A read-write lock: multiple readers may hold the lock simultaneously,
23
+ * while writers hold it exclusively.
24
+ *
25
+ * Grant order is first-in-first-out, and the lock is write-preferring by
26
+ * default: a pending writer blocks new readers, and consecutive queued
27
+ * readers are granted together once the lock turns over to them. Set the
28
+ * `priority` option to `'read'` to let new readers join active readers even
29
+ * while a writer is queued — better read latency, at the cost that
30
+ * continuous reads can starve a queued writer. The lock is always released
31
+ * when the callback settles, whether it resolves or throws.
32
+ */
33
+ export declare class ReadWriteLock {
34
+ #private;
35
+ /**
36
+ * @param options - Lock options.
37
+ * @param options.priority - Which mode takes priority when both are
38
+ * contended. Defaults to `'write'`.
39
+ */
40
+ constructor(options?: ReadWriteLockOptions);
41
+ /**
42
+ * Run a read operation — parallel with other readers, exclusive with
43
+ * writers. New readers queue behind any pending writer.
44
+ *
45
+ * @param fn - The operation to run while holding the read lock.
46
+ * @returns Whatever `fn` resolves to.
47
+ */
48
+ withReadLock<Result>(fn: () => Promise<Result>): Promise<Result>;
49
+ /**
50
+ * Run a write operation — exclusive with readers and other writers.
51
+ *
52
+ * @param fn - The operation to run while holding the write lock.
53
+ * @returns Whatever `fn` resolves to.
54
+ */
55
+ withWriteLock<Result>(fn: () => Promise<Result>): Promise<Result>;
56
+ }
57
+ //# sourceMappingURL=read-write-lock.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-write-lock.d.mts","sourceRoot":"","sources":["../src/read-write-lock.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,CAAC;AAErD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,qBAAa,aAAa;;IASxB;;;;OAIG;gBACS,OAAO,GAAE,oBAAyB;IAI9C;;;;;;OAMG;IACG,YAAY,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAItE;;;;;OAKG;IACG,aAAa,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;CAiJxE"}
@@ -0,0 +1,146 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _ReadWriteLock_instances, _ReadWriteLock_readerCount, _ReadWriteLock_writerActive, _ReadWriteLock_waiters, _ReadWriteLock_priority, _ReadWriteLock_withLock, _ReadWriteLock_hasWaiters, _ReadWriteLock_isReading, _ReadWriteLock_isWriting, _ReadWriteLock_acquire, _ReadWriteLock_grant, _ReadWriteLock_release, _ReadWriteLock_drain;
13
+ /**
14
+ * A read-write lock: multiple readers may hold the lock simultaneously,
15
+ * while writers hold it exclusively.
16
+ *
17
+ * Grant order is first-in-first-out, and the lock is write-preferring by
18
+ * default: a pending writer blocks new readers, and consecutive queued
19
+ * readers are granted together once the lock turns over to them. Set the
20
+ * `priority` option to `'read'` to let new readers join active readers even
21
+ * while a writer is queued — better read latency, at the cost that
22
+ * continuous reads can starve a queued writer. The lock is always released
23
+ * when the callback settles, whether it resolves or throws.
24
+ */
25
+ export class ReadWriteLock {
26
+ /**
27
+ * @param options - Lock options.
28
+ * @param options.priority - Which mode takes priority when both are
29
+ * contended. Defaults to `'write'`.
30
+ */
31
+ constructor(options = {}) {
32
+ _ReadWriteLock_instances.add(this);
33
+ _ReadWriteLock_readerCount.set(this, 0);
34
+ _ReadWriteLock_writerActive.set(this, false);
35
+ _ReadWriteLock_waiters.set(this, []);
36
+ _ReadWriteLock_priority.set(this, void 0);
37
+ __classPrivateFieldSet(this, _ReadWriteLock_priority, options.priority ?? 'write', "f");
38
+ }
39
+ /**
40
+ * Run a read operation — parallel with other readers, exclusive with
41
+ * writers. New readers queue behind any pending writer.
42
+ *
43
+ * @param fn - The operation to run while holding the read lock.
44
+ * @returns Whatever `fn` resolves to.
45
+ */
46
+ async withReadLock(fn) {
47
+ return __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_withLock).call(this, 'read', fn);
48
+ }
49
+ /**
50
+ * Run a write operation — exclusive with readers and other writers.
51
+ *
52
+ * @param fn - The operation to run while holding the write lock.
53
+ * @returns Whatever `fn` resolves to.
54
+ */
55
+ async withWriteLock(fn) {
56
+ return __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_withLock).call(this, 'write', fn);
57
+ }
58
+ }
59
+ _ReadWriteLock_readerCount = new WeakMap(), _ReadWriteLock_writerActive = new WeakMap(), _ReadWriteLock_waiters = new WeakMap(), _ReadWriteLock_priority = new WeakMap(), _ReadWriteLock_instances = new WeakSet(), _ReadWriteLock_withLock =
60
+ /**
61
+ * Acquire the lock in the given mode, run the operation, and release the
62
+ * lock — including on a throw, so the lock can never leak.
63
+ *
64
+ * @param mode - The mode in which to hold the lock.
65
+ * @param fn - The operation to run while holding the lock.
66
+ * @returns Whatever `fn` resolves to.
67
+ */
68
+ async function _ReadWriteLock_withLock(mode, fn) {
69
+ await __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_acquire).call(this, mode);
70
+ try {
71
+ return await fn();
72
+ }
73
+ finally {
74
+ __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_release).call(this, mode);
75
+ }
76
+ }, _ReadWriteLock_hasWaiters = function _ReadWriteLock_hasWaiters() {
77
+ return __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").length > 0;
78
+ }, _ReadWriteLock_isReading = function _ReadWriteLock_isReading() {
79
+ return __classPrivateFieldGet(this, _ReadWriteLock_readerCount, "f") > 0;
80
+ }, _ReadWriteLock_isWriting = function _ReadWriteLock_isWriting() {
81
+ return __classPrivateFieldGet(this, _ReadWriteLock_writerActive, "f");
82
+ }, _ReadWriteLock_acquire =
83
+ /**
84
+ * Acquire the lock in the given mode, queuing when it cannot be granted
85
+ * immediately.
86
+ *
87
+ * @param mode - The mode to acquire.
88
+ * @returns A promise that resolves once the lock is held.
89
+ */
90
+ async function _ReadWriteLock_acquire(mode) {
91
+ return new Promise((resolve) => {
92
+ // Writing is exclusive, so we cannot acquire the lock if a writer is active.
93
+ const blockedByWriter = __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_isWriting).call(this);
94
+ // Writing is exclusive with readers, so we cannot acquire the lock if any reader is active.
95
+ const blockedByReaders = mode === 'write' && __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_isReading).call(this);
96
+ // We cannot acquire the lock for reading if there are waiters, unless priority is 'read'.
97
+ const blockedByWriterPriority = mode === 'read' && __classPrivateFieldGet(this, _ReadWriteLock_priority, "f") === 'write' && __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_hasWaiters).call(this); // We might have pending write requests in the queue.
98
+ const acquire = !(blockedByWriter ||
99
+ blockedByReaders ||
100
+ blockedByWriterPriority);
101
+ if (acquire) {
102
+ __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_grant).call(this, mode);
103
+ resolve();
104
+ }
105
+ else {
106
+ // We cannot grant the lock immediately, so we queue the waiter.
107
+ __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").push({ mode, resolve });
108
+ }
109
+ });
110
+ }, _ReadWriteLock_grant = function _ReadWriteLock_grant(mode) {
111
+ if (mode === 'read') {
112
+ __classPrivateFieldSet(this, _ReadWriteLock_readerCount, __classPrivateFieldGet(this, _ReadWriteLock_readerCount, "f") + 1, "f");
113
+ }
114
+ else {
115
+ __classPrivateFieldSet(this, _ReadWriteLock_writerActive, true, "f");
116
+ }
117
+ }, _ReadWriteLock_release = function _ReadWriteLock_release(mode) {
118
+ if (mode === 'read') {
119
+ __classPrivateFieldSet(this, _ReadWriteLock_readerCount, __classPrivateFieldGet(this, _ReadWriteLock_readerCount, "f") - 1, "f");
120
+ }
121
+ else {
122
+ __classPrivateFieldSet(this, _ReadWriteLock_writerActive, false, "f");
123
+ }
124
+ __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_drain).call(this);
125
+ }, _ReadWriteLock_drain = function _ReadWriteLock_drain() {
126
+ let waiter = __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").shift();
127
+ while (waiter !== undefined) {
128
+ // Writing is exclusive, so we cannot acquire the lock if a writer is active.
129
+ const blockedByWriter = __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_isWriting).call(this);
130
+ // Reading blocks a writer, so we cannot acquire the lock for writing if any reader is active.
131
+ const blockedByReaders = waiter.mode === 'write' && __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_isReading).call(this);
132
+ const acquire = !(blockedByWriter || blockedByReaders);
133
+ if (acquire) {
134
+ // Grant the front waiter and continue with the next one, so consecutive queued readers are granted together.
135
+ __classPrivateFieldGet(this, _ReadWriteLock_instances, "m", _ReadWriteLock_grant).call(this, waiter.mode);
136
+ waiter.resolve();
137
+ waiter = __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").shift();
138
+ }
139
+ else {
140
+ // The waiter is blocked by an active holder; put it back at the front and stop.
141
+ __classPrivateFieldGet(this, _ReadWriteLock_waiters, "f").unshift(waiter);
142
+ return;
143
+ }
144
+ }
145
+ };
146
+ //# sourceMappingURL=read-write-lock.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-write-lock.mjs","sourceRoot":"","sources":["../src/read-write-lock.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A mode in which the lock can be held: shared (`'read'`) or exclusive\n * (`'write'`).\n */\ntype LockMode = 'read' | 'write';\n\n/**\n * A queued lock-acquisition request.\n */\ntype LockWaiter = {\n /** The mode being acquired. */\n mode: LockMode;\n /** Resolves once the lock has been granted. */\n resolve: () => void;\n};\n\n/**\n * Which mode a {@link ReadWriteLock} prioritizes when both are contended.\n */\nexport type ReadWriteLockPriority = 'read' | 'write';\n\n/**\n * Options for constructing a {@link ReadWriteLock}.\n */\nexport type ReadWriteLockOptions = {\n /**\n * Which mode takes priority when both are contended. Defaults to\n * `'write'`.\n *\n * - `'write'`: a pending writer blocks new readers, so a steady stream of\n * reads cannot starve a long write.\n * - `'read'`: new readers join active readers even while a writer is\n * queued, for better read latency — at the cost that a continuous stream\n * of reads can starve a queued writer.\n */\n priority?: ReadWriteLockPriority;\n};\n\n/**\n * A read-write lock: multiple readers may hold the lock simultaneously,\n * while writers hold it exclusively.\n *\n * Grant order is first-in-first-out, and the lock is write-preferring by\n * default: a pending writer blocks new readers, and consecutive queued\n * readers are granted together once the lock turns over to them. Set the\n * `priority` option to `'read'` to let new readers join active readers even\n * while a writer is queued — better read latency, at the cost that\n * continuous reads can starve a queued writer. The lock is always released\n * when the callback settles, whether it resolves or throws.\n */\nexport class ReadWriteLock {\n #readerCount: number = 0;\n\n #writerActive: boolean = false;\n\n readonly #waiters: LockWaiter[] = [];\n\n readonly #priority: ReadWriteLockPriority;\n\n /**\n * @param options - Lock options.\n * @param options.priority - Which mode takes priority when both are\n * contended. Defaults to `'write'`.\n */\n constructor(options: ReadWriteLockOptions = {}) {\n this.#priority = options.priority ?? 'write';\n }\n\n /**\n * Run a read operation — parallel with other readers, exclusive with\n * writers. New readers queue behind any pending writer.\n *\n * @param fn - The operation to run while holding the read lock.\n * @returns Whatever `fn` resolves to.\n */\n async withReadLock<Result>(fn: () => Promise<Result>): Promise<Result> {\n return this.#withLock('read', fn);\n }\n\n /**\n * Run a write operation — exclusive with readers and other writers.\n *\n * @param fn - The operation to run while holding the write lock.\n * @returns Whatever `fn` resolves to.\n */\n async withWriteLock<Result>(fn: () => Promise<Result>): Promise<Result> {\n return this.#withLock('write', fn);\n }\n\n /**\n * Acquire the lock in the given mode, run the operation, and release the\n * lock — including on a throw, so the lock can never leak.\n *\n * @param mode - The mode in which to hold the lock.\n * @param fn - The operation to run while holding the lock.\n * @returns Whatever `fn` resolves to.\n */\n async #withLock<Result>(\n mode: LockMode,\n fn: () => Promise<Result>,\n ): Promise<Result> {\n await this.#acquire(mode);\n try {\n return await fn();\n } finally {\n this.#release(mode);\n }\n }\n\n /**\n * Whether any acquisition request is queued, waiting for the lock to turn\n * over to it. A queued waiter always blocks newcomers, which keeps the\n * grant order first-in-first-out.\n *\n * @returns Whether any acquisition request is queued.\n */\n #hasWaiters(): boolean {\n return this.#waiters.length > 0;\n }\n\n /**\n * Whether the lock is currently held for reading. Readers hold the lock\n * in parallel, so this does not mean an exclusive hold.\n *\n * @returns Whether any reader is active.\n */\n #isReading(): boolean {\n return this.#readerCount > 0;\n }\n\n /**\n * Whether the lock is currently held for writing. A writer holds the lock\n * exclusively: while writing, no other holder — reader or writer — can\n * be active.\n *\n * @returns Whether a writer is active.\n */\n #isWriting(): boolean {\n return this.#writerActive;\n }\n\n /**\n * Acquire the lock in the given mode, queuing when it cannot be granted\n * immediately.\n *\n * @param mode - The mode to acquire.\n * @returns A promise that resolves once the lock is held.\n */\n async #acquire(mode: LockMode): Promise<void> {\n return new Promise<void>((resolve) => {\n // Writing is exclusive, so we cannot acquire the lock if a writer is active.\n const blockedByWriter = this.#isWriting();\n // Writing is exclusive with readers, so we cannot acquire the lock if any reader is active.\n const blockedByReaders = mode === 'write' && this.#isReading();\n // We cannot acquire the lock for reading if there are waiters, unless priority is 'read'.\n const blockedByWriterPriority =\n mode === 'read' && this.#priority === 'write' && this.#hasWaiters(); // We might have pending write requests in the queue.\n\n const acquire = !(\n blockedByWriter ||\n blockedByReaders ||\n blockedByWriterPriority\n );\n\n if (acquire) {\n this.#grant(mode);\n resolve();\n } else {\n // We cannot grant the lock immediately, so we queue the waiter.\n this.#waiters.push({ mode, resolve });\n }\n });\n }\n\n /**\n * Grant the lock in the given mode. Only called when the grant is legal.\n *\n * @param mode - The mode to grant.\n */\n #grant(mode: LockMode): void {\n if (mode === 'read') {\n this.#readerCount += 1;\n } else {\n this.#writerActive = true;\n }\n }\n\n /**\n * Release the lock in the given mode, then grant queued waiters in order.\n *\n * @param mode - The mode being released.\n */\n #release(mode: LockMode): void {\n if (mode === 'read') {\n this.#readerCount -= 1;\n } else {\n this.#writerActive = false;\n }\n this.#drain();\n }\n\n /**\n * Grant queued waiters, in order, while the front of the queue can be\n * granted. Consecutive queued readers are granted together; a granted\n * writer blocks everything behind it. Unlike a newcomer, the front\n * waiter is never blocked by queued waiters — everyone behind it\n * arrived later — so only active holders block a grant here.\n */\n #drain(): void {\n let waiter: LockWaiter | undefined = this.#waiters.shift();\n while (waiter !== undefined) {\n // Writing is exclusive, so we cannot acquire the lock if a writer is active.\n const blockedByWriter = this.#isWriting();\n // Reading blocks a writer, so we cannot acquire the lock for writing if any reader is active.\n const blockedByReaders = waiter.mode === 'write' && this.#isReading();\n\n const acquire = !(blockedByWriter || blockedByReaders);\n\n if (acquire) {\n // Grant the front waiter and continue with the next one, so consecutive queued readers are granted together.\n this.#grant(waiter.mode);\n waiter.resolve();\n waiter = this.#waiters.shift();\n } else {\n // The waiter is blocked by an active holder; put it back at the front and stop.\n this.#waiters.unshift(waiter);\n return;\n }\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask/keyring-sdk",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "MetaMask Keyring SDK",
5
5
  "keywords": [
6
6
  "keyring",