zapo-js 1.8.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist/auth/credentials-flow.js +2 -3
- package/dist/client/WaClient.d.ts +4 -0
- package/dist/client/WaClient.js +5 -1
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +11 -4
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +11 -0
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +15 -5
- package/dist/client/messaging/ignore-key.d.ts +2 -1
- package/dist/client/messaging/ignore-key.js +24 -3
- package/dist/esm/auth/credentials-flow.js +3 -4
- package/dist/esm/client/WaClient.js +5 -1
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +14 -5
- package/dist/esm/client/messaging/ignore-key.js +25 -4
- package/dist/esm/message/encode/content.js +4 -1
- package/dist/esm/store/cache/identity.cache.js +30 -5
- package/dist/esm/store/cache/privacy-token.cache.js +30 -5
- package/dist/esm/store/cache/sender-key.cache.js +38 -7
- package/dist/esm/store/cache/session.cache.js +30 -4
- package/dist/esm/store/createStore.js +39 -4
- package/dist/esm/store/memory/identity.store.js +28 -4
- package/dist/esm/store/memory/privacy-token.store.js +24 -7
- package/dist/esm/store/memory/sender-key.store.js +36 -7
- package/dist/esm/store/memory/session.store.js +41 -7
- package/dist/esm/transport/node/WaMobileTcpSocket.js +69 -9
- package/dist/esm/transport/noise/WaMobileClientPayload.js +27 -7
- package/dist/esm/transport/proxy.js +83 -0
- package/dist/esm/util/collections.js +94 -0
- package/dist/esm/util/index.js +2 -2
- package/dist/message/encode/content.js +4 -1
- package/dist/message/primitives/media-retry.d.ts +11 -0
- package/dist/store/cache/identity.cache.d.ts +6 -4
- package/dist/store/cache/identity.cache.js +30 -5
- package/dist/store/cache/privacy-token.cache.d.ts +6 -4
- package/dist/store/cache/privacy-token.cache.js +30 -5
- package/dist/store/cache/sender-key.cache.d.ts +5 -3
- package/dist/store/cache/sender-key.cache.js +38 -7
- package/dist/store/cache/session.cache.d.ts +6 -3
- package/dist/store/cache/session.cache.js +30 -4
- package/dist/store/createStore.js +39 -4
- package/dist/store/memory/identity.store.d.ts +7 -1
- package/dist/store/memory/identity.store.js +28 -4
- package/dist/store/memory/privacy-token.store.d.ts +7 -1
- package/dist/store/memory/privacy-token.store.js +24 -7
- package/dist/store/memory/sender-key.store.d.ts +12 -2
- package/dist/store/memory/sender-key.store.js +36 -7
- package/dist/store/memory/session.store.d.ts +7 -1
- package/dist/store/memory/session.store.js +41 -7
- package/dist/store/types.d.ts +43 -4
- package/dist/transport/node/WaMobileTcpSocket.d.ts +3 -1
- package/dist/transport/node/WaMobileTcpSocket.js +68 -8
- package/dist/transport/noise/WaMobileClientPayload.d.ts +22 -0
- package/dist/transport/noise/WaMobileClientPayload.js +27 -7
- package/dist/transport/proxy.d.ts +24 -0
- package/dist/transport/proxy.js +85 -0
- package/dist/util/collections.d.ts +46 -0
- package/dist/util/collections.js +98 -0
- package/dist/util/index.d.ts +2 -2
- package/dist/util/index.js +3 -1
- package/package.json +1 -1
|
@@ -24,6 +24,100 @@ export function normalizeQueryLimit(limit, defaultLimit) {
|
|
|
24
24
|
}
|
|
25
25
|
return limit;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Sweep period shared by a set of idle TTLs: the smallest `ttlMs / 2`,
|
|
29
|
+
* clamped to [1 s, 60 s].
|
|
30
|
+
*/
|
|
31
|
+
export function resolveIdleSweepPeriodMs(ttlsMs) {
|
|
32
|
+
let periodMs = 60000;
|
|
33
|
+
for (let i = 0; i < ttlsMs.length; i += 1) {
|
|
34
|
+
periodMs = Math.min(periodMs, Math.max(1000, Math.floor(ttlsMs[i] / 2)));
|
|
35
|
+
}
|
|
36
|
+
return periodMs;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Coarse clock for idle expiry: one `unref`'d interval bumps `tick` and runs
|
|
40
|
+
* every registered sweep, and only while a sweep is registered. Wall-clock
|
|
41
|
+
* steps never move `tick`.
|
|
42
|
+
*/
|
|
43
|
+
export class IdleSweepClock {
|
|
44
|
+
constructor(periodMs) {
|
|
45
|
+
this.advance = () => {
|
|
46
|
+
this.currentTick += 1;
|
|
47
|
+
for (const sweep of this.sweeps)
|
|
48
|
+
sweep(this.currentTick);
|
|
49
|
+
};
|
|
50
|
+
this.periodMs = periodMs;
|
|
51
|
+
this.currentTick = 0;
|
|
52
|
+
this.sweeps = new Set();
|
|
53
|
+
this.timer = null;
|
|
54
|
+
}
|
|
55
|
+
get tick() {
|
|
56
|
+
return this.currentTick;
|
|
57
|
+
}
|
|
58
|
+
/** Runs `sweep` on every tick until the returned function is called. */
|
|
59
|
+
register(sweep) {
|
|
60
|
+
this.sweeps.add(sweep);
|
|
61
|
+
if (this.timer === null) {
|
|
62
|
+
this.timer = setInterval(this.advance, this.periodMs);
|
|
63
|
+
this.timer.unref?.();
|
|
64
|
+
}
|
|
65
|
+
return () => {
|
|
66
|
+
this.sweeps.delete(sweep);
|
|
67
|
+
if (this.sweeps.size > 0 || this.timer === null)
|
|
68
|
+
return;
|
|
69
|
+
clearInterval(this.timer);
|
|
70
|
+
this.timer = null;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Idle expiry and LRU order for the entries of `data`. A touch moves the key
|
|
76
|
+
* to the young end of the index and of `data`, at most once per tick, so the
|
|
77
|
+
* sweep stops at the first live key and `data` iterates least recently used
|
|
78
|
+
* first. A key expires after `ceil(ttlMs / periodMs) + 1` untouched ticks:
|
|
79
|
+
* idle for at least `ttlMs` and, timer lateness aside, under `ttlMs + 2 * periodMs`.
|
|
80
|
+
*/
|
|
81
|
+
export class IdleExpiryIndex {
|
|
82
|
+
constructor(data, expiry) {
|
|
83
|
+
this.sweep = (tick) => {
|
|
84
|
+
const cutoff = tick - this.idleTicks;
|
|
85
|
+
for (const [key, touchedAt] of this.touchedAt) {
|
|
86
|
+
if (touchedAt > cutoff)
|
|
87
|
+
return;
|
|
88
|
+
this.touchedAt.delete(key);
|
|
89
|
+
this.data.delete(key);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
this.data = data;
|
|
93
|
+
this.clock = expiry.clock;
|
|
94
|
+
this.idleTicks = Math.ceil(expiry.ttlMs / expiry.clock.periodMs) + 1;
|
|
95
|
+
this.touchedAt = new Map();
|
|
96
|
+
this.unregister = expiry.clock.register(this.sweep);
|
|
97
|
+
}
|
|
98
|
+
touch(key) {
|
|
99
|
+
const tick = this.clock.tick;
|
|
100
|
+
if (this.touchedAt.get(key) === tick)
|
|
101
|
+
return;
|
|
102
|
+
this.touchedAt.delete(key);
|
|
103
|
+
this.touchedAt.set(key, tick);
|
|
104
|
+
const value = this.data.get(key);
|
|
105
|
+
if (value === undefined)
|
|
106
|
+
return;
|
|
107
|
+
this.data.delete(key);
|
|
108
|
+
this.data.set(key, value);
|
|
109
|
+
}
|
|
110
|
+
delete(key) {
|
|
111
|
+
this.touchedAt.delete(key);
|
|
112
|
+
}
|
|
113
|
+
clear() {
|
|
114
|
+
this.touchedAt.clear();
|
|
115
|
+
}
|
|
116
|
+
/** Stops sweeping; the entries stay until the owner clears them. */
|
|
117
|
+
detach() {
|
|
118
|
+
this.unregister();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
27
121
|
export function setBoundedMapEntry(map, key, value, maxEntries, onEvict) {
|
|
28
122
|
map.delete(key);
|
|
29
123
|
map.set(key, value);
|
package/dist/esm/util/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { delay } from './async.js';
|
|
2
2
|
export { base64ToBytes, bytesToBase64, bytesToBase64UrlSafe, bytesToHex, concatBytes, decodeBase64Url, EMPTY_BYTES, hexToBytes, TEXT_DECODER, TEXT_ENCODER, toBytesView, uint8Equal, uint8TimingSafeEqual } from './bytes.js';
|
|
3
|
-
export { asBytes, asNumber, asOptionalBytes, asOptionalNumber, asOptionalString, asString, resolveOptionalPositive, resolvePositive, toBoolOrUndef } from './coercion.js';
|
|
4
|
-
export { normalizeQueryLimit } from './collections.js';
|
|
3
|
+
export { asBytes, asNumber, asOptionalBytes, asOptionalNumber, asOptionalString, asString, resolveOptionalPositive, resolvePositive, toBoolOrUndef, tryAsNumber } from './coercion.js';
|
|
4
|
+
export { normalizeQueryLimit, setBoundedMapEntry } from './collections.js';
|
|
5
5
|
export { toError, toSafeNumber } from './primitives.js';
|
|
6
6
|
export { isBunRuntime } from './runtime.js';
|
|
@@ -180,7 +180,8 @@ function unwrapMessage(message) {
|
|
|
180
180
|
msg.viewOnceMessageV2?.message ??
|
|
181
181
|
msg.documentWithCaptionMessage?.message ??
|
|
182
182
|
msg.groupStatusMessage?.message ??
|
|
183
|
-
msg.groupStatusMessageV2?.message
|
|
183
|
+
msg.groupStatusMessageV2?.message ??
|
|
184
|
+
msg.botForwardedMessage?.message;
|
|
184
185
|
if (!inner)
|
|
185
186
|
return msg;
|
|
186
187
|
msg = inner;
|
|
@@ -228,6 +229,8 @@ function resolveMessageTypeAttrFrom(msg) {
|
|
|
228
229
|
msg.pollResultSnapshotMessageV3 ||
|
|
229
230
|
msg.templateButtonReplyMessage ||
|
|
230
231
|
msg.messageHistoryNotice ||
|
|
232
|
+
msg.richResponseMessage ||
|
|
233
|
+
msg.botForwardedMessage ||
|
|
231
234
|
msg.secretEncryptedMessage?.secretEncType ===
|
|
232
235
|
_proto_1.proto.Message.SecretEncryptedMessage.SecretEncType.MESSAGE_EDIT ||
|
|
233
236
|
msg.secretEncryptedMessage?.secretEncType ===
|
|
@@ -2,6 +2,17 @@ import type { WaAuthCredentials } from '../../auth/types';
|
|
|
2
2
|
import type { Logger } from '../../infra/log/types';
|
|
3
3
|
import type { BinaryNode } from '../../transport/types';
|
|
4
4
|
export type WaMediaRetryResultType = 'success' | 'not_found' | 'decryption_error' | 'general_error';
|
|
5
|
+
/**
|
|
6
|
+
* Answer to a media reupload request.
|
|
7
|
+
*
|
|
8
|
+
* On `result: 'success'` the fresh `directPath` usually points at the same
|
|
9
|
+
* ciphertext the original message described, so its media key, hashes and
|
|
10
|
+
* length stay valid. Some primaries re-encrypt the file on every re-upload
|
|
11
|
+
* instead: the answer is still `success`, but the re-served blob no longer
|
|
12
|
+
* matches `fileEncSha256` and decryption fails with a MAC mismatch. Nothing in
|
|
13
|
+
* the round-trip carries key material for the new ciphertext, so treat that
|
|
14
|
+
* message as unrecoverable rather than retrying.
|
|
15
|
+
*/
|
|
5
16
|
export interface WaMediaRetryResult {
|
|
6
17
|
readonly messageId: string;
|
|
7
18
|
readonly result: WaMediaRetryResultType;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { WaIdentityStore } from '../contracts/identity.store';
|
|
2
2
|
import type { WithDestroyLifecycle } from '../types';
|
|
3
|
+
import { type IdleExpiry } from '../../util/collections';
|
|
3
4
|
/**
|
|
4
5
|
* Read-through / write-through in-process cache for a persistent remote
|
|
5
|
-
* identity backend. Reuses {@link WaIdentityMemoryStore} as the bounded
|
|
6
|
-
*
|
|
6
|
+
* identity backend. Reuses {@link WaIdentityMemoryStore} as the bounded L1.
|
|
7
|
+
* Remote identities are read alongside sessions on the send path (the
|
|
7
8
|
* identity-mismatch guard), so caching them complements
|
|
8
|
-
* {@link withSessionCache}
|
|
9
|
+
* {@link withSessionCache}, which also describes the eviction with and
|
|
10
|
+
* without `expiry`.
|
|
9
11
|
*
|
|
10
12
|
* The identity store has no per-key delete: identities are overwritten on
|
|
11
13
|
* re-establishment, so a peer's key change propagates through the
|
|
12
14
|
* write-through `setRemoteIdentity`. See {@link withSessionCache} for the
|
|
13
15
|
* shared coherence model and the single-writer-per-session assumption.
|
|
14
16
|
*/
|
|
15
|
-
export declare function withIdentityCache(backend: WaIdentityStore, maxEntries?: number): WithDestroyLifecycle<WaIdentityStore>;
|
|
17
|
+
export declare function withIdentityCache(backend: WaIdentityStore, maxEntries?: number, expiry?: IdleExpiry): WithDestroyLifecycle<WaIdentityStore>;
|
|
@@ -2,20 +2,43 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.withIdentityCache = withIdentityCache;
|
|
4
4
|
const identity_store_1 = require("../memory/identity.store");
|
|
5
|
+
const collections_1 = require("../../util/collections");
|
|
6
|
+
/** L1 with idle expiry; hits keep it in access order, so the cap evicts the LRU entry. */
|
|
7
|
+
class IdleIdentityMemoryStore extends identity_store_1.WaIdentityMemoryStore {
|
|
8
|
+
constructor(maxRemoteIdentities, expiry) {
|
|
9
|
+
super({ maxRemoteIdentities });
|
|
10
|
+
this.idle = new collections_1.IdleExpiryIndex(this.remoteIdentities, expiry);
|
|
11
|
+
}
|
|
12
|
+
detach() {
|
|
13
|
+
this.idle.detach();
|
|
14
|
+
}
|
|
15
|
+
onEntryAccess(key) {
|
|
16
|
+
this.idle.touch(key);
|
|
17
|
+
}
|
|
18
|
+
onEntryRemove(key) {
|
|
19
|
+
this.idle.delete(key);
|
|
20
|
+
}
|
|
21
|
+
onEntriesClear() {
|
|
22
|
+
this.idle.clear();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* Read-through / write-through in-process cache for a persistent remote
|
|
7
|
-
* identity backend. Reuses {@link WaIdentityMemoryStore} as the bounded
|
|
8
|
-
*
|
|
27
|
+
* identity backend. Reuses {@link WaIdentityMemoryStore} as the bounded L1.
|
|
28
|
+
* Remote identities are read alongside sessions on the send path (the
|
|
9
29
|
* identity-mismatch guard), so caching them complements
|
|
10
|
-
* {@link withSessionCache}
|
|
30
|
+
* {@link withSessionCache}, which also describes the eviction with and
|
|
31
|
+
* without `expiry`.
|
|
11
32
|
*
|
|
12
33
|
* The identity store has no per-key delete: identities are overwritten on
|
|
13
34
|
* re-establishment, so a peer's key change propagates through the
|
|
14
35
|
* write-through `setRemoteIdentity`. See {@link withSessionCache} for the
|
|
15
36
|
* shared coherence model and the single-writer-per-session assumption.
|
|
16
37
|
*/
|
|
17
|
-
function withIdentityCache(backend, maxEntries) {
|
|
18
|
-
const l1 =
|
|
38
|
+
function withIdentityCache(backend, maxEntries, expiry) {
|
|
39
|
+
const l1 = expiry
|
|
40
|
+
? new IdleIdentityMemoryStore(maxEntries, expiry)
|
|
41
|
+
: new identity_store_1.WaIdentityMemoryStore({ maxRemoteIdentities: maxEntries });
|
|
19
42
|
let generation = 0;
|
|
20
43
|
return {
|
|
21
44
|
getRemoteIdentity: async (address) => {
|
|
@@ -71,6 +94,8 @@ function withIdentityCache(backend, maxEntries) {
|
|
|
71
94
|
await l1.clear();
|
|
72
95
|
},
|
|
73
96
|
destroy: async () => {
|
|
97
|
+
if (l1 instanceof IdleIdentityMemoryStore)
|
|
98
|
+
l1.detach();
|
|
74
99
|
await l1.clear();
|
|
75
100
|
await backend.destroy?.();
|
|
76
101
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { WaPrivacyTokenStore } from '../contracts/privacy-token.store';
|
|
2
2
|
import type { WithDestroyLifecycle } from '../types';
|
|
3
|
+
import { type IdleExpiry } from '../../util/collections';
|
|
3
4
|
/**
|
|
4
5
|
* Read-through cache for a persistent privacy-token backend. Reuses
|
|
5
|
-
* {@link WaPrivacyTokenMemoryStore} as the bounded
|
|
6
|
+
* {@link WaPrivacyTokenMemoryStore} as the bounded L1.
|
|
6
7
|
*
|
|
7
8
|
* Unlike the signal caches this is **invalidate-on-write, not
|
|
8
9
|
* write-through**: `upsert` merges partial fields into the existing row on
|
|
9
10
|
* the backend, so caching the partial incoming record would diverge from the
|
|
10
11
|
* backend's merged result. Instead each upsert drops the L1 entry and the
|
|
11
12
|
* next read re-populates from the merged backend truth. See
|
|
12
|
-
* {@link withSessionCache} for the shared coherence model
|
|
13
|
-
* single-writer-per-session assumption
|
|
13
|
+
* {@link withSessionCache} for the shared coherence model, the
|
|
14
|
+
* single-writer-per-session assumption and the eviction with and without
|
|
15
|
+
* `expiry`.
|
|
14
16
|
*/
|
|
15
|
-
export declare function withPrivacyTokenCache(backend: WaPrivacyTokenStore, maxEntries?: number): WithDestroyLifecycle<WaPrivacyTokenStore>;
|
|
17
|
+
export declare function withPrivacyTokenCache(backend: WaPrivacyTokenStore, maxEntries?: number, expiry?: IdleExpiry): WithDestroyLifecycle<WaPrivacyTokenStore>;
|
|
@@ -2,20 +2,43 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.withPrivacyTokenCache = withPrivacyTokenCache;
|
|
4
4
|
const privacy_token_store_1 = require("../memory/privacy-token.store");
|
|
5
|
+
const collections_1 = require("../../util/collections");
|
|
6
|
+
/** L1 with idle expiry; hits keep it in access order, so the cap evicts the LRU entry. */
|
|
7
|
+
class IdlePrivacyTokenMemoryStore extends privacy_token_store_1.WaPrivacyTokenMemoryStore {
|
|
8
|
+
constructor(maxEntries, expiry) {
|
|
9
|
+
super(maxEntries);
|
|
10
|
+
this.idle = new collections_1.IdleExpiryIndex(this.records, expiry);
|
|
11
|
+
}
|
|
12
|
+
detach() {
|
|
13
|
+
this.idle.detach();
|
|
14
|
+
}
|
|
15
|
+
onEntryAccess(jid) {
|
|
16
|
+
this.idle.touch(jid);
|
|
17
|
+
}
|
|
18
|
+
onEntryRemove(jid) {
|
|
19
|
+
this.idle.delete(jid);
|
|
20
|
+
}
|
|
21
|
+
onEntriesClear() {
|
|
22
|
+
this.idle.clear();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* Read-through cache for a persistent privacy-token backend. Reuses
|
|
7
|
-
* {@link WaPrivacyTokenMemoryStore} as the bounded
|
|
27
|
+
* {@link WaPrivacyTokenMemoryStore} as the bounded L1.
|
|
8
28
|
*
|
|
9
29
|
* Unlike the signal caches this is **invalidate-on-write, not
|
|
10
30
|
* write-through**: `upsert` merges partial fields into the existing row on
|
|
11
31
|
* the backend, so caching the partial incoming record would diverge from the
|
|
12
32
|
* backend's merged result. Instead each upsert drops the L1 entry and the
|
|
13
33
|
* next read re-populates from the merged backend truth. See
|
|
14
|
-
* {@link withSessionCache} for the shared coherence model
|
|
15
|
-
* single-writer-per-session assumption
|
|
34
|
+
* {@link withSessionCache} for the shared coherence model, the
|
|
35
|
+
* single-writer-per-session assumption and the eviction with and without
|
|
36
|
+
* `expiry`.
|
|
16
37
|
*/
|
|
17
|
-
function withPrivacyTokenCache(backend, maxEntries) {
|
|
18
|
-
const l1 =
|
|
38
|
+
function withPrivacyTokenCache(backend, maxEntries, expiry) {
|
|
39
|
+
const l1 = expiry
|
|
40
|
+
? new IdlePrivacyTokenMemoryStore(maxEntries, expiry)
|
|
41
|
+
: new privacy_token_store_1.WaPrivacyTokenMemoryStore(maxEntries);
|
|
19
42
|
let generation = 0;
|
|
20
43
|
return {
|
|
21
44
|
upsert: async (record) => {
|
|
@@ -53,6 +76,8 @@ function withPrivacyTokenCache(backend, maxEntries) {
|
|
|
53
76
|
await l1.clear();
|
|
54
77
|
},
|
|
55
78
|
destroy: async () => {
|
|
79
|
+
if (l1 instanceof IdlePrivacyTokenMemoryStore)
|
|
80
|
+
l1.detach();
|
|
56
81
|
await l1.clear();
|
|
57
82
|
await backend.destroy?.();
|
|
58
83
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { WaSenderKeyStore } from '../contracts/sender-key.store';
|
|
2
2
|
import type { WithDestroyLifecycle } from '../types';
|
|
3
|
+
import { type IdleExpiry } from '../../util/collections';
|
|
3
4
|
/**
|
|
4
5
|
* Read-through / write-through in-process cache for a persistent sender-key
|
|
5
|
-
* backend. Reuses {@link SenderKeyMemoryStore} as the bounded
|
|
6
|
+
* backend. Reuses {@link SenderKeyMemoryStore} as the bounded L1 so the
|
|
6
7
|
* per-(group, sender) lookups repeated across a group fan-out skip the
|
|
7
8
|
* backend round-trip.
|
|
8
9
|
*
|
|
@@ -13,6 +14,7 @@ import type { WithDestroyLifecycle } from '../types';
|
|
|
13
14
|
* (`deleteDeviceSenderKey`/`markForgetSenderKey`) run on the backend for the
|
|
14
15
|
* authoritative count and then invalidate the matching L1 entries via the
|
|
15
16
|
* memory store's own sweep. See {@link withSessionCache} for the shared
|
|
16
|
-
* coherence model
|
|
17
|
+
* coherence model, the single-writer-per-session assumption and the eviction
|
|
18
|
+
* with and without `expiry`.
|
|
17
19
|
*/
|
|
18
|
-
export declare function withSenderKeyCache(backend: WaSenderKeyStore, maxEntries?: number): WithDestroyLifecycle<WaSenderKeyStore>;
|
|
20
|
+
export declare function withSenderKeyCache(backend: WaSenderKeyStore, maxEntries?: number, expiry?: IdleExpiry): WithDestroyLifecycle<WaSenderKeyStore>;
|
|
@@ -2,9 +2,35 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.withSenderKeyCache = withSenderKeyCache;
|
|
4
4
|
const sender_key_store_1 = require("../memory/sender-key.store");
|
|
5
|
+
const collections_1 = require("../../util/collections");
|
|
6
|
+
/** L1 with idle expiry; hits keep both maps in access order, so the caps evict LRU entries. */
|
|
7
|
+
class IdleSenderKeyMemoryStore extends sender_key_store_1.SenderKeyMemoryStore {
|
|
8
|
+
constructor(maxEntries, expiry) {
|
|
9
|
+
super({ maxSenderKeys: maxEntries, maxSenderDistributions: maxEntries });
|
|
10
|
+
this.idleSenderKeys = new collections_1.IdleExpiryIndex(this.senderKeys, expiry);
|
|
11
|
+
this.idleDistributions = new collections_1.IdleExpiryIndex(this.senderDistributions, expiry);
|
|
12
|
+
}
|
|
13
|
+
detach() {
|
|
14
|
+
this.idleSenderKeys.detach();
|
|
15
|
+
this.idleDistributions.detach();
|
|
16
|
+
}
|
|
17
|
+
onEntryAccess(entries, key) {
|
|
18
|
+
this.indexFor(entries).touch(key);
|
|
19
|
+
}
|
|
20
|
+
onEntryRemove(entries, key) {
|
|
21
|
+
this.indexFor(entries).delete(key);
|
|
22
|
+
}
|
|
23
|
+
onEntriesClear() {
|
|
24
|
+
this.idleSenderKeys.clear();
|
|
25
|
+
this.idleDistributions.clear();
|
|
26
|
+
}
|
|
27
|
+
indexFor(entries) {
|
|
28
|
+
return entries === this.senderKeys ? this.idleSenderKeys : this.idleDistributions;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
5
31
|
/**
|
|
6
32
|
* Read-through / write-through in-process cache for a persistent sender-key
|
|
7
|
-
* backend. Reuses {@link SenderKeyMemoryStore} as the bounded
|
|
33
|
+
* backend. Reuses {@link SenderKeyMemoryStore} as the bounded L1 so the
|
|
8
34
|
* per-(group, sender) lookups repeated across a group fan-out skip the
|
|
9
35
|
* backend round-trip.
|
|
10
36
|
*
|
|
@@ -15,13 +41,16 @@ const sender_key_store_1 = require("../memory/sender-key.store");
|
|
|
15
41
|
* (`deleteDeviceSenderKey`/`markForgetSenderKey`) run on the backend for the
|
|
16
42
|
* authoritative count and then invalidate the matching L1 entries via the
|
|
17
43
|
* memory store's own sweep. See {@link withSessionCache} for the shared
|
|
18
|
-
* coherence model
|
|
44
|
+
* coherence model, the single-writer-per-session assumption and the eviction
|
|
45
|
+
* with and without `expiry`.
|
|
19
46
|
*/
|
|
20
|
-
function withSenderKeyCache(backend, maxEntries) {
|
|
21
|
-
const l1 =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
function withSenderKeyCache(backend, maxEntries, expiry) {
|
|
48
|
+
const l1 = expiry
|
|
49
|
+
? new IdleSenderKeyMemoryStore(maxEntries, expiry)
|
|
50
|
+
: new sender_key_store_1.SenderKeyMemoryStore({
|
|
51
|
+
maxSenderKeys: maxEntries,
|
|
52
|
+
maxSenderDistributions: maxEntries
|
|
53
|
+
});
|
|
25
54
|
let generation = 0;
|
|
26
55
|
return {
|
|
27
56
|
upsertSenderKey: async (record) => {
|
|
@@ -97,6 +126,8 @@ function withSenderKeyCache(backend, maxEntries) {
|
|
|
97
126
|
await l1.clear();
|
|
98
127
|
},
|
|
99
128
|
destroy: async () => {
|
|
129
|
+
if (l1 instanceof IdleSenderKeyMemoryStore)
|
|
130
|
+
l1.detach();
|
|
100
131
|
await l1.clear();
|
|
101
132
|
await backend.destroy?.();
|
|
102
133
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { WaSessionStore } from '../contracts/session.store';
|
|
2
2
|
import type { WithDestroyLifecycle } from '../types';
|
|
3
|
+
import { type IdleExpiry } from '../../util/collections';
|
|
3
4
|
/**
|
|
4
5
|
* Read-through / write-through in-process cache for a persistent session
|
|
5
|
-
* backend. Reuses {@link WaSessionMemoryStore} as the bounded
|
|
6
|
+
* backend. Reuses {@link WaSessionMemoryStore} as the bounded L1 so that
|
|
6
7
|
* repeated reads of the same peer on the send/recv path skip the backend
|
|
7
|
-
* round-trip.
|
|
8
|
+
* round-trip. Without `expiry` the cap evicts the least recently written
|
|
9
|
+
* entry; with it, the least recently used, and entries idle past the TTL
|
|
10
|
+
* leave the L1 (never the backend) and are re-read on next use.
|
|
8
11
|
*
|
|
9
12
|
* Coherence model (single process):
|
|
10
13
|
* - every mutation (set/delete/clear) writes the backend then the L1 in
|
|
@@ -20,4 +23,4 @@ import type { WithDestroyLifecycle } from '../types';
|
|
|
20
23
|
* session with the cache enabled - there is no cross-process invalidation
|
|
21
24
|
* channel, so another process's writes would leave this L1 stale.
|
|
22
25
|
*/
|
|
23
|
-
export declare function withSessionCache(backend: WaSessionStore, maxEntries?: number): WithDestroyLifecycle<WaSessionStore>;
|
|
26
|
+
export declare function withSessionCache(backend: WaSessionStore, maxEntries?: number, expiry?: IdleExpiry): WithDestroyLifecycle<WaSessionStore>;
|
|
@@ -2,11 +2,33 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.withSessionCache = withSessionCache;
|
|
4
4
|
const session_store_1 = require("../memory/session.store");
|
|
5
|
+
const collections_1 = require("../../util/collections");
|
|
6
|
+
/** L1 with idle expiry; hits keep it in access order, so the cap evicts the LRU entry. */
|
|
7
|
+
class IdleSessionMemoryStore extends session_store_1.WaSessionMemoryStore {
|
|
8
|
+
constructor(maxSessions, expiry) {
|
|
9
|
+
super({ maxSessions });
|
|
10
|
+
this.idle = new collections_1.IdleExpiryIndex(this.signalSessions, expiry);
|
|
11
|
+
}
|
|
12
|
+
detach() {
|
|
13
|
+
this.idle.detach();
|
|
14
|
+
}
|
|
15
|
+
onEntryAccess(key) {
|
|
16
|
+
this.idle.touch(key);
|
|
17
|
+
}
|
|
18
|
+
onEntryRemove(key) {
|
|
19
|
+
this.idle.delete(key);
|
|
20
|
+
}
|
|
21
|
+
onEntriesClear() {
|
|
22
|
+
this.idle.clear();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* Read-through / write-through in-process cache for a persistent session
|
|
7
|
-
* backend. Reuses {@link WaSessionMemoryStore} as the bounded
|
|
27
|
+
* backend. Reuses {@link WaSessionMemoryStore} as the bounded L1 so that
|
|
8
28
|
* repeated reads of the same peer on the send/recv path skip the backend
|
|
9
|
-
* round-trip.
|
|
29
|
+
* round-trip. Without `expiry` the cap evicts the least recently written
|
|
30
|
+
* entry; with it, the least recently used, and entries idle past the TTL
|
|
31
|
+
* leave the L1 (never the backend) and are re-read on next use.
|
|
10
32
|
*
|
|
11
33
|
* Coherence model (single process):
|
|
12
34
|
* - every mutation (set/delete/clear) writes the backend then the L1 in
|
|
@@ -22,8 +44,10 @@ const session_store_1 = require("../memory/session.store");
|
|
|
22
44
|
* session with the cache enabled - there is no cross-process invalidation
|
|
23
45
|
* channel, so another process's writes would leave this L1 stale.
|
|
24
46
|
*/
|
|
25
|
-
function withSessionCache(backend, maxEntries) {
|
|
26
|
-
const l1 =
|
|
47
|
+
function withSessionCache(backend, maxEntries, expiry) {
|
|
48
|
+
const l1 = expiry
|
|
49
|
+
? new IdleSessionMemoryStore(maxEntries, expiry)
|
|
50
|
+
: new session_store_1.WaSessionMemoryStore({ maxSessions: maxEntries });
|
|
27
51
|
let generation = 0;
|
|
28
52
|
return {
|
|
29
53
|
hasSession: async (address) => {
|
|
@@ -88,6 +112,8 @@ function withSessionCache(backend, maxEntries) {
|
|
|
88
112
|
await l1.clear();
|
|
89
113
|
},
|
|
90
114
|
destroy: async () => {
|
|
115
|
+
if (l1 instanceof IdleSessionMemoryStore)
|
|
116
|
+
l1.detach();
|
|
91
117
|
await l1.clear();
|
|
92
118
|
await backend.destroy?.();
|
|
93
119
|
}
|
|
@@ -38,6 +38,7 @@ const signal_store_1 = require("./memory/signal.store");
|
|
|
38
38
|
const thread_store_1 = require("./memory/thread.store");
|
|
39
39
|
const noop_store_1 = require("./noop.store");
|
|
40
40
|
const coercion_1 = require("../util/coercion");
|
|
41
|
+
const collections_1 = require("../util/collections");
|
|
41
42
|
const primitives_1 = require("../util/primitives");
|
|
42
43
|
const DEFAULT_CACHE_TTLS_MS = Object.freeze({
|
|
43
44
|
retryMs: 60 * 1000,
|
|
@@ -59,6 +60,8 @@ const REQUIRED_PROVIDER_DOMAINS = [
|
|
|
59
60
|
'contacts',
|
|
60
61
|
'privacyToken'
|
|
61
62
|
];
|
|
63
|
+
const CACHE_LAYER_DOMAINS = ['session', 'identity', 'senderKey', 'privacyToken'];
|
|
64
|
+
const MIN_CACHE_LAYER_TTL_MS = 1000;
|
|
62
65
|
function hasDestroy(value) {
|
|
63
66
|
return (!!value &&
|
|
64
67
|
typeof value === 'object' &&
|
|
@@ -73,6 +76,37 @@ async function destroyIfSupported(value) {
|
|
|
73
76
|
function usesBackend(provider) {
|
|
74
77
|
return !!provider && provider !== 'memory' && provider !== 'none';
|
|
75
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Validates `cacheLayer.ttlMs` and hands each domain that gets an L1 its idle
|
|
81
|
+
* expiry, all on one clock. A TTL on a domain without an L1 is warned and dropped.
|
|
82
|
+
*/
|
|
83
|
+
function resolveCacheLayerExpiry(cacheLayer, providers, logger) {
|
|
84
|
+
const ttls = [];
|
|
85
|
+
for (const domain of CACHE_LAYER_DOMAINS) {
|
|
86
|
+
const ttlMs = cacheLayer.ttlMs?.[`${domain}Ms`];
|
|
87
|
+
if (ttlMs === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs < MIN_CACHE_LAYER_TTL_MS) {
|
|
90
|
+
throw new Error(`cacheLayer.ttlMs.${domain}Ms must be a safe integer >= ${MIN_CACHE_LAYER_TTL_MS}`);
|
|
91
|
+
}
|
|
92
|
+
if (cacheLayer[domain] && usesBackend(providers[domain])) {
|
|
93
|
+
ttls.push({ domain, ttlMs });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
logger?.warn('cacheLayer ttl ignored: domain has no L1', {
|
|
97
|
+
domain,
|
|
98
|
+
cached: cacheLayer[domain] === true,
|
|
99
|
+
provider: providers[domain] ?? 'memory'
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const expiry = {};
|
|
103
|
+
if (ttls.length === 0)
|
|
104
|
+
return expiry;
|
|
105
|
+
const clock = new collections_1.IdleSweepClock((0, collections_1.resolveIdleSweepPeriodMs)(ttls.map((entry) => entry.ttlMs)));
|
|
106
|
+
for (const { domain, ttlMs } of ttls)
|
|
107
|
+
expiry[domain] = { clock, ttlMs };
|
|
108
|
+
return expiry;
|
|
109
|
+
}
|
|
76
110
|
function resolveStore(sessionId, backends, provider, domain, kind, fallback) {
|
|
77
111
|
if (!provider || provider === 'memory' || provider === 'none') {
|
|
78
112
|
return fallback();
|
|
@@ -115,6 +149,7 @@ function createStore(options) {
|
|
|
115
149
|
deviceList: (0, coercion_1.resolvePositive)(options.memory?.cacheTtlMs?.deviceListMs, DEFAULT_CACHE_TTLS_MS.deviceListMs, 'memory.cacheTtlMs.deviceListMs'),
|
|
116
150
|
messageSecret: (0, coercion_1.resolvePositive)(options.memory?.cacheTtlMs?.messageSecretMs, DEFAULT_CACHE_TTLS_MS.messageSecretMs, 'memory.cacheTtlMs.messageSecretMs')
|
|
117
151
|
});
|
|
152
|
+
const cacheLayerExpiry = resolveCacheLayerExpiry(cacheLayer, providers, storeLogger);
|
|
118
153
|
const sessions = new Map();
|
|
119
154
|
const pendingSessionDestroys = new Set();
|
|
120
155
|
let storeDestroyed = false;
|
|
@@ -211,20 +246,20 @@ function createStore(options) {
|
|
|
211
246
|
const signalStore = (0, signal_lock_1.withSignalLock)(rawSignal);
|
|
212
247
|
const preKeyStore = (0, pre_key_lock_1.withPreKeyLock)(rawPreKey);
|
|
213
248
|
const sessionStore = (0, session_lock_1.withSessionLock)(cacheLayer.session && usesBackend(providers.session)
|
|
214
|
-
? (0, session_cache_1.withSessionCache)(rawSession, cacheLayer.limits?.session)
|
|
249
|
+
? (0, session_cache_1.withSessionCache)(rawSession, cacheLayer.limits?.session, cacheLayerExpiry.session)
|
|
215
250
|
: rawSession);
|
|
216
251
|
const identityStore = (0, identity_lock_1.withIdentityLock)(cacheLayer.identity && usesBackend(providers.identity)
|
|
217
|
-
? (0, identity_cache_1.withIdentityCache)(rawIdentity, cacheLayer.limits?.identity)
|
|
252
|
+
? (0, identity_cache_1.withIdentityCache)(rawIdentity, cacheLayer.limits?.identity, cacheLayerExpiry.identity)
|
|
218
253
|
: rawIdentity);
|
|
219
254
|
const senderKeyStore = (0, sender_key_lock_1.withSenderKeyLock)(cacheLayer.senderKey && usesBackend(providers.senderKey)
|
|
220
|
-
? (0, sender_key_cache_1.withSenderKeyCache)(rawSenderKey, cacheLayer.limits?.senderKey)
|
|
255
|
+
? (0, sender_key_cache_1.withSenderKeyCache)(rawSenderKey, cacheLayer.limits?.senderKey, cacheLayerExpiry.senderKey)
|
|
221
256
|
: rawSenderKey);
|
|
222
257
|
const appStateStore = (0, appstate_lock_1.withAppStateLock)(rawAppState);
|
|
223
258
|
const messageStore = (0, message_lock_1.withMessageLock)(rawMessages);
|
|
224
259
|
const threadStore = (0, thread_lock_1.withThreadLock)(rawThreads);
|
|
225
260
|
const contactStore = (0, contact_lock_1.withContactLock)(rawContacts);
|
|
226
261
|
const privacyTokenStore = (0, privacy_token_lock_1.withPrivacyTokenLock)(cacheLayer.privacyToken && usesBackend(providers.privacyToken)
|
|
227
|
-
? (0, privacy_token_cache_1.withPrivacyTokenCache)(rawPrivacyToken, cacheLayer.limits?.privacyToken)
|
|
262
|
+
? (0, privacy_token_cache_1.withPrivacyTokenCache)(rawPrivacyToken, cacheLayer.limits?.privacyToken, cacheLayerExpiry.privacyToken)
|
|
228
263
|
: rawPrivacyToken);
|
|
229
264
|
let sessionDestroyed = false;
|
|
230
265
|
let destroyPromise = null;
|
|
@@ -4,7 +4,7 @@ export interface WaIdentityMemoryStoreOptions {
|
|
|
4
4
|
readonly maxRemoteIdentities?: number;
|
|
5
5
|
}
|
|
6
6
|
export declare class WaIdentityMemoryStore implements WaIdentityStoreContract {
|
|
7
|
-
|
|
7
|
+
protected readonly remoteIdentities: Map<string, Uint8Array>;
|
|
8
8
|
private readonly maxRemoteIdentities;
|
|
9
9
|
constructor(options?: WaIdentityMemoryStoreOptions);
|
|
10
10
|
getRemoteIdentity(address: SignalAddress): Promise<Uint8Array | null>;
|
|
@@ -15,4 +15,10 @@ export declare class WaIdentityMemoryStore implements WaIdentityStoreContract {
|
|
|
15
15
|
readonly identityKey: Uint8Array;
|
|
16
16
|
}[]): Promise<void>;
|
|
17
17
|
clear(): Promise<void>;
|
|
18
|
+
/** Runs with the key of every read hit and write. No-op unless a subclass tracks access. */
|
|
19
|
+
protected onEntryAccess(key: string): void;
|
|
20
|
+
/** Runs with the key of every cap-evicted entry. */
|
|
21
|
+
protected onEntryRemove(key: string): void;
|
|
22
|
+
protected onEntriesClear(): void;
|
|
23
|
+
private readonly onEvict;
|
|
18
24
|
}
|
|
@@ -7,29 +7,53 @@ const collections_1 = require("../../util/collections");
|
|
|
7
7
|
const DEFAULT_MAX_REMOTE_IDENTITIES = 8192;
|
|
8
8
|
class WaIdentityMemoryStore {
|
|
9
9
|
constructor(options = {}) {
|
|
10
|
+
this.onEvict = (key) => this.onEntryRemove(key);
|
|
10
11
|
this.remoteIdentities = new Map();
|
|
11
12
|
this.maxRemoteIdentities = (0, coercion_1.resolvePositive)(options.maxRemoteIdentities, DEFAULT_MAX_REMOTE_IDENTITIES, 'WaIdentityMemoryStoreOptions.maxRemoteIdentities');
|
|
12
13
|
}
|
|
13
14
|
async getRemoteIdentity(address) {
|
|
14
|
-
|
|
15
|
+
const key = (0, jid_1.signalAddressKey)(address);
|
|
16
|
+
const identityKey = this.remoteIdentities.get(key);
|
|
17
|
+
if (identityKey === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
this.onEntryAccess(key);
|
|
20
|
+
return identityKey;
|
|
15
21
|
}
|
|
16
22
|
async getRemoteIdentities(addresses) {
|
|
17
23
|
const result = new Array(addresses.length);
|
|
18
24
|
for (let i = 0; i < addresses.length; i += 1) {
|
|
19
|
-
|
|
25
|
+
const key = (0, jid_1.signalAddressKey)(addresses[i]);
|
|
26
|
+
const identityKey = this.remoteIdentities.get(key);
|
|
27
|
+
if (identityKey === undefined) {
|
|
28
|
+
result[i] = null;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.onEntryAccess(key);
|
|
32
|
+
result[i] = identityKey;
|
|
33
|
+
}
|
|
20
34
|
}
|
|
21
35
|
return result;
|
|
22
36
|
}
|
|
23
37
|
async setRemoteIdentity(address, identityKey) {
|
|
24
|
-
|
|
38
|
+
const key = (0, jid_1.signalAddressKey)(address);
|
|
39
|
+
(0, collections_1.setBoundedMapEntry)(this.remoteIdentities, key, identityKey, this.maxRemoteIdentities, this.onEvict);
|
|
40
|
+
this.onEntryAccess(key);
|
|
25
41
|
}
|
|
26
42
|
async setRemoteIdentities(entries) {
|
|
27
43
|
for (const entry of entries) {
|
|
28
|
-
|
|
44
|
+
const key = (0, jid_1.signalAddressKey)(entry.address);
|
|
45
|
+
(0, collections_1.setBoundedMapEntry)(this.remoteIdentities, key, entry.identityKey, this.maxRemoteIdentities, this.onEvict);
|
|
46
|
+
this.onEntryAccess(key);
|
|
29
47
|
}
|
|
30
48
|
}
|
|
31
49
|
async clear() {
|
|
32
50
|
this.remoteIdentities.clear();
|
|
51
|
+
this.onEntriesClear();
|
|
33
52
|
}
|
|
53
|
+
/** Runs with the key of every read hit and write. No-op unless a subclass tracks access. */
|
|
54
|
+
onEntryAccess(key) { }
|
|
55
|
+
/** Runs with the key of every cap-evicted entry. */
|
|
56
|
+
onEntryRemove(key) { }
|
|
57
|
+
onEntriesClear() { }
|
|
34
58
|
}
|
|
35
59
|
exports.WaIdentityMemoryStore = WaIdentityMemoryStore;
|