@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.
Files changed (124) hide show
  1. package/dist/cjs/src/auth/call-cred.d.ts +2 -2
  2. package/dist/cjs/src/auth/call-cred.d.ts.map +1 -1
  3. package/dist/cjs/src/auth/call-cred.js.map +1 -1
  4. package/dist/cjs/src/client/grpc-manager.d.ts +4 -1
  5. package/dist/cjs/src/client/grpc-manager.d.ts.map +1 -1
  6. package/dist/cjs/src/client/grpc-manager.js +76 -19
  7. package/dist/cjs/src/client/grpc-manager.js.map +1 -1
  8. package/dist/cjs/src/client/payload-store.d.ts +1 -0
  9. package/dist/cjs/src/client/payload-store.d.ts.map +1 -1
  10. package/dist/cjs/src/client/payload-store.js +1 -1
  11. package/dist/cjs/src/client/payload-store.js.map +1 -1
  12. package/dist/cjs/src/client/puppet-service.d.ts +39 -2
  13. package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
  14. package/dist/cjs/src/client/puppet-service.js +593 -133
  15. package/dist/cjs/src/client/puppet-service.js.map +1 -1
  16. package/dist/cjs/src/client/recovery.d.ts +65 -0
  17. package/dist/cjs/src/client/recovery.d.ts.map +1 -0
  18. package/dist/cjs/src/client/recovery.js +339 -0
  19. package/dist/cjs/src/client/recovery.js.map +1 -0
  20. package/dist/cjs/src/client/recovery.spec.d.ts +2 -0
  21. package/dist/cjs/src/client/recovery.spec.d.ts.map +1 -0
  22. package/dist/cjs/src/client/recovery.spec.js +686 -0
  23. package/dist/cjs/src/client/recovery.spec.js.map +1 -0
  24. package/dist/cjs/src/client/room-cache-coordinator.d.ts +14 -0
  25. package/dist/cjs/src/client/room-cache-coordinator.d.ts.map +1 -0
  26. package/dist/cjs/src/client/room-cache-coordinator.js +53 -0
  27. package/dist/cjs/src/client/room-cache-coordinator.js.map +1 -0
  28. package/dist/cjs/src/client/room-member-cache.spec.d.ts +2 -0
  29. package/dist/cjs/src/client/room-member-cache.spec.d.ts.map +1 -0
  30. package/dist/cjs/src/client/room-member-cache.spec.js +417 -0
  31. package/dist/cjs/src/client/room-member-cache.spec.js.map +1 -0
  32. package/dist/cjs/src/client/room-member-load.spec.d.ts +2 -0
  33. package/dist/cjs/src/client/room-member-load.spec.d.ts.map +1 -0
  34. package/dist/cjs/src/client/room-member-load.spec.js +324 -0
  35. package/dist/cjs/src/client/room-member-load.spec.js.map +1 -0
  36. package/dist/cjs/src/mod.d.ts +3 -0
  37. package/dist/cjs/src/mod.d.ts.map +1 -1
  38. package/dist/cjs/src/mod.js +3 -1
  39. package/dist/cjs/src/mod.js.map +1 -1
  40. package/dist/cjs/src/package-json.js +4 -4
  41. package/dist/cjs/src/server/event-stream-manager.d.ts.map +1 -1
  42. package/dist/cjs/src/server/event-stream-manager.js +16 -0
  43. package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
  44. package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
  45. package/dist/cjs/src/server/puppet-implementation.js +91 -28
  46. package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
  47. package/dist/cjs/src/utils/pb-payload-helper.d.ts +10 -0
  48. package/dist/cjs/src/utils/pb-payload-helper.d.ts.map +1 -1
  49. package/dist/cjs/src/utils/pb-payload-helper.js +180 -1
  50. package/dist/cjs/src/utils/pb-payload-helper.js.map +1 -1
  51. package/dist/cjs/src/utils/room-member-load.spec.d.ts +2 -0
  52. package/dist/cjs/src/utils/room-member-load.spec.d.ts.map +1 -0
  53. package/dist/cjs/src/utils/room-member-load.spec.js +83 -0
  54. package/dist/cjs/src/utils/room-member-load.spec.js.map +1 -0
  55. package/dist/esm/src/auth/call-cred.d.ts +2 -2
  56. package/dist/esm/src/auth/call-cred.d.ts.map +1 -1
  57. package/dist/esm/src/auth/call-cred.js.map +1 -1
  58. package/dist/esm/src/client/grpc-manager.d.ts +4 -1
  59. package/dist/esm/src/client/grpc-manager.d.ts.map +1 -1
  60. package/dist/esm/src/client/grpc-manager.js +76 -19
  61. package/dist/esm/src/client/grpc-manager.js.map +1 -1
  62. package/dist/esm/src/client/payload-store.d.ts +1 -0
  63. package/dist/esm/src/client/payload-store.d.ts.map +1 -1
  64. package/dist/esm/src/client/payload-store.js +1 -1
  65. package/dist/esm/src/client/payload-store.js.map +1 -1
  66. package/dist/esm/src/client/puppet-service.d.ts +39 -2
  67. package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
  68. package/dist/esm/src/client/puppet-service.js +594 -134
  69. package/dist/esm/src/client/puppet-service.js.map +1 -1
  70. package/dist/esm/src/client/recovery.d.ts +65 -0
  71. package/dist/esm/src/client/recovery.d.ts.map +1 -0
  72. package/dist/esm/src/client/recovery.js +334 -0
  73. package/dist/esm/src/client/recovery.js.map +1 -0
  74. package/dist/esm/src/client/recovery.spec.d.ts +2 -0
  75. package/dist/esm/src/client/recovery.spec.d.ts.map +1 -0
  76. package/dist/esm/src/client/recovery.spec.js +681 -0
  77. package/dist/esm/src/client/recovery.spec.js.map +1 -0
  78. package/dist/esm/src/client/room-cache-coordinator.d.ts +14 -0
  79. package/dist/esm/src/client/room-cache-coordinator.d.ts.map +1 -0
  80. package/dist/esm/src/client/room-cache-coordinator.js +49 -0
  81. package/dist/esm/src/client/room-cache-coordinator.js.map +1 -0
  82. package/dist/esm/src/client/room-member-cache.spec.d.ts +2 -0
  83. package/dist/esm/src/client/room-member-cache.spec.d.ts.map +1 -0
  84. package/dist/esm/src/client/room-member-cache.spec.js +415 -0
  85. package/dist/esm/src/client/room-member-cache.spec.js.map +1 -0
  86. package/dist/esm/src/client/room-member-load.spec.d.ts +2 -0
  87. package/dist/esm/src/client/room-member-load.spec.d.ts.map +1 -0
  88. package/dist/esm/src/client/room-member-load.spec.js +296 -0
  89. package/dist/esm/src/client/room-member-load.spec.js.map +1 -0
  90. package/dist/esm/src/mod.d.ts +3 -0
  91. package/dist/esm/src/mod.d.ts.map +1 -1
  92. package/dist/esm/src/mod.js +1 -0
  93. package/dist/esm/src/mod.js.map +1 -1
  94. package/dist/esm/src/package-json.js +4 -4
  95. package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
  96. package/dist/esm/src/server/event-stream-manager.js +16 -0
  97. package/dist/esm/src/server/event-stream-manager.js.map +1 -1
  98. package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
  99. package/dist/esm/src/server/puppet-implementation.js +92 -29
  100. package/dist/esm/src/server/puppet-implementation.js.map +1 -1
  101. package/dist/esm/src/utils/pb-payload-helper.d.ts +10 -0
  102. package/dist/esm/src/utils/pb-payload-helper.d.ts.map +1 -1
  103. package/dist/esm/src/utils/pb-payload-helper.js +169 -0
  104. package/dist/esm/src/utils/pb-payload-helper.js.map +1 -1
  105. package/dist/esm/src/utils/room-member-load.spec.d.ts +2 -0
  106. package/dist/esm/src/utils/room-member-load.spec.d.ts.map +1 -0
  107. package/dist/esm/src/utils/room-member-load.spec.js +58 -0
  108. package/dist/esm/src/utils/room-member-load.spec.js.map +1 -0
  109. package/package.json +4 -4
  110. package/src/auth/call-cred.ts +1 -1
  111. package/src/client/grpc-manager.ts +57 -22
  112. package/src/client/payload-store.ts +2 -1
  113. package/src/client/puppet-service.ts +489 -147
  114. package/src/client/recovery.spec.ts +595 -0
  115. package/src/client/recovery.ts +301 -0
  116. package/src/client/room-cache-coordinator.ts +50 -0
  117. package/src/client/room-member-cache.spec.ts +448 -0
  118. package/src/client/room-member-load.spec.ts +319 -0
  119. package/src/mod.ts +3 -0
  120. package/src/package-json.ts +4 -4
  121. package/src/server/event-stream-manager.ts +15 -0
  122. package/src/server/puppet-implementation.ts +94 -29
  123. package/src/utils/pb-payload-helper.ts +150 -0
  124. 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.getMiscellaneousStoreData();
