@juzi/wechaty-puppet-service 1.0.130 → 1.0.132
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/cjs/src/auth/call-cred.d.ts +2 -2
- package/dist/cjs/src/auth/call-cred.d.ts.map +1 -1
- package/dist/cjs/src/auth/call-cred.js.map +1 -1
- package/dist/cjs/src/client/grpc-manager.d.ts +4 -1
- package/dist/cjs/src/client/grpc-manager.d.ts.map +1 -1
- package/dist/cjs/src/client/grpc-manager.js +76 -19
- package/dist/cjs/src/client/grpc-manager.js.map +1 -1
- package/dist/cjs/src/client/payload-store.d.ts +1 -0
- package/dist/cjs/src/client/payload-store.d.ts.map +1 -1
- package/dist/cjs/src/client/payload-store.js +1 -1
- package/dist/cjs/src/client/payload-store.js.map +1 -1
- package/dist/cjs/src/client/puppet-service.d.ts +39 -2
- package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
- package/dist/cjs/src/client/puppet-service.js +593 -133
- package/dist/cjs/src/client/puppet-service.js.map +1 -1
- package/dist/cjs/src/client/recovery.d.ts +65 -0
- package/dist/cjs/src/client/recovery.d.ts.map +1 -0
- package/dist/cjs/src/client/recovery.js +339 -0
- package/dist/cjs/src/client/recovery.js.map +1 -0
- package/dist/cjs/src/client/recovery.spec.d.ts +2 -0
- package/dist/cjs/src/client/recovery.spec.d.ts.map +1 -0
- package/dist/cjs/src/client/recovery.spec.js +686 -0
- package/dist/cjs/src/client/recovery.spec.js.map +1 -0
- package/dist/cjs/src/client/room-cache-coordinator.d.ts +14 -0
- package/dist/cjs/src/client/room-cache-coordinator.d.ts.map +1 -0
- package/dist/cjs/src/client/room-cache-coordinator.js +53 -0
- package/dist/cjs/src/client/room-cache-coordinator.js.map +1 -0
- package/dist/cjs/src/client/room-member-cache.spec.d.ts +2 -0
- package/dist/cjs/src/client/room-member-cache.spec.d.ts.map +1 -0
- package/dist/cjs/src/client/room-member-cache.spec.js +417 -0
- package/dist/cjs/src/client/room-member-cache.spec.js.map +1 -0
- package/dist/cjs/src/client/room-member-load.spec.d.ts +2 -0
- package/dist/cjs/src/client/room-member-load.spec.d.ts.map +1 -0
- package/dist/cjs/src/client/room-member-load.spec.js +324 -0
- package/dist/cjs/src/client/room-member-load.spec.js.map +1 -0
- package/dist/cjs/src/mod.d.ts +3 -0
- package/dist/cjs/src/mod.d.ts.map +1 -1
- package/dist/cjs/src/mod.js +3 -1
- package/dist/cjs/src/mod.js.map +1 -1
- package/dist/cjs/src/package-json.js +4 -4
- package/dist/cjs/src/server/event-stream-manager.d.ts.map +1 -1
- package/dist/cjs/src/server/event-stream-manager.js +16 -0
- package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
- package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
- package/dist/cjs/src/server/puppet-implementation.js +91 -28
- package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
- package/dist/cjs/src/utils/pb-payload-helper.d.ts +10 -0
- package/dist/cjs/src/utils/pb-payload-helper.d.ts.map +1 -1
- package/dist/cjs/src/utils/pb-payload-helper.js +180 -1
- package/dist/cjs/src/utils/pb-payload-helper.js.map +1 -1
- package/dist/cjs/src/utils/room-member-load.spec.d.ts +2 -0
- package/dist/cjs/src/utils/room-member-load.spec.d.ts.map +1 -0
- package/dist/cjs/src/utils/room-member-load.spec.js +83 -0
- package/dist/cjs/src/utils/room-member-load.spec.js.map +1 -0
- package/dist/esm/src/auth/call-cred.d.ts +2 -2
- package/dist/esm/src/auth/call-cred.d.ts.map +1 -1
- package/dist/esm/src/auth/call-cred.js.map +1 -1
- package/dist/esm/src/client/grpc-manager.d.ts +4 -1
- package/dist/esm/src/client/grpc-manager.d.ts.map +1 -1
- package/dist/esm/src/client/grpc-manager.js +76 -19
- package/dist/esm/src/client/grpc-manager.js.map +1 -1
- package/dist/esm/src/client/payload-store.d.ts +1 -0
- package/dist/esm/src/client/payload-store.d.ts.map +1 -1
- package/dist/esm/src/client/payload-store.js +1 -1
- package/dist/esm/src/client/payload-store.js.map +1 -1
- package/dist/esm/src/client/puppet-service.d.ts +39 -2
- package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
- package/dist/esm/src/client/puppet-service.js +594 -134
- package/dist/esm/src/client/puppet-service.js.map +1 -1
- package/dist/esm/src/client/recovery.d.ts +65 -0
- package/dist/esm/src/client/recovery.d.ts.map +1 -0
- package/dist/esm/src/client/recovery.js +334 -0
- package/dist/esm/src/client/recovery.js.map +1 -0
- package/dist/esm/src/client/recovery.spec.d.ts +2 -0
- package/dist/esm/src/client/recovery.spec.d.ts.map +1 -0
- package/dist/esm/src/client/recovery.spec.js +681 -0
- package/dist/esm/src/client/recovery.spec.js.map +1 -0
- package/dist/esm/src/client/room-cache-coordinator.d.ts +14 -0
- package/dist/esm/src/client/room-cache-coordinator.d.ts.map +1 -0
- package/dist/esm/src/client/room-cache-coordinator.js +49 -0
- package/dist/esm/src/client/room-cache-coordinator.js.map +1 -0
- package/dist/esm/src/client/room-member-cache.spec.d.ts +2 -0
- package/dist/esm/src/client/room-member-cache.spec.d.ts.map +1 -0
- package/dist/esm/src/client/room-member-cache.spec.js +415 -0
- package/dist/esm/src/client/room-member-cache.spec.js.map +1 -0
- package/dist/esm/src/client/room-member-load.spec.d.ts +2 -0
- package/dist/esm/src/client/room-member-load.spec.d.ts.map +1 -0
- package/dist/esm/src/client/room-member-load.spec.js +296 -0
- package/dist/esm/src/client/room-member-load.spec.js.map +1 -0
- package/dist/esm/src/mod.d.ts +3 -0
- package/dist/esm/src/mod.d.ts.map +1 -1
- package/dist/esm/src/mod.js +1 -0
- package/dist/esm/src/mod.js.map +1 -1
- package/dist/esm/src/package-json.js +4 -4
- package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
- package/dist/esm/src/server/event-stream-manager.js +16 -0
- package/dist/esm/src/server/event-stream-manager.js.map +1 -1
- package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
- package/dist/esm/src/server/puppet-implementation.js +92 -29
- package/dist/esm/src/server/puppet-implementation.js.map +1 -1
- package/dist/esm/src/utils/pb-payload-helper.d.ts +10 -0
- package/dist/esm/src/utils/pb-payload-helper.d.ts.map +1 -1
- package/dist/esm/src/utils/pb-payload-helper.js +169 -0
- package/dist/esm/src/utils/pb-payload-helper.js.map +1 -1
- package/dist/esm/src/utils/room-member-load.spec.d.ts +2 -0
- package/dist/esm/src/utils/room-member-load.spec.d.ts.map +1 -0
- package/dist/esm/src/utils/room-member-load.spec.js +58 -0
- package/dist/esm/src/utils/room-member-load.spec.js.map +1 -0
- package/package.json +4 -4
- package/src/auth/call-cred.ts +1 -1
- package/src/client/grpc-manager.ts +57 -22
- package/src/client/payload-store.ts +2 -1
- package/src/client/puppet-service.ts +489 -147
- package/src/client/recovery.spec.ts +595 -0
- package/src/client/recovery.ts +301 -0
- package/src/client/room-cache-coordinator.ts +50 -0
- package/src/client/room-member-cache.spec.ts +448 -0
- package/src/client/room-member-load.spec.ts +319 -0
- package/src/mod.ts +3 -0
- package/src/package-json.ts +4 -4
- package/src/server/event-stream-manager.ts +15 -0
- package/src/server/puppet-implementation.ts +94 -29
- package/src/utils/pb-payload-helper.ts +150 -0
- package/src/utils/room-member-load.spec.ts +67 -0
|
@@ -56,7 +56,9 @@ const config_js_1 = require("../config.js");
|
|
|
56
56
|
const event_type_rev_js_1 = require("../event-type-rev.js");
|
|
57
57
|
const package_json_js_1 = require("../package-json.js");
|
|
58
58
|
const grpc_manager_js_1 = require("./grpc-manager.js");
|
|
59
|
+
const recovery_js_1 = require("./recovery.js");
|
|
59
60
|
const payload_store_js_1 = require("./payload-store.js");
|
|
61
|
+
const room_cache_coordinator_js_1 = require("./room-cache-coordinator.js");
|
|
60
62
|
const pb_payload_helper_js_1 = require("../utils/pb-payload-helper.js");
|
|
61
63
|
const call_media_mapping_js_1 = require("../utils/call-media-mapping.js");
|
|
62
64
|
const gerror_1 = require("gerror");
|
|
@@ -76,7 +78,28 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
76
78
|
options;
|
|
77
79
|
static VERSION = config_js_1.VERSION;
|
|
78
80
|
_payloadStore;
|
|
81
|
+
roomCache = new room_cache_coordinator_js_1.RoomCacheCoordinator();
|
|
82
|
+
eventQueue = Promise.resolve();
|
|
83
|
+
eventEpoch = 0;
|
|
84
|
+
eventsBlocked = false;
|
|
85
|
+
failedEventSeq;
|
|
86
|
+
activeEventSeq;
|
|
87
|
+
pendingEventSeq;
|
|
88
|
+
checkpointQueue = Promise.resolve();
|
|
89
|
+
pendingCheckpointRollback;
|
|
79
90
|
timeoutMilliseconds;
|
|
91
|
+
recovery;
|
|
92
|
+
recoveryTimer;
|
|
93
|
+
inactivityTimer;
|
|
94
|
+
recoveryStopped = true;
|
|
95
|
+
recoveryProcessedSeq = 0;
|
|
96
|
+
get recoveryState() { return this.recovery.state; }
|
|
97
|
+
get eventMetadataEnabled() {
|
|
98
|
+
return this.recovery.enabled || !config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE();
|
|
99
|
+
}
|
|
100
|
+
withRecoveryRequestContext(context, operation) {
|
|
101
|
+
return (0, recovery_js_1.withRecoveryRequestContext)(context, operation);
|
|
102
|
+
}
|
|
80
103
|
_grpcManager;
|
|
81
104
|
get grpcManager() {
|
|
82
105
|
if (!this._grpcManager) {
|
|
@@ -94,9 +117,14 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
94
117
|
constructor(options = {}) {
|
|
95
118
|
super(options);
|
|
96
119
|
this.options = options;
|
|
120
|
+
this.recovery = new recovery_js_1.RecoveryGate(options.recovery, () => {
|
|
121
|
+
this.stopRecovery('Recovery budget exhausted');
|
|
122
|
+
this.reportStreamError(new Error('Recovery budget exhausted'));
|
|
123
|
+
});
|
|
97
124
|
this._payloadStore = new payload_store_js_1.PayloadStore({
|
|
98
125
|
token: config_js_1.envVars.WECHATY_PUPPET_SERVICE_TOKEN(this.options.token),
|
|
99
126
|
logger: this.options.logger,
|
|
127
|
+
recoveryCursor: this.recovery.enabled,
|
|
100
128
|
});
|
|
101
129
|
this.hookPayloadStore();
|
|
102
130
|
this.FileBoxUuid = (0, mod_js_1.uuidifyFileBoxGrpc)(() => this.grpcManager.client);
|
|
@@ -129,12 +157,14 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
129
157
|
this.log.verbose('PuppetService', 'onStart()');
|
|
130
158
|
this.waitingForLogin = false;
|
|
131
159
|
this.waitingForReady = false;
|
|
160
|
+
this.suspendEventProcessing();
|
|
161
|
+
this.eventsBlocked = false;
|
|
132
162
|
if (this._grpcManager) {
|
|
133
163
|
this.log.warn('PuppetService', 'onStart() found this.grpc is already existed. dropped.');
|
|
134
164
|
this._grpcManager = undefined;
|
|
135
165
|
}
|
|
136
166
|
this.log.info('PuppetService', 'start() instanciating GrpcManager ...');
|
|
137
|
-
const grpcManager = new grpc_manager_js_1.GrpcManager(this.options);
|
|
167
|
+
const grpcManager = new grpc_manager_js_1.GrpcManager(this.options, this.recovery);
|
|
138
168
|
this.log.info('PuppetService', 'start() instanciating GrpcManager ... done');
|
|
139
169
|
/**
|
|
140
170
|
* Huan(202108): when we started the event stream,
|
|
@@ -145,8 +175,20 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
145
175
|
this.bridgeGrpcEventStream(grpcManager);
|
|
146
176
|
this.log.info('PuppetService', 'start() setting up bridge grpc event stream ... done');
|
|
147
177
|
this.log.info('PuppetService', 'start() starting grpc manager...');
|
|
148
|
-
const { lastEventSeq, accountId } = await this.
|
|
149
|
-
|
|
178
|
+
const { lastEventSeq, accountId } = await this.eventResumePosition();
|
|
179
|
+
this.recoveryStopped = false;
|
|
180
|
+
this.recovery.begin(accountId, true);
|
|
181
|
+
this.recoveryProcessedSeq = Number(lastEventSeq ?? 0);
|
|
182
|
+
this.touchRecoveryStream();
|
|
183
|
+
try {
|
|
184
|
+
await grpcManager.start(lastEventSeq, accountId);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (!this.recovery.enabled) {
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
this.requestRecovery(grpcManager);
|
|
191
|
+
}
|
|
150
192
|
this.log.info('PuppetService', 'start() starting grpc manager... done');
|
|
151
193
|
this.log.info('PuppetService', 'start healthCheck');
|
|
152
194
|
this.startHealthCheck();
|
|
@@ -154,11 +196,14 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
154
196
|
}
|
|
155
197
|
async onStop() {
|
|
156
198
|
this.log.info('PuppetService', 'onStop()');
|
|
199
|
+
this.stopRecovery('Puppet stopped');
|
|
200
|
+
this.suspendEventProcessing();
|
|
201
|
+
await this.checkpointQueue;
|
|
157
202
|
if (this._grpcManager) {
|
|
158
203
|
this.log.info('PuppetService', 'onStop() stopping grpc manager ...');
|
|
159
204
|
const grpcManager = this._grpcManager;
|
|
160
205
|
this._grpcManager = undefined;
|
|
161
|
-
await grpcManager.stop();
|
|
206
|
+
await this.recovery.runDetached(async () => grpcManager.stop());
|
|
162
207
|
this.log.info('PuppetService', 'onStop() stopping grpc manager ... done');
|
|
163
208
|
}
|
|
164
209
|
this.log.info('PuppetService', 'onStop() ... done');
|
|
@@ -189,11 +234,79 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
189
234
|
bridgeGrpcEventStream(client) {
|
|
190
235
|
this.log.verbose('PuppetService', 'bridgeGrpcEventStream(client)');
|
|
191
236
|
client
|
|
192
|
-
.on('data',
|
|
237
|
+
.on('data', (event) => {
|
|
238
|
+
if (client !== this._grpcManager || this.eventsBlocked) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.touchRecoveryStream();
|
|
242
|
+
if (this.recovery.enabled && event.getType() === wechaty_grpc_1.puppet.EventType.EVENT_TYPE_LOGIN) {
|
|
243
|
+
try {
|
|
244
|
+
const payload = JSON.parse(event.getPayload());
|
|
245
|
+
if (!this.recovery.checkAccount(payload.contactId)) {
|
|
246
|
+
this.stopRecovery('Account changed or invalid login');
|
|
247
|
+
this.reportStreamError(new Error('Account changed or invalid login'));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
this.stopRecovery('Invalid login payload');
|
|
253
|
+
this.reportStreamError(error);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (this.recovery.enabled && event.getType() === wechaty_grpc_1.puppet.EventType.EVENT_TYPE_LOGOUT) {
|
|
258
|
+
this.stopRecovery('Account logged out', false);
|
|
259
|
+
}
|
|
260
|
+
const epoch = this.eventEpoch;
|
|
261
|
+
this.eventQueue = this.eventQueue.then(async () => {
|
|
262
|
+
if (!this.ownsEventWorker(client, epoch)) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
this.activeEventSeq = event.getSeq();
|
|
266
|
+
try {
|
|
267
|
+
if (this.failedEventSeq !== undefined && event.getSeq() > this.failedEventSeq) {
|
|
268
|
+
throw new Error(`Replay skipped failed event ${this.failedEventSeq}; stream halted until reset`);
|
|
269
|
+
}
|
|
270
|
+
await this.recovery.runEvent(() => this.ownsEventWorker(client, epoch), async () => this.onGrpcStreamEvent(event, epoch));
|
|
271
|
+
if (!this.ownsEventWorker(client, epoch)) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
if (event.getSeq()) {
|
|
275
|
+
this.recoveryProcessedSeq = event.getSeq();
|
|
276
|
+
}
|
|
277
|
+
if (event.getSeq() === this.failedEventSeq) {
|
|
278
|
+
this.failedEventSeq = undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
if (!this.ownsEventWorker(client, epoch)) {
|
|
283
|
+
this.log.error('PuppetService', 'superseded event worker failed: %s', error);
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
this.haltEventStream(client, event, error);
|
|
287
|
+
}
|
|
288
|
+
if (epoch === this.eventEpoch) {
|
|
289
|
+
this.activeEventSeq = undefined;
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
}).catch((error) => { this.reportStreamError(error); });
|
|
293
|
+
})
|
|
193
294
|
.on('end', () => {
|
|
295
|
+
if (this.recovery.enabled) {
|
|
296
|
+
this.requestRecovery(client);
|
|
297
|
+
}
|
|
194
298
|
this.log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(end)');
|
|
195
299
|
})
|
|
196
300
|
.on('error', (e) => {
|
|
301
|
+
if (this.recovery.enabled) {
|
|
302
|
+
if (this.recoveryStopped || client !== this._grpcManager) {
|
|
303
|
+
this.reportStreamError(e);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
this.requestRecovery(client);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
197
310
|
this.emit('error', e);
|
|
198
311
|
// https://github.com/wechaty/wechaty-puppet-service/issues/16
|
|
199
312
|
// this.log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(error) %s', e)
|
|
@@ -211,10 +324,143 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
211
324
|
// }
|
|
212
325
|
})
|
|
213
326
|
.on('cancel', (...args) => {
|
|
327
|
+
if (this.recovery.enabled) {
|
|
328
|
+
this.requestRecovery(client);
|
|
329
|
+
}
|
|
214
330
|
this.log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(cancel), %s', JSON.stringify(args));
|
|
215
331
|
});
|
|
216
332
|
}
|
|
217
|
-
|
|
333
|
+
suspendEventProcessing() {
|
|
334
|
+
if (this.activeEventSeq) {
|
|
335
|
+
this.pendingEventSeq ??= this.activeEventSeq;
|
|
336
|
+
}
|
|
337
|
+
this.eventEpoch++;
|
|
338
|
+
this.eventsBlocked = true;
|
|
339
|
+
this.activeEventSeq = undefined;
|
|
340
|
+
this.eventQueue = Promise.resolve();
|
|
341
|
+
}
|
|
342
|
+
ownsEventWorker(manager, epoch) {
|
|
343
|
+
return manager === this._grpcManager && this.isEventEpochCurrent(epoch);
|
|
344
|
+
}
|
|
345
|
+
isEventEpochCurrent(epoch) {
|
|
346
|
+
return epoch === this.eventEpoch && !this.eventsBlocked;
|
|
347
|
+
}
|
|
348
|
+
async eventResumePosition(epoch = this.eventEpoch) {
|
|
349
|
+
return this.withEventCheckpoint(async () => {
|
|
350
|
+
if (this.recovery.enabled && !this._payloadStore.miscellaneous) {
|
|
351
|
+
throw new Error('Recovery cursor storage is unavailable');
|
|
352
|
+
}
|
|
353
|
+
if (this.pendingCheckpointRollback) {
|
|
354
|
+
await this.restoreEventMetadata(this.pendingCheckpointRollback);
|
|
355
|
+
this.pendingCheckpointRollback = undefined;
|
|
356
|
+
}
|
|
357
|
+
const lastEventSeq = await this._payloadStore.miscellaneous?.get('lastEventSeq');
|
|
358
|
+
if (epoch !== this.eventEpoch) {
|
|
359
|
+
return { lastEventSeq };
|
|
360
|
+
}
|
|
361
|
+
if (this.pendingEventSeq !== undefined && this.pendingEventSeq > Number(lastEventSeq ?? 0)) {
|
|
362
|
+
this.failedEventSeq ??= this.pendingEventSeq;
|
|
363
|
+
}
|
|
364
|
+
this.pendingEventSeq = undefined;
|
|
365
|
+
return { lastEventSeq, accountId: await this._payloadStore.miscellaneous?.get('accountId') };
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async withEventCheckpoint(operation) {
|
|
369
|
+
const result = this.checkpointQueue.then(operation);
|
|
370
|
+
this.checkpointQueue = result.then(() => undefined, () => undefined);
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
async restoreEventMetadata(previous) {
|
|
374
|
+
for (const [key, value] of previous) {
|
|
375
|
+
if (value === undefined) {
|
|
376
|
+
await this._payloadStore.miscellaneous?.delete(key);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
await this._payloadStore.miscellaneous?.set(key, value);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async writeEventMetadata(changes, epoch) {
|
|
384
|
+
const isCurrent = () => epoch === undefined || this.isEventEpochCurrent(epoch);
|
|
385
|
+
if (!isCurrent()) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const previous = new Map();
|
|
389
|
+
for (const key of changes.keys()) {
|
|
390
|
+
previous.set(key, await this._payloadStore.miscellaneous?.get(key));
|
|
391
|
+
}
|
|
392
|
+
if (!isCurrent()) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
for (const [key, value] of changes) {
|
|
397
|
+
if (value === undefined) {
|
|
398
|
+
await this._payloadStore.miscellaneous?.delete(key);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
await this._payloadStore.miscellaneous?.set(key, value);
|
|
402
|
+
}
|
|
403
|
+
if (!isCurrent()) {
|
|
404
|
+
await this.restoreEventMetadata(previous);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
try {
|
|
411
|
+
await this.restoreEventMetadata(previous);
|
|
412
|
+
}
|
|
413
|
+
catch (rollbackError) {
|
|
414
|
+
this.pendingCheckpointRollback = previous;
|
|
415
|
+
this.log.error('PuppetService', 'checkpoint rollback failed: %s', rollbackError);
|
|
416
|
+
}
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async commitEventCheckpoint(seq, timestamp, epoch) {
|
|
421
|
+
await this.withEventCheckpoint(async () => {
|
|
422
|
+
if (!this.isEventEpochCurrent(epoch)) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const lastEventSeq = await this._payloadStore.miscellaneous?.get('lastEventSeq');
|
|
426
|
+
if (lastEventSeq && seq <= Number(lastEventSeq) && seq !== 1) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
await this.writeEventMetadata(new Map([
|
|
430
|
+
['lastEventTimestamp', timestamp], ['lastEventSeq', String(seq)],
|
|
431
|
+
]), epoch);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
reportStreamError(error) {
|
|
435
|
+
this.log.error('PuppetService', 'event processing failed: %s', error);
|
|
436
|
+
try {
|
|
437
|
+
this.emit('error', error);
|
|
438
|
+
}
|
|
439
|
+
catch (listenerError) {
|
|
440
|
+
this.log.error('PuppetService', 'error listener failed: %s', listenerError);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
haltEventStream(manager, event, error) {
|
|
444
|
+
this.failedEventSeq ??= event.getSeq();
|
|
445
|
+
this.suspendEventProcessing();
|
|
446
|
+
try {
|
|
447
|
+
manager.stopStream();
|
|
448
|
+
}
|
|
449
|
+
catch (stopError) {
|
|
450
|
+
this.log.error('PuppetService', 'stopping failed event stream failed: %s', stopError);
|
|
451
|
+
}
|
|
452
|
+
if (this.recovery.enabled) {
|
|
453
|
+
this.log.error('PuppetService', 'recovering after event processing failure: %s', error);
|
|
454
|
+
this.requestRecovery(manager);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
this.reportStreamError(error);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async onGrpcStreamEvent(event, epoch = this.eventEpoch) {
|
|
461
|
+
if (!this.isEventEpochCurrent(epoch)) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
218
464
|
const type = event.getType();
|
|
219
465
|
const payload = event.getPayload();
|
|
220
466
|
const seq = event.getSeq();
|
|
@@ -228,15 +474,6 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
228
474
|
data: `onGrpcStreamEvent(${event_type_rev_js_1.EventTypeRev[type]})`,
|
|
229
475
|
});
|
|
230
476
|
}
|
|
231
|
-
if (seq && !config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
|
|
232
|
-
const { lastEventSeq } = await this.getMiscellaneousStoreData();
|
|
233
|
-
if (!lastEventSeq || (seq > Number(lastEventSeq) || seq === 1)) {
|
|
234
|
-
await this.setMiscellaneousStoreData({
|
|
235
|
-
lastEventSeq: seq.toString(),
|
|
236
|
-
lastEventTimestamp: timestamp,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
477
|
switch (type) {
|
|
241
478
|
case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_DONG:
|
|
242
479
|
this.emit('dong', JSON.parse(payload));
|
|
@@ -252,28 +489,51 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
252
489
|
break;
|
|
253
490
|
case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_LOGIN:
|
|
254
491
|
{
|
|
492
|
+
const loginPayload = JSON.parse(payload);
|
|
493
|
+
if (this.recovery.enabled) {
|
|
494
|
+
this.recovery.login(loginPayload.contactId);
|
|
495
|
+
if (this.recovery.state.status === 'offline') {
|
|
496
|
+
this.stopRecovery('Account changed during recovery');
|
|
497
|
+
this.reportStreamError(new Error('Account changed during recovery'));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (this.isLoggedIn) {
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
255
504
|
if (this.waitingForLogin && this.isLoggedIn) {
|
|
256
505
|
this.log.warn('PuppetService', 'this login event is ignored because the it is expected by event stream reconnect and this puppet is already logged in');
|
|
257
506
|
return;
|
|
258
507
|
}
|
|
259
|
-
|
|
260
|
-
if (!config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
|
|
508
|
+
if (this.eventMetadataEnabled) {
|
|
261
509
|
const { accountId } = await this.getMiscellaneousStoreData();
|
|
510
|
+
if (!this.isEventEpochCurrent(epoch)) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
262
513
|
if (accountId !== loginPayload.contactId) {
|
|
263
|
-
await this.resetMiscellaneousStoreData();
|
|
514
|
+
await this.resetMiscellaneousStoreData(epoch);
|
|
515
|
+
if (!this.isEventEpochCurrent(epoch)) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
264
518
|
await this.setMiscellaneousStoreData({
|
|
265
519
|
accountId: loginPayload.contactId,
|
|
266
|
-
});
|
|
520
|
+
}, epoch);
|
|
521
|
+
if (!this.isEventEpochCurrent(epoch)) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
267
524
|
}
|
|
268
525
|
}
|
|
526
|
+
if (!this.isEventEpochCurrent(epoch)) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
269
529
|
(async () => this.login(loginPayload.contactId))().catch(e => this.log.error('PuppetService', 'onGrpcStreamEvent() this.login() rejection %s', e.message));
|
|
270
530
|
}
|
|
271
531
|
break;
|
|
272
532
|
case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_LOGOUT:
|
|
273
533
|
{
|
|
274
534
|
const logoutPayload = JSON.parse(payload);
|
|
275
|
-
if (
|
|
276
|
-
await this.resetMiscellaneousStoreData();
|
|
535
|
+
if (this.eventMetadataEnabled) {
|
|
536
|
+
await this.resetMiscellaneousStoreData(epoch);
|
|
277
537
|
}
|
|
278
538
|
;
|
|
279
539
|
(async () => this.logout(logoutPayload.data))().catch(e => this.log.error('PuppetService', 'onGrpcStreamEvent() this.logout() rejection %s', e.message));
|
|
@@ -281,8 +541,25 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
281
541
|
break;
|
|
282
542
|
case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_DIRTY:
|
|
283
543
|
await this.fastDirty(JSON.parse(payload));
|
|
544
|
+
if (!this.isEventEpochCurrent(epoch)) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
284
547
|
this.emit('dirty', JSON.parse(payload));
|
|
285
548
|
break;
|
|
549
|
+
case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_ROOM_MEMBER_LOAD: {
|
|
550
|
+
const loadPayload = JSON.parse(payload);
|
|
551
|
+
(0, pb_payload_helper_js_1.validateRoomMemberLoadState)(loadPayload);
|
|
552
|
+
this.roomCache.invalidate(loadPayload.roomId);
|
|
553
|
+
await this.roomCache.run(loadPayload.roomId, async () => {
|
|
554
|
+
await this._payloadStore.room?.delete(loadPayload.roomId);
|
|
555
|
+
await this._payloadStore.roomMember?.delete(loadPayload.roomId);
|
|
556
|
+
});
|
|
557
|
+
if (!this.isEventEpochCurrent(epoch)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
this.emit('room-member-load', loadPayload);
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
286
563
|
case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_MESSAGE:
|
|
287
564
|
this.emit('message', JSON.parse(payload));
|
|
288
565
|
break;
|
|
@@ -296,12 +573,40 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
296
573
|
this.emit('post-tap', JSON.parse(payload));
|
|
297
574
|
break;
|
|
298
575
|
case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_READY:
|
|
576
|
+
if (this.recovery.enabled) {
|
|
577
|
+
this.recovery.ready();
|
|
578
|
+
if (this.readyIndicator.value()) {
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
299
582
|
if (this.waitingForReady && this.readyIndicator.value()) {
|
|
300
583
|
this.log.warn('PuppetService', 'this ready event is ignored because the it is expected by event stream reconnect and this puppet is already ready');
|
|
301
584
|
return;
|
|
302
585
|
}
|
|
303
586
|
this.emit('ready', JSON.parse(payload));
|
|
304
587
|
break;
|
|
588
|
+
case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_REPLAY_COMPLETE: {
|
|
589
|
+
if (!this.recovery.enabled) {
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
const barrier = JSON.parse(payload);
|
|
593
|
+
if (seq !== 0 || typeof barrier !== 'object' || barrier === null
|
|
594
|
+
|| !('accountId' in barrier) || !('throughSeq' in barrier)) {
|
|
595
|
+
throw new Error('Invalid replay barrier');
|
|
596
|
+
}
|
|
597
|
+
const value = barrier;
|
|
598
|
+
if (typeof value.accountId !== 'string' || !value.accountId || typeof value.throughSeq !== 'number'
|
|
599
|
+
|| !Number.isSafeInteger(value.throughSeq) || value.throughSeq < 0
|
|
600
|
+
|| value.throughSeq > this.recoveryProcessedSeq || this.failedEventSeq !== undefined) {
|
|
601
|
+
throw new Error('Invalid or incomplete replay barrier');
|
|
602
|
+
}
|
|
603
|
+
this.recovery.barrier(value.accountId);
|
|
604
|
+
if (this.recovery.state.status === 'offline') {
|
|
605
|
+
this.stopRecovery(this.recovery.state.reason);
|
|
606
|
+
this.reportStreamError(new Error(this.recovery.state.reason));
|
|
607
|
+
}
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
305
610
|
case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_ROOM_INVITE:
|
|
306
611
|
this.emit('room-invite', JSON.parse(payload));
|
|
307
612
|
break;
|
|
@@ -355,8 +660,12 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
355
660
|
// Huan(202003): in default, the `type` type should be `never`, please check.
|
|
356
661
|
this.log.error(`eventType ${type} unsupported! data: ${payload}`);
|
|
357
662
|
}
|
|
663
|
+
if (seq && this.isEventEpochCurrent(epoch) && this.eventMetadataEnabled) {
|
|
664
|
+
await this.commitEventCheckpoint(seq, timestamp, epoch);
|
|
665
|
+
}
|
|
358
666
|
}
|
|
359
667
|
async logout(reason) {
|
|
668
|
+
this.stopRecovery(reason ?? 'Account logged out', false);
|
|
360
669
|
this.log.verbose('PuppetService', 'logout(%s)', reason ? `"${reason}"` : '');
|
|
361
670
|
await super.logout(reason);
|
|
362
671
|
try {
|
|
@@ -410,8 +719,14 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
410
719
|
[PUPPET.types.Dirty.Friendship]: async (_) => { },
|
|
411
720
|
[PUPPET.types.Dirty.Message]: async (_) => { },
|
|
412
721
|
[PUPPET.types.Dirty.Post]: async (_) => { },
|
|
413
|
-
[PUPPET.types.Dirty.Room]: async (id) =>
|
|
414
|
-
|
|
722
|
+
[PUPPET.types.Dirty.Room]: async (id) => {
|
|
723
|
+
this.roomCache.invalidate(id);
|
|
724
|
+
return this.roomCache.run(id, async () => this._payloadStore.room?.delete(id));
|
|
725
|
+
},
|
|
726
|
+
[PUPPET.types.Dirty.RoomMember]: async (id) => {
|
|
727
|
+
this.roomCache.invalidate(id);
|
|
728
|
+
return this.roomCache.run(id, async () => this._payloadStore.roomMember?.delete(id));
|
|
729
|
+
},
|
|
415
730
|
[PUPPET.types.Dirty.Tag]: async (id) => this._payloadStore.tag?.delete(id),
|
|
416
731
|
[PUPPET.types.Dirty.TagGroup]: async (id) => this._payloadStore.tagGroup?.delete(id),
|
|
417
732
|
[PUPPET.types.Dirty.Unspecified]: async (id) => { throw new Error('Unspecified type with id: ' + id); },
|
|
@@ -1580,8 +1895,9 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
1580
1895
|
*/
|
|
1581
1896
|
async roomRawPayload(id) {
|
|
1582
1897
|
this.log.verbose('PuppetService', 'roomRawPayload(%s)', id);
|
|
1583
|
-
const
|
|
1584
|
-
|
|
1898
|
+
const generation = this.roomCache.generation(id);
|
|
1899
|
+
const cachedPayload = await this.roomCache.run(id, async () => this._payloadStore.room?.get(id));
|
|
1900
|
+
if (cachedPayload && generation === this.roomCache.generation(id)) {
|
|
1585
1901
|
this.log.silly('PuppetService', 'roomRawPayload(%s) cache HIT', id);
|
|
1586
1902
|
return cachedPayload;
|
|
1587
1903
|
}
|
|
@@ -1589,26 +1905,52 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
1589
1905
|
request.setId(id);
|
|
1590
1906
|
const response = await util_1.default.promisify(this.grpcManager.client.roomPayload
|
|
1591
1907
|
.bind(this.grpcManager.client))(request);
|
|
1592
|
-
const payload =
|
|
1593
|
-
|
|
1594
|
-
avatar: response.getAvatar(),
|
|
1595
|
-
handle: response.getHandle(),
|
|
1596
|
-
id: response.getId(),
|
|
1597
|
-
memberIdList: response.getMemberIdsList(),
|
|
1598
|
-
ownerId: response.getOwnerId(),
|
|
1599
|
-
topic: response.getTopic(),
|
|
1600
|
-
additionalInfo: response.getAdditionalInfo(),
|
|
1601
|
-
remark: response.getRoomRemark(),
|
|
1602
|
-
external: response.getExternal(),
|
|
1603
|
-
};
|
|
1604
|
-
const createTime = response.getCreateTime();
|
|
1605
|
-
if (createTime) {
|
|
1606
|
-
payload.createTime = (0, timestamp_js_1.millisecondsFromTimestamp)(createTime);
|
|
1607
|
-
}
|
|
1608
|
-
await this._payloadStore.room?.set(id, payload);
|
|
1908
|
+
const payload = (0, pb_payload_helper_js_1.roomPbToPayload)(response);
|
|
1909
|
+
await this.roomCache.commit(id, generation, async () => this._payloadStore.room?.set(id, payload));
|
|
1609
1910
|
this.log.silly('PuppetService', 'roomRawPayload(%s) cache SET', id);
|
|
1610
1911
|
return payload;
|
|
1611
1912
|
}
|
|
1913
|
+
async roomMemberSync(roomId, options) {
|
|
1914
|
+
if (!roomId) {
|
|
1915
|
+
throw new Error('roomId is required');
|
|
1916
|
+
}
|
|
1917
|
+
const request = new wechaty_grpc_1.puppet.RoomMemberSyncRequest();
|
|
1918
|
+
request.setId(roomId);
|
|
1919
|
+
if (options?.force !== undefined) {
|
|
1920
|
+
request.setForce(options.force);
|
|
1921
|
+
}
|
|
1922
|
+
const response = await util_1.default.promisify(this.grpcManager.client.roomMemberSync.bind(this.grpcManager.client))(request);
|
|
1923
|
+
return (0, pb_payload_helper_js_1.roomMemberLoadStatePbToPayload)(response, roomId);
|
|
1924
|
+
}
|
|
1925
|
+
async roomMemberLoadState(roomId) {
|
|
1926
|
+
if (!roomId) {
|
|
1927
|
+
throw new Error('roomId is required');
|
|
1928
|
+
}
|
|
1929
|
+
const request = new wechaty_grpc_1.puppet.RoomMemberLoadStateRequest();
|
|
1930
|
+
request.setId(roomId);
|
|
1931
|
+
const response = await util_1.default.promisify(this.grpcManager.client.roomMemberLoadState.bind(this.grpcManager.client))(request);
|
|
1932
|
+
return (0, pb_payload_helper_js_1.roomMemberLoadStatePbToPayload)(response, roomId);
|
|
1933
|
+
}
|
|
1934
|
+
async roomMemberSnapshot(roomId, revision, options) {
|
|
1935
|
+
if (!roomId) {
|
|
1936
|
+
throw new Error('roomId is required');
|
|
1937
|
+
}
|
|
1938
|
+
(0, pb_payload_helper_js_1.validateRoomMemberRevision)(revision);
|
|
1939
|
+
const request = new wechaty_grpc_1.puppet.RoomMemberSnapshotRequest();
|
|
1940
|
+
request.setId(roomId);
|
|
1941
|
+
request.setRevision(revision);
|
|
1942
|
+
if (options?.pageToken !== undefined) {
|
|
1943
|
+
request.setPageToken(options.pageToken);
|
|
1944
|
+
}
|
|
1945
|
+
if (options?.pageSize !== undefined) {
|
|
1946
|
+
if (!Number.isInteger(options.pageSize) || options.pageSize < 0 || options.pageSize > 500) {
|
|
1947
|
+
throw new Error('pageSize must be an integer between 0 and 500');
|
|
1948
|
+
}
|
|
1949
|
+
request.setPageSize(options.pageSize);
|
|
1950
|
+
}
|
|
1951
|
+
const response = await util_1.default.promisify(this.grpcManager.client.roomMemberSnapshot.bind(this.grpcManager.client))(request);
|
|
1952
|
+
return (0, pb_payload_helper_js_1.roomMemberSnapshotPbToPayload)(response, roomId, revision);
|
|
1953
|
+
}
|
|
1612
1954
|
async roomRawPayloadParser(payload) {
|
|
1613
1955
|
// this.log.silly('PuppetService', 'roomRawPayloadParser({id:%s})', payload.id)
|
|
1614
1956
|
// passthrough
|
|
@@ -1618,10 +1960,11 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
1618
1960
|
this.log.verbose('PuppetService', 'batchRoomRawPayload(%s)', roomIdList);
|
|
1619
1961
|
const result = new Map();
|
|
1620
1962
|
const roomIdSet = new Set(roomIdList);
|
|
1963
|
+
const generations = new Map(Array.from(roomIdSet, id => [id, this.roomCache.generation(id)]));
|
|
1621
1964
|
const needGetSet = new Set();
|
|
1622
1965
|
for (const roomId of roomIdSet) {
|
|
1623
|
-
const cachedRoomPayload = await this._payloadStore.room?.get(roomId);
|
|
1624
|
-
if (cachedRoomPayload) {
|
|
1966
|
+
const cachedRoomPayload = await this.roomCache.run(roomId, async () => this._payloadStore.room?.get(roomId));
|
|
1967
|
+
if (cachedRoomPayload && generations.get(roomId) === this.roomCache.generation(roomId)) {
|
|
1625
1968
|
result.set(roomId, cachedRoomPayload);
|
|
1626
1969
|
}
|
|
1627
1970
|
else {
|
|
@@ -1637,27 +1980,18 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
1637
1980
|
const payloads = response.getRoomPayloadsList();
|
|
1638
1981
|
for (const payload of payloads) {
|
|
1639
1982
|
const roomId = payload.getId();
|
|
1640
|
-
const puppetPayload =
|
|
1641
|
-
adminIdList: payload.getAdminIdsList(),
|
|
1642
|
-
avatar: payload.getAvatar(),
|
|
1643
|
-
handle: payload.getHandle(),
|
|
1644
|
-
id: payload.getId(),
|
|
1645
|
-
memberIdList: payload.getMemberIdsList(),
|
|
1646
|
-
ownerId: payload.getOwnerId(),
|
|
1647
|
-
topic: payload.getTopic(),
|
|
1648
|
-
additionalInfo: payload.getAdditionalInfo(),
|
|
1649
|
-
remark: payload.getRoomRemark(),
|
|
1650
|
-
external: payload.getExternal(),
|
|
1651
|
-
};
|
|
1652
|
-
const createTime = payload.getCreateTime();
|
|
1653
|
-
if (createTime) {
|
|
1654
|
-
puppetPayload.createTime = (0, timestamp_js_1.millisecondsFromTimestamp)(createTime);
|
|
1655
|
-
}
|
|
1983
|
+
const puppetPayload = (0, pb_payload_helper_js_1.roomPbToPayload)(payload);
|
|
1656
1984
|
result.set(roomId, puppetPayload);
|
|
1657
|
-
|
|
1985
|
+
const generation = generations.get(roomId);
|
|
1986
|
+
if (generation !== undefined) {
|
|
1987
|
+
await this.roomCache.commit(roomId, generation, async () => this._payloadStore.room?.set(roomId, puppetPayload));
|
|
1988
|
+
}
|
|
1658
1989
|
}
|
|
1659
1990
|
}
|
|
1660
1991
|
catch (e) {
|
|
1992
|
+
if (e instanceof room_cache_coordinator_js_1.RoomCacheOperationError) {
|
|
1993
|
+
throw e;
|
|
1994
|
+
}
|
|
1661
1995
|
this.log.error('PuppetService', 'batchRoomRawPayload(%s, %s) error: %s, use one by one method', roomIdList, needGetSet, e);
|
|
1662
1996
|
let fallbackSuccessCount = 0;
|
|
1663
1997
|
for (const roomId of needGetSet) {
|
|
@@ -1843,9 +2177,10 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
1843
2177
|
}
|
|
1844
2178
|
async roomMemberRawPayload(roomId, contactId) {
|
|
1845
2179
|
this.log.verbose('PuppetService', 'roomMemberRawPayload(%s, %s)', roomId, contactId);
|
|
1846
|
-
const
|
|
2180
|
+
const generation = this.roomCache.generation(roomId);
|
|
2181
|
+
const cachedPayload = await this.roomCache.run(roomId, async () => this._payloadStore.roomMember?.get(roomId));
|
|
1847
2182
|
const cachedRoomMemberPayload = cachedPayload && cachedPayload[contactId];
|
|
1848
|
-
if (cachedRoomMemberPayload) {
|
|
2183
|
+
if (cachedRoomMemberPayload && generation === this.roomCache.generation(roomId)) {
|
|
1849
2184
|
this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache HIT', roomId, contactId);
|
|
1850
2185
|
return cachedRoomMemberPayload;
|
|
1851
2186
|
}
|
|
@@ -1864,9 +2199,9 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
1864
2199
|
joinScene: response.getJoinScene(),
|
|
1865
2200
|
joinTime: response.getJoinTime(),
|
|
1866
2201
|
};
|
|
1867
|
-
await this.
|
|
1868
|
-
|
|
1869
|
-
[contactId]: payload
|
|
2202
|
+
await this.roomCache.commit(roomId, generation, async () => {
|
|
2203
|
+
const current = await this._payloadStore.roomMember?.get(roomId);
|
|
2204
|
+
await this._payloadStore.roomMember?.set(roomId, { ...current, [contactId]: payload });
|
|
1870
2205
|
});
|
|
1871
2206
|
this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache SET', roomId, contactId);
|
|
1872
2207
|
return payload;
|
|
@@ -1878,10 +2213,12 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
1878
2213
|
}
|
|
1879
2214
|
async batchRoomMemberRawPayload(roomId, contactIdList) {
|
|
1880
2215
|
this.log.verbose('PuppetService', 'batchRoomMemberRawPayload(%s, %s)', roomId, contactIdList);
|
|
2216
|
+
const generation = this.roomCache.generation(roomId);
|
|
1881
2217
|
const result = new Map();
|
|
1882
2218
|
const contactIdSet = new Set(contactIdList);
|
|
1883
2219
|
let needGetSet = new Set();
|
|
1884
|
-
const
|
|
2220
|
+
const cached = await this.roomCache.run(roomId, async () => this._payloadStore.roomMember?.get(roomId));
|
|
2221
|
+
const cachedPayload = generation === this.roomCache.generation(roomId) ? cached || {} : {};
|
|
1885
2222
|
if (Object.keys(cachedPayload).length > 0) {
|
|
1886
2223
|
for (const contactId of contactIdSet) {
|
|
1887
2224
|
const cachedRoomMemberPayload = cachedPayload[contactId];
|
|
@@ -1904,15 +2241,22 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
1904
2241
|
const response = await util_1.default.promisify(this.grpcManager.client.batchRoomMemberPayload
|
|
1905
2242
|
.bind(this.grpcManager.client))(request);
|
|
1906
2243
|
const payloads = response.getMemberPayloadsList();
|
|
2244
|
+
const fetched = {};
|
|
1907
2245
|
for (const payload of payloads) {
|
|
1908
2246
|
const contactId = payload.getId();
|
|
1909
2247
|
const puppetPayload = (0, pb_payload_helper_js_1.roomMemberPbToPayload)(payload);
|
|
1910
2248
|
result.set(contactId, puppetPayload);
|
|
1911
|
-
|
|
2249
|
+
fetched[contactId] = puppetPayload;
|
|
1912
2250
|
}
|
|
1913
|
-
await this.
|
|
2251
|
+
await this.roomCache.commit(roomId, generation, async () => {
|
|
2252
|
+
const current = await this._payloadStore.roomMember?.get(roomId);
|
|
2253
|
+
await this._payloadStore.roomMember?.set(roomId, { ...current, ...fetched });
|
|
2254
|
+
});
|
|
1914
2255
|
}
|
|
1915
2256
|
catch (e) {
|
|
2257
|
+
if (e instanceof room_cache_coordinator_js_1.RoomCacheOperationError) {
|
|
2258
|
+
throw e;
|
|
2259
|
+
}
|
|
1916
2260
|
this.log.error('PuppetService', 'batchRoomMemberRawPayload(%s, %s) error: %s, use one by one method', roomId, needGetSet, e);
|
|
1917
2261
|
for (const contactId of needGetSet) {
|
|
1918
2262
|
const payload = await this.roomMemberRawPayload(roomId, contactId);
|
|
@@ -2641,6 +2985,9 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
2641
2985
|
}
|
|
2642
2986
|
healthCheckInterval;
|
|
2643
2987
|
startHealthCheck() {
|
|
2988
|
+
if (this.recovery.enabled) {
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2644
2991
|
this.healthCheckInterval = setInterval(() => {
|
|
2645
2992
|
this.ding('healthCheck');
|
|
2646
2993
|
}, 60 * 1000);
|
|
@@ -2654,23 +3001,106 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
2654
3001
|
waitingForReady = false;
|
|
2655
3002
|
reconnectIndicator;
|
|
2656
3003
|
async reset() {
|
|
2657
|
-
if (
|
|
2658
|
-
this.
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
this.log.warn('PuppetService', 'puppet not logged in, perform regular reset');
|
|
2663
|
-
return super.reset();
|
|
3004
|
+
if (this.recovery.enabled) {
|
|
3005
|
+
if (this._grpcManager) {
|
|
3006
|
+
this.requestRecovery(this._grpcManager);
|
|
3007
|
+
}
|
|
3008
|
+
return;
|
|
2664
3009
|
}
|
|
2665
3010
|
if (this.reconnectIndicator.value()) {
|
|
2666
3011
|
this.log.warn('PuppetService', 'already trying to reconnect, pass this one');
|
|
2667
3012
|
return;
|
|
2668
3013
|
}
|
|
2669
3014
|
this.reconnectIndicator.value(true);
|
|
2670
|
-
this.
|
|
2671
|
-
const
|
|
3015
|
+
this.suspendEventProcessing();
|
|
3016
|
+
const epoch = this.eventEpoch;
|
|
3017
|
+
const manager = this._grpcManager;
|
|
3018
|
+
try {
|
|
3019
|
+
await this.checkpointQueue;
|
|
3020
|
+
if (epoch !== this.eventEpoch) {
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
if (!manager || !this.isLoggedIn) {
|
|
3024
|
+
this.log.warn('PuppetService', 'grpc manager unavailable or puppet logged out, perform regular reset');
|
|
3025
|
+
return await super.reset();
|
|
3026
|
+
}
|
|
3027
|
+
await this.resetEventStream(manager, epoch);
|
|
3028
|
+
}
|
|
3029
|
+
finally {
|
|
3030
|
+
this.reconnectIndicator.value(false);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
touchRecoveryStream() {
|
|
3034
|
+
if (!this.recovery.enabled || this.recoveryStopped) {
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
clearTimeout(this.inactivityTimer);
|
|
3038
|
+
this.inactivityTimer = setTimeout(() => {
|
|
3039
|
+
if (this._grpcManager) {
|
|
3040
|
+
this.requestRecovery(this._grpcManager);
|
|
3041
|
+
}
|
|
3042
|
+
}, this.recovery.inactivity);
|
|
3043
|
+
}
|
|
3044
|
+
stopRecovery(reason, stopStream = true) {
|
|
3045
|
+
if (!this.recovery.enabled) {
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
this.recoveryStopped = true;
|
|
3049
|
+
clearTimeout(this.recoveryTimer);
|
|
3050
|
+
clearTimeout(this.inactivityTimer);
|
|
3051
|
+
this.recoveryTimer = undefined;
|
|
3052
|
+
this.recovery.fail(reason);
|
|
3053
|
+
if (stopStream) {
|
|
3054
|
+
this.suspendEventProcessing();
|
|
3055
|
+
this._grpcManager?.stopStream();
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
requestRecovery(manager) {
|
|
3059
|
+
if (this.recoveryStopped || manager !== this._grpcManager || this.recoveryTimer) {
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
this.recovery.begin();
|
|
3063
|
+
this.suspendEventProcessing();
|
|
3064
|
+
manager.stopStream();
|
|
3065
|
+
this.recovery.replaceControls();
|
|
3066
|
+
clearTimeout(this.inactivityTimer);
|
|
3067
|
+
const epoch = this.eventEpoch;
|
|
3068
|
+
this.recoveryTimer = setTimeout(() => {
|
|
3069
|
+
this.recoveryTimer = undefined;
|
|
3070
|
+
void this.recovery.runDetached(async () => this.connectRecoveryStream(manager, epoch)).catch((error) => {
|
|
3071
|
+
this.log.warn('PuppetService', 'recovery connection failed: %s', error);
|
|
3072
|
+
if (epoch === this.eventEpoch) {
|
|
3073
|
+
this.requestRecovery(manager);
|
|
3074
|
+
}
|
|
3075
|
+
});
|
|
3076
|
+
}, 1000);
|
|
3077
|
+
}
|
|
3078
|
+
async connectRecoveryStream(manager, epoch) {
|
|
3079
|
+
const { lastEventSeq, accountId } = await this.eventResumePosition(epoch);
|
|
3080
|
+
if (epoch !== this.eventEpoch || this.recoveryStopped || manager !== this._grpcManager) {
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
this.recovery.begin(accountId);
|
|
3084
|
+
this.recoveryProcessedSeq = Number(lastEventSeq ?? 0);
|
|
3085
|
+
this.eventsBlocked = false;
|
|
3086
|
+
this.touchRecoveryStream();
|
|
3087
|
+
await manager.startStream(lastEventSeq, accountId);
|
|
3088
|
+
if (!this.ownsEventWorker(manager, epoch)) {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
await util_1.default.promisify(manager.client.start.bind(manager.client))(new wechaty_grpc_1.puppet.StartRequest());
|
|
3092
|
+
}
|
|
3093
|
+
async resetEventStream(manager, epoch) {
|
|
3094
|
+
manager.stopStream();
|
|
3095
|
+
const { lastEventSeq, accountId } = await this.eventResumePosition(epoch);
|
|
3096
|
+
if (epoch !== this.eventEpoch || manager !== this._grpcManager) {
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
2672
3099
|
const onLoginResolve = (resolve) => {
|
|
2673
3100
|
const onLogin = (event) => {
|
|
3101
|
+
if (!this.ownsEventWorker(manager, epoch)) {
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
2674
3104
|
const type = event.getType();
|
|
2675
3105
|
const payload = event.getPayload();
|
|
2676
3106
|
if (this.waitingForLogin && type === wechaty_grpc_1.puppet.EventType.EVENT_TYPE_LOGIN) {
|
|
@@ -2686,6 +3116,9 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
2686
3116
|
};
|
|
2687
3117
|
const onReadyResolve = (resolve) => {
|
|
2688
3118
|
const onReady = (event) => {
|
|
3119
|
+
if (!this.ownsEventWorker(manager, epoch)) {
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
2689
3122
|
const type = event.getType();
|
|
2690
3123
|
if (this.waitingForReady && type === wechaty_grpc_1.puppet.EventType.EVENT_TYPE_READY) {
|
|
2691
3124
|
this.waitingForReady = false;
|
|
@@ -2694,64 +3127,89 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
2694
3127
|
};
|
|
2695
3128
|
return onReady;
|
|
2696
3129
|
};
|
|
2697
|
-
|
|
2698
|
-
let onReady;
|
|
3130
|
+
const removeListeners = [];
|
|
2699
3131
|
const loginFuture = new Promise(resolve => {
|
|
2700
|
-
onLogin = onLoginResolve(resolve);
|
|
2701
|
-
|
|
3132
|
+
const onLogin = onLoginResolve(resolve);
|
|
3133
|
+
manager.on('data', onLogin);
|
|
3134
|
+
removeListeners.push(() => { manager.off('data', onLogin); });
|
|
2702
3135
|
});
|
|
2703
3136
|
const readyFuture = new Promise(resolve => {
|
|
2704
|
-
onReady = onReadyResolve(resolve);
|
|
2705
|
-
|
|
3137
|
+
const onReady = onReadyResolve(resolve);
|
|
3138
|
+
manager.on('data', onReady);
|
|
3139
|
+
removeListeners.push(() => { manager.off('data', onReady); });
|
|
2706
3140
|
});
|
|
2707
3141
|
const startTime = Date.now();
|
|
2708
3142
|
const timeoutMilliseconds = this.timeoutMilliseconds / 10; // 2 min default, 4 min xiaoju-bot
|
|
2709
|
-
|
|
3143
|
+
this.waitingForLogin = true;
|
|
3144
|
+
this.waitingForReady = true;
|
|
3145
|
+
this.eventsBlocked = false;
|
|
3146
|
+
try {
|
|
3147
|
+
while (true) {
|
|
3148
|
+
try {
|
|
3149
|
+
if (!this.ownsEventWorker(manager, epoch)) {
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
await (0, gerror_1.timeoutPromise)(manager.startStream(lastEventSeq, accountId), 30 * 1000);
|
|
3153
|
+
if (!this.ownsEventWorker(manager, epoch)) {
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
break;
|
|
3157
|
+
}
|
|
3158
|
+
catch (e) {
|
|
3159
|
+
if (!this.ownsEventWorker(manager, epoch)) {
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
if (Date.now() - startTime < timeoutMilliseconds) {
|
|
3163
|
+
this.log.warn('failed to start stream, will try again in 15 seconds');
|
|
3164
|
+
await new Promise(resolve => {
|
|
3165
|
+
setTimeout(resolve, 5000);
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
else {
|
|
3169
|
+
this.log.warn('failed to start stream and reaches timeout, will perform regular reset');
|
|
3170
|
+
return super.reset();
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
2710
3174
|
try {
|
|
2711
|
-
await (0, gerror_1.timeoutPromise)(
|
|
2712
|
-
|
|
3175
|
+
await (0, gerror_1.timeoutPromise)(loginFuture, ResetLoginTimeout)
|
|
3176
|
+
.finally(() => {
|
|
3177
|
+
if (epoch === this.eventEpoch) {
|
|
3178
|
+
this.waitingForLogin = false;
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
2713
3181
|
}
|
|
2714
3182
|
catch (e) {
|
|
2715
|
-
if (
|
|
2716
|
-
|
|
2717
|
-
await new Promise(resolve => {
|
|
2718
|
-
setTimeout(resolve, 5000);
|
|
2719
|
-
});
|
|
2720
|
-
}
|
|
2721
|
-
else {
|
|
2722
|
-
this.log.warn('failed to start stream and reaches timeout, will perform regular reset');
|
|
2723
|
-
this.reconnectIndicator.value(false);
|
|
2724
|
-
return super.reset();
|
|
3183
|
+
if (!this.ownsEventWorker(manager, epoch)) {
|
|
3184
|
+
return;
|
|
2725
3185
|
}
|
|
3186
|
+
this.log.warn('PuppetService', 'waiting for event reset login error, will perform regular reset');
|
|
3187
|
+
return super.reset();
|
|
3188
|
+
}
|
|
3189
|
+
try {
|
|
3190
|
+
await (0, gerror_1.timeoutPromise)(readyFuture, ResetReadyTimeout)
|
|
3191
|
+
.finally(() => {
|
|
3192
|
+
if (epoch === this.eventEpoch) {
|
|
3193
|
+
this.waitingForReady = false;
|
|
3194
|
+
}
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
catch (e) {
|
|
3198
|
+
this.log.warn('PuppetService', 'waiting for event reset ready error, will do nothing');
|
|
2726
3199
|
}
|
|
2727
3200
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
3201
|
+
finally {
|
|
3202
|
+
for (const removeListener of removeListeners) {
|
|
3203
|
+
removeListener();
|
|
3204
|
+
}
|
|
3205
|
+
if (epoch === this.eventEpoch) {
|
|
2733
3206
|
this.waitingForLogin = false;
|
|
2734
|
-
this.reconnectIndicator.value(false);
|
|
2735
|
-
this.grpcManager.off('data', onLogin);
|
|
2736
|
-
});
|
|
2737
|
-
}
|
|
2738
|
-
catch (e) {
|
|
2739
|
-
this.log.warn('PuppetService', 'waiting for event reset login error, will perform regular reset');
|
|
2740
|
-
return super.reset();
|
|
2741
|
-
}
|
|
2742
|
-
try {
|
|
2743
|
-
await (0, gerror_1.timeoutPromise)(readyFuture, ResetReadyTimeout)
|
|
2744
|
-
.finally(() => {
|
|
2745
3207
|
this.waitingForReady = false;
|
|
2746
|
-
|
|
2747
|
-
});
|
|
2748
|
-
}
|
|
2749
|
-
catch (e) {
|
|
2750
|
-
this.log.warn('PuppetService', 'waiting for event reset ready error, will do nothing');
|
|
3208
|
+
}
|
|
2751
3209
|
}
|
|
2752
3210
|
}
|
|
2753
3211
|
async getMiscellaneousStoreData() {
|
|
2754
|
-
if (
|
|
3212
|
+
if (!this.eventMetadataEnabled) {
|
|
2755
3213
|
return {
|
|
2756
3214
|
lastEventSeq: undefined,
|
|
2757
3215
|
lastEventTimestamp: undefined,
|
|
@@ -2760,7 +3218,7 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
2760
3218
|
}
|
|
2761
3219
|
const lastEventTimestamp = await this._payloadStore.miscellaneous?.get('lastEventTimestamp');
|
|
2762
3220
|
let lastEventSeq = await this._payloadStore.miscellaneous?.get('lastEventSeq');
|
|
2763
|
-
if ((Date.now() - Number(lastEventTimestamp || 0)) > this.timeoutMilliseconds) {
|
|
3221
|
+
if (!this.recovery.enabled && (Date.now() - Number(lastEventTimestamp || 0)) > this.timeoutMilliseconds) {
|
|
2764
3222
|
this.log.warn(`last event was ${(Date.now() - Number(lastEventTimestamp || 0)) / 1000} seconds ago, will not request event cache`);
|
|
2765
3223
|
lastEventSeq = undefined;
|
|
2766
3224
|
}
|
|
@@ -2771,27 +3229,29 @@ class PuppetService extends PUPPET.Puppet {
|
|
|
2771
3229
|
accountId,
|
|
2772
3230
|
};
|
|
2773
3231
|
}
|
|
2774
|
-
async setMiscellaneousStoreData(data) {
|
|
2775
|
-
if (
|
|
3232
|
+
async setMiscellaneousStoreData(data, epoch) {
|
|
3233
|
+
if (!this.eventMetadataEnabled) {
|
|
2776
3234
|
return;
|
|
2777
3235
|
}
|
|
2778
|
-
|
|
2779
|
-
|
|
3236
|
+
const changes = new Map();
|
|
3237
|
+
if (data.lastEventTimestamp !== undefined) {
|
|
3238
|
+
changes.set('lastEventTimestamp', data.lastEventTimestamp);
|
|
2780
3239
|
}
|
|
2781
|
-
if (
|
|
2782
|
-
|
|
3240
|
+
if (data.accountId !== undefined) {
|
|
3241
|
+
changes.set('accountId', data.accountId);
|
|
2783
3242
|
}
|
|
2784
|
-
if (
|
|
2785
|
-
|
|
3243
|
+
if (data.lastEventSeq !== undefined) {
|
|
3244
|
+
changes.set('lastEventSeq', data.lastEventSeq);
|
|
2786
3245
|
}
|
|
3246
|
+
await this.withEventCheckpoint(async () => this.writeEventMetadata(changes, epoch));
|
|
2787
3247
|
}
|
|
2788
|
-
async resetMiscellaneousStoreData() {
|
|
2789
|
-
if (
|
|
3248
|
+
async resetMiscellaneousStoreData(epoch) {
|
|
3249
|
+
if (!this.eventMetadataEnabled) {
|
|
2790
3250
|
return;
|
|
2791
3251
|
}
|
|
2792
|
-
await this.
|
|
2793
|
-
|
|
2794
|
-
|
|
3252
|
+
await this.withEventCheckpoint(async () => this.writeEventMetadata(new Map([
|
|
3253
|
+
['lastEventSeq', undefined], ['lastEventTimestamp', undefined], ['accountId', undefined],
|
|
3254
|
+
]), epoch));
|
|
2795
3255
|
}
|
|
2796
3256
|
}
|
|
2797
3257
|
exports.PuppetService = PuppetService;
|