whalibmob 5.10.9 → 5.10.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -24
- package/lib/auth-utils.js +357 -330
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -928,55 +928,65 @@ const {
|
|
|
928
928
|
|
|
929
929
|
### `makeCacheableSignalKeyStore`
|
|
930
930
|
|
|
931
|
-
Wraps a `SignalStore` with an in-memory
|
|
931
|
+
Wraps a `SignalStore` with an in-memory cache (5-minute TTL). Reads for sessions, pre-keys, signed pre-keys and identity keys are served from cache on subsequent accesses; `store*` calls write through to both, and `remove*` / `delete*` calls drop the entry.
|
|
932
|
+
|
|
933
|
+
A lookup that finds nothing is **not** cached. Absence is the state most likely to change from underneath — a session about to be built, a pre-key about to be uploaded — so a remembered miss is the one that would hurt.
|
|
932
934
|
|
|
933
935
|
`useClones` is set to `false` so that `SessionRecord` objects — which carry internal state and methods — are returned by reference and never deep-cloned.
|
|
934
936
|
|
|
935
|
-
|
|
937
|
+
Every method the underlying store has is forwarded, including `transaction()` and `isInTransaction()` when present, so the wrapper is a drop-in replacement and safe to stack with `addTransactionCapability`.
|
|
936
938
|
|
|
937
939
|
```js
|
|
938
|
-
const { SignalStore } = require('whalibmob')
|
|
939
|
-
const { makeCacheableSignalKeyStore } = require('whalibmob')
|
|
940
|
+
const { SignalStore, makeCacheableSignalKeyStore } = require('whalibmob')
|
|
940
941
|
|
|
941
|
-
const store = new SignalStore(
|
|
942
|
+
const store = new SignalStore()
|
|
942
943
|
const cached = makeCacheableSignalKeyStore(store)
|
|
943
944
|
|
|
944
945
|
// reads hit cache after first access
|
|
945
|
-
const session = await cached.
|
|
946
|
+
const session = await cached.loadSession('919634847671.0')
|
|
946
947
|
```
|
|
947
948
|
|
|
949
|
+
Call `await cached.flushCache()` to drop everything — after a key rotation, or when another process may have written to the same session file.
|
|
950
|
+
|
|
948
951
|
**When to use:** whenever your `SignalStore` is backed by a remote or disk-based store (database, Redis, file system) and you want to reduce repeated lookups for sessions that haven't changed between sends.
|
|
949
952
|
|
|
950
953
|
### `addTransactionCapability`
|
|
951
954
|
|
|
952
|
-
Wraps a `SignalStore` with batched-write (transaction) semantics. During a transaction all writes are buffered in memory
|
|
955
|
+
Wraps a `SignalStore` with batched-write (transaction) semantics. During a transaction all writes are buffered in memory and flushed to the underlying store in one shot when the callback returns — there is no `commit()` to call. Reads check the buffer first, so a transaction sees its own writes. If the callback throws, nothing is written at all and the error reaches the caller.
|
|
953
956
|
|
|
954
|
-
Uses `AsyncLocalStorage` to propagate transaction context across async call chains, and a
|
|
957
|
+
Uses `AsyncLocalStorage` to propagate transaction context across async call chains, and a `Mutex` with reference-counting per transaction key.
|
|
955
958
|
|
|
956
959
|
```js
|
|
957
|
-
const { addTransactionCapability, makeCacheableSignalKeyStore } = require('whalibmob')
|
|
960
|
+
const { SignalStore, addTransactionCapability, makeCacheableSignalKeyStore } = require('whalibmob')
|
|
958
961
|
|
|
959
962
|
// recommended: cache first, then transactions on top
|
|
960
|
-
const base
|
|
961
|
-
const cached
|
|
962
|
-
const txnStore
|
|
963
|
+
const base = new SignalStore()
|
|
964
|
+
const cached = makeCacheableSignalKeyStore(base)
|
|
965
|
+
const txnStore = addTransactionCapability(cached)
|
|
963
966
|
|
|
964
967
|
// inside a send flow
|
|
965
968
|
await txnStore.transaction(async () => {
|
|
966
|
-
// all writes are buffered
|
|
967
|
-
await txnStore.
|
|
968
|
-
await txnStore.
|
|
969
|
-
|
|
970
|
-
})
|
|
969
|
+
// all writes are buffered until this callback returns
|
|
970
|
+
await txnStore.storeSession('919634847671.0', sessionRecord)
|
|
971
|
+
await txnStore.storePreKey(1, preKeyPair)
|
|
972
|
+
}, 'send')
|
|
971
973
|
```
|
|
972
974
|
|
|
973
|
-
|
|
975
|
+
The second argument is a scope: two transactions with different keys run concurrently, two with the same key run one after the other. It defaults to `'default'`. Any key is safe — the transaction locks are kept separate from the locks reads take, so no choice of key can make a transaction wait on itself.
|
|
976
|
+
|
|
977
|
+
Nested `transaction()` calls reuse the enclosing context instead of opening a second one, so an inner transaction does not commit on its own.
|
|
978
|
+
|
|
979
|
+
Stacking order matters: put `makeCacheableSignalKeyStore` below `addTransactionCapability`, so that a transaction which rolls back never reaches the cache. The other order works too — a failed transaction flushes the cache rather than leave it holding writes the store never took — but it throws away good entries to do it.
|
|
974
980
|
|
|
975
981
|
**When to use:** for high-throughput servers that send to many recipients concurrently and need to batch Signal key writes into a single atomic flush per message.
|
|
976
982
|
|
|
977
983
|
### `assertMeId`
|
|
978
984
|
|
|
979
|
-
|
|
985
|
+
Returns the account's JID, or throws an `Error` describing what is missing.
|
|
986
|
+
|
|
987
|
+
Works with both kinds of store — the SMS store from `initAuthCreds` / `createNewStore`, and the companion store from `createNewWebStore`. Only the wording of the error differs, since the way out of "not registered yet" is SMS verification in one case and pairing in the other.
|
|
988
|
+
|
|
989
|
+
Once registered, the JID the server assigned is preferred (`store.me.id`) — on a companion it carries the device suffix, and rebuilding it from the phone number drops that silently. Before registration it throws, including during the pairing window: requesting a pairing code writes a placeholder `me` with no suffix, and that is not treated as being linked.
|
|
980
990
|
|
|
981
991
|
```js
|
|
982
992
|
const { assertMeId } = require('whalibmob')
|
|
@@ -985,7 +995,8 @@ const store = loadStore(sessFile)
|
|
|
985
995
|
|
|
986
996
|
try {
|
|
987
997
|
const jid = assertMeId(store)
|
|
988
|
-
//
|
|
998
|
+
// '919634847671:12@s.whatsapp.net' once connected,
|
|
999
|
+
// '919634847671@s.whatsapp.net' before that
|
|
989
1000
|
console.log('account JID:', jid)
|
|
990
1001
|
} catch (err) {
|
|
991
1002
|
console.error('store is not registered:', err.message)
|
|
@@ -996,7 +1007,9 @@ try {
|
|
|
996
1007
|
|
|
997
1008
|
### `initAuthCreds`
|
|
998
1009
|
|
|
999
|
-
Creates a fresh credential store for the given phone number
|
|
1010
|
+
Creates a fresh credential store for the given phone number — the key pairs, registration ID and device identifiers a number needs before it can even ask for an SMS code. This is what `/reg code` calls.
|
|
1011
|
+
|
|
1012
|
+
It returns everything `createNewStore` does, plus a few fields kept for application code that expects them: `nextPreKeyId`, `firstUnuploadedPreKeyId`, `accountSyncCounter`, `accountSettings`, `processedHistoryMessages` and `advSecretKey`. Those extras live on the object only — `saveStore` does not write them, so they are not there again after a reload. Nothing in the library reads them; treat them as a convenience, not as state.
|
|
1000
1013
|
|
|
1001
1014
|
```js
|
|
1002
1015
|
const { initAuthCreds, saveStore } = require('whalibmob')
|
|
@@ -1013,10 +1026,35 @@ const store = initAuthCreds(phone)
|
|
|
1013
1026
|
saveStore(store, sessFile)
|
|
1014
1027
|
```
|
|
1015
1028
|
|
|
1016
|
-
This is the function
|
|
1029
|
+
This is the function the CLI uses for every new SMS session. Prefer it over `createNewStore` for forward compatibility.
|
|
1017
1030
|
|
|
1018
1031
|
> [!NOTE]
|
|
1019
|
-
> `initAuthCreds` and `createNewStore` produce equivalent stores for
|
|
1032
|
+
> `initAuthCreds` and `createNewStore` produce equivalent stores for every current library operation, and identical files on disk. Use `createNewWebStore` instead when the device will be linked as a companion rather than registered by SMS — that one adds the pairing fields, and its own serialiser persists them.
|
|
1033
|
+
|
|
1034
|
+
The file it writes has exactly these 16 keys:
|
|
1035
|
+
|
|
1036
|
+
```json
|
|
1037
|
+
{
|
|
1038
|
+
"phoneNumber": "40712345678",
|
|
1039
|
+
"noiseKeyPair": { "private": "…", "public": "…" },
|
|
1040
|
+
"identityKeyPair": { "private": "…", "public": "…" },
|
|
1041
|
+
"signedPreKey": { "id": 3649616, "private": "…", "public": "…", "signature": "…" },
|
|
1042
|
+
"registrationId": 2183,
|
|
1043
|
+
"fdid": "2f701f8b-d693-4728-…",
|
|
1044
|
+
"deviceId": "CBIyyJRAokOdYoTYEjTbng==",
|
|
1045
|
+
"identityId": "SE8Q785oful7dGdBgNpwjg==",
|
|
1046
|
+
"advertisingId": "5c97eea9-783f-4fcc-…",
|
|
1047
|
+
"backupToken": "…",
|
|
1048
|
+
"registered": true,
|
|
1049
|
+
"codePending": false,
|
|
1050
|
+
"name": "Boss",
|
|
1051
|
+
"version": "2.26.9.75",
|
|
1052
|
+
"device": { "os": "ios", "platform": 1, "model": "iPhone 15 Pro", "…": "…" },
|
|
1053
|
+
"advIdentity": "…"
|
|
1054
|
+
}
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
`registered` and `codePending` are the two that move: both `false` when the store is created, `codePending` flips to `true` once a code has been requested, and `verifyCode` sets `registered` to `true` and `codePending` back to `false`. `advIdentity` stays `null` until the server sends the signed device identity in `<success>`.
|
|
1020
1058
|
|
|
1021
1059
|
### Recommended Stacking Pattern
|
|
1022
1060
|
|
|
@@ -1036,7 +1074,8 @@ const {
|
|
|
1036
1074
|
let store = loadStore(sessFile) || initAuthCreds(phone)
|
|
1037
1075
|
|
|
1038
1076
|
// 2. build the layered Signal key store
|
|
1039
|
-
const signalStore = new SignalStore(
|
|
1077
|
+
const signalStore = new SignalStore()
|
|
1078
|
+
signalStore.attachFile(sessFile)
|
|
1040
1079
|
const cachedStore = makeCacheableSignalKeyStore(signalStore)
|
|
1041
1080
|
const txnStore = addTransactionCapability(cachedStore)
|
|
1042
1081
|
|
package/lib/auth-utils.js
CHANGED
|
@@ -2,26 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
// ─── auth-utils.js ────────────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// Optional helpers for code that manages its own SignalStore — a custom storage
|
|
6
|
+
// backend, a multi-account server, anything that wants caching or transactional
|
|
7
|
+
// writes around the store the library would otherwise use directly.
|
|
7
8
|
//
|
|
8
|
-
// 1. makeCacheableSignalKeyStore —
|
|
9
|
-
//
|
|
10
|
-
// 2. addTransactionCapability —
|
|
11
|
-
//
|
|
12
|
-
// 3. assertMeId —
|
|
13
|
-
// 4. initAuthCreds —
|
|
9
|
+
// 1. makeCacheableSignalKeyStore — an in-memory cache with a 5-minute TTL in
|
|
10
|
+
// front of the store, serialised by a Mutex
|
|
11
|
+
// 2. addTransactionCapability — buffered writes committed in one shot,
|
|
12
|
+
// scoped with AsyncLocalStorage
|
|
13
|
+
// 3. assertMeId — the account's JID, or a clear error
|
|
14
|
+
// 4. initAuthCreds — a fresh credential set for a phone number
|
|
14
15
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
16
|
+
// Stacking order matters. The cache belongs *underneath* the transaction:
|
|
17
|
+
//
|
|
18
|
+
// const { SignalStore } = require('whalibmob');
|
|
17
19
|
// const { makeCacheableSignalKeyStore,
|
|
18
|
-
// addTransactionCapability
|
|
19
|
-
//
|
|
20
|
+
// addTransactionCapability } = require('whalibmob/lib/auth-utils');
|
|
21
|
+
//
|
|
22
|
+
// const raw = new SignalStore();
|
|
23
|
+
// const cached = makeCacheableSignalKeyStore(raw, logger);
|
|
24
|
+
// const store = addTransactionCapability(cached, logger);
|
|
20
25
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
26
|
+
// That way a transaction that throws never reaches the cache at all — its
|
|
27
|
+
// writes are still sitting in the transaction buffer when it is discarded. The
|
|
28
|
+
// other order works too, and a rollback flushes the cache to stay honest, but
|
|
29
|
+
// it throws away entries that were perfectly good.
|
|
25
30
|
//
|
|
26
31
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
32
|
|
|
@@ -33,8 +38,14 @@ const { createNewStore } = require('./Store');
|
|
|
33
38
|
|
|
34
39
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
35
40
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
41
|
+
/**
|
|
42
|
+
* How long a cached entry lives, in seconds.
|
|
43
|
+
*
|
|
44
|
+
* The option is `stdTTL` and the unit is seconds — `ttl`, and a string like
|
|
45
|
+
* '5m', are both accepted without complaint and then ignored, which leaves a
|
|
46
|
+
* cache that never evicts anything and answers from it forever.
|
|
47
|
+
*/
|
|
48
|
+
const SIGNAL_STORE_TTL_SECONDS = 5 * 60;
|
|
38
49
|
|
|
39
50
|
/** Default max retry count for transaction commits. */
|
|
40
51
|
const MAX_COMMIT_RETRIES = 5;
|
|
@@ -48,26 +59,63 @@ function _delay(ms) {
|
|
|
48
59
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
49
60
|
}
|
|
50
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Forward every method the wrapper did not define itself.
|
|
64
|
+
*
|
|
65
|
+
* Both wrappers claim to be drop-in replacements, and a hand-written list stops
|
|
66
|
+
* being one the moment the store grows a method — the call does not fail
|
|
67
|
+
* loudly, it lands on `undefined`. Anything the wrapper has an opinion about is
|
|
68
|
+
* already on it and is left alone; the rest is passed straight through.
|
|
69
|
+
*/
|
|
70
|
+
function _fillPassthrough(wrapper, store) {
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
let level = store;
|
|
73
|
+
|
|
74
|
+
while (level && level !== Object.prototype) {
|
|
75
|
+
for (const name of Object.getOwnPropertyNames(level)) {
|
|
76
|
+
if (seen.has(name) || name === 'constructor' || name.startsWith('_')) continue;
|
|
77
|
+
seen.add(name);
|
|
78
|
+
if (name in wrapper) continue; // the wrapper handles it
|
|
79
|
+
let fn;
|
|
80
|
+
try { fn = store[name]; } catch (_) { continue; } // skip throwing getters
|
|
81
|
+
if (typeof fn !== 'function') continue;
|
|
82
|
+
wrapper[name] = (...args) => store[name](...args);
|
|
83
|
+
}
|
|
84
|
+
level = Object.getPrototypeOf(level);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return wrapper;
|
|
88
|
+
}
|
|
89
|
+
|
|
51
90
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
91
|
// 1. makeCacheableSignalKeyStore
|
|
53
92
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
93
|
|
|
55
94
|
/**
|
|
56
|
-
* Wraps a
|
|
57
|
-
* • NodeCache — in-memory cache with a 5-minute TTL for
|
|
95
|
+
* Wraps a SignalStore with:
|
|
96
|
+
* • NodeCache — an in-memory cache with a 5-minute TTL for every key type
|
|
58
97
|
* (sessions, pre-keys, signed pre-keys, identity keys, registration ID).
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
98
|
+
* Each entry is evicted after that and re-read on the next access, which
|
|
99
|
+
* bounds memory and puts a ceiling on how long another writer's change can
|
|
100
|
+
* go unnoticed.
|
|
101
|
+
* • Mutex — every get/set is serialised, so two concurrent operations cannot
|
|
102
|
+
* interleave a read against a write to the same key.
|
|
103
|
+
*
|
|
104
|
+
* A lookup that finds nothing is *not* cached. Absence is the state most likely
|
|
105
|
+
* to change from underneath — a session about to be built, a pre-key about to
|
|
106
|
+
* be uploaded — and a remembered miss is the one that hurts.
|
|
63
107
|
*
|
|
64
108
|
* @param {SignalStore} store - underlying SignalStore instance
|
|
65
109
|
* @param {object} logger - optional logger with a .trace() method
|
|
66
|
-
* @param {NodeCache} _cache - optional pre-built
|
|
110
|
+
* @param {NodeCache} _cache - optional pre-built cache (for testing / sharing)
|
|
67
111
|
* @returns {object} drop-in SignalStore replacement with caching and serialisation
|
|
68
112
|
*/
|
|
69
113
|
function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
70
|
-
const cache = _cache || new NodeCache({
|
|
114
|
+
const cache = _cache || new NodeCache({
|
|
115
|
+
stdTTL: SIGNAL_STORE_TTL_SECONDS,
|
|
116
|
+
useClones: false,
|
|
117
|
+
deleteOnExpire: true
|
|
118
|
+
});
|
|
71
119
|
const mutex = new Mutex();
|
|
72
120
|
|
|
73
121
|
/** Unified cache key: "type:id" */
|
|
@@ -75,8 +123,59 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
75
123
|
|
|
76
124
|
const log = (msg, data) => logger && logger.trace(data, msg);
|
|
77
125
|
|
|
78
|
-
|
|
79
|
-
|
|
126
|
+
// Read through the cache, remembering only what was actually found.
|
|
127
|
+
function cachedRead(type, id, load) {
|
|
128
|
+
return mutex.runExclusive(async () => {
|
|
129
|
+
const k = cKey(type, id);
|
|
130
|
+
const hit = cache.get(k);
|
|
131
|
+
if (hit !== undefined) {
|
|
132
|
+
log(type + ' cache hit', { id });
|
|
133
|
+
return hit;
|
|
134
|
+
}
|
|
135
|
+
const val = await load();
|
|
136
|
+
if (val !== undefined && val !== null) cache.set(k, val);
|
|
137
|
+
return val;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function cachedWrite(type, id, value, write) {
|
|
142
|
+
return mutex.runExclusive(async () => {
|
|
143
|
+
cache.set(cKey(type, id), value);
|
|
144
|
+
return write();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function cachedDelete(type, id, remove) {
|
|
149
|
+
return mutex.runExclusive(async () => {
|
|
150
|
+
cache.del(cKey(type, id));
|
|
151
|
+
return remove();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Named rather than reached for through `this`, so a caller may pull a method
|
|
156
|
+
// off the object — `const { loadSession } = cachedStore` — and still call it.
|
|
157
|
+
async function getLocalRegistrationId() {
|
|
158
|
+
return mutex.runExclusive(async () => {
|
|
159
|
+
const hit = cache.get('meta:registrationId');
|
|
160
|
+
if (hit !== undefined) return hit;
|
|
161
|
+
const val = await store.getLocalRegistrationId();
|
|
162
|
+
if (val !== undefined && val !== null) cache.set('meta:registrationId', val);
|
|
163
|
+
return val;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function identity(load) {
|
|
168
|
+
return mutex.runExclusive(async () => {
|
|
169
|
+
const hit = cache.get('meta:identityKeyPair');
|
|
170
|
+
if (hit !== undefined) return hit;
|
|
171
|
+
const val = await load();
|
|
172
|
+
if (val) cache.set('meta:identityKeyPair', val);
|
|
173
|
+
return val;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const wrapper = {
|
|
178
|
+
// ── Initialisation ───────────────────────────────────────────────────────
|
|
80
179
|
|
|
81
180
|
init(identityKeyPair, registrationId) {
|
|
82
181
|
cache.set('meta:identityKeyPair', identityKeyPair);
|
|
@@ -84,171 +183,83 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
84
183
|
return store.init(identityKeyPair, registrationId);
|
|
85
184
|
},
|
|
86
185
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
getLidMappings() { return store.getLidMappings(); },
|
|
90
|
-
preKeyCount() { return store.preKeyCount(); },
|
|
91
|
-
nextPreKeyId() { return store.nextPreKeyId(); },
|
|
92
|
-
/** Synchronous hasSession — never cached; always reads from in-memory sessions map. */
|
|
93
|
-
hasSession(encodedAddress) { return store.hasSession(encodedAddress); },
|
|
186
|
+
/** Synchronous — never cached; reads the store's own in-memory session map. */
|
|
187
|
+
hasSession(encodedAddress) { return store.hasSession(encodedAddress); },
|
|
94
188
|
|
|
95
189
|
// ── Identity / registration ───────────────────────────────────────────────
|
|
96
190
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const val = await store.getOurIdentity();
|
|
102
|
-
if (val) cache.set('meta:identityKeyPair', val);
|
|
103
|
-
return val;
|
|
104
|
-
});
|
|
105
|
-
},
|
|
191
|
+
getOurIdentity() { return identity(() => store.getOurIdentity()); },
|
|
192
|
+
getIdentityKeyPair() { return identity(() => store.getIdentityKeyPair()); },
|
|
193
|
+
getLocalRegistrationId,
|
|
194
|
+
getOurRegistrationId() { return getLocalRegistrationId(); },
|
|
106
195
|
|
|
107
|
-
|
|
108
|
-
return mutex.runExclusive(async () => {
|
|
109
|
-
const hit = cache.get('meta:identityKeyPair');
|
|
110
|
-
if (hit !== undefined) return hit;
|
|
111
|
-
const val = await store.getIdentityKeyPair();
|
|
112
|
-
if (val) cache.set('meta:identityKeyPair', val);
|
|
113
|
-
return val;
|
|
114
|
-
});
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
async getLocalRegistrationId() {
|
|
118
|
-
return mutex.runExclusive(async () => {
|
|
119
|
-
const hit = cache.get('meta:registrationId');
|
|
120
|
-
if (hit !== undefined) return hit;
|
|
121
|
-
const val = await store.getLocalRegistrationId();
|
|
122
|
-
cache.set('meta:registrationId', val);
|
|
123
|
-
return val;
|
|
124
|
-
});
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
async getOurRegistrationId() {
|
|
128
|
-
return this.getLocalRegistrationId();
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
/** isTrustedIdentity — never cached; always delegates to underlying store. */
|
|
196
|
+
/** Never cached; the store decides trust from its own current state. */
|
|
132
197
|
async isTrustedIdentity(identifier, identityKey) {
|
|
133
198
|
return store.isTrustedIdentity(identifier, identityKey);
|
|
134
199
|
},
|
|
135
200
|
|
|
136
|
-
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
return store.saveIdentity(identifier, identityKey);
|
|
140
|
-
});
|
|
201
|
+
saveIdentity(identifier, identityKey) {
|
|
202
|
+
return cachedDelete('identity', identifier,
|
|
203
|
+
() => store.saveIdentity(identifier, identityKey));
|
|
141
204
|
},
|
|
142
205
|
|
|
143
|
-
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
const hit = cache.get(k);
|
|
147
|
-
if (hit !== undefined) {
|
|
148
|
-
log('identity cache hit', { identifier });
|
|
149
|
-
return hit;
|
|
150
|
-
}
|
|
151
|
-
const val = await store.loadIdentityKey(identifier);
|
|
152
|
-
cache.set(k, val !== null && val !== undefined ? val : null);
|
|
153
|
-
return val;
|
|
154
|
-
});
|
|
206
|
+
loadIdentityKey(identifier) {
|
|
207
|
+
return cachedRead('identity', identifier,
|
|
208
|
+
() => store.loadIdentityKey(identifier));
|
|
155
209
|
},
|
|
156
210
|
|
|
157
|
-
// ── Sessions (hot path — every message send
|
|
211
|
+
// ── Sessions (hot path — every message send and receive touches these) ────
|
|
158
212
|
|
|
159
|
-
|
|
160
|
-
return
|
|
161
|
-
|
|
162
|
-
const hit = cache.get(k);
|
|
163
|
-
if (hit !== undefined) {
|
|
164
|
-
log('session cache hit', { encodedAddress });
|
|
165
|
-
return hit;
|
|
166
|
-
}
|
|
167
|
-
const val = await store.loadSession(encodedAddress);
|
|
168
|
-
if (val) cache.set(k, val);
|
|
169
|
-
return val;
|
|
170
|
-
});
|
|
213
|
+
loadSession(encodedAddress) {
|
|
214
|
+
return cachedRead('session', encodedAddress,
|
|
215
|
+
() => store.loadSession(encodedAddress));
|
|
171
216
|
},
|
|
172
217
|
|
|
173
|
-
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
return store.storeSession(encodedAddress, record);
|
|
177
|
-
});
|
|
218
|
+
storeSession(encodedAddress, record) {
|
|
219
|
+
return cachedWrite('session', encodedAddress, record,
|
|
220
|
+
() => store.storeSession(encodedAddress, record));
|
|
178
221
|
},
|
|
179
222
|
|
|
180
|
-
|
|
181
|
-
return
|
|
182
|
-
|
|
183
|
-
return store.deleteSession(encodedAddress);
|
|
184
|
-
});
|
|
223
|
+
deleteSession(encodedAddress) {
|
|
224
|
+
return cachedDelete('session', encodedAddress,
|
|
225
|
+
() => store.deleteSession(encodedAddress));
|
|
185
226
|
},
|
|
186
227
|
|
|
187
228
|
// ── Pre-keys ──────────────────────────────────────────────────────────────
|
|
188
229
|
|
|
189
|
-
|
|
190
|
-
return
|
|
191
|
-
const k = cKey('prekey', keyId);
|
|
192
|
-
const hit = cache.get(k);
|
|
193
|
-
if (hit !== undefined) {
|
|
194
|
-
log('prekey cache hit', { keyId });
|
|
195
|
-
return hit;
|
|
196
|
-
}
|
|
197
|
-
const val = await store.loadPreKey(keyId);
|
|
198
|
-
if (val) cache.set(k, val);
|
|
199
|
-
return val;
|
|
200
|
-
});
|
|
230
|
+
loadPreKey(keyId) {
|
|
231
|
+
return cachedRead('prekey', keyId, () => store.loadPreKey(keyId));
|
|
201
232
|
},
|
|
202
233
|
|
|
203
|
-
|
|
204
|
-
return
|
|
205
|
-
|
|
206
|
-
return store.storePreKey(keyId, keyPair);
|
|
207
|
-
});
|
|
234
|
+
storePreKey(keyId, keyPair) {
|
|
235
|
+
return cachedWrite('prekey', keyId, keyPair,
|
|
236
|
+
() => store.storePreKey(keyId, keyPair));
|
|
208
237
|
},
|
|
209
238
|
|
|
210
|
-
|
|
211
|
-
return
|
|
212
|
-
cache.del(cKey('prekey', keyId));
|
|
213
|
-
return store.removePreKey(keyId);
|
|
214
|
-
});
|
|
239
|
+
removePreKey(keyId) {
|
|
240
|
+
return cachedDelete('prekey', keyId, () => store.removePreKey(keyId));
|
|
215
241
|
},
|
|
216
242
|
|
|
217
243
|
// ── Signed pre-keys ───────────────────────────────────────────────────────
|
|
218
244
|
|
|
219
|
-
|
|
220
|
-
return
|
|
221
|
-
const k = cKey('signedprekey', keyId);
|
|
222
|
-
const hit = cache.get(k);
|
|
223
|
-
if (hit !== undefined) {
|
|
224
|
-
log('signed-prekey cache hit', { keyId });
|
|
225
|
-
return hit;
|
|
226
|
-
}
|
|
227
|
-
const val = await store.loadSignedPreKey(keyId);
|
|
228
|
-
if (val) cache.set(k, val);
|
|
229
|
-
return val;
|
|
230
|
-
});
|
|
245
|
+
loadSignedPreKey(keyId) {
|
|
246
|
+
return cachedRead('signedprekey', keyId, () => store.loadSignedPreKey(keyId));
|
|
231
247
|
},
|
|
232
248
|
|
|
233
|
-
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
return store.storeSignedPreKey(keyId, keyPair);
|
|
237
|
-
});
|
|
249
|
+
storeSignedPreKey(keyId, keyPair) {
|
|
250
|
+
return cachedWrite('signedprekey', keyId, keyPair,
|
|
251
|
+
() => store.storeSignedPreKey(keyId, keyPair));
|
|
238
252
|
},
|
|
239
253
|
|
|
240
|
-
|
|
241
|
-
return
|
|
242
|
-
cache.del(cKey('signedprekey', keyId));
|
|
243
|
-
return store.removeSignedPreKey(keyId);
|
|
244
|
-
});
|
|
254
|
+
removeSignedPreKey(keyId) {
|
|
255
|
+
return cachedDelete('signedprekey', keyId, () => store.removeSignedPreKey(keyId));
|
|
245
256
|
},
|
|
246
257
|
|
|
247
258
|
// ── Transaction forwarding ────────────────────────────────────────────────
|
|
248
|
-
//
|
|
249
|
-
//
|
|
259
|
+
// Present so the cache can sit on top of addTransactionCapability as well as
|
|
260
|
+
// underneath it, even though underneath is the better place for it.
|
|
250
261
|
|
|
251
|
-
/**
|
|
262
|
+
/** True when the current async context is inside a transaction. */
|
|
252
263
|
isInTransaction() {
|
|
253
264
|
return typeof store.isInTransaction === 'function'
|
|
254
265
|
? store.isInTransaction()
|
|
@@ -256,9 +267,12 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
256
267
|
},
|
|
257
268
|
|
|
258
269
|
/**
|
|
259
|
-
* Delegate to the underlying store's transaction
|
|
260
|
-
*
|
|
261
|
-
*
|
|
270
|
+
* Delegate to the underlying store's transaction, dropping the cache if it
|
|
271
|
+
* does not go through.
|
|
272
|
+
*
|
|
273
|
+
* Writes reach the cache as they are made, but a transaction that throws
|
|
274
|
+
* never commits them — so without this the cache would keep serving values
|
|
275
|
+
* the store never took, and go on doing it until the entries expired.
|
|
262
276
|
*/
|
|
263
277
|
transaction(work, key) {
|
|
264
278
|
if (typeof store.transaction !== 'function') {
|
|
@@ -267,16 +281,27 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
267
281
|
'with addTransactionCapability()'
|
|
268
282
|
);
|
|
269
283
|
}
|
|
270
|
-
return
|
|
284
|
+
return (async () => {
|
|
285
|
+
try {
|
|
286
|
+
return await store.transaction(work, key);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
cache.flushAll();
|
|
289
|
+
logger && logger.trace('transaction failed — cache flushed');
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
})();
|
|
271
293
|
},
|
|
272
294
|
|
|
273
295
|
// ── Cache control ─────────────────────────────────────────────────────────
|
|
274
296
|
|
|
275
|
-
/**
|
|
297
|
+
/** Drop every cached entry, here and in anything wrapped below. */
|
|
276
298
|
async flushCache() {
|
|
277
299
|
cache.flushAll();
|
|
300
|
+
if (typeof store.flushCache === 'function') await store.flushCache();
|
|
278
301
|
}
|
|
279
302
|
};
|
|
303
|
+
|
|
304
|
+
return _fillPassthrough(wrapper, store);
|
|
280
305
|
}
|
|
281
306
|
|
|
282
307
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -284,16 +309,13 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
284
309
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
285
310
|
|
|
286
311
|
/**
|
|
287
|
-
* Adds DB-like transaction capability to a
|
|
312
|
+
* Adds DB-like transaction capability to a SignalStore.
|
|
288
313
|
*
|
|
289
|
-
* Within a transaction all writes are buffered in an in-memory context
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* Per-type Mutex objects serialise concurrent reads against the underlying
|
|
296
|
-
* store when multiple transactions are in flight simultaneously.
|
|
314
|
+
* Within a transaction all writes are buffered in an in-memory context (scoped
|
|
315
|
+
* with AsyncLocalStorage). Reads check the buffer first so they see their own
|
|
316
|
+
* writes. On commit the buffer is flushed to the underlying store in one shot,
|
|
317
|
+
* with retries. Nested transactions reuse the enclosing context, so there is no
|
|
318
|
+
* double commit.
|
|
297
319
|
*
|
|
298
320
|
* @param {SignalStore} state - underlying store (may already be cached)
|
|
299
321
|
* @param {object} logger - optional logger with .trace() / .warn() / .error()
|
|
@@ -302,33 +324,47 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
302
324
|
*/
|
|
303
325
|
function addTransactionCapability(state, logger, opts) {
|
|
304
326
|
opts = opts || {};
|
|
305
|
-
const maxRetries = opts.maxCommitRetries
|
|
327
|
+
const maxRetries = opts.maxCommitRetries ?? MAX_COMMIT_RETRIES;
|
|
306
328
|
const retryDelay = opts.delayBetweenTriesMs ?? RETRY_DELAY_MS;
|
|
307
329
|
|
|
308
|
-
const txStorage
|
|
309
|
-
|
|
310
|
-
|
|
330
|
+
const txStorage = new AsyncLocalStorage();
|
|
331
|
+
|
|
332
|
+
// Two separate sets of locks, and they must stay separate.
|
|
333
|
+
//
|
|
334
|
+
// A transaction holds its lock for as long as the work runs, and reads inside
|
|
335
|
+
// that work take a lock of their own. Sharing one map means transaction('…',
|
|
336
|
+
// 'session') takes the session lock and then waits for itself the first time
|
|
337
|
+
// it loads a session — a deadlock with no error and no timeout, reachable
|
|
338
|
+
// from nothing worse than an unlucky choice of key.
|
|
339
|
+
const readMutexes = new Map(); // data type → Mutex, held only for one read
|
|
340
|
+
const txMutexes = new Map(); // transaction key → Mutex, held for the work
|
|
341
|
+
const txMutexRefs = new Map(); // transaction key → refcount
|
|
342
|
+
|
|
343
|
+
function getReadMutex(type) {
|
|
344
|
+
if (!readMutexes.has(type)) readMutexes.set(type, new Mutex());
|
|
345
|
+
return readMutexes.get(type);
|
|
346
|
+
}
|
|
311
347
|
|
|
312
|
-
function
|
|
313
|
-
if (!
|
|
314
|
-
|
|
315
|
-
|
|
348
|
+
function getTxMutex(key) {
|
|
349
|
+
if (!txMutexes.has(key)) {
|
|
350
|
+
txMutexes.set(key, new Mutex());
|
|
351
|
+
txMutexRefs.set(key, 0);
|
|
316
352
|
}
|
|
317
|
-
return
|
|
353
|
+
return txMutexes.get(key);
|
|
318
354
|
}
|
|
319
355
|
|
|
320
|
-
function acquireRef(
|
|
321
|
-
|
|
356
|
+
function acquireRef(key) {
|
|
357
|
+
txMutexRefs.set(key, (txMutexRefs.get(key) ?? 0) + 1);
|
|
322
358
|
}
|
|
323
359
|
|
|
324
|
-
function releaseRef(
|
|
325
|
-
const count = (
|
|
326
|
-
|
|
360
|
+
function releaseRef(key) {
|
|
361
|
+
const count = (txMutexRefs.get(key) ?? 1) - 1;
|
|
362
|
+
txMutexRefs.set(key, count);
|
|
327
363
|
if (count <= 0) {
|
|
328
|
-
const m =
|
|
364
|
+
const m = txMutexes.get(key);
|
|
329
365
|
if (m && !m.isLocked()) {
|
|
330
|
-
|
|
331
|
-
|
|
366
|
+
txMutexes.delete(key);
|
|
367
|
+
txMutexRefs.delete(key);
|
|
332
368
|
}
|
|
333
369
|
}
|
|
334
370
|
}
|
|
@@ -340,7 +376,7 @@ function addTransactionCapability(state, logger, opts) {
|
|
|
340
376
|
async function commitWithRetry(ctx) {
|
|
341
377
|
const { sessions, preKeys, signedPreKeys, identities } = ctx.mutations;
|
|
342
378
|
const total = (
|
|
343
|
-
Object.keys(sessions).length
|
|
379
|
+
Object.keys(sessions).length +
|
|
344
380
|
Object.keys(preKeys).length +
|
|
345
381
|
Object.keys(signedPreKeys).length +
|
|
346
382
|
Object.keys(identities).length
|
|
@@ -357,33 +393,22 @@ function addTransactionCapability(state, logger, opts) {
|
|
|
357
393
|
const proms = [];
|
|
358
394
|
|
|
359
395
|
for (const [addr, rec] of Object.entries(sessions)) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
} else {
|
|
363
|
-
proms.push(state.storeSession(addr, rec));
|
|
364
|
-
}
|
|
396
|
+
proms.push(rec === null ? state.deleteSession(addr)
|
|
397
|
+
: state.storeSession(addr, rec));
|
|
365
398
|
}
|
|
366
399
|
|
|
367
400
|
for (const [keyId, kp] of Object.entries(preKeys)) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
} else {
|
|
371
|
-
proms.push(state.storePreKey(Number(keyId), kp));
|
|
372
|
-
}
|
|
401
|
+
proms.push(kp === null ? state.removePreKey(Number(keyId))
|
|
402
|
+
: state.storePreKey(Number(keyId), kp));
|
|
373
403
|
}
|
|
374
404
|
|
|
375
405
|
for (const [keyId, kp] of Object.entries(signedPreKeys)) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
} else {
|
|
379
|
-
proms.push(state.storeSignedPreKey(Number(keyId), kp));
|
|
380
|
-
}
|
|
406
|
+
proms.push(kp === null ? state.removeSignedPreKey(Number(keyId))
|
|
407
|
+
: state.storeSignedPreKey(Number(keyId), kp));
|
|
381
408
|
}
|
|
382
409
|
|
|
383
410
|
for (const [identifier, key] of Object.entries(identities)) {
|
|
384
|
-
if (key !== null)
|
|
385
|
-
proms.push(state.saveIdentity(identifier, key));
|
|
386
|
-
}
|
|
411
|
+
if (key !== null) proms.push(state.saveIdentity(identifier, key));
|
|
387
412
|
}
|
|
388
413
|
|
|
389
414
|
await Promise.all(proms);
|
|
@@ -398,135 +423,101 @@ function addTransactionCapability(state, logger, opts) {
|
|
|
398
423
|
}
|
|
399
424
|
}
|
|
400
425
|
|
|
401
|
-
//
|
|
426
|
+
// The empty read buffer and pending writes for a new transaction.
|
|
402
427
|
function makeCtx() {
|
|
403
428
|
return {
|
|
404
|
-
// Read buffer: what
|
|
405
|
-
sessions:
|
|
406
|
-
preKeys:
|
|
407
|
-
signedPreKeys:{},
|
|
408
|
-
identities:
|
|
409
|
-
// Pending writes:
|
|
429
|
+
// Read buffer: what has already been loaded from the underlying store
|
|
430
|
+
sessions: {},
|
|
431
|
+
preKeys: {},
|
|
432
|
+
signedPreKeys: {},
|
|
433
|
+
identities: {},
|
|
434
|
+
// Pending writes: flushed to the store when the transaction ends
|
|
410
435
|
mutations: {
|
|
411
|
-
sessions:
|
|
412
|
-
preKeys:
|
|
413
|
-
signedPreKeys:{},
|
|
414
|
-
identities:
|
|
436
|
+
sessions: {},
|
|
437
|
+
preKeys: {},
|
|
438
|
+
signedPreKeys: {},
|
|
439
|
+
identities: {}
|
|
415
440
|
},
|
|
416
441
|
dbQueries: 0
|
|
417
442
|
};
|
|
418
443
|
}
|
|
419
444
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
getOurRegistrationId() { return state.getOurRegistrationId(); },
|
|
433
|
-
isTrustedIdentity(a, b) { return state.isTrustedIdentity(a, b); },
|
|
445
|
+
// Read through the transaction buffer, or straight through when there is no
|
|
446
|
+
// transaction in this async context.
|
|
447
|
+
async function bufferedRead(bucket, type, id, load) {
|
|
448
|
+
const ctx = txStorage.getStore();
|
|
449
|
+
if (!ctx) return load();
|
|
450
|
+
const k = String(id);
|
|
451
|
+
if (k in ctx[bucket]) return ctx[bucket][k];
|
|
452
|
+
const val = await getReadMutex(type).runExclusive(load);
|
|
453
|
+
ctx[bucket][k] = val;
|
|
454
|
+
ctx.dbQueries++;
|
|
455
|
+
return val;
|
|
456
|
+
}
|
|
434
457
|
|
|
458
|
+
async function bufferedWrite(bucket, id, value, write) {
|
|
459
|
+
const ctx = txStorage.getStore();
|
|
460
|
+
if (!ctx) return write();
|
|
461
|
+
const k = String(id);
|
|
462
|
+
ctx[bucket][k] = value;
|
|
463
|
+
ctx.mutations[bucket][k] = value;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const wrapper = {
|
|
435
467
|
// ── Session ───────────────────────────────────────────────────────────────
|
|
436
468
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (encodedAddress in ctx.sessions) return ctx.sessions[encodedAddress];
|
|
441
|
-
// Not in buffer: fetch under mutex, cache in buffer
|
|
442
|
-
const val = await getMutex('session').runExclusive(() => state.loadSession(encodedAddress));
|
|
443
|
-
ctx.sessions[encodedAddress] = val;
|
|
444
|
-
ctx.dbQueries++;
|
|
445
|
-
return val;
|
|
469
|
+
loadSession(encodedAddress) {
|
|
470
|
+
return bufferedRead('sessions', 'session', encodedAddress,
|
|
471
|
+
() => state.loadSession(encodedAddress));
|
|
446
472
|
},
|
|
447
473
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
ctx.sessions[encodedAddress] = record;
|
|
452
|
-
ctx.mutations.sessions[encodedAddress] = record;
|
|
474
|
+
storeSession(encodedAddress, record) {
|
|
475
|
+
return bufferedWrite('sessions', encodedAddress, record,
|
|
476
|
+
() => state.storeSession(encodedAddress, record));
|
|
453
477
|
},
|
|
454
478
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
ctx.sessions[encodedAddress] = null;
|
|
459
|
-
ctx.mutations.sessions[encodedAddress] = null;
|
|
479
|
+
deleteSession(encodedAddress) {
|
|
480
|
+
return bufferedWrite('sessions', encodedAddress, null,
|
|
481
|
+
() => state.deleteSession(encodedAddress));
|
|
460
482
|
},
|
|
461
483
|
|
|
462
484
|
// ── Pre-keys ──────────────────────────────────────────────────────────────
|
|
463
485
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const k = String(keyId);
|
|
467
|
-
if (!ctx) return state.loadPreKey(keyId);
|
|
468
|
-
if (k in ctx.preKeys) return ctx.preKeys[k];
|
|
469
|
-
const val = await getMutex('preKey').runExclusive(() => state.loadPreKey(keyId));
|
|
470
|
-
ctx.preKeys[k] = val;
|
|
471
|
-
ctx.dbQueries++;
|
|
472
|
-
return val;
|
|
486
|
+
loadPreKey(keyId) {
|
|
487
|
+
return bufferedRead('preKeys', 'preKey', keyId, () => state.loadPreKey(keyId));
|
|
473
488
|
},
|
|
474
489
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (!ctx) return state.storePreKey(keyId, keyPair);
|
|
479
|
-
ctx.preKeys[k] = keyPair;
|
|
480
|
-
ctx.mutations.preKeys[k] = keyPair;
|
|
490
|
+
storePreKey(keyId, keyPair) {
|
|
491
|
+
return bufferedWrite('preKeys', keyId, keyPair,
|
|
492
|
+
() => state.storePreKey(keyId, keyPair));
|
|
481
493
|
},
|
|
482
494
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const k = String(keyId);
|
|
486
|
-
if (!ctx) return state.removePreKey(keyId);
|
|
487
|
-
ctx.preKeys[k] = null;
|
|
488
|
-
ctx.mutations.preKeys[k] = null;
|
|
495
|
+
removePreKey(keyId) {
|
|
496
|
+
return bufferedWrite('preKeys', keyId, null, () => state.removePreKey(keyId));
|
|
489
497
|
},
|
|
490
498
|
|
|
491
499
|
// ── Signed pre-keys ───────────────────────────────────────────────────────
|
|
492
500
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (!ctx) return state.loadSignedPreKey(keyId);
|
|
497
|
-
if (k in ctx.signedPreKeys) return ctx.signedPreKeys[k];
|
|
498
|
-
const val = await getMutex('signedPreKey').runExclusive(() => state.loadSignedPreKey(keyId));
|
|
499
|
-
ctx.signedPreKeys[k] = val;
|
|
500
|
-
ctx.dbQueries++;
|
|
501
|
-
return val;
|
|
501
|
+
loadSignedPreKey(keyId) {
|
|
502
|
+
return bufferedRead('signedPreKeys', 'signedPreKey', keyId,
|
|
503
|
+
() => state.loadSignedPreKey(keyId));
|
|
502
504
|
},
|
|
503
505
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (!ctx) return state.storeSignedPreKey(keyId, keyPair);
|
|
508
|
-
ctx.signedPreKeys[k] = keyPair;
|
|
509
|
-
ctx.mutations.signedPreKeys[k] = keyPair;
|
|
506
|
+
storeSignedPreKey(keyId, keyPair) {
|
|
507
|
+
return bufferedWrite('signedPreKeys', keyId, keyPair,
|
|
508
|
+
() => state.storeSignedPreKey(keyId, keyPair));
|
|
510
509
|
},
|
|
511
510
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (!ctx) return state.removeSignedPreKey(keyId);
|
|
516
|
-
ctx.signedPreKeys[k] = null;
|
|
517
|
-
ctx.mutations.signedPreKeys[k] = null;
|
|
511
|
+
removeSignedPreKey(keyId) {
|
|
512
|
+
return bufferedWrite('signedPreKeys', keyId, null,
|
|
513
|
+
() => state.removeSignedPreKey(keyId));
|
|
518
514
|
},
|
|
519
515
|
|
|
520
516
|
// ── Identity keys ─────────────────────────────────────────────────────────
|
|
521
517
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (identifier in ctx.identities) return ctx.identities[identifier];
|
|
526
|
-
const val = await getMutex('identity').runExclusive(() => state.loadIdentityKey(identifier));
|
|
527
|
-
ctx.identities[identifier] = val;
|
|
528
|
-
ctx.dbQueries++;
|
|
529
|
-
return val;
|
|
518
|
+
loadIdentityKey(identifier) {
|
|
519
|
+
return bufferedRead('identities', 'identity', identifier,
|
|
520
|
+
() => state.loadIdentityKey(identifier));
|
|
530
521
|
},
|
|
531
522
|
|
|
532
523
|
async saveIdentity(identifier, identityKey) {
|
|
@@ -534,32 +525,39 @@ function addTransactionCapability(state, logger, opts) {
|
|
|
534
525
|
if (!ctx) return state.saveIdentity(identifier, identityKey);
|
|
535
526
|
ctx.identities[identifier] = identityKey;
|
|
536
527
|
ctx.mutations.identities[identifier] = identityKey;
|
|
537
|
-
//
|
|
538
|
-
//
|
|
528
|
+
// The store answers whether the key changed, and that answer only exists
|
|
529
|
+
// once the write lands. Inside a transaction it has not, so say so.
|
|
539
530
|
return false;
|
|
540
531
|
},
|
|
541
532
|
|
|
542
533
|
// ── Transaction control ───────────────────────────────────────────────────
|
|
543
534
|
|
|
544
|
-
/**
|
|
535
|
+
/** True when the current async context is inside a transaction. */
|
|
545
536
|
isInTransaction,
|
|
546
537
|
|
|
538
|
+
/** Forwarded so a cache wrapped underneath can still be flushed. */
|
|
539
|
+
async flushCache() {
|
|
540
|
+
if (typeof state.flushCache === 'function') return state.flushCache();
|
|
541
|
+
},
|
|
542
|
+
|
|
547
543
|
/**
|
|
548
544
|
* Run `work` inside a transaction scoped to `key`.
|
|
545
|
+
*
|
|
549
546
|
* @param {function} work async function to execute
|
|
550
|
-
* @param {string} key
|
|
547
|
+
* @param {string} key scope; separate keys run concurrently
|
|
551
548
|
*/
|
|
552
549
|
transaction: async (work, key) => {
|
|
553
550
|
key = key || 'default';
|
|
554
551
|
const existing = txStorage.getStore();
|
|
555
552
|
|
|
556
|
-
// Nested transaction — reuse the enclosing context
|
|
553
|
+
// Nested transaction — reuse the enclosing context rather than opening a
|
|
554
|
+
// second one that would commit separately.
|
|
557
555
|
if (existing) {
|
|
558
556
|
logger && logger.trace('reusing existing transaction context');
|
|
559
557
|
return work();
|
|
560
558
|
}
|
|
561
559
|
|
|
562
|
-
const mutex =
|
|
560
|
+
const mutex = getTxMutex(key);
|
|
563
561
|
acquireRef(key);
|
|
564
562
|
|
|
565
563
|
try {
|
|
@@ -581,6 +579,8 @@ function addTransactionCapability(state, logger, opts) {
|
|
|
581
579
|
}
|
|
582
580
|
}
|
|
583
581
|
};
|
|
582
|
+
|
|
583
|
+
return _fillPassthrough(wrapper, state);
|
|
584
584
|
}
|
|
585
585
|
|
|
586
586
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -588,18 +588,33 @@ function addTransactionCapability(state, logger, opts) {
|
|
|
588
588
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
589
589
|
|
|
590
590
|
/**
|
|
591
|
-
* Returns the authenticated
|
|
592
|
-
*
|
|
591
|
+
* Returns the authenticated account's JID, or throws a descriptive error when
|
|
592
|
+
* the store is not authenticated yet.
|
|
593
|
+
*
|
|
594
|
+
* Use this anywhere the alternative is reaching for `store.phoneNumber` and
|
|
595
|
+
* hoping, so the failure arrives as a sentence rather than as a null reference
|
|
596
|
+
* several frames later.
|
|
597
|
+
*
|
|
598
|
+
* The JID the server assigned is preferred once there is one — on a companion
|
|
599
|
+
* it carries the device suffix, and rebuilding it from the phone number drops
|
|
600
|
+
* that silently. It is only trusted after registration, though: requesting a
|
|
601
|
+
* pairing code writes a placeholder `me` with no suffix before the exchange has
|
|
602
|
+
* happened, and returning that would defeat the point of the check.
|
|
593
603
|
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
604
|
+
* Works the same for both kinds of store. Only the wording of the error
|
|
605
|
+
* differs, because the way out of "not registered yet" is not the same.
|
|
596
606
|
*
|
|
597
|
-
* @param {object} store —
|
|
598
|
-
* @returns {string} — JID
|
|
599
|
-
* @throws {Error} —
|
|
607
|
+
* @param {object} store — store object (from createNewStore / createNewWebStore / loadStore)
|
|
608
|
+
* @returns {string} — JID, e.g. "40712345678:12@s.whatsapp.net"
|
|
609
|
+
* @throws {Error} — when the store is not initialised or not registered
|
|
600
610
|
*/
|
|
601
611
|
function assertMeId(store) {
|
|
602
|
-
if (!store
|
|
612
|
+
if (!store) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
'Cannot proceed: no store was given — call createNewStore() first'
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (!store.phoneNumber) {
|
|
603
618
|
throw new Error(
|
|
604
619
|
'Cannot proceed: store has no phoneNumber — call createNewStore() first'
|
|
605
620
|
);
|
|
@@ -607,9 +622,15 @@ function assertMeId(store) {
|
|
|
607
622
|
if (!store.registered) {
|
|
608
623
|
throw new Error(
|
|
609
624
|
'Cannot proceed: device is not registered yet — ' +
|
|
610
|
-
|
|
625
|
+
(store.mode === 'web'
|
|
626
|
+
? 'link this device first, with a pairing code or by scanning the QR'
|
|
627
|
+
: 'complete SMS verification with verifyCode() before sending messages')
|
|
611
628
|
);
|
|
612
629
|
}
|
|
630
|
+
|
|
631
|
+
const assigned = store.me && store.me.id;
|
|
632
|
+
if (assigned) return String(assigned);
|
|
633
|
+
|
|
613
634
|
const phone = String(store.phoneNumber).replace(/^\+/, '').replace(/\D/g, '');
|
|
614
635
|
return `${phone}@s.whatsapp.net`;
|
|
615
636
|
}
|
|
@@ -619,27 +640,32 @@ function assertMeId(store) {
|
|
|
619
640
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
620
641
|
|
|
621
642
|
/**
|
|
622
|
-
* Creates a fresh set of
|
|
623
|
-
*
|
|
643
|
+
* Creates a fresh set of auth credentials (key pairs, registration ID, device
|
|
644
|
+
* identifiers) for a given phone number.
|
|
624
645
|
*
|
|
625
|
-
* Returns
|
|
626
|
-
* { id, public, private, signature }
|
|
646
|
+
* Returns the library's store format: Buffer key pairs, and signedPreKey as
|
|
647
|
+
* { id, public, private, signature }.
|
|
627
648
|
*
|
|
628
649
|
* A few extra fields (nextPreKeyId, firstUnuploadedPreKeyId, accountSyncCounter,
|
|
629
|
-
* accountSettings, advSecretKey
|
|
650
|
+
* accountSettings, advSecretKey and so on) are appended for account sync and for
|
|
630
651
|
* application code that expects them.
|
|
631
652
|
*
|
|
632
|
-
*
|
|
653
|
+
* This builds a store for the SMS flow. A companion device is registered by
|
|
654
|
+
* pairing rather than by SMS and needs the extra fields that go with it, so it
|
|
655
|
+
* has its own constructor — createNewWebStore — which starts from the same base
|
|
656
|
+
* as this one.
|
|
657
|
+
*
|
|
658
|
+
* @param {string|number} phoneNumber — full international number, e.g. "40712345678"
|
|
633
659
|
* @param {object} options — optional overrides:
|
|
634
|
-
* registered {boolean} — mark credentials as already registered (default false)
|
|
635
|
-
* name {string} —
|
|
636
|
-
* @returns {object} fresh
|
|
660
|
+
* registered {boolean} — mark the credentials as already registered (default false)
|
|
661
|
+
* name {string} — display name (default 'User')
|
|
662
|
+
* @returns {object} fresh store / credential object
|
|
637
663
|
*/
|
|
638
664
|
function initAuthCreds(phoneNumber, options) {
|
|
639
665
|
if (!phoneNumber) throw new Error('initAuthCreds: phoneNumber is required');
|
|
640
666
|
options = options || {};
|
|
641
667
|
|
|
642
|
-
//
|
|
668
|
+
// The library's own key generation — curve25519, with the right prefixes.
|
|
643
669
|
const store = createNewStore(phoneNumber);
|
|
644
670
|
|
|
645
671
|
// ── Extra credential fields ──────────────────────────────────────────────────
|
|
@@ -648,7 +674,7 @@ function initAuthCreds(phoneNumber, options) {
|
|
|
648
674
|
/** Next pre-key ID to generate (monotonically increasing). */
|
|
649
675
|
store.nextPreKeyId = 1;
|
|
650
676
|
|
|
651
|
-
/** First pre-key ID not yet uploaded to the
|
|
677
|
+
/** First pre-key ID not yet uploaded to the server. */
|
|
652
678
|
store.firstUnuploadedPreKeyId = 1;
|
|
653
679
|
|
|
654
680
|
/** Sync counter used in account-sync IQ stanzas. */
|
|
@@ -657,16 +683,16 @@ function initAuthCreds(phoneNumber, options) {
|
|
|
657
683
|
/** Account-level settings. */
|
|
658
684
|
store.accountSettings = { unarchiveChats: false };
|
|
659
685
|
|
|
660
|
-
/** Processed history messages —
|
|
686
|
+
/** Processed history messages — avoids handling the same one twice on reconnect. */
|
|
661
687
|
store.processedHistoryMessages = [];
|
|
662
688
|
|
|
663
689
|
/**
|
|
664
|
-
* 32-byte random secret used for
|
|
690
|
+
* 32-byte random secret used for multi-device poll encryption.
|
|
665
691
|
* Stored as a base64 string.
|
|
666
692
|
*/
|
|
667
693
|
store.advSecretKey = crypto.randomBytes(32).toString('base64');
|
|
668
694
|
|
|
669
|
-
// Fields
|
|
695
|
+
// Fields left undefined until the pairing / registration flow fills them in
|
|
670
696
|
store.pairingCode = undefined;
|
|
671
697
|
store.lastPropHash = undefined;
|
|
672
698
|
store.routingInfo = undefined;
|
|
@@ -687,5 +713,6 @@ module.exports = {
|
|
|
687
713
|
makeCacheableSignalKeyStore,
|
|
688
714
|
addTransactionCapability,
|
|
689
715
|
assertMeId,
|
|
690
|
-
initAuthCreds
|
|
716
|
+
initAuthCreds,
|
|
717
|
+
SIGNAL_STORE_TTL_SECONDS
|
|
691
718
|
};
|