149
- await grpcManager.start(lastEventSeq, accountId);
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', this.onGrpcStreamEvent.bind(this))
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
- async onGrpcStreamEvent(event) {
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
- const loginPayload = JSON.parse(payload);
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 (!config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
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) => this._payloadStore.room?.delete(id),
414
- [PUPPET.types.Dirty.RoomMember]: async (id) => this._payloadStore.roomMember?.delete(id),
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 cachedPayload = await this._payloadStore.room?.get(id);
1584
- if (cachedPayload) {
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
- adminIdList: response.getAdminIdsList(),
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
- await this._payloadStore.room?.set(roomId, puppetPayload);
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 cachedPayload = await this._payloadStore.roomMember?.get(roomId);
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._payloadStore.roomMember?.set(roomId, {
1868
- ...cachedPayload,
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 cachedPayload = await this._payloadStore.roomMember?.get(roomId) || {};
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
- cachedPayload[contactId] = puppetPayload;
2249
+ fetched[contactId] = puppetPayload;
1912
2250
  }
1913
- await this._payloadStore.roomMember?.set(roomId, cachedPayload);
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 (!this._grpcManager) {
2658
- this.log.warn('PuppetService', 'grpc manager not constructed, perform regular reset');
2659
- return super.reset();
2660
- }
2661
- if (!this.isLoggedIn) {
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.grpcManager.stopStream();
2671
- const { lastEventSeq, accountId } = await this.getMiscellaneousStoreData();
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
- let onLogin;
2698
- let onReady;
3130
+ const removeListeners = [];
2699
3131
  const loginFuture = new Promise(resolve => {
2700
- onLogin = onLoginResolve(resolve);
2701
- this.grpcManager.on('data', onLogin);
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
- this.grpcManager.on('data', onReady);
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
- while (true) {
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)(this.grpcManager.startStream(lastEventSeq, accountId), 30 * 1000);
2712
- break;
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 (Date.now() - startTime < timeoutMilliseconds) {
2716
- this.log.warn('failed to start stream, will try again in 15 seconds');
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
- this.waitingForLogin = true;
2729
- this.waitingForReady = true;
2730
- try {
2731
- await (0, gerror_1.timeoutPromise)(loginFuture, ResetLoginTimeout)
2732
- .finally(() => {
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
- this.grpcManager.off('data', onReady);
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 (config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
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 (config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
3232
+ async setMiscellaneousStoreData(data, epoch) {
3233
+ if (!this.eventMetadataEnabled) {
2776
3234
  return;
2777
3235
  }
2778
- if (typeof data.lastEventSeq !== 'undefined') {
2779
- await this._payloadStore.miscellaneous?.set('lastEventSeq', data.lastEventSeq);
3236
+ const changes = new Map();
3237
+ if (data.lastEventTimestamp !== undefined) {
3238
+ changes.set('lastEventTimestamp', data.lastEventTimestamp);
2780
3239
  }
2781
- if (typeof data.lastEventTimestamp !== 'undefined') {
2782
- await this._payloadStore.miscellaneous?.set('lastEventTimestamp', data.lastEventTimestamp);
3240
+ if (data.accountId !== undefined) {
3241
+ changes.set('accountId', data.accountId);
2783
3242
  }
2784
- if (typeof data.accountId !== 'undefined') {
2785
- await this._payloadStore.miscellaneous?.set('accountId', data.accountId);
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 (config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
3248
+ async resetMiscellaneousStoreData(epoch) {
3249
+ if (!this.eventMetadataEnabled) {
2790
3250
  return;
2791
3251
  }
2792
- await this._payloadStore.miscellaneous?.delete('lastEventSeq');
2793
- await this._payloadStore.miscellaneous?.delete('lastEventTimestamp');
2794
- await this._payloadStore.miscellaneous?.delete('accountId');
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;