@juzi/wechaty-puppet-service 1.0.130 → 1.0.131

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 (79) 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/puppet-service.d.ts +25 -2
  5. package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
  6. package/dist/cjs/src/client/puppet-service.js +389 -124
  7. package/dist/cjs/src/client/puppet-service.js.map +1 -1
  8. package/dist/cjs/src/client/room-cache-coordinator.d.ts +14 -0
  9. package/dist/cjs/src/client/room-cache-coordinator.d.ts.map +1 -0
  10. package/dist/cjs/src/client/room-cache-coordinator.js +53 -0
  11. package/dist/cjs/src/client/room-cache-coordinator.js.map +1 -0
  12. package/dist/cjs/src/client/room-member-cache.spec.d.ts +2 -0
  13. package/dist/cjs/src/client/room-member-cache.spec.d.ts.map +1 -0
  14. package/dist/cjs/src/client/room-member-cache.spec.js +417 -0
  15. package/dist/cjs/src/client/room-member-cache.spec.js.map +1 -0
  16. package/dist/cjs/src/client/room-member-load.spec.d.ts +2 -0
  17. package/dist/cjs/src/client/room-member-load.spec.d.ts.map +1 -0
  18. package/dist/cjs/src/client/room-member-load.spec.js +324 -0
  19. package/dist/cjs/src/client/room-member-load.spec.js.map +1 -0
  20. package/dist/cjs/src/package-json.js +4 -4
  21. package/dist/cjs/src/server/event-stream-manager.d.ts.map +1 -1
  22. package/dist/cjs/src/server/event-stream-manager.js +16 -0
  23. package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
  24. package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
  25. package/dist/cjs/src/server/puppet-implementation.js +91 -28
  26. package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
  27. package/dist/cjs/src/utils/pb-payload-helper.d.ts +10 -0
  28. package/dist/cjs/src/utils/pb-payload-helper.d.ts.map +1 -1
  29. package/dist/cjs/src/utils/pb-payload-helper.js +180 -1
  30. package/dist/cjs/src/utils/pb-payload-helper.js.map +1 -1
  31. package/dist/cjs/src/utils/room-member-load.spec.d.ts +2 -0
  32. package/dist/cjs/src/utils/room-member-load.spec.d.ts.map +1 -0
  33. package/dist/cjs/src/utils/room-member-load.spec.js +83 -0
  34. package/dist/cjs/src/utils/room-member-load.spec.js.map +1 -0
  35. package/dist/esm/src/auth/call-cred.d.ts +2 -2
  36. package/dist/esm/src/auth/call-cred.d.ts.map +1 -1
  37. package/dist/esm/src/auth/call-cred.js.map +1 -1
  38. package/dist/esm/src/client/puppet-service.d.ts +25 -2
  39. package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
  40. package/dist/esm/src/client/puppet-service.js +390 -125
  41. package/dist/esm/src/client/puppet-service.js.map +1 -1
  42. package/dist/esm/src/client/room-cache-coordinator.d.ts +14 -0
  43. package/dist/esm/src/client/room-cache-coordinator.d.ts.map +1 -0
  44. package/dist/esm/src/client/room-cache-coordinator.js +49 -0
  45. package/dist/esm/src/client/room-cache-coordinator.js.map +1 -0
  46. package/dist/esm/src/client/room-member-cache.spec.d.ts +2 -0
  47. package/dist/esm/src/client/room-member-cache.spec.d.ts.map +1 -0
  48. package/dist/esm/src/client/room-member-cache.spec.js +415 -0
  49. package/dist/esm/src/client/room-member-cache.spec.js.map +1 -0
  50. package/dist/esm/src/client/room-member-load.spec.d.ts +2 -0
  51. package/dist/esm/src/client/room-member-load.spec.d.ts.map +1 -0
  52. package/dist/esm/src/client/room-member-load.spec.js +296 -0
  53. package/dist/esm/src/client/room-member-load.spec.js.map +1 -0
  54. package/dist/esm/src/package-json.js +4 -4
  55. package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
  56. package/dist/esm/src/server/event-stream-manager.js +16 -0
  57. package/dist/esm/src/server/event-stream-manager.js.map +1 -1
  58. package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
  59. package/dist/esm/src/server/puppet-implementation.js +92 -29
  60. package/dist/esm/src/server/puppet-implementation.js.map +1 -1
  61. package/dist/esm/src/utils/pb-payload-helper.d.ts +10 -0
  62. package/dist/esm/src/utils/pb-payload-helper.d.ts.map +1 -1
  63. package/dist/esm/src/utils/pb-payload-helper.js +169 -0
  64. package/dist/esm/src/utils/pb-payload-helper.js.map +1 -1
  65. package/dist/esm/src/utils/room-member-load.spec.d.ts +2 -0
  66. package/dist/esm/src/utils/room-member-load.spec.d.ts.map +1 -0
  67. package/dist/esm/src/utils/room-member-load.spec.js +58 -0
  68. package/dist/esm/src/utils/room-member-load.spec.js.map +1 -0
  69. package/package.json +4 -4
  70. package/src/auth/call-cred.ts +1 -1
  71. package/src/client/puppet-service.ts +324 -138
  72. package/src/client/room-cache-coordinator.ts +50 -0
  73. package/src/client/room-member-cache.spec.ts +448 -0
  74. package/src/client/room-member-load.spec.ts +319 -0
  75. package/src/package-json.ts +4 -4
  76. package/src/server/event-stream-manager.ts +15 -0
  77. package/src/server/puppet-implementation.ts +94 -29
  78. package/src/utils/pb-payload-helper.ts +150 -0
  79. package/src/utils/room-member-load.spec.ts +67 -0
