@metamask/keyring-sdk 4.0.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 +25 -1
- package/README.md +91 -0
- package/dist/atomic-keyring.cjs +126 -0
- package/dist/atomic-keyring.cjs.map +1 -0
- package/dist/atomic-keyring.d.cts +90 -0
- package/dist/atomic-keyring.d.cts.map +1 -0
- package/dist/atomic-keyring.d.mts +90 -0
- package/dist/atomic-keyring.d.mts.map +1 -0
- package/dist/atomic-keyring.mjs +121 -0
- package/dist/atomic-keyring.mjs.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -1
- package/dist/read-write-lock.cjs +150 -0
- package/dist/read-write-lock.cjs.map +1 -0
- package/dist/read-write-lock.d.cts +57 -0
- package/dist/read-write-lock.d.cts.map +1 -0
- package/dist/read-write-lock.d.mts +57 -0
- package/dist/read-write-lock.d.mts.map +1 -0
- package/dist/read-write-lock.mjs +146 -0
- package/dist/read-write-lock.mjs.map +1 -0
- package/dist/v2/keyring-v1-adapter.cjs +26 -1
- package/dist/v2/keyring-v1-adapter.cjs.map +1 -1
- package/dist/v2/keyring-v1-adapter.d.cts +15 -0
- package/dist/v2/keyring-v1-adapter.d.cts.map +1 -1
- package/dist/v2/keyring-v1-adapter.d.mts +15 -0
- package/dist/v2/keyring-v1-adapter.d.mts.map +1 -1
- package/dist/v2/keyring-v1-adapter.mjs +24 -0
- package/dist/v2/keyring-v1-adapter.mjs.map +1 -1
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ 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
|
+
|
|
21
|
+
## [4.1.0]
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Add `isKeyringV1Adapter` type guard ([#685](https://github.com/MetaMask/accounts/pull/685))
|
|
26
|
+
- Use this instead of doing type checks with `instanceof`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Bump `@metamask/keyring-api` from `^24.2.0` to `^24.3.0` ([#674](https://github.com/MetaMask/accounts/pull/674))
|
|
31
|
+
|
|
10
32
|
## [4.0.0]
|
|
11
33
|
|
|
12
34
|
### Changed
|
|
@@ -130,7 +152,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
130
152
|
|
|
131
153
|
- Initial release, extracted from `@metamask/keyring-api` ([#478](https://github.com/MetaMask/accounts/pull/478), [#482](https://github.com/MetaMask/accounts/pull/482))
|
|
132
154
|
|
|
133
|
-
[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@4.
|
|
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
|
|
157
|
+
[4.1.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@4.0.0...@metamask/keyring-sdk@4.1.0
|
|
134
158
|
[4.0.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@3.1.0...@metamask/keyring-sdk@4.0.0
|
|
135
159
|
[3.1.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@3.0.0...@metamask/keyring-sdk@3.1.0
|
|
136
160
|
[3.0.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@2.3.0...@metamask/keyring-sdk@3.0.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
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
package/dist/index.d.cts.map
CHANGED
|
@@ -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
|