zapo-js 1.6.2 → 1.7.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/dist/abprops-spec.d.ts +10 -0
- package/dist/abprops-spec.js +25 -0
- package/dist/client/WaClient.d.ts +11 -0
- package/dist/client/WaClient.js +60 -0
- package/dist/client/WaClientFactory.d.ts +2 -2
- package/dist/client/WaClientFactory.js +50 -12
- package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
- package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
- package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
- package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
- package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +217 -30
- package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
- package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
- package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
- package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
- package/dist/client/events/dirty.d.ts +3 -0
- package/dist/client/events/dirty.js +12 -7
- package/dist/client/events/offline.d.ts +9 -0
- package/dist/client/events/offline.js +95 -0
- package/dist/client/events/privacy.d.ts +45 -0
- package/dist/client/events/privacy.js +130 -0
- package/dist/client/messaging/group-metadata.d.ts +12 -0
- package/dist/client/messaging/group-metadata.js +24 -8
- package/dist/client/messaging/link-preview.d.ts +7 -0
- package/dist/client/messaging/link-preview.js +27 -0
- package/dist/client/newsletter/content.d.ts +4 -1
- package/dist/client/newsletter/content.js +43 -18
- package/dist/client/newsletter/messaging.d.ts +2 -0
- package/dist/client/newsletter/messaging.js +2 -1
- package/dist/client/newsletter/mex.d.ts +2 -2
- package/dist/client/persistence/WriteBehindPersistence.js +2 -1
- package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
- package/dist/client/persistence/ephemeral-setting.js +120 -0
- package/dist/client/persistence/group-history.d.ts +29 -0
- package/dist/client/persistence/group-history.js +149 -0
- package/dist/client/persistence/history-blob.d.ts +25 -0
- package/dist/client/persistence/history-blob.js +38 -0
- package/dist/client/persistence/history-sync.d.ts +2 -0
- package/dist/client/persistence/history-sync.js +31 -29
- package/dist/client/types.d.ts +150 -16
- package/dist/crypto/core/xeddsa.js +16 -0
- package/dist/crypto/curves/X25519.js +13 -0
- package/dist/crypto/nativeBackend.d.ts +20 -0
- package/dist/crypto/nativeBackend.js +95 -0
- package/dist/esm/abprops-spec.js +15 -0
- package/dist/esm/client/WaClient.js +60 -0
- package/dist/esm/client/WaClientFactory.js +51 -13
- package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
- package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
- package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +218 -32
- package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
- package/dist/esm/client/events/dirty.js +12 -7
- package/dist/esm/client/events/offline.js +92 -0
- package/dist/esm/client/events/privacy.js +123 -0
- package/dist/esm/client/messaging/group-metadata.js +25 -9
- package/dist/esm/client/messaging/link-preview.js +29 -2
- package/dist/esm/client/newsletter/content.js +44 -19
- package/dist/esm/client/newsletter/messaging.js +2 -1
- package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
- package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
- package/dist/esm/client/persistence/group-history.js +145 -0
- package/dist/esm/client/persistence/history-blob.js +34 -0
- package/dist/esm/client/persistence/history-sync.js +24 -22
- package/dist/esm/crypto/core/xeddsa.js +17 -1
- package/dist/esm/crypto/curves/X25519.js +13 -0
- package/dist/esm/crypto/nativeBackend.js +93 -0
- package/dist/esm/index.js +3 -1
- package/dist/esm/message/context-info.js +39 -0
- package/dist/esm/message/encode/content.js +20 -4
- package/dist/esm/message/encode/media-payload.js +3 -1
- package/dist/esm/message/kinds/group-history.js +36 -0
- package/dist/esm/message/primitives/incoming.js +18 -6
- package/dist/esm/protocol/abprops.js +18 -162
- package/dist/esm/protocol/constants.js +4 -3
- package/dist/esm/protocol/defaults.js +6 -0
- package/dist/esm/protocol/media.js +1 -0
- package/dist/esm/protocol/message.js +8 -0
- package/dist/esm/protocol/notification.js +1 -0
- package/dist/esm/protocol/privacy.js +70 -4
- package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
- package/dist/esm/store/createStore.js +19 -2
- package/dist/esm/store/index.js +1 -0
- package/dist/esm/store/memory/chat-metadata.store.js +66 -0
- package/dist/esm/store/noop.store.js +8 -0
- package/dist/esm/transport/node/builders/privacy.js +63 -4
- package/dist/esm/util/coercion.js +12 -4
- package/dist/esm/util/index.js +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.js +15 -2
- package/dist/media/types.d.ts +1 -1
- package/dist/message/context-info.d.ts +10 -0
- package/dist/message/context-info.js +40 -0
- package/dist/message/encode/content.d.ts +7 -3
- package/dist/message/encode/content.js +20 -4
- package/dist/message/encode/media-payload.d.ts +5 -2
- package/dist/message/encode/media-payload.js +3 -1
- package/dist/message/kinds/group-history.d.ts +30 -0
- package/dist/message/kinds/group-history.js +40 -0
- package/dist/message/primitives/incoming.d.ts +5 -0
- package/dist/message/primitives/incoming.js +18 -6
- package/dist/protocol/abprops.d.ts +27 -140
- package/dist/protocol/abprops.js +18 -163
- package/dist/protocol/constants.d.ts +4 -3
- package/dist/protocol/constants.js +10 -3
- package/dist/protocol/defaults.d.ts +6 -0
- package/dist/protocol/defaults.js +6 -0
- package/dist/protocol/media.d.ts +1 -0
- package/dist/protocol/media.js +1 -0
- package/dist/protocol/message.d.ts +6 -0
- package/dist/protocol/message.js +9 -0
- package/dist/protocol/notification.d.ts +1 -0
- package/dist/protocol/notification.js +1 -0
- package/dist/protocol/privacy.d.ts +52 -0
- package/dist/protocol/privacy.js +71 -5
- package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
- package/dist/store/contracts/chat-metadata.store.js +2 -0
- package/dist/store/contracts/group-metadata.store.d.ts +7 -0
- package/dist/store/contracts/thread.store.d.ts +6 -0
- package/dist/store/createStore.d.ts +10 -2
- package/dist/store/createStore.js +18 -1
- package/dist/store/index.d.ts +3 -1
- package/dist/store/index.js +3 -1
- package/dist/store/memory/chat-metadata.store.d.ts +26 -0
- package/dist/store/memory/chat-metadata.store.js +70 -0
- package/dist/store/noop.store.d.ts +2 -0
- package/dist/store/noop.store.js +9 -1
- package/dist/store/types.d.ts +190 -39
- package/dist/transport/node/builders/privacy.d.ts +26 -2
- package/dist/transport/node/builders/privacy.js +63 -3
- package/dist/util/coercion.d.ts +6 -0
- package/dist/util/coercion.js +13 -4
- package/dist/util/index.d.ts +1 -1
- package/dist/util/index.js +2 -1
- package/package.json +3 -1
- package/spec/abprops/index.d.ts +4402 -0
- package/spec/abprops/index.js +4372 -0
package/dist/client/types.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import type { AppStateCollectionName } from '../appstate/types';
|
|
|
2
2
|
import type { DataForKey, WaAppstateActionKey, WaAppstateIndexArgs } from '../appstate-spec';
|
|
3
3
|
import type { WaShortcakeAssertionSigner } from '../auth/pairing/WaShortcakeFlow';
|
|
4
4
|
import type { WaAuthClientOptions, WaAuthCredentials, WaAuthDangerousOptions, WaAuthSocketOptions } from '../auth/types';
|
|
5
|
+
import type { WaPrivacyAccountSyncResult } from './coordinators/WaPrivacyCoordinator';
|
|
5
6
|
import type { WaCallGroupParticipant, WaCallType } from './events/call';
|
|
6
7
|
import type { IncomingPresenceType, PresenceLastSeen } from './events/presence';
|
|
8
|
+
import type { WaBlocklistResult } from './events/privacy';
|
|
7
9
|
import type { CompanionHostPersistence } from './persistence/companion-host';
|
|
8
10
|
import type { WaClientPluginDefinition } from './plugins/types';
|
|
9
11
|
import type { WaMediaProcessor } from '../media/processor';
|
|
@@ -148,7 +150,9 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
|
|
|
148
150
|
* on-demand backfill triggered by `message.requestHistorySync`) are
|
|
149
151
|
* downloaded and emitted as `history_sync_chunk` events. Set
|
|
150
152
|
* `enabled: false` to drop them; `requireFullSync: true` asks the
|
|
151
|
-
* primary device for a full history download instead of just recent
|
|
153
|
+
* primary device for a full history download instead of just recent;
|
|
154
|
+
* `groupBundles: true` opts into the group-history bundle a member may
|
|
155
|
+
* share after somebody joins a group.
|
|
152
156
|
*/
|
|
153
157
|
readonly history?: WaHistorySyncOptions;
|
|
154
158
|
/**
|
|
@@ -315,6 +319,14 @@ export interface WaHistorySyncOptions {
|
|
|
315
319
|
*/
|
|
316
320
|
readonly enabled?: boolean;
|
|
317
321
|
readonly requireFullSync?: boolean;
|
|
322
|
+
/**
|
|
323
|
+
* Whether to download and process the group-history bundle a member may
|
|
324
|
+
* share after somebody joins a group, emitting `group_history_bundle`.
|
|
325
|
+
* **Off by default** - a bundle is media a third party pushes at this
|
|
326
|
+
* account unprompted, so fetching it is opt-in. Bundles addressed to
|
|
327
|
+
* other members are dropped either way.
|
|
328
|
+
*/
|
|
329
|
+
readonly groupBundles?: boolean;
|
|
318
330
|
}
|
|
319
331
|
export interface WaSignalMessagePublishInput {
|
|
320
332
|
readonly to: string;
|
|
@@ -375,9 +387,23 @@ export interface WaSendMessageOptions extends WaMessagePublishOptions {
|
|
|
375
387
|
*/
|
|
376
388
|
readonly expirationSeconds?: number;
|
|
377
389
|
/**
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
390
|
+
* Unix seconds when disappearing-mode was enabled, sent as
|
|
391
|
+
* `contextInfo.ephemeralSettingTimestamp`. 1:1 only – groups never carry it.
|
|
392
|
+
* Overrides the value the auto-inject resolves from the thread store; omit
|
|
393
|
+
* it unless you have a reason, since a missing timestamp makes the peer warn
|
|
394
|
+
* that the message will not disappear.
|
|
395
|
+
*/
|
|
396
|
+
readonly ephemeralSettingTimestamp?: number;
|
|
397
|
+
/**
|
|
398
|
+
* Overrides `contextInfo.disappearingMode.trigger`. The 1:1 auto-inject
|
|
399
|
+
* already sets `CHAT_SETTING`.
|
|
400
|
+
*/
|
|
401
|
+
readonly disappearingModeTrigger?: Proto.DisappearingMode.Trigger;
|
|
402
|
+
/**
|
|
403
|
+
* Skip the automatic disappearing-message injection on **group** sends, which
|
|
404
|
+
* would otherwise stamp `contextInfo.expiration` and `disappearingMode` from
|
|
405
|
+
* the group metadata cache. Has no effect on 1:1 – see
|
|
406
|
+
* {@link disableDirectEphemeralAutoInject}. Off by default.
|
|
381
407
|
*
|
|
382
408
|
* Relationship with {@link expirationSeconds}: a non-undefined
|
|
383
409
|
* `expirationSeconds` already short-circuits the auto-inject, so this flag is
|
|
@@ -385,6 +411,17 @@ export interface WaSendMessageOptions extends WaMessagePublishOptions {
|
|
|
385
411
|
* auto-inject AND not set any expiration yourself.
|
|
386
412
|
*/
|
|
387
413
|
readonly disableGroupEphemeralAutoInject?: boolean;
|
|
414
|
+
/**
|
|
415
|
+
* Skip the automatic disappearing-message injection on **1:1** sends, which
|
|
416
|
+
* would otherwise stamp `contextInfo.expiration`,
|
|
417
|
+
* `ephemeralSettingTimestamp` and `disappearingMode` from the chat metadata
|
|
418
|
+
* cache. Also skips the cache lookup itself. Has no effect on groups – see
|
|
419
|
+
* {@link disableGroupEphemeralAutoInject}. Off by default.
|
|
420
|
+
*
|
|
421
|
+
* Same relationship with {@link expirationSeconds} as the group flag: a
|
|
422
|
+
* non-undefined `expirationSeconds` already short-circuits the auto-inject.
|
|
423
|
+
*/
|
|
424
|
+
readonly disableDirectEphemeralAutoInject?: boolean;
|
|
388
425
|
/** Raw child nodes appended to the `<message>` stanza. Escape hatch for protocol features the typed API doesn't cover. */
|
|
389
426
|
readonly customNodes?: readonly BinaryNode[];
|
|
390
427
|
/** Wrap the outgoing message as view-once. Only valid for image/video/audio content. */
|
|
@@ -788,19 +825,26 @@ export interface WaIncomingUnhandledStanzaEvent extends WaIncomingBaseEvent {
|
|
|
788
825
|
readonly reason: string;
|
|
789
826
|
}
|
|
790
827
|
/**
|
|
791
|
-
* Why an incoming message arrived as a content-less placeholder. `view_once`:
|
|
792
|
-
*
|
|
793
|
-
* could not be fanned out. `
|
|
794
|
-
*
|
|
828
|
+
* Why an incoming message arrived as a content-less placeholder. `view_once`: a
|
|
829
|
+
* view-once already consumed elsewhere. `hosted`: a hosted account whose message
|
|
830
|
+
* could not be fanned out. `bot`: a bot message whose fanout never ran. `other`:
|
|
831
|
+
* a plain fanout placeholder – the only kind the primary device resends.
|
|
795
832
|
*/
|
|
796
|
-
export type WaUnavailableMessageKind = 'view_once' | 'hosted' | 'other';
|
|
833
|
+
export type WaUnavailableMessageKind = 'view_once' | 'hosted' | 'bot' | 'other';
|
|
797
834
|
export interface WaIncomingUnavailableMessageEvent extends Omit<WaIncomingBaseEvent, 'chatJid' | 'stanzaId'> {
|
|
798
835
|
/** Which flavour of content the server signalled as unavailable. */
|
|
799
836
|
readonly kind: WaUnavailableMessageKind;
|
|
837
|
+
/**
|
|
838
|
+
* `true` when a resend was queued for the primary device; the payload then
|
|
839
|
+
* arrives as a `message` event with the same key. Best-effort, like wa-web:
|
|
840
|
+
* the request is not persisted, so a failed peer message is not retried.
|
|
841
|
+
* `false` for the unrecoverable flavours, messages past the server age
|
|
842
|
+
* window, and mobile-primary sessions.
|
|
843
|
+
*/
|
|
844
|
+
readonly resendRequested: boolean;
|
|
800
845
|
/**
|
|
801
846
|
* The message key (chat, stanza id, author, addressing metadata) – same
|
|
802
|
-
* shape the `message` event carries, so it can be stored or correlated.
|
|
803
|
-
* is no decrypted `message`: the payload is unavailable and cannot be fetched.
|
|
847
|
+
* shape the `message` event carries, so it can be stored or correlated.
|
|
804
848
|
*/
|
|
805
849
|
readonly key: WaIncomingMessageKey;
|
|
806
850
|
/** Stanza `t` attr (seconds since epoch). */
|
|
@@ -1032,6 +1076,32 @@ export interface WaHistorySyncChunkEvent {
|
|
|
1032
1076
|
/** Server-reported progress, 0–100. */
|
|
1033
1077
|
readonly progress?: number;
|
|
1034
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* A group-history bundle shared with this account after it joined a group,
|
|
1081
|
+
* already downloaded, filtered and persisted. Emitted once per bundle, only
|
|
1082
|
+
* when `history.groupBundles` is enabled.
|
|
1083
|
+
*/
|
|
1084
|
+
export interface WaGroupHistoryBundleEvent {
|
|
1085
|
+
readonly groupJid: string;
|
|
1086
|
+
/** Member who shared the history. */
|
|
1087
|
+
readonly senderJid?: string;
|
|
1088
|
+
/** Stanza id of the message that carried the bundle. */
|
|
1089
|
+
readonly bundleMessageId?: string;
|
|
1090
|
+
/** Messages persisted from this bundle, after filtering. */
|
|
1091
|
+
readonly messagesCount: number;
|
|
1092
|
+
/** Pinned messages older than the shared window, exempt from the age cutoff. */
|
|
1093
|
+
readonly outOfWindowPinsCount: number;
|
|
1094
|
+
/** Entries skipped as stubs, foreign-chat, ephemeral-expired or too old. */
|
|
1095
|
+
readonly droppedCount: number;
|
|
1096
|
+
/** Oldest persisted message timestamp, in ms. */
|
|
1097
|
+
readonly oldestTimestampMs?: number;
|
|
1098
|
+
/**
|
|
1099
|
+
* Members the sender addressed the bundle to (PN or LID form). Copied out
|
|
1100
|
+
* of the decoded payload, so mutating it cannot corrupt the message proto
|
|
1101
|
+
* the `message` event handed to the same listener.
|
|
1102
|
+
*/
|
|
1103
|
+
readonly historyReceivers: readonly string[];
|
|
1104
|
+
}
|
|
1035
1105
|
export type WaAppStateMutationSource = 'snapshot' | 'patch' | 'local';
|
|
1036
1106
|
type MutationEventBase = {
|
|
1037
1107
|
readonly source: WaAppStateMutationSource;
|
|
@@ -1168,11 +1238,11 @@ export interface WaClientEventMap {
|
|
|
1168
1238
|
*/
|
|
1169
1239
|
readonly message_protocol: (event: WaIncomingProtocolMessageEvent) => void;
|
|
1170
1240
|
/**
|
|
1171
|
-
* A message the server delivered as a content-less placeholder
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1241
|
+
* A message the server delivered as a content-less placeholder. The lib acks
|
|
1242
|
+
* it and emits this instead of a `message` event for the same stanza. A plain
|
|
1243
|
+
* fanout placeholder is asked back from the primary device and arrives later
|
|
1244
|
+
* as a `message` event (see `event.resendRequested`); a consumed view-once or
|
|
1245
|
+
* a hosted/bot message that could not be fanned out is never resent.
|
|
1176
1246
|
*/
|
|
1177
1247
|
readonly message_unavailable: (event: WaIncomingUnavailableMessageEvent) => void;
|
|
1178
1248
|
/**
|
|
@@ -1205,6 +1275,28 @@ export interface WaClientEventMap {
|
|
|
1205
1275
|
readonly business: (event: WaBusinessEvent) => void;
|
|
1206
1276
|
/** Profile/group/community picture change notification – the new picture must still be fetched explicitly. */
|
|
1207
1277
|
readonly picture: (event: WaPictureEvent) => void;
|
|
1278
|
+
/**
|
|
1279
|
+
* The account's privacy, refetched after the primary or another companion
|
|
1280
|
+
* changed it: the full category set plus any disallowed list the server
|
|
1281
|
+
* reported. The two halves are read by separate queries, so a change
|
|
1282
|
+
* landing mid-refresh can leave them describing slightly different server
|
|
1283
|
+
* states - the payload is exactly what
|
|
1284
|
+
* {@link WaPrivacyCoordinator.refreshFromAccountSync} returns.
|
|
1285
|
+
*
|
|
1286
|
+
* Two paths trigger it: the live `account_sync` notification (debounced,
|
|
1287
|
+
* so a burst of changes on the phone collapses into one refresh) and the
|
|
1288
|
+
* `account_sync` dirty bit that catches a session up after being offline.
|
|
1289
|
+
* Neither is trusted for its payload - the values always come from a
|
|
1290
|
+
* fresh read. Categories the library does not model are dropped.
|
|
1291
|
+
*/
|
|
1292
|
+
readonly privacy: (event: WaPrivacyAccountSyncResult) => void;
|
|
1293
|
+
/**
|
|
1294
|
+
* The account blocklist after a block/unblock made on another device -
|
|
1295
|
+
* same refetch path as `privacy`, from its own account-sync protocol. The
|
|
1296
|
+
* payload is the full list ({@link WaPrivacyCoordinator.getBlocklist}'s
|
|
1297
|
+
* shape), never a delta.
|
|
1298
|
+
*/
|
|
1299
|
+
readonly blocklist: (event: WaBlocklistResult) => void;
|
|
1208
1300
|
/**
|
|
1209
1301
|
* A parsed app-state mutation arriving from a sync – chat mute/star/read/
|
|
1210
1302
|
* pin/archive/contact/label/etc. changed on another device. Inbound only;
|
|
@@ -1228,12 +1320,24 @@ export interface WaClientEventMap {
|
|
|
1228
1320
|
* Skipped only when `history.enabled` is explicitly `false`.
|
|
1229
1321
|
*/
|
|
1230
1322
|
readonly history_sync_chunk: (event: WaHistorySyncChunkEvent) => void;
|
|
1323
|
+
/**
|
|
1324
|
+
* A group-history bundle another member shared with this account after it
|
|
1325
|
+
* joined a group, already downloaded and persisted. Requires
|
|
1326
|
+
* `history.groupBundles: true` - the download is opt-in because a third
|
|
1327
|
+
* party triggers it.
|
|
1328
|
+
*/
|
|
1329
|
+
readonly group_history_bundle: (event: WaGroupHistoryBundleEvent) => void;
|
|
1231
1330
|
/**
|
|
1232
1331
|
* Offline-message queue progress after a reconnect (`'resuming'` ticks
|
|
1233
1332
|
* with `remainingStanzas`, then `'complete'`). Useful to defer UI updates
|
|
1234
1333
|
* until the catch-up finishes.
|
|
1235
1334
|
*/
|
|
1236
1335
|
readonly offline_resume: (event: WaOfflineResumeEvent) => void;
|
|
1336
|
+
/**
|
|
1337
|
+
* Preview manifest of the offline queue, sent just before the flush. Not
|
|
1338
|
+
* guaranteed to arrive - use `offline_resume` for progress, never this.
|
|
1339
|
+
*/
|
|
1340
|
+
readonly offline_thread_metadata: (event: WaOfflineThreadMetadataEvent) => void;
|
|
1237
1341
|
/**
|
|
1238
1342
|
* Fatal stream-level error from the server (e.g. logged out from another
|
|
1239
1343
|
* device, stream conflict). The connection will close right after.
|
|
@@ -1314,6 +1418,36 @@ export interface WaOfflineResumeEvent {
|
|
|
1314
1418
|
/** `true` when triggered by an explicit catch-up request rather than auto-resume on reconnect. */
|
|
1315
1419
|
readonly forced: boolean;
|
|
1316
1420
|
}
|
|
1421
|
+
export interface WaOfflineThreadPreview {
|
|
1422
|
+
/** User (`@lid`/`@s.whatsapp.net`) or group (`@g.us`) jid. */
|
|
1423
|
+
readonly jid: string;
|
|
1424
|
+
/** Unix seconds of the most recent queued stanza for this thread. */
|
|
1425
|
+
readonly timestampSeconds: number;
|
|
1426
|
+
}
|
|
1427
|
+
export interface WaOfflineThreadReadWatermark {
|
|
1428
|
+
readonly jid: string;
|
|
1429
|
+
/** Unix seconds up to which the peer has already read this thread. */
|
|
1430
|
+
readonly readTimestampSeconds: number;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Which threads have queued traffic, announced right before the offline flush.
|
|
1434
|
+
* Informational only - every listed thread still delivers its stanzas
|
|
1435
|
+
* normally. All fields but `threads` depend on server gating and are commonly
|
|
1436
|
+
* absent.
|
|
1437
|
+
*/
|
|
1438
|
+
export interface WaOfflineThreadMetadataEvent {
|
|
1439
|
+
readonly threads: readonly WaOfflineThreadPreview[];
|
|
1440
|
+
readonly readWatermarks?: readonly WaOfflineThreadReadWatermark[];
|
|
1441
|
+
/** Status-update backlog still queued behind this flush. */
|
|
1442
|
+
readonly pendingStatusMessages?: {
|
|
1443
|
+
readonly count: number;
|
|
1444
|
+
readonly jids: readonly string[];
|
|
1445
|
+
};
|
|
1446
|
+
/** Notification backlog still queued behind this flush. */
|
|
1447
|
+
readonly pendingNotifications?: {
|
|
1448
|
+
readonly count: number;
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1317
1451
|
export interface WaPrivacyTokenUpdateEvent {
|
|
1318
1452
|
/** Peer whose trusted-contact-token (TC token) changed. */
|
|
1319
1453
|
readonly jid: string;
|
|
@@ -9,17 +9,30 @@ const X25519_1 = require("../curves/X25519");
|
|
|
9
9
|
const edwards_1 = require("../math/edwards");
|
|
10
10
|
const le_1 = require("../math/le");
|
|
11
11
|
const mod_1 = require("../math/mod");
|
|
12
|
+
const nativeBackend_1 = require("../nativeBackend");
|
|
12
13
|
const bytes_1 = require("../../util/bytes");
|
|
13
14
|
const PREFIX_SIGNATURE_RANDOM = new Uint8Array([
|
|
14
15
|
0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
15
16
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
|
|
16
17
|
]);
|
|
18
|
+
const nativeBinding = (() => {
|
|
19
|
+
if (process.env.ZAPO_XEDDSA_FORCE_JS)
|
|
20
|
+
return null;
|
|
21
|
+
const mod = (0, nativeBackend_1.resolveNativeCryptoBackend)();
|
|
22
|
+
if (mod && typeof mod.xeddsaSign === 'function' && typeof mod.xeddsaVerify === 'function') {
|
|
23
|
+
return { xeddsaSign: mod.xeddsaSign, xeddsaVerify: mod.xeddsaVerify };
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
})();
|
|
17
27
|
/**
|
|
18
28
|
* Verifies an XEdDSA signature over `message` against an X25519 (Montgomery)
|
|
19
29
|
* public key, converting to the Edwards form internally. Returns `false`
|
|
20
30
|
* on shape mismatch instead of throwing.
|
|
21
31
|
*/
|
|
22
32
|
async function xeddsaVerify(curvePublicKey, message, signature) {
|
|
33
|
+
if (nativeBinding) {
|
|
34
|
+
return nativeBinding.xeddsaVerify(curvePublicKey, message, signature);
|
|
35
|
+
}
|
|
23
36
|
if (signature.length !== 64) {
|
|
24
37
|
return false;
|
|
25
38
|
}
|
|
@@ -44,6 +57,9 @@ async function xeddsaVerify(curvePublicKey, message, signature) {
|
|
|
44
57
|
*/
|
|
45
58
|
async function xeddsaSign(privateKey, message) {
|
|
46
59
|
(0, bytes_1.assertByteLength)(privateKey, 32, `invalid curve25519 private key length ${privateKey.length}`);
|
|
60
|
+
if (nativeBinding) {
|
|
61
|
+
return (0, bytes_1.toBytesView)(nativeBinding.xeddsaSign(privateKey, message));
|
|
62
|
+
}
|
|
47
63
|
const clampedPrivateKey = (0, X25519_1.clampCurvePrivateKeyInPlace)(privateKey);
|
|
48
64
|
const privateScalar = (0, le_1.bytesToBigIntLE)(clampedPrivateKey);
|
|
49
65
|
const encodedPublic = (0, edwards_1.encodeExtendedPoint)((0, edwards_1.scalarMultBase)(privateScalar));
|
|
@@ -10,7 +10,17 @@ const constants_1 = require("../curves/constants");
|
|
|
10
10
|
const types_1 = require("../curves/types");
|
|
11
11
|
const constants_2 = require("../math/constants");
|
|
12
12
|
const fe_1 = require("../math/fe");
|
|
13
|
+
const nativeBackend_1 = require("../nativeBackend");
|
|
13
14
|
const bytes_1 = require("../../util/bytes");
|
|
15
|
+
const nativeX25519ScalarMult = (() => {
|
|
16
|
+
if (process.env.ZAPO_X25519_FORCE_JS)
|
|
17
|
+
return null;
|
|
18
|
+
const mod = (0, nativeBackend_1.resolveNativeCryptoBackend)();
|
|
19
|
+
if (mod && typeof mod.x25519ScalarMult === 'function') {
|
|
20
|
+
return mod.x25519ScalarMult;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
})();
|
|
14
24
|
const diffieHellmanWithCallback = node_crypto_1.diffieHellman;
|
|
15
25
|
let diffieHellmanAsync = null;
|
|
16
26
|
let diffieHellmanAsyncProbed = false;
|
|
@@ -120,6 +130,9 @@ class X25519 {
|
|
|
120
130
|
static async scalarMult(privKey, pubKey) {
|
|
121
131
|
(0, bytes_1.assertByteLength)(privKey, 32, 'x25519 private key must be 32 bytes');
|
|
122
132
|
(0, bytes_1.assertByteLength)(pubKey, 32, 'x25519 public key must be 32 bytes');
|
|
133
|
+
if (nativeX25519ScalarMult) {
|
|
134
|
+
return (0, bytes_1.toBytesView)(nativeX25519ScalarMult(privKey, pubKey));
|
|
135
|
+
}
|
|
123
136
|
const opts = {
|
|
124
137
|
privateKey: x25519PrivateKeyObject(privKey),
|
|
125
138
|
publicKey: x25519PublicKeyObject(pubKey)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface NativeCryptoModule {
|
|
2
|
+
readonly x25519ScalarMult?: (privateKey: Uint8Array, publicKey: Uint8Array) => Uint8Array;
|
|
3
|
+
readonly xeddsaSign?: (privateKey: Uint8Array, message: Uint8Array) => Uint8Array;
|
|
4
|
+
readonly xeddsaVerify?: (publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => boolean;
|
|
5
|
+
}
|
|
6
|
+
declare const ABSENCE_TOKENS: {
|
|
7
|
+
readonly napi: readonly ["'@zapo-js/native'", "binding.js", "Cannot find native binding"];
|
|
8
|
+
readonly wasm: readonly ["'@zapo-js/native'", "zapo_native_wasm.js"];
|
|
9
|
+
};
|
|
10
|
+
export type NativeBackendKind = keyof typeof ABSENCE_TOKENS;
|
|
11
|
+
/**
|
|
12
|
+
* Classifies a backend load failure: `true` means the backend is simply not
|
|
13
|
+
* available on this install (missing optional package, entry file absent,
|
|
14
|
+
* no prebuilt binary, ESM build without `require`) and the fallback should
|
|
15
|
+
* stay silent; `false` means the backend is present but broken and the
|
|
16
|
+
* degradation deserves a process warning.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isBackendAbsence(backend: NativeBackendKind, error: unknown): boolean;
|
|
19
|
+
export declare function resolveNativeCryptoBackend(): NativeCryptoModule | null;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isBackendAbsence = isBackendAbsence;
|
|
4
|
+
exports.resolveNativeCryptoBackend = resolveNativeCryptoBackend;
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the optional native crypto accelerator backend shared by the
|
|
7
|
+
* X25519 and XEdDSA paths. Selected via `ZAPO_NATIVE_BACKEND`:
|
|
8
|
+
*
|
|
9
|
+
* - `auto` (default) – try the NAPI addon first, then the WASM build,
|
|
10
|
+
* then no accelerator.
|
|
11
|
+
* - `napi` – only the compiled Rust NAPI addon (`@zapo-js/native`).
|
|
12
|
+
* - `wasm` – only the wasm-bindgen build of the same Rust core.
|
|
13
|
+
* The shipped artifact targets the web (ESM glue +
|
|
14
|
+
* explicit init) so one build serves browsers and
|
|
15
|
+
* Node; loading it here relies on `require(esm)`,
|
|
16
|
+
* available unflagged on Node 20.19+ / 22.12+.
|
|
17
|
+
* - `js` / `none` – no accelerator; callers fall back to the pure-JS
|
|
18
|
+
* (and `node:crypto`) implementations.
|
|
19
|
+
*
|
|
20
|
+
* The module is resolved once and memoised. A failed backend resolves to
|
|
21
|
+
* `null` (or, under `auto`, falls through to the next one) so callers
|
|
22
|
+
* transparently fall back to JS. An absent backend stays silent - that is
|
|
23
|
+
* the expected shape of an optional accelerator - but a present-yet-broken
|
|
24
|
+
* one (corrupt binary, ABI mismatch, unsupported runtime) emits a process
|
|
25
|
+
* warning with code `ZAPO_NATIVE_LOAD_FAILED` so the degradation is
|
|
26
|
+
* visible. The per-primitive `ZAPO_X25519_FORCE_JS` / `ZAPO_XEDDSA_FORCE_JS`
|
|
27
|
+
* escape hatches are honoured by the callers, not here.
|
|
28
|
+
*/
|
|
29
|
+
const node_fs_1 = require("node:fs");
|
|
30
|
+
const primitives_1 = require("../util/primitives");
|
|
31
|
+
const WASM_MODULE_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm.js';
|
|
32
|
+
const WASM_BINARY_SPECIFIER = '@zapo-js/native/wasm/pkg/zapo_native_wasm_bg.wasm';
|
|
33
|
+
function loadWasmBackend() {
|
|
34
|
+
const mod = require(WASM_MODULE_SPECIFIER);
|
|
35
|
+
const wasmBytes = (0, node_fs_1.readFileSync)(require.resolve(WASM_BINARY_SPECIFIER));
|
|
36
|
+
mod.initSync({ module: wasmBytes });
|
|
37
|
+
return mod;
|
|
38
|
+
}
|
|
39
|
+
let cached;
|
|
40
|
+
const ABSENCE_TOKENS = {
|
|
41
|
+
napi: ["'@zapo-js/native'", 'binding.js', 'Cannot find native binding'],
|
|
42
|
+
wasm: ["'@zapo-js/native'", 'zapo_native_wasm.js']
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Classifies a backend load failure: `true` means the backend is simply not
|
|
46
|
+
* available on this install (missing optional package, entry file absent,
|
|
47
|
+
* no prebuilt binary, ESM build without `require`) and the fallback should
|
|
48
|
+
* stay silent; `false` means the backend is present but broken and the
|
|
49
|
+
* degradation deserves a process warning.
|
|
50
|
+
*/
|
|
51
|
+
function isBackendAbsence(backend, error) {
|
|
52
|
+
if (error instanceof ReferenceError) {
|
|
53
|
+
return error.message.includes('require is not defined');
|
|
54
|
+
}
|
|
55
|
+
const { code, message } = (error ?? {});
|
|
56
|
+
if (typeof message !== 'string')
|
|
57
|
+
return false;
|
|
58
|
+
const notFound = code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND';
|
|
59
|
+
if (!notFound && !message.includes('Cannot find native binding'))
|
|
60
|
+
return false;
|
|
61
|
+
return ABSENCE_TOKENS[backend].some((token) => message.includes(token));
|
|
62
|
+
}
|
|
63
|
+
function warnLoadFailure(backend, error) {
|
|
64
|
+
process.emitWarning('failed to load native crypto accelerator, falling back', {
|
|
65
|
+
code: 'ZAPO_NATIVE_LOAD_FAILED',
|
|
66
|
+
detail: `${backend}: ${(0, primitives_1.toError)(error).message}`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function resolveNativeCryptoBackend() {
|
|
70
|
+
if (cached !== undefined)
|
|
71
|
+
return cached;
|
|
72
|
+
const backend = (process.env.ZAPO_NATIVE_BACKEND ?? 'auto').toLowerCase();
|
|
73
|
+
cached = null;
|
|
74
|
+
if (backend === 'js' || backend === 'none')
|
|
75
|
+
return cached;
|
|
76
|
+
if (backend === 'napi' || backend === 'auto') {
|
|
77
|
+
try {
|
|
78
|
+
cached = require('@zapo-js/native');
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!isBackendAbsence('napi', error))
|
|
82
|
+
warnLoadFailure('napi', error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (cached === null && (backend === 'wasm' || backend === 'auto')) {
|
|
86
|
+
try {
|
|
87
|
+
cached = loadWasmBackend();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!isBackendAbsence('wasm', error))
|
|
91
|
+
warnLoadFailure('wasm', error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return cached;
|
|
95
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { WA_ABPROPS_BY_CODE } from '../../spec/abprops/index.js';
|
|
2
|
+
export { WA_ABPROPS, WA_ABPROPS_BY_CODE, WA_ABPROPS_SPECIAL_EARLY, WA_ABPROPS_USED_BEFORE_INIT, WA_GROUP_ABPROPS, WA_GROUP_ABPROPS_BY_CODE } from '../../spec/abprops/index.js';
|
|
3
|
+
// The reverse map is emitted with numeric literal keys, so it cannot be indexed
|
|
4
|
+
// with a plain `number`. Widening it keeps the lookup a single property read and
|
|
5
|
+
// avoids allocating a parallel Map with 2000+ entries at module init.
|
|
6
|
+
const AB_PROP_NAME_BY_CODE = WA_ABPROPS_BY_CODE;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the client-side prop name for a wire config code.
|
|
9
|
+
*
|
|
10
|
+
* Returns `undefined` for codes this build does not know about - the server
|
|
11
|
+
* advertises props from newer bundles, and WA Web drops those rows too.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveAbPropNameByCode(configCode) {
|
|
14
|
+
return AB_PROP_NAME_BY_CODE[configCode];
|
|
15
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { createIgnoreKeyFilter, validateIgnoreKey } from './messaging/ignore-key.js';
|
|
3
|
+
import { createEphemeralObserver, persistIncomingEphemeralSetting } from './persistence/ephemeral-setting.js';
|
|
4
|
+
import { runGroupHistoryBundle } from './persistence/group-history.js';
|
|
3
5
|
import { runHistorySyncNotification } from './persistence/history-sync.js';
|
|
4
6
|
import { persistIncomingMailboxEntities } from './persistence/mailbox.js';
|
|
5
7
|
import { WriteBehindPersistence } from './persistence/WriteBehindPersistence.js';
|
|
6
8
|
import { installWaClientPlugins } from './plugins/install.js';
|
|
7
9
|
import { buildWaClientDependencies, resolveWaClientBase } from './WaClientFactory.js';
|
|
8
10
|
import { ConsoleLogger } from '../infra/log/ConsoleLogger.js';
|
|
11
|
+
import { unwrapMessage } from '../message/encode/content.js';
|
|
9
12
|
import { proto } from '../proto.js';
|
|
10
13
|
import { WA_DEFAULTS, WA_MESSAGE_TYPES } from '../protocol/constants.js';
|
|
11
14
|
import { normalizeDeviceJid } from '../protocol/jid.js';
|
|
@@ -42,6 +45,10 @@ class WaClientImpl extends EventEmitter {
|
|
|
42
45
|
threadStore: this.stores.threads,
|
|
43
46
|
contactStore: this.stores.contacts
|
|
44
47
|
}, this.logger, this.options.writeBehind);
|
|
48
|
+
this.observeEphemeralSetting = createEphemeralObserver({
|
|
49
|
+
logger: this.logger,
|
|
50
|
+
chatMetadataStore: this.stores.chatMetadata
|
|
51
|
+
});
|
|
45
52
|
const dependencies = buildWaClientDependencies({
|
|
46
53
|
base,
|
|
47
54
|
runtime: {
|
|
@@ -182,6 +189,7 @@ class WaClientImpl extends EventEmitter {
|
|
|
182
189
|
}
|
|
183
190
|
try {
|
|
184
191
|
this.emit('message', event);
|
|
192
|
+
this.observeEphemeralSetting(event);
|
|
185
193
|
void persistIncomingMailboxEntities({
|
|
186
194
|
logger: this.logger,
|
|
187
195
|
writeBehind: this.writeBehind,
|
|
@@ -207,6 +215,9 @@ class WaClientImpl extends EventEmitter {
|
|
|
207
215
|
});
|
|
208
216
|
});
|
|
209
217
|
}
|
|
218
|
+
if (this.options.history?.groupBundles === true && event.message && !event.key.fromMe) {
|
|
219
|
+
this.tryProcessGroupHistoryBundle(event);
|
|
220
|
+
}
|
|
210
221
|
const protocolMessage = event.message?.protocolMessage;
|
|
211
222
|
if (!protocolMessage) {
|
|
212
223
|
return;
|
|
@@ -258,6 +269,7 @@ class WaClientImpl extends EventEmitter {
|
|
|
258
269
|
await runHistorySyncNotification({
|
|
259
270
|
logger: this.logger,
|
|
260
271
|
mediaTransfer: this.mediaTransfer,
|
|
272
|
+
chatMetadataStore: this.stores.chatMetadata,
|
|
261
273
|
writeBehind: this.writeBehind,
|
|
262
274
|
emitEvent: this.emit.bind(this),
|
|
263
275
|
onPrivacyTokens: (conversations) => this.deps.trustedContactToken.hydrateFromHistorySync(conversations),
|
|
@@ -270,6 +282,16 @@ class WaClientImpl extends EventEmitter {
|
|
|
270
282
|
}
|
|
271
283
|
return;
|
|
272
284
|
}
|
|
285
|
+
if (protocolType === proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING) {
|
|
286
|
+
persistIncomingEphemeralSetting({
|
|
287
|
+
logger: this.logger,
|
|
288
|
+
writeBehind: this.writeBehind,
|
|
289
|
+
chatMetadataStore: this.stores.chatMetadata,
|
|
290
|
+
event,
|
|
291
|
+
protocolMessage
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
273
295
|
if (SYNC_RELATED_PROTOCOL_TYPES.has(protocolType)) {
|
|
274
296
|
this.logger.info('incoming sync-related protocol message', {
|
|
275
297
|
id: event.key.id,
|
|
@@ -288,6 +310,43 @@ class WaClientImpl extends EventEmitter {
|
|
|
288
310
|
this.leaveIncomingHandler();
|
|
289
311
|
}
|
|
290
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Kicks off the group-history bundle download when the incoming message
|
|
315
|
+
* carries one. Fire-and-forget: the blob can be large, and the incoming
|
|
316
|
+
* handler must not stall behind a CDN fetch.
|
|
317
|
+
*
|
|
318
|
+
* The download outlives the handler that started it, so it takes a slot of
|
|
319
|
+
* its own in the drain accounting - `disconnect()` and `clearStoredState()`
|
|
320
|
+
* must not flush or wipe the stores while a bundle is still writing.
|
|
321
|
+
*/
|
|
322
|
+
tryProcessGroupHistoryBundle(event) {
|
|
323
|
+
const bundle = unwrapMessage(event.message ?? {}).messageHistoryBundle;
|
|
324
|
+
const groupJid = event.key.remoteJid;
|
|
325
|
+
if (!bundle || !groupJid) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (!this.tryEnterIncomingHandler()) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const credentials = this.deps.authClient.getCurrentCredentials();
|
|
332
|
+
void runGroupHistoryBundle({
|
|
333
|
+
logger: this.logger,
|
|
334
|
+
mediaTransfer: this.mediaTransfer,
|
|
335
|
+
writeBehind: this.writeBehind,
|
|
336
|
+
emitEvent: this.emit.bind(this),
|
|
337
|
+
meJid: credentials?.meJid,
|
|
338
|
+
meLid: credentials?.meLid,
|
|
339
|
+
getAbPropNumber: (name) => this.deps.abPropsCoordinator.getConfigValue(name)
|
|
340
|
+
}, {
|
|
341
|
+
bundle,
|
|
342
|
+
groupJid,
|
|
343
|
+
senderJid: event.key.participant ?? undefined,
|
|
344
|
+
bundleMessageId: event.key.id,
|
|
345
|
+
sentAtSeconds: event.timestampSeconds
|
|
346
|
+
}).finally(() => {
|
|
347
|
+
this.leaveIncomingHandler();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
291
350
|
async queryWithContext(context, node, timeoutMs = this.options.iqTimeoutMs ?? WA_DEFAULTS.IQ_TIMEOUT_MS, contextData = {}, options = {}) {
|
|
292
351
|
return queryNodeWithContext(async (queryNode, queryTimeoutMs) => this.deps.lowLevelCoordinator.query(queryNode, queryTimeoutMs, options), this.logger, context, node, timeoutMs, contextData);
|
|
293
352
|
}
|
|
@@ -362,6 +421,7 @@ class WaClientImpl extends EventEmitter {
|
|
|
362
421
|
* your own backoff.
|
|
363
422
|
*/
|
|
364
423
|
async disconnect() {
|
|
424
|
+
this.deps.privacyCoordinator.stopAccountSyncRefresh();
|
|
365
425
|
await this.pauseIncomingEventsAndWaitDrain();
|
|
366
426
|
const writeBehindFlush = await this.writeBehind.flush(this.options.writeBehind?.flushTimeoutMs);
|
|
367
427
|
if (writeBehindFlush.remaining > 0) {
|