@@ -57,6 +57,7 @@ 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
59
  const payload_store_js_1 = require("./payload-store.js");
60
+ const room_cache_coordinator_js_1 = require("./room-cache-coordinator.js");
60
61
  const pb_payload_helper_js_1 = require("../utils/pb-payload-helper.js");
61
62
  const call_media_mapping_js_1 = require("../utils/call-media-mapping.js");
62
63
  const gerror_1 = require("gerror");
@@ -76,6 +77,15 @@ class PuppetService extends PUPPET.Puppet {
76
77
  options;
77
78
  static VERSION = config_js_1.VERSION;
78
79
  _payloadStore;
80
+ roomCache = new room_cache_coordinator_js_1.RoomCacheCoordinator();
81
+ eventQueue = Promise.resolve();
82
+ eventEpoch = 0;
83
+ eventsBlocked = false;
84
+ failedEventSeq;
85
+ activeEventSeq;
86
+ pendingEventSeq;
87
+ checkpointQueue = Promise.resolve();
88
+ pendingCheckpointRollback;
79
89
  timeoutMilliseconds;
80
90
  _grpcManager;
81
91
  get grpcManager() {
@@ -129,6 +139,8 @@ class PuppetService extends PUPPET.Puppet {
129
139
  this.log.verbose('PuppetService', 'onStart()');
130
140
  this.waitingForLogin = false;
131
141
  this.waitingForReady = false;
142
+ this.suspendEventProcessing();
143
+ this.eventsBlocked = false;
132
144
  if (this._grpcManager) {
133
145
  this.log.warn('PuppetService', 'onStart() found this.grpc is already existed. dropped.');
134
146
  this._grpcManager = undefined;
@@ -145,7 +157,7 @@ class PuppetService extends PUPPET.Puppet {
145
157
  this.bridgeGrpcEventStream(grpcManager);
146
158
  this.log.info('PuppetService', 'start() setting up bridge grpc event stream ... done');
147
159
  this.log.info('PuppetService', 'start() starting grpc manager...');
148
- const { lastEventSeq, accountId } = await this.getMiscellaneousStoreData();
160
+ const { lastEventSeq, accountId } = await this.eventResumePosition();
149
161
  await grpcManager.start(lastEventSeq, accountId);
150
162
  this.log.info('PuppetService', 'start() starting grpc manager... done');
151
163
  this.log.info('PuppetService', 'start healthCheck');
@@ -154,6 +166,8 @@ class PuppetService extends PUPPET.Puppet {
154
166
  }
155
167
  async onStop() {
156
168
  this.log.info('PuppetService', 'onStop()');
169
+ this.suspendEventProcessing();
170
+ await this.checkpointQueue;
157
171
  if (this._grpcManager) {
158
172
  this.log.info('PuppetService', 'onStop() stopping grpc manager ...');
159
173
  const grpcManager = this._grpcManager;
@@ -189,7 +203,41 @@ class PuppetService extends PUPPET.Puppet {
189
203
  bridgeGrpcEventStream(client) {
190
204
  this.log.verbose('PuppetService', 'bridgeGrpcEventStream(client)');
191
205
  client
192
- .on('data', this.onGrpcStreamEvent.bind(this))
206
+ .on('data', (event) => {
207
+ if (client !== this._grpcManager || this.eventsBlocked) {
208
+ return;
209
+ }
210
+ const epoch = this.eventEpoch;
211
+ this.eventQueue = this.eventQueue.then(async () => {
212
+ if (!this.ownsEventWorker(client, epoch)) {
213
+ return undefined;
214
+ }
215
+ this.activeEventSeq = event.getSeq();
216
+ try {
217
+ if (this.failedEventSeq !== undefined && event.getSeq() > this.failedEventSeq) {
218
+ throw new Error(`Replay skipped failed event ${this.failedEventSeq}; stream halted until reset`);
219
+ }
220
+ await this.onGrpcStreamEvent(event, epoch);
221
+ if (!this.ownsEventWorker(client, epoch)) {
222
+ return undefined;
223
+ }
224
+ if (event.getSeq() === this.failedEventSeq) {
225
+ this.failedEventSeq = undefined;
226
+ }
227
+ }
228
+ catch (error) {
229
+ if (!this.ownsEventWorker(client, epoch)) {
230
+ this.log.error('PuppetService', 'superseded event worker failed: %s', error);
231
+ return undefined;
232
+ }
233
+ this.haltEventStream(client, event, error);
234
+ }
235
+ if (epoch === this.eventEpoch) {
236
+ this.activeEventSeq = undefined;
237
+ }
238
+ return undefined;
239
+ }).catch((error) => { this.reportStreamError(error); });
240
+ })
193
241
  .on('end', () => {
194
242
  this.log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(end)');
195
243
  })
@@ -214,7 +262,128 @@ class PuppetService extends PUPPET.Puppet {
214
262
  this.log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(cancel), %s', JSON.stringify(args));
215
263
  });
216
264
  }
217
- async onGrpcStreamEvent(event) {
265
+ suspendEventProcessing() {
266
+ if (this.activeEventSeq) {
267
+ this.pendingEventSeq ??= this.activeEventSeq;
268
+ }
269
+ this.eventEpoch++;
270
+ this.eventsBlocked = true;
271
+ this.activeEventSeq = undefined;
272
+ this.eventQueue = Promise.resolve();
273
+ }
274
+ ownsEventWorker(manager, epoch) {
275
+ return manager === this._grpcManager && this.isEventEpochCurrent(epoch);
276
+ }
277
+ isEventEpochCurrent(epoch) {
278
+ return epoch === this.eventEpoch && !this.eventsBlocked;
279
+ }
280
+ async eventResumePosition(epoch = this.eventEpoch) {
281
+ return this.withEventCheckpoint(async () => {
282
+ if (this.pendingCheckpointRollback) {
283
+ await this.restoreEventMetadata(this.pendingCheckpointRollback);
284
+ this.pendingCheckpointRollback = undefined;
285
+ }
286
+ const lastEventSeq = await this._payloadStore.miscellaneous?.get('lastEventSeq');
287
+ if (epoch !== this.eventEpoch) {
288
+ return { lastEventSeq };
289
+ }
290
+ if (this.pendingEventSeq !== undefined && this.pendingEventSeq > Number(lastEventSeq ?? 0)) {
291
+ this.failedEventSeq ??= this.pendingEventSeq;
292
+ }
293
+ this.pendingEventSeq = undefined;
294
+ return { lastEventSeq, accountId: await this._payloadStore.miscellaneous?.get('accountId') };
295
+ });
296
+ }
297
+ async withEventCheckpoint(operation) {
298
+ const result = this.checkpointQueue.then(operation);
299
+ this.checkpointQueue = result.then(() => undefined, () => undefined);
300
+ return result;
301
+ }
302
+ async restoreEventMetadata(previous) {
303
+ for (const [key, value] of previous) {
304
+ if (value === undefined) {
305
+ await this._payloadStore.miscellaneous?.delete(key);
306
+ }
307
+ else {
308
+ await this._payloadStore.miscellaneous?.set(key, value);
309
+ }
310
+ }
311
+ }
312
+ async writeEventMetadata(changes, epoch) {
313
+ const isCurrent = () => epoch === undefined || this.isEventEpochCurrent(epoch);
314
+ if (!isCurrent()) {
315
+ return;
316
+ }
317
+ const previous = new Map();
318
+ for (const key of changes.keys()) {
319
+ previous.set(key, await this._payloadStore.miscellaneous?.get(key));
320
+ }
321
+ if (!isCurrent()) {
322
+ return;
323
+ }
324
+ try {
325
+ for (const [key, value] of changes) {
326
+ if (value === undefined) {
327
+ await this._payloadStore.miscellaneous?.delete(key);
328
+ }
329
+ else {
330
+ await this._payloadStore.miscellaneous?.set(key, value);
331
+ }
332
+ if (!isCurrent()) {
333
+ await this.restoreEventMetadata(previous);
334
+ return;
335
+ }
336
+ }
337
+ }
338
+ catch (error) {
339
+ try {
340
+ await this.restoreEventMetadata(previous);
341
+ }
342
+ catch (rollbackError) {
343
+ this.pendingCheckpointRollback = previous;
344
+ this.log.error('PuppetService', 'checkpoint rollback failed: %s', rollbackError);
345
+ }
346
+ throw error;
347
+ }
348
+ }
349
+ async commitEventCheckpoint(seq, timestamp, epoch) {
350
+ await this.withEventCheckpoint(async () => {
351
+ if (!this.isEventEpochCurrent(epoch)) {
352
+ return;
353
+ }
354
+ const lastEventSeq = await this._payloadStore.miscellaneous?.get('lastEventSeq');
355
+ if (lastEventSeq && seq <= Number(lastEventSeq) && seq !== 1) {
356
+ return;
357
+ }
358
+ await this.writeEventMetadata(new Map([
359
+ ['lastEventTimestamp', timestamp], ['lastEventSeq', String(seq)],
360
+ ]), epoch);
361
+ });
362
+ }
363
+ reportStreamError(error) {
364
+ this.log.error('PuppetService', 'event processing failed: %s', error);
365
+ try {
366
+ this.emit('error', error);
367
+ }
368
+ catch (listenerError) {
369
+ this.log.error('PuppetService', 'error listener failed: %s', listenerError);
370
+ }
371
+ }
372
+ haltEventStream(manager, event, error) {
373
+ this.failedEventSeq ??= event.getSeq();
374
+ this.suspendEventProcessing();
375
+ try {
376
+ manager.stopStream();
377
+ }
378
+ catch (stopError) {
379
+ this.log.error('PuppetService', 'stopping failed event stream failed: %s', stopError);
380
+ }
381
+ this.reportStreamError(error);
382
+ }
383
+ async onGrpcStreamEvent(event, epoch = this.eventEpoch) {
384
+ if (!this.isEventEpochCurrent(epoch)) {
385
+ return;
386
+ }
218
387
  const type = event.getType();
219
388
  const payload = event.getPayload();
220
389
  const seq = event.getSeq();
@@ -228,15 +397,6 @@ class PuppetService extends PUPPET.Puppet {
228
397
  data: `onGrpcStreamEvent(${event_type_rev_js_1.EventTypeRev[type]})`,
229
398
  });
230
399
  }
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
400
  switch (type) {
241
401
  case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_DONG:
242
402
  this.emit('dong', JSON.parse(payload));
@@ -259,11 +419,14 @@ class PuppetService extends PUPPET.Puppet {
259
419
  const loginPayload = JSON.parse(payload);
260
420
  if (!config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
261
421
  const { accountId } = await this.getMiscellaneousStoreData();
422
+ if (!this.isEventEpochCurrent(epoch)) {
423
+ return;
424
+ }
262
425
  if (accountId !== loginPayload.contactId) {
263
- await this.resetMiscellaneousStoreData();
426
+ await this.resetMiscellaneousStoreData(epoch);
264
427
  await this.setMiscellaneousStoreData({
265
428
  accountId: loginPayload.contactId,
266
- });
429
+ }, epoch);
267
430
  }
268
431
  }
269
432
  (async () => this.login(loginPayload.contactId))().catch(e => this.log.error('PuppetService', 'onGrpcStreamEvent() this.login() rejection %s', e.message));
@@ -273,7 +436,7 @@ class PuppetService extends PUPPET.Puppet {
273
436
  {
274
437
  const logoutPayload = JSON.parse(payload);
275
438
  if (!config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
276
- await this.resetMiscellaneousStoreData();
439
+ await this.resetMiscellaneousStoreData(epoch);
277
440
  }
278
441
  ;
279
442
  (async () => this.logout(logoutPayload.data))().catch(e => this.log.error('PuppetService', 'onGrpcStreamEvent() this.logout() rejection %s', e.message));
@@ -281,8 +444,25 @@ class PuppetService extends PUPPET.Puppet {
281
444
  break;
282
445
  case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_DIRTY:
283
446
  await this.fastDirty(JSON.parse(payload));
447
+ if (!this.isEventEpochCurrent(epoch)) {
448
+ return;
449
+ }
284
450
  this.emit('dirty', JSON.parse(payload));
285
451
  break;
452
+ case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_ROOM_MEMBER_LOAD: {
453
+ const loadPayload = JSON.parse(payload);
454
+ (0, pb_payload_helper_js_1.validateRoomMemberLoadState)(loadPayload);
455
+ this.roomCache.invalidate(loadPayload.roomId);
456
+ await this.roomCache.run(loadPayload.roomId, async () => {
457
+ await this._payloadStore.room?.delete(loadPayload.roomId);
458
+ await this._payloadStore.roomMember?.delete(loadPayload.roomId);
459
+ });
460
+ if (!this.isEventEpochCurrent(epoch)) {
461
+ return;
462
+ }
463
+ this.emit('room-member-load', loadPayload);
464
+ break;
465
+ }
286
466
  case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_MESSAGE:
287
467
  this.emit('message', JSON.parse(payload));
288
468
  break;
@@ -355,6 +535,9 @@ class PuppetService extends PUPPET.Puppet {
355
535
  // Huan(202003): in default, the `type` type should be `never`, please check.
356
536
  this.log.error(`eventType ${type} unsupported! data: ${payload}`);
357
537
  }
538
+ if (seq && this.isEventEpochCurrent(epoch) && !config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
539
+ await this.commitEventCheckpoint(seq, timestamp, epoch);
540
+ }
358
541
  }
359
542
  async logout(reason) {
360
543
  this.log.verbose('PuppetService', 'logout(%s)', reason ? `"${reason}"` : '');
@@ -410,8 +593,14 @@ class PuppetService extends PUPPET.Puppet {
410
593
  [PUPPET.types.Dirty.Friendship]: async (_) => { },
411
594
  [PUPPET.types.Dirty.Message]: async (_) => { },
412
595
  [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),
596
+ [PUPPET.types.Dirty.Room]: async (id) => {
597
+ this.roomCache.invalidate(id);
598
+ return this.roomCache.run(id, async () => this._payloadStore.room?.delete(id));
599
+ },
600
+ [PUPPET.types.Dirty.RoomMember]: async (id) => {
601
+ this.roomCache.invalidate(id);
602
+ return this.roomCache.run(id, async () => this._payloadStore.roomMember?.delete(id));
603
+ },
415
604
  [PUPPET.types.Dirty.Tag]: async (id) => this._payloadStore.tag?.delete(id),
416
605
  [PUPPET.types.Dirty.TagGroup]: async (id) => this._payloadStore.tagGroup?.delete(id),
417
606
  [PUPPET.types.Dirty.Unspecified]: async (id) => { throw new Error('Unspecified type with id: ' + id); },
@@ -1580,8 +1769,9 @@ class PuppetService extends PUPPET.Puppet {
1580
1769
  */
1581
1770
  async roomRawPayload(id) {
1582
1771
  this.log.verbose('PuppetService', 'roomRawPayload(%s)', id);
1583
- const cachedPayload = await this._payloadStore.room?.get(id);
1584
- if (cachedPayload) {
1772
+ const generation = this.roomCache.generation(id);
1773
+ const cachedPayload = await this.roomCache.run(id, async () => this._payloadStore.room?.get(id));
1774
+ if (cachedPayload && generation === this.roomCache.generation(id)) {
1585
1775
  this.log.silly('PuppetService', 'roomRawPayload(%s) cache HIT', id);
1586
1776
  return cachedPayload;
1587
1777
  }
@@ -1589,26 +1779,52 @@ class PuppetService extends PUPPET.Puppet {
1589
1779
  request.setId(id);
1590
1780
  const response = await util_1.default.promisify(this.grpcManager.client.roomPayload
1591
1781
  .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);
1782
+ const payload = (0, pb_payload_helper_js_1.roomPbToPayload)(response);
1783
+ await this.roomCache.commit(id, generation, async () => this._payloadStore.room?.set(id, payload));
1609
1784
  this.log.silly('PuppetService', 'roomRawPayload(%s) cache SET', id);
1610
1785
  return payload;
1611
1786
  }
1787
+ async roomMemberSync(roomId, options) {
1788
+ if (!roomId) {
1789
+ throw new Error('roomId is required');
1790
+ }
1791
+ const request = new wechaty_grpc_1.puppet.RoomMemberSyncRequest();
1792
+ request.setId(roomId);
1793
+ if (options?.force !== undefined) {
1794
+ request.setForce(options.force);
1795
+ }
1796
+ const response = await util_1.default.promisify(this.grpcManager.client.roomMemberSync.bind(this.grpcManager.client))(request);
1797
+ return (0, pb_payload_helper_js_1.roomMemberLoadStatePbToPayload)(response, roomId);
1798
+ }
1799
+ async roomMemberLoadState(roomId) {
1800
+ if (!roomId) {
1801
+ throw new Error('roomId is required');
1802
+ }
1803
+ const request = new wechaty_grpc_1.puppet.RoomMemberLoadStateRequest();
1804
+ request.setId(roomId);
1805
+ const response = await util_1.default.promisify(this.grpcManager.client.roomMemberLoadState.bind(this.grpcManager.client))(request);
1806
+ return (0, pb_payload_helper_js_1.roomMemberLoadStatePbToPayload)(response, roomId);
1807
+ }
1808
+ async roomMemberSnapshot(roomId, revision, options) {
1809
+ if (!roomId) {
1810
+ throw new Error('roomId is required');
1811
+ }
1812
+ (0, pb_payload_helper_js_1.validateRoomMemberRevision)(revision);
1813
+ const request = new wechaty_grpc_1.puppet.RoomMemberSnapshotRequest();
1814
+ request.setId(roomId);
1815
+ request.setRevision(revision);
1816
+ if (options?.pageToken !== undefined) {
1817
+ request.setPageToken(options.pageToken);
1818
+ }
1819
+ if (options?.pageSize !== undefined) {
1820
+ if (!Number.isInteger(options.pageSize) || options.pageSize < 0 || options.pageSize > 500) {
1821
+ throw new Error('pageSize must be an integer between 0 and 500');
1822
+ }
1823
+ request.setPageSize(options.pageSize);
1824
+ }
1825
+ const response = await util_1.default.promisify(this.grpcManager.client.roomMemberSnapshot.bind(this.grpcManager.client))(request);
1826
+ return (0, pb_payload_helper_js_1.roomMemberSnapshotPbToPayload)(response, roomId, revision);
1827
+ }
1612
1828
  async roomRawPayloadParser(payload) {
1613
1829
  // this.log.silly('PuppetService', 'roomRawPayloadParser({id:%s})', payload.id)
1614
1830
  // passthrough
@@ -1618,10 +1834,11 @@ class PuppetService extends PUPPET.Puppet {
1618
1834
  this.log.verbose('PuppetService', 'batchRoomRawPayload(%s)', roomIdList);
1619
1835
  const result = new Map();
1620
1836
  const roomIdSet = new Set(roomIdList);
1837
+ const generations = new Map(Array.from(roomIdSet, id => [id, this.roomCache.generation(id)]));
1621
1838
  const needGetSet = new Set();
1622
1839
  for (const roomId of roomIdSet) {
1623
- const cachedRoomPayload = await this._payloadStore.room?.get(roomId);
1624
- if (cachedRoomPayload) {
1840
+ const cachedRoomPayload = await this.roomCache.run(roomId, async () => this._payloadStore.room?.get(roomId));
1841
+ if (cachedRoomPayload && generations.get(roomId) === this.roomCache.generation(roomId)) {
1625
1842
  result.set(roomId, cachedRoomPayload);
1626
1843
  }
1627
1844
  else {
@@ -1637,27 +1854,18 @@ class PuppetService extends PUPPET.Puppet {
1637
1854
  const payloads = response.getRoomPayloadsList();
1638
1855
  for (const payload of payloads) {
1639
1856
  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
- }
1857
+ const puppetPayload = (0, pb_payload_helper_js_1.roomPbToPayload)(payload);
1656
1858
  result.set(roomId, puppetPayload);
1657
- await this._payloadStore.room?.set(roomId, puppetPayload);
1859
+ const generation = generations.get(roomId);
1860
+ if (generation !== undefined) {
1861
+ await this.roomCache.commit(roomId, generation, async () => this._payloadStore.room?.set(roomId, puppetPayload));
1862
+ }
1658
1863
  }
1659
1864
  }
1660
1865
  catch (e) {
1866
+ if (e instanceof room_cache_coordinator_js_1.RoomCacheOperationError) {
1867
+ throw e;
1868
+ }
1661
1869
  this.log.error('PuppetService', 'batchRoomRawPayload(%s, %s) error: %s, use one by one method', roomIdList, needGetSet, e);
1662
1870
  let fallbackSuccessCount = 0;
1663
1871
  for (const roomId of needGetSet) {
@@ -1843,9 +2051,10 @@ class PuppetService extends PUPPET.Puppet {
1843
2051
  }
1844
2052
  async roomMemberRawPayload(roomId, contactId) {
1845
2053
  this.log.verbose('PuppetService', 'roomMemberRawPayload(%s, %s)', roomId, contactId);
1846
- const cachedPayload = await this._payloadStore.roomMember?.get(roomId);
2054
+ const generation = this.roomCache.generation(roomId);
2055
+ const cachedPayload = await this.roomCache.run(roomId, async () => this._payloadStore.roomMember?.get(roomId));
1847
2056
  const cachedRoomMemberPayload = cachedPayload && cachedPayload[contactId];
1848
- if (cachedRoomMemberPayload) {
2057
+ if (cachedRoomMemberPayload && generation === this.roomCache.generation(roomId)) {
1849
2058
  this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache HIT', roomId, contactId);
1850
2059
  return cachedRoomMemberPayload;
1851
2060
  }
@@ -1864,9 +2073,9 @@ class PuppetService extends PUPPET.Puppet {
1864
2073
  joinScene: response.getJoinScene(),
1865
2074
  joinTime: response.getJoinTime(),
1866
2075
  };
1867
- await this._payloadStore.roomMember?.set(roomId, {
1868
- ...cachedPayload,
1869
- [contactId]: payload,
2076
+ await this.roomCache.commit(roomId, generation, async () => {
2077
+ const current = await this._payloadStore.roomMember?.get(roomId);
2078
+ await this._payloadStore.roomMember?.set(roomId, { ...current, [contactId]: payload });
1870
2079
  });
1871
2080
  this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache SET', roomId, contactId);
1872
2081
  return payload;
@@ -1878,10 +2087,12 @@ class PuppetService extends PUPPET.Puppet {
1878
2087
  }
1879
2088
  async batchRoomMemberRawPayload(roomId, contactIdList) {
1880
2089
  this.log.verbose('PuppetService', 'batchRoomMemberRawPayload(%s, %s)', roomId, contactIdList);
2090
+ const generation = this.roomCache.generation(roomId);
1881
2091
  const result = new Map();
1882
2092
  const contactIdSet = new Set(contactIdList);
1883
2093
  let needGetSet = new Set();
1884
- const cachedPayload = await this._payloadStore.roomMember?.get(roomId) || {};
2094
+ const cached = await this.roomCache.run(roomId, async () => this._payloadStore.roomMember?.get(roomId));
2095
+ const cachedPayload = generation === this.roomCache.generation(roomId) ? cached || {} : {};
1885
2096
  if (Object.keys(cachedPayload).length > 0) {
1886
2097
  for (const contactId of contactIdSet) {
1887
2098
  const cachedRoomMemberPayload = cachedPayload[contactId];
@@ -1904,15 +2115,22 @@ class PuppetService extends PUPPET.Puppet {
1904
2115
  const response = await util_1.default.promisify(this.grpcManager.client.batchRoomMemberPayload
1905
2116
  .bind(this.grpcManager.client))(request);
1906
2117
  const payloads = response.getMemberPayloadsList();
2118
+ const fetched = {};
1907
2119
  for (const payload of payloads) {
1908
2120
  const contactId = payload.getId();
1909
2121
  const puppetPayload = (0, pb_payload_helper_js_1.roomMemberPbToPayload)(payload);
1910
2122
  result.set(contactId, puppetPayload);
1911
- cachedPayload[contactId] = puppetPayload;
2123
+ fetched[contactId] = puppetPayload;
1912
2124
  }
1913
- await this._payloadStore.roomMember?.set(roomId, cachedPayload);
2125
+ await this.roomCache.commit(roomId, generation, async () => {
2126
+ const current = await this._payloadStore.roomMember?.get(roomId);
2127
+ await this._payloadStore.roomMember?.set(roomId, { ...current, ...fetched });
2128
+ });
1914
2129
  }
1915
2130
  catch (e) {
2131
+ if (e instanceof room_cache_coordinator_js_1.RoomCacheOperationError) {
2132
+ throw e;
2133
+ }
1916
2134
  this.log.error('PuppetService', 'batchRoomMemberRawPayload(%s, %s) error: %s, use one by one method', roomId, needGetSet, e);
1917
2135
  for (const contactId of needGetSet) {
1918
2136
  const payload = await this.roomMemberRawPayload(roomId, contactId);
@@ -2654,23 +2872,40 @@ class PuppetService extends PUPPET.Puppet {
2654
2872
  waitingForReady = false;
2655
2873
  reconnectIndicator;
2656
2874
  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();
2664
- }
2665
2875
  if (this.reconnectIndicator.value()) {
2666
2876
  this.log.warn('PuppetService', 'already trying to reconnect, pass this one');
2667
2877
  return;
2668
2878
  }
2669
2879
  this.reconnectIndicator.value(true);
2670
- this.grpcManager.stopStream();
2671
- const { lastEventSeq, accountId } = await this.getMiscellaneousStoreData();
2880
+ this.suspendEventProcessing();
2881
+ const epoch = this.eventEpoch;
2882
+ const manager = this._grpcManager;
2883
+ try {
2884
+ await this.checkpointQueue;
2885
+ if (epoch !== this.eventEpoch) {
2886
+ return;
2887
+ }
2888
+ if (!manager || !this.isLoggedIn) {
2889
+ this.log.warn('PuppetService', 'grpc manager unavailable or puppet logged out, perform regular reset');
2890
+ return await super.reset();
2891
+ }
2892
+ await this.resetEventStream(manager, epoch);
2893
+ }
2894
+ finally {
2895
+ this.reconnectIndicator.value(false);
2896
+ }
2897
+ }
2898
+ async resetEventStream(manager, epoch) {
2899
+ manager.stopStream();
2900
+ const { lastEventSeq, accountId } = await this.eventResumePosition(epoch);
2901
+ if (epoch !== this.eventEpoch || manager !== this._grpcManager) {
2902
+ return;
2903
+ }
2672
2904
  const onLoginResolve = (resolve) => {
2673
2905
  const onLogin = (event) => {
2906
+ if (!this.ownsEventWorker(manager, epoch)) {
2907
+ return;
2908
+ }
2674
2909
  const type = event.getType();
2675
2910
  const payload = event.getPayload();
2676
2911
  if (this.waitingForLogin && type === wechaty_grpc_1.puppet.EventType.EVENT_TYPE_LOGIN) {
@@ -2686,6 +2921,9 @@ class PuppetService extends PUPPET.Puppet {
2686
2921
  };
2687
2922
  const onReadyResolve = (resolve) => {
2688
2923
  const onReady = (event) => {
2924
+ if (!this.ownsEventWorker(manager, epoch)) {
2925
+ return;
2926
+ }
2689
2927
  const type = event.getType();
2690
2928
  if (this.waitingForReady && type === wechaty_grpc_1.puppet.EventType.EVENT_TYPE_READY) {
2691
2929
  this.waitingForReady = false;
@@ -2694,60 +2932,85 @@ class PuppetService extends PUPPET.Puppet {
2694
2932
  };
2695
2933
  return onReady;
2696
2934
  };
2697
- let onLogin;
2698
- let onReady;
2935
+ const removeListeners = [];
2699
2936
  const loginFuture = new Promise(resolve => {
2700
- onLogin = onLoginResolve(resolve);
2701
- this.grpcManager.on('data', onLogin);
2937
+ const onLogin = onLoginResolve(resolve);
2938
+ manager.on('data', onLogin);
2939
+ removeListeners.push(() => { manager.off('data', onLogin); });
2702
2940
  });
2703
2941
  const readyFuture = new Promise(resolve => {
2704
- onReady = onReadyResolve(resolve);
2705
- this.grpcManager.on('data', onReady);
2942
+ const onReady = onReadyResolve(resolve);
2943
+ manager.on('data', onReady);
2944
+ removeListeners.push(() => { manager.off('data', onReady); });
2706
2945
  });
2707
2946
  const startTime = Date.now();
2708
2947
  const timeoutMilliseconds = this.timeoutMilliseconds / 10; // 2 min default, 4 min xiaoju-bot
2709
- while (true) {
2948
+ this.waitingForLogin = true;
2949
+ this.waitingForReady = true;
2950
+ this.eventsBlocked = false;
2951
+ try {
2952
+ while (true) {
2953
+ try {
2954
+ if (!this.ownsEventWorker(manager, epoch)) {
2955
+ return;
2956
+ }
2957
+ await (0, gerror_1.timeoutPromise)(manager.startStream(lastEventSeq, accountId), 30 * 1000);
2958
+ if (!this.ownsEventWorker(manager, epoch)) {
2959
+ return;
2960
+ }
2961
+ break;
2962
+ }
2963
+ catch (e) {
2964
+ if (!this.ownsEventWorker(manager, epoch)) {
2965
+ return;
2966
+ }
2967
+ if (Date.now() - startTime < timeoutMilliseconds) {
2968
+ this.log.warn('failed to start stream, will try again in 15 seconds');
2969
+ await new Promise(resolve => {
2970
+ setTimeout(resolve, 5000);
2971
+ });
2972
+ }
2973
+ else {
2974
+ this.log.warn('failed to start stream and reaches timeout, will perform regular reset');
2975
+ return super.reset();
2976
+ }
2977
+ }
2978
+ }
2710
2979
  try {
2711
- await (0, gerror_1.timeoutPromise)(this.grpcManager.startStream(lastEventSeq, accountId), 30 * 1000);
2712
- break;
2980
+ await (0, gerror_1.timeoutPromise)(loginFuture, ResetLoginTimeout)
2981
+ .finally(() => {
2982
+ if (epoch === this.eventEpoch) {
2983
+ this.waitingForLogin = false;
2984
+ }
2985
+ });
2713
2986
  }
2714
2987
  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();
2988
+ if (!this.ownsEventWorker(manager, epoch)) {
2989
+ return;
2725
2990
  }
2991
+ this.log.warn('PuppetService', 'waiting for event reset login error, will perform regular reset');
2992
+ return super.reset();
2993
+ }
2994
+ try {
2995
+ await (0, gerror_1.timeoutPromise)(readyFuture, ResetReadyTimeout)
2996
+ .finally(() => {
2997
+ if (epoch === this.eventEpoch) {
2998
+ this.waitingForReady = false;
2999
+ }
3000
+ });
3001
+ }
3002
+ catch (e) {
3003
+ this.log.warn('PuppetService', 'waiting for event reset ready error, will do nothing');
2726
3004
  }
2727
3005
  }
2728
- this.waitingForLogin = true;
2729
- this.waitingForReady = true;
2730
- try {
2731
- await (0, gerror_1.timeoutPromise)(loginFuture, ResetLoginTimeout)
2732
- .finally(() => {
3006
+ finally {
3007
+ for (const removeListener of removeListeners) {
3008
+ removeListener();
3009
+ }
3010
+ if (epoch === this.eventEpoch) {
2733
3011
  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
3012
  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');
3013
+ }
2751
3014
  }
2752
3015
  }
2753
3016
  async getMiscellaneousStoreData() {
@@ -2771,27 +3034,29 @@ class PuppetService extends PUPPET.Puppet {
2771
3034
  accountId,
2772
3035
  };
2773
3036
  }
2774
- async setMiscellaneousStoreData(data) {
3037
+ async setMiscellaneousStoreData(data, epoch) {
2775
3038
  if (config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
2776
3039
  return;
2777
3040
  }
2778
- if (typeof data.lastEventSeq !== 'undefined') {
2779
- await this._payloadStore.miscellaneous?.set('lastEventSeq', data.lastEventSeq);
3041
+ const changes = new Map();
3042
+ if (data.lastEventTimestamp !== undefined) {
3043
+ changes.set('lastEventTimestamp', data.lastEventTimestamp);
2780
3044
  }
2781
- if (typeof data.lastEventTimestamp !== 'undefined') {
2782
- await this._payloadStore.miscellaneous?.set('lastEventTimestamp', data.lastEventTimestamp);
3045
+ if (data.accountId !== undefined) {
3046
+ changes.set('accountId', data.accountId);
2783
3047
  }
2784
- if (typeof data.accountId !== 'undefined') {
2785
- await this._payloadStore.miscellaneous?.set('accountId', data.accountId);
3048
+ if (data.lastEventSeq !== undefined) {
3049
+ changes.set('lastEventSeq', data.lastEventSeq);
2786
3050
  }
3051
+ await this.withEventCheckpoint(async () => this.writeEventMetadata(changes, epoch));
2787
3052
  }
2788
- async resetMiscellaneousStoreData() {
3053
+ async resetMiscellaneousStoreData(epoch) {
2789
3054
  if (config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
2790
3055
  return;
2791
3056
  }
2792
- await this._payloadStore.miscellaneous?.delete('lastEventSeq');
2793
- await this._payloadStore.miscellaneous?.delete('lastEventTimestamp');
2794
- await this._payloadStore.miscellaneous?.delete('accountId');
3057
+ await this.withEventCheckpoint(async () => this.writeEventMetadata(new Map([
3058
+ ['lastEventSeq', undefined], ['lastEventTimestamp', undefined], ['accountId', undefined],
3059
+ ]), epoch));
2795
3060
  }
2796
3061
  }
2797
3062
  exports.PuppetService = PuppetService;