@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
@@ -28,7 +28,8 @@ import { EventTypeRev, } from '../event-type-rev.js';
28
28
  import { packageJson } from '../package-json.js';
29
29
  import { GrpcManager } from './grpc-manager.js';
30
30
  import { PayloadStore } from './payload-store.js';
31
- import { OptionalBooleanUnwrapper, OptionalBooleanWrapper, callRecordPbToPayload, channelPayloadToPb, channelPbToPayload, chatHistoryPbToPayload, contactPbToPayload, postPayloadToPb, roomMemberPbToPayload, urlLinkPbToPayload, channelCardPayloadToPb, channelCardPbToPayload, miniProgramPayloadToPb, urlLinkPayloadToPb, locationPayloadToPb } from '../utils/pb-payload-helper.js';
31
+ import { RoomCacheCoordinator, RoomCacheOperationError } from './room-cache-coordinator.js';
32
+ import { OptionalBooleanUnwrapper, OptionalBooleanWrapper, callRecordPbToPayload, channelPayloadToPb, channelPbToPayload, chatHistoryPbToPayload, contactPbToPayload, postPayloadToPb, roomMemberPbToPayload, urlLinkPbToPayload, channelCardPayloadToPb, channelCardPbToPayload, miniProgramPayloadToPb, urlLinkPayloadToPb, locationPayloadToPb, roomPbToPayload, roomMemberLoadStatePbToPayload, roomMemberSnapshotPbToPayload, validateRoomMemberRevision, validateRoomMemberLoadState } from '../utils/pb-payload-helper.js';
32
33
  import { puppetCallMediaTypeToGrpc, grpcCallTypeToPuppetMedia } from '../utils/call-media-mapping.js';
33
34
  import { timeoutPromise } from 'gerror';
34
35
  import { BooleanIndicator } from 'state-switch';
@@ -47,6 +48,15 @@ class PuppetService extends PUPPET.Puppet {
47
48
  options;
48
49
  static VERSION = VERSION;
49
50
  _payloadStore;
51
+ roomCache = new RoomCacheCoordinator();
52
+ eventQueue = Promise.resolve();
53
+ eventEpoch = 0;
54
+ eventsBlocked = false;
55
+ failedEventSeq;
56
+ activeEventSeq;
57
+ pendingEventSeq;
58
+ checkpointQueue = Promise.resolve();
59
+ pendingCheckpointRollback;
50
60
  timeoutMilliseconds;
51
61
  _grpcManager;
52
62
  get grpcManager() {
@@ -100,6 +110,8 @@ class PuppetService extends PUPPET.Puppet {
100
110
  this.log.verbose('PuppetService', 'onStart()');
101
111
  this.waitingForLogin = false;
102
112
  this.waitingForReady = false;
113
+ this.suspendEventProcessing();
114
+ this.eventsBlocked = false;
103
115
  if (this._grpcManager) {
104
116
  this.log.warn('PuppetService', 'onStart() found this.grpc is already existed. dropped.');
105
117
  this._grpcManager = undefined;
@@ -116,7 +128,7 @@ class PuppetService extends PUPPET.Puppet {
116
128
  this.bridgeGrpcEventStream(grpcManager);
117
129
  this.log.info('PuppetService', 'start() setting up bridge grpc event stream ... done');
118
130
  this.log.info('PuppetService', 'start() starting grpc manager...');
119
- const { lastEventSeq, accountId } = await this.getMiscellaneousStoreData();
131
+ const { lastEventSeq, accountId } = await this.eventResumePosition();
120
132
  await grpcManager.start(lastEventSeq, accountId);
121
133
  this.log.info('PuppetService', 'start() starting grpc manager... done');
122
134
  this.log.info('PuppetService', 'start healthCheck');
@@ -125,6 +137,8 @@ class PuppetService extends PUPPET.Puppet {
125
137
  }
126
138
  async onStop() {
127
139
  this.log.info('PuppetService', 'onStop()');
140
+ this.suspendEventProcessing();
141
+ await this.checkpointQueue;
128
142
  if (this._grpcManager) {
129
143
  this.log.info('PuppetService', 'onStop() stopping grpc manager ...');
130
144
  const grpcManager = this._grpcManager;
@@ -160,7 +174,41 @@ class PuppetService extends PUPPET.Puppet {
160
174
  bridgeGrpcEventStream(client) {
161
175
  this.log.verbose('PuppetService', 'bridgeGrpcEventStream(client)');
162
176
  client
163
- .on('data', this.onGrpcStreamEvent.bind(this))
177
+ .on('data', (event) => {
178
+ if (client !== this._grpcManager || this.eventsBlocked) {
179
+ return;
180
+ }
181
+ const epoch = this.eventEpoch;
182
+ this.eventQueue = this.eventQueue.then(async () => {
183
+ if (!this.ownsEventWorker(client, epoch)) {
184
+ return undefined;
185
+ }
186
+ this.activeEventSeq = event.getSeq();
187
+ try {
188
+ if (this.failedEventSeq !== undefined && event.getSeq() > this.failedEventSeq) {
189
+ throw new Error(`Replay skipped failed event ${this.failedEventSeq}; stream halted until reset`);
190
+ }
191
+ await this.onGrpcStreamEvent(event, epoch);
192
+ if (!this.ownsEventWorker(client, epoch)) {
193
+ return undefined;
194
+ }
195
+ if (event.getSeq() === this.failedEventSeq) {
196
+ this.failedEventSeq = undefined;
197
+ }
198
+ }
199
+ catch (error) {
200
+ if (!this.ownsEventWorker(client, epoch)) {
201
+ this.log.error('PuppetService', 'superseded event worker failed: %s', error);
202
+ return undefined;
203
+ }
204
+ this.haltEventStream(client, event, error);
205
+ }
206
+ if (epoch === this.eventEpoch) {
207
+ this.activeEventSeq = undefined;
208
+ }
209
+ return undefined;
210
+ }).catch((error) => { this.reportStreamError(error); });
211
+ })
164
212
  .on('end', () => {
165
213
  this.log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(end)');
166
214
  })
@@ -185,7 +233,128 @@ class PuppetService extends PUPPET.Puppet {
185
233
  this.log.verbose('PuppetService', 'bridgeGrpcEventStream() eventStream.on(cancel), %s', JSON.stringify(args));
186
234
  });
187
235
  }
188
- async onGrpcStreamEvent(event) {
236
+ suspendEventProcessing() {
237
+ if (this.activeEventSeq) {
238
+ this.pendingEventSeq ??= this.activeEventSeq;
239
+ }
240
+ this.eventEpoch++;
241
+ this.eventsBlocked = true;
242
+ this.activeEventSeq = undefined;
243
+ this.eventQueue = Promise.resolve();
244
+ }
245
+ ownsEventWorker(manager, epoch) {
246
+ return manager === this._grpcManager && this.isEventEpochCurrent(epoch);
247
+ }
248
+ isEventEpochCurrent(epoch) {
249
+ return epoch === this.eventEpoch && !this.eventsBlocked;
250
+ }
251
+ async eventResumePosition(epoch = this.eventEpoch) {
252
+ return this.withEventCheckpoint(async () => {
253
+ if (this.pendingCheckpointRollback) {
254
+ await this.restoreEventMetadata(this.pendingCheckpointRollback);
255
+ this.pendingCheckpointRollback = undefined;
256
+ }
257
+ const lastEventSeq = await this._payloadStore.miscellaneous?.get('lastEventSeq');
258
+ if (epoch !== this.eventEpoch) {
259
+ return { lastEventSeq };
260
+ }
261
+ if (this.pendingEventSeq !== undefined && this.pendingEventSeq > Number(lastEventSeq ?? 0)) {
262
+ this.failedEventSeq ??= this.pendingEventSeq;
263
+ }
264
+ this.pendingEventSeq = undefined;
265
+ return { lastEventSeq, accountId: await this._payloadStore.miscellaneous?.get('accountId') };
266
+ });
267
+ }
268
+ async withEventCheckpoint(operation) {
269
+ const result = this.checkpointQueue.then(operation);
270
+ this.checkpointQueue = result.then(() => undefined, () => undefined);
271
+ return result;
272
+ }
273
+ async restoreEventMetadata(previous) {
274
+ for (const [key, value] of previous) {
275
+ if (value === undefined) {
276
+ await this._payloadStore.miscellaneous?.delete(key);
277
+ }
278
+ else {
279
+ await this._payloadStore.miscellaneous?.set(key, value);
280
+ }
281
+ }
282
+ }
283
+ async writeEventMetadata(changes, epoch) {
284
+ const isCurrent = () => epoch === undefined || this.isEventEpochCurrent(epoch);
285
+ if (!isCurrent()) {
286
+ return;
287
+ }
288
+ const previous = new Map();
289
+ for (const key of changes.keys()) {
290
+ previous.set(key, await this._payloadStore.miscellaneous?.get(key));
291
+ }
292
+ if (!isCurrent()) {
293
+ return;
294
+ }
295
+ try {
296
+ for (const [key, value] of changes) {
297
+ if (value === undefined) {
298
+ await this._payloadStore.miscellaneous?.delete(key);
299
+ }
300
+ else {
301
+ await this._payloadStore.miscellaneous?.set(key, value);
302
+ }
303
+ if (!isCurrent()) {
304
+ await this.restoreEventMetadata(previous);
305
+ return;
306
+ }
307
+ }
308
+ }
309
+ catch (error) {
310
+ try {
311
+ await this.restoreEventMetadata(previous);
312
+ }
313
+ catch (rollbackError) {
314
+ this.pendingCheckpointRollback = previous;
315
+ this.log.error('PuppetService', 'checkpoint rollback failed: %s', rollbackError);
316
+ }
317
+ throw error;
318
+ }
319
+ }
320
+ async commitEventCheckpoint(seq, timestamp, epoch) {
321
+ await this.withEventCheckpoint(async () => {
322
+ if (!this.isEventEpochCurrent(epoch)) {
323
+ return;
324
+ }
325
+ const lastEventSeq = await this._payloadStore.miscellaneous?.get('lastEventSeq');
326
+ if (lastEventSeq && seq <= Number(lastEventSeq) && seq !== 1) {
327
+ return;
328
+ }
329
+ await this.writeEventMetadata(new Map([
330
+ ['lastEventTimestamp', timestamp], ['lastEventSeq', String(seq)],
331
+ ]), epoch);
332
+ });
333
+ }
334
+ reportStreamError(error) {
335
+ this.log.error('PuppetService', 'event processing failed: %s', error);
336
+ try {
337
+ this.emit('error', error);
338
+ }
339
+ catch (listenerError) {
340
+ this.log.error('PuppetService', 'error listener failed: %s', listenerError);
341
+ }
342
+ }
343
+ haltEventStream(manager, event, error) {
344
+ this.failedEventSeq ??= event.getSeq();
345
+ this.suspendEventProcessing();
346
+ try {
347
+ manager.stopStream();
348
+ }
349
+ catch (stopError) {
350
+ this.log.error('PuppetService', 'stopping failed event stream failed: %s', stopError);
351
+ }
352
+ this.reportStreamError(error);
353
+ }
354
+ async onGrpcStreamEvent(event, epoch = this.eventEpoch) {
355
+ if (!this.isEventEpochCurrent(epoch)) {
356
+ return;
357
+ }
189
358
  const type = event.getType();
190
359
  const payload = event.getPayload();
191
360
  const seq = event.getSeq();
@@ -199,15 +368,6 @@ class PuppetService extends PUPPET.Puppet {
199
368
  data: `onGrpcStreamEvent(${EventTypeRev[type]})`,
200
369
  });
201
370
  }
202
- if (seq && !envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
203
- const { lastEventSeq } = await this.getMiscellaneousStoreData();
204
- if (!lastEventSeq || (seq > Number(lastEventSeq) || seq === 1)) {
205
- await this.setMiscellaneousStoreData({
206
- lastEventSeq: seq.toString(),
207
- lastEventTimestamp: timestamp,
208
- });
209
- }
210
- }
211
371
  switch (type) {
212
372
  case grpcPuppet.EventType.EVENT_TYPE_DONG:
213
373
  this.emit('dong', JSON.parse(payload));
@@ -230,11 +390,14 @@ class PuppetService extends PUPPET.Puppet {
230
390
  const loginPayload = JSON.parse(payload);
231
391
  if (!envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
232
392
  const { accountId } = await this.getMiscellaneousStoreData();
393
+ if (!this.isEventEpochCurrent(epoch)) {
394
+ return;
395
+ }
233
396
  if (accountId !== loginPayload.contactId) {
234
- await this.resetMiscellaneousStoreData();
397
+ await this.resetMiscellaneousStoreData(epoch);
235
398
  await this.setMiscellaneousStoreData({
236
399
  accountId: loginPayload.contactId,
237
- });
400
+ }, epoch);
238
401
  }
239
402
  }
240
403
  (async () => this.login(loginPayload.contactId))().catch(e => this.log.error('PuppetService', 'onGrpcStreamEvent() this.login() rejection %s', e.message));
@@ -244,7 +407,7 @@ class PuppetService extends PUPPET.Puppet {
244
407
  {
245
408
  const logoutPayload = JSON.parse(payload);
246
409
  if (!envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
247
- await this.resetMiscellaneousStoreData();
410
+ await this.resetMiscellaneousStoreData(epoch);
248
411
  }
249
412
  ;
250
413
  (async () => this.logout(logoutPayload.data))().catch(e => this.log.error('PuppetService', 'onGrpcStreamEvent() this.logout() rejection %s', e.message));
@@ -252,8 +415,25 @@ class PuppetService extends PUPPET.Puppet {
252
415
  break;
253
416
  case grpcPuppet.EventType.EVENT_TYPE_DIRTY:
254
417
  await this.fastDirty(JSON.parse(payload));
418
+ if (!this.isEventEpochCurrent(epoch)) {
419
+ return;
420
+ }
255
421
  this.emit('dirty', JSON.parse(payload));
256
422
  break;
423
+ case grpcPuppet.EventType.EVENT_TYPE_ROOM_MEMBER_LOAD: {
424
+ const loadPayload = JSON.parse(payload);
425
+ validateRoomMemberLoadState(loadPayload);
426
+ this.roomCache.invalidate(loadPayload.roomId);
427
+ await this.roomCache.run(loadPayload.roomId, async () => {
428
+ await this._payloadStore.room?.delete(loadPayload.roomId);
429
+ await this._payloadStore.roomMember?.delete(loadPayload.roomId);
430
+ });
431
+ if (!this.isEventEpochCurrent(epoch)) {
432
+ return;
433
+ }
434
+ this.emit('room-member-load', loadPayload);
435
+ break;
436
+ }
257
437
  case grpcPuppet.EventType.EVENT_TYPE_MESSAGE:
258
438
  this.emit('message', JSON.parse(payload));
259
439
  break;
@@ -326,6 +506,9 @@ class PuppetService extends PUPPET.Puppet {
326
506
  // Huan(202003): in default, the `type` type should be `never`, please check.
327
507
  this.log.error(`eventType ${type} unsupported! data: ${payload}`);
328
508
  }
509
+ if (seq && this.isEventEpochCurrent(epoch) && !envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
510
+ await this.commitEventCheckpoint(seq, timestamp, epoch);
511
+ }
329
512
  }
330
513
  async logout(reason) {
331
514
  this.log.verbose('PuppetService', 'logout(%s)', reason ? `"${reason}"` : '');
@@ -381,8 +564,14 @@ class PuppetService extends PUPPET.Puppet {
381
564
  [PUPPET.types.Dirty.Friendship]: async (_) => { },
382
565
  [PUPPET.types.Dirty.Message]: async (_) => { },
383
566
  [PUPPET.types.Dirty.Post]: async (_) => { },
384
- [PUPPET.types.Dirty.Room]: async (id) => this._payloadStore.room?.delete(id),
385
- [PUPPET.types.Dirty.RoomMember]: async (id) => this._payloadStore.roomMember?.delete(id),
567
+ [PUPPET.types.Dirty.Room]: async (id) => {
568
+ this.roomCache.invalidate(id);
569
+ return this.roomCache.run(id, async () => this._payloadStore.room?.delete(id));
570
+ },
571
+ [PUPPET.types.Dirty.RoomMember]: async (id) => {
572
+ this.roomCache.invalidate(id);
573
+ return this.roomCache.run(id, async () => this._payloadStore.roomMember?.delete(id));
574
+ },
386
575
  [PUPPET.types.Dirty.Tag]: async (id) => this._payloadStore.tag?.delete(id),
387
576
  [PUPPET.types.Dirty.TagGroup]: async (id) => this._payloadStore.tagGroup?.delete(id),
388
577
  [PUPPET.types.Dirty.Unspecified]: async (id) => { throw new Error('Unspecified type with id: ' + id); },
@@ -1551,8 +1740,9 @@ class PuppetService extends PUPPET.Puppet {
1551
1740
  */
1552
1741
  async roomRawPayload(id) {
1553
1742
  this.log.verbose('PuppetService', 'roomRawPayload(%s)', id);
1554
- const cachedPayload = await this._payloadStore.room?.get(id);
1555
- if (cachedPayload) {
1743
+ const generation = this.roomCache.generation(id);
1744
+ const cachedPayload = await this.roomCache.run(id, async () => this._payloadStore.room?.get(id));
1745
+ if (cachedPayload && generation === this.roomCache.generation(id)) {
1556
1746
  this.log.silly('PuppetService', 'roomRawPayload(%s) cache HIT', id);
1557
1747
  return cachedPayload;
1558
1748
  }
@@ -1560,26 +1750,52 @@ class PuppetService extends PUPPET.Puppet {
1560
1750
  request.setId(id);
1561
1751
  const response = await util.promisify(this.grpcManager.client.roomPayload
1562
1752
  .bind(this.grpcManager.client))(request);
1563
- const payload = {
1564
- adminIdList: response.getAdminIdsList(),
1565
- avatar: response.getAvatar(),
1566
- handle: response.getHandle(),
1567
- id: response.getId(),
1568
- memberIdList: response.getMemberIdsList(),
1569
- ownerId: response.getOwnerId(),
1570
- topic: response.getTopic(),
1571
- additionalInfo: response.getAdditionalInfo(),
1572
- remark: response.getRoomRemark(),
1573
- external: response.getExternal(),
1574
- };
1575
- const createTime = response.getCreateTime();
1576
- if (createTime) {
1577
- payload.createTime = millisecondsFromTimestamp(createTime);
1578
- }
1579
- await this._payloadStore.room?.set(id, payload);
1753
+ const payload = roomPbToPayload(response);
1754
+ await this.roomCache.commit(id, generation, async () => this._payloadStore.room?.set(id, payload));
1580
1755
  this.log.silly('PuppetService', 'roomRawPayload(%s) cache SET', id);
1581
1756
  return payload;
1582
1757
  }
1758
+ async roomMemberSync(roomId, options) {
1759
+ if (!roomId) {
1760
+ throw new Error('roomId is required');
1761
+ }
1762
+ const request = new grpcPuppet.RoomMemberSyncRequest();
1763
+ request.setId(roomId);
1764
+ if (options?.force !== undefined) {
1765
+ request.setForce(options.force);
1766
+ }
1767
+ const response = await util.promisify(this.grpcManager.client.roomMemberSync.bind(this.grpcManager.client))(request);
1768
+ return roomMemberLoadStatePbToPayload(response, roomId);
1769
+ }
1770
+ async roomMemberLoadState(roomId) {
1771
+ if (!roomId) {
1772
+ throw new Error('roomId is required');
1773
+ }
1774
+ const request = new grpcPuppet.RoomMemberLoadStateRequest();
1775
+ request.setId(roomId);
1776
+ const response = await util.promisify(this.grpcManager.client.roomMemberLoadState.bind(this.grpcManager.client))(request);
1777
+ return roomMemberLoadStatePbToPayload(response, roomId);
1778
+ }
1779
+ async roomMemberSnapshot(roomId, revision, options) {
1780
+ if (!roomId) {
1781
+ throw new Error('roomId is required');
1782
+ }
1783
+ validateRoomMemberRevision(revision);
1784
+ const request = new grpcPuppet.RoomMemberSnapshotRequest();
1785
+ request.setId(roomId);
1786
+ request.setRevision(revision);
1787
+ if (options?.pageToken !== undefined) {
1788
+ request.setPageToken(options.pageToken);
1789
+ }
1790
+ if (options?.pageSize !== undefined) {
1791
+ if (!Number.isInteger(options.pageSize) || options.pageSize < 0 || options.pageSize > 500) {
1792
+ throw new Error('pageSize must be an integer between 0 and 500');
1793
+ }
1794
+ request.setPageSize(options.pageSize);
1795
+ }
1796
+ const response = await util.promisify(this.grpcManager.client.roomMemberSnapshot.bind(this.grpcManager.client))(request);
1797
+ return roomMemberSnapshotPbToPayload(response, roomId, revision);
1798
+ }
1583
1799
  async roomRawPayloadParser(payload) {
1584
1800
  // this.log.silly('PuppetService', 'roomRawPayloadParser({id:%s})', payload.id)
1585
1801
  // passthrough
@@ -1589,10 +1805,11 @@ class PuppetService extends PUPPET.Puppet {
1589
1805
  this.log.verbose('PuppetService', 'batchRoomRawPayload(%s)', roomIdList);
1590
1806
  const result = new Map();
1591
1807
  const roomIdSet = new Set(roomIdList);
1808
+ const generations = new Map(Array.from(roomIdSet, id => [id, this.roomCache.generation(id)]));
1592
1809
  const needGetSet = new Set();
1593
1810
  for (const roomId of roomIdSet) {
1594
- const cachedRoomPayload = await this._payloadStore.room?.get(roomId);
1595
- if (cachedRoomPayload) {
1811
+ const cachedRoomPayload = await this.roomCache.run(roomId, async () => this._payloadStore.room?.get(roomId));
1812
+ if (cachedRoomPayload && generations.get(roomId) === this.roomCache.generation(roomId)) {
1596
1813
  result.set(roomId, cachedRoomPayload);
1597
1814
  }
1598
1815
  else {
@@ -1608,27 +1825,18 @@ class PuppetService extends PUPPET.Puppet {
1608
1825
  const payloads = response.getRoomPayloadsList();
1609
1826
  for (const payload of payloads) {
1610
1827
  const roomId = payload.getId();
1611
- const puppetPayload = {
1612
- adminIdList: payload.getAdminIdsList(),
1613
- avatar: payload.getAvatar(),
1614
- handle: payload.getHandle(),
1615
- id: payload.getId(),
1616
- memberIdList: payload.getMemberIdsList(),
1617
- ownerId: payload.getOwnerId(),
1618
- topic: payload.getTopic(),
1619
- additionalInfo: payload.getAdditionalInfo(),
1620
- remark: payload.getRoomRemark(),
1621
- external: payload.getExternal(),
1622
- };
1623
- const createTime = payload.getCreateTime();
1624
- if (createTime) {
1625
- puppetPayload.createTime = millisecondsFromTimestamp(createTime);
1626
- }
1828
+ const puppetPayload = roomPbToPayload(payload);
1627
1829
  result.set(roomId, puppetPayload);
1628
- await this._payloadStore.room?.set(roomId, puppetPayload);
1830
+ const generation = generations.get(roomId);
1831
+ if (generation !== undefined) {
1832
+ await this.roomCache.commit(roomId, generation, async () => this._payloadStore.room?.set(roomId, puppetPayload));
1833
+ }
1629
1834
  }
1630
1835
  }
1631
1836
  catch (e) {
1837
+ if (e instanceof RoomCacheOperationError) {
1838
+ throw e;
1839
+ }
1632
1840
  this.log.error('PuppetService', 'batchRoomRawPayload(%s, %s) error: %s, use one by one method', roomIdList, needGetSet, e);
1633
1841
  let fallbackSuccessCount = 0;
1634
1842
  for (const roomId of needGetSet) {
@@ -1814,9 +2022,10 @@ class PuppetService extends PUPPET.Puppet {
1814
2022
  }
1815
2023
  async roomMemberRawPayload(roomId, contactId) {
1816
2024
  this.log.verbose('PuppetService', 'roomMemberRawPayload(%s, %s)', roomId, contactId);
1817
- const cachedPayload = await this._payloadStore.roomMember?.get(roomId);
2025
+ const generation = this.roomCache.generation(roomId);
2026
+ const cachedPayload = await this.roomCache.run(roomId, async () => this._payloadStore.roomMember?.get(roomId));
1818
2027
  const cachedRoomMemberPayload = cachedPayload && cachedPayload[contactId];
1819
- if (cachedRoomMemberPayload) {
2028
+ if (cachedRoomMemberPayload && generation === this.roomCache.generation(roomId)) {
1820
2029
  this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache HIT', roomId, contactId);
1821
2030
  return cachedRoomMemberPayload;
1822
2031
  }
@@ -1835,9 +2044,9 @@ class PuppetService extends PUPPET.Puppet {
1835
2044
  joinScene: response.getJoinScene(),
1836
2045
  joinTime: response.getJoinTime(),
1837
2046
  };
1838
- await this._payloadStore.roomMember?.set(roomId, {
1839
- ...cachedPayload,
1840
- [contactId]: payload,
2047
+ await this.roomCache.commit(roomId, generation, async () => {
2048
+ const current = await this._payloadStore.roomMember?.get(roomId);
2049
+ await this._payloadStore.roomMember?.set(roomId, { ...current, [contactId]: payload });
1841
2050
  });
1842
2051
  this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache SET', roomId, contactId);
1843
2052
  return payload;
@@ -1849,10 +2058,12 @@ class PuppetService extends PUPPET.Puppet {
1849
2058
  }
1850
2059
  async batchRoomMemberRawPayload(roomId, contactIdList) {
1851
2060
  this.log.verbose('PuppetService', 'batchRoomMemberRawPayload(%s, %s)', roomId, contactIdList);
2061
+ const generation = this.roomCache.generation(roomId);
1852
2062
  const result = new Map();
1853
2063
  const contactIdSet = new Set(contactIdList);
1854
2064
  let needGetSet = new Set();
1855
- const cachedPayload = await this._payloadStore.roomMember?.get(roomId) || {};
2065
+ const cached = await this.roomCache.run(roomId, async () => this._payloadStore.roomMember?.get(roomId));
2066
+ const cachedPayload = generation === this.roomCache.generation(roomId) ? cached || {} : {};
1856
2067
  if (Object.keys(cachedPayload).length > 0) {
1857
2068
  for (const contactId of contactIdSet) {
1858
2069
  const cachedRoomMemberPayload = cachedPayload[contactId];
@@ -1875,15 +2086,22 @@ class PuppetService extends PUPPET.Puppet {
1875
2086
  const response = await util.promisify(this.grpcManager.client.batchRoomMemberPayload
1876
2087
  .bind(this.grpcManager.client))(request);
1877
2088
  const payloads = response.getMemberPayloadsList();
2089
+ const fetched = {};
1878
2090
  for (const payload of payloads) {
1879
2091
  const contactId = payload.getId();
1880
2092
  const puppetPayload = roomMemberPbToPayload(payload);
1881
2093
  result.set(contactId, puppetPayload);
1882
- cachedPayload[contactId] = puppetPayload;
2094
+ fetched[contactId] = puppetPayload;
1883
2095
  }
1884
- await this._payloadStore.roomMember?.set(roomId, cachedPayload);
2096
+ await this.roomCache.commit(roomId, generation, async () => {
2097
+ const current = await this._payloadStore.roomMember?.get(roomId);
2098
+ await this._payloadStore.roomMember?.set(roomId, { ...current, ...fetched });
2099
+ });
1885
2100
  }
1886
2101
  catch (e) {
2102
+ if (e instanceof RoomCacheOperationError) {
2103
+ throw e;
2104
+ }
1887
2105
  this.log.error('PuppetService', 'batchRoomMemberRawPayload(%s, %s) error: %s, use one by one method', roomId, needGetSet, e);
1888
2106
  for (const contactId of needGetSet) {
1889
2107
  const payload = await this.roomMemberRawPayload(roomId, contactId);
@@ -2625,23 +2843,40 @@ class PuppetService extends PUPPET.Puppet {
2625
2843
  waitingForReady = false;
2626
2844
  reconnectIndicator;
2627
2845
  async reset() {
2628
- if (!this._grpcManager) {
2629
- this.log.warn('PuppetService', 'grpc manager not constructed, perform regular reset');
2630
- return super.reset();
2631
- }
2632
- if (!this.isLoggedIn) {
2633
- this.log.warn('PuppetService', 'puppet not logged in, perform regular reset');
2634
- return super.reset();
2635
- }
2636
2846
  if (this.reconnectIndicator.value()) {
2637
2847
  this.log.warn('PuppetService', 'already trying to reconnect, pass this one');
2638
2848
  return;
2639
2849
  }
2640
2850
  this.reconnectIndicator.value(true);
2641
- this.grpcManager.stopStream();
2642
- const { lastEventSeq, accountId } = await this.getMiscellaneousStoreData();
2851
+ this.suspendEventProcessing();
2852
+ const epoch = this.eventEpoch;
2853
+ const manager = this._grpcManager;
2854
+ try {
2855
+ await this.checkpointQueue;
2856
+ if (epoch !== this.eventEpoch) {
2857
+ return;
2858
+ }
2859
+ if (!manager || !this.isLoggedIn) {
2860
+ this.log.warn('PuppetService', 'grpc manager unavailable or puppet logged out, perform regular reset');
2861
+ return await super.reset();
2862
+ }
2863
+ await this.resetEventStream(manager, epoch);
2864
+ }
2865
+ finally {
2866
+ this.reconnectIndicator.value(false);
2867
+ }
2868
+ }
2869
+ async resetEventStream(manager, epoch) {
2870
+ manager.stopStream();
2871
+ const { lastEventSeq, accountId } = await this.eventResumePosition(epoch);
2872
+ if (epoch !== this.eventEpoch || manager !== this._grpcManager) {
2873
+ return;
2874
+ }
2643
2875
  const onLoginResolve = (resolve) => {
2644
2876
  const onLogin = (event) => {
2877
+ if (!this.ownsEventWorker(manager, epoch)) {
2878
+ return;
2879
+ }
2645
2880
  const type = event.getType();
2646
2881
  const payload = event.getPayload();
2647
2882
  if (this.waitingForLogin && type === grpcPuppet.EventType.EVENT_TYPE_LOGIN) {
@@ -2657,6 +2892,9 @@ class PuppetService extends PUPPET.Puppet {
2657
2892
  };
2658
2893
  const onReadyResolve = (resolve) => {
2659
2894
  const onReady = (event) => {
2895
+ if (!this.ownsEventWorker(manager, epoch)) {
2896
+ return;
2897
+ }
2660
2898
  const type = event.getType();
2661
2899
  if (this.waitingForReady && type === grpcPuppet.EventType.EVENT_TYPE_READY) {
2662
2900
  this.waitingForReady = false;
@@ -2665,60 +2903,85 @@ class PuppetService extends PUPPET.Puppet {
2665
2903
  };
2666
2904
  return onReady;
2667
2905
  };
2668
- let onLogin;
2669
- let onReady;
2906
+ const removeListeners = [];
2670
2907
  const loginFuture = new Promise(resolve => {
2671
- onLogin = onLoginResolve(resolve);
2672
- this.grpcManager.on('data', onLogin);
2908
+ const onLogin = onLoginResolve(resolve);
2909
+ manager.on('data', onLogin);
2910
+ removeListeners.push(() => { manager.off('data', onLogin); });
2673
2911
  });
2674
2912
  const readyFuture = new Promise(resolve => {
2675
- onReady = onReadyResolve(resolve);
2676
- this.grpcManager.on('data', onReady);
2913
+ const onReady = onReadyResolve(resolve);
2914
+ manager.on('data', onReady);
2915
+ removeListeners.push(() => { manager.off('data', onReady); });
2677
2916
  });
2678
2917
  const startTime = Date.now();
2679
2918
  const timeoutMilliseconds = this.timeoutMilliseconds / 10; // 2 min default, 4 min xiaoju-bot
2680
- while (true) {
2919
+ this.waitingForLogin = true;
2920
+ this.waitingForReady = true;
2921
+ this.eventsBlocked = false;
2922
+ try {
2923
+ while (true) {
2924
+ try {
2925
+ if (!this.ownsEventWorker(manager, epoch)) {
2926
+ return;
2927
+ }
2928
+ await timeoutPromise(manager.startStream(lastEventSeq, accountId), 30 * 1000);
2929
+ if (!this.ownsEventWorker(manager, epoch)) {
2930
+ return;
2931
+ }
2932
+ break;
2933
+ }
2934
+ catch (e) {
2935
+ if (!this.ownsEventWorker(manager, epoch)) {
2936
+ return;
2937
+ }
2938
+ if (Date.now() - startTime < timeoutMilliseconds) {
2939
+ this.log.warn('failed to start stream, will try again in 15 seconds');
2940
+ await new Promise(resolve => {
2941
+ setTimeout(resolve, 5000);
2942
+ });
2943
+ }
2944
+ else {
2945
+ this.log.warn('failed to start stream and reaches timeout, will perform regular reset');
2946
+ return super.reset();
2947
+ }
2948
+ }
2949
+ }
2681
2950
  try {
2682
- await timeoutPromise(this.grpcManager.startStream(lastEventSeq, accountId), 30 * 1000);
2683
- break;
2951
+ await timeoutPromise(loginFuture, ResetLoginTimeout)
2952
+ .finally(() => {
2953
+ if (epoch === this.eventEpoch) {
2954
+ this.waitingForLogin = false;
2955
+ }
2956
+ });
2684
2957
  }
2685
2958
  catch (e) {
2686
- if (Date.now() - startTime < timeoutMilliseconds) {
2687
- this.log.warn('failed to start stream, will try again in 15 seconds');
2688
- await new Promise(resolve => {
2689
- setTimeout(resolve, 5000);
2690
- });
2691
- }
2692
- else {
2693
- this.log.warn('failed to start stream and reaches timeout, will perform regular reset');
2694
- this.reconnectIndicator.value(false);
2695
- return super.reset();
2959
+ if (!this.ownsEventWorker(manager, epoch)) {
2960
+ return;
2696
2961
  }
2962
+ this.log.warn('PuppetService', 'waiting for event reset login error, will perform regular reset');
2963
+ return super.reset();
2964
+ }
2965
+ try {
2966
+ await timeoutPromise(readyFuture, ResetReadyTimeout)
2967
+ .finally(() => {
2968
+ if (epoch === this.eventEpoch) {
2969
+ this.waitingForReady = false;
2970
+ }
2971
+ });
2972
+ }
2973
+ catch (e) {
2974
+ this.log.warn('PuppetService', 'waiting for event reset ready error, will do nothing');
2697
2975
  }
2698
2976
  }
2699
- this.waitingForLogin = true;
2700
- this.waitingForReady = true;
2701
- try {
2702
- await timeoutPromise(loginFuture, ResetLoginTimeout)
2703
- .finally(() => {
2977
+ finally {
2978
+ for (const removeListener of removeListeners) {
2979
+ removeListener();
2980
+ }
2981
+ if (epoch === this.eventEpoch) {
2704
2982
  this.waitingForLogin = false;
2705
- this.reconnectIndicator.value(false);
2706
- this.grpcManager.off('data', onLogin);
2707
- });
2708
- }
2709
- catch (e) {
2710
- this.log.warn('PuppetService', 'waiting for event reset login error, will perform regular reset');
2711
- return super.reset();
2712
- }
2713
- try {
2714
- await timeoutPromise(readyFuture, ResetReadyTimeout)
2715
- .finally(() => {
2716
2983
  this.waitingForReady = false;
2717
- this.grpcManager.off('data', onReady);
2718
- });
2719
- }
2720
- catch (e) {
2721
- this.log.warn('PuppetService', 'waiting for event reset ready error, will do nothing');
2984
+ }
2722
2985
  }
2723
2986
  }
2724
2987
  async getMiscellaneousStoreData() {
@@ -2742,27 +3005,29 @@ class PuppetService extends PUPPET.Puppet {
2742
3005
  accountId,
2743
3006
  };
2744
3007
  }
2745
- async setMiscellaneousStoreData(data) {
3008
+ async setMiscellaneousStoreData(data, epoch) {
2746
3009
  if (envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
2747
3010
  return;
2748
3011
  }
2749
- if (typeof data.lastEventSeq !== 'undefined') {
2750
- await this._payloadStore.miscellaneous?.set('lastEventSeq', data.lastEventSeq);
3012
+ const changes = new Map();
3013
+ if (data.lastEventTimestamp !== undefined) {
3014
+ changes.set('lastEventTimestamp', data.lastEventTimestamp);
2751
3015
  }
2752
- if (typeof data.lastEventTimestamp !== 'undefined') {
2753
- await this._payloadStore.miscellaneous?.set('lastEventTimestamp', data.lastEventTimestamp);
3016
+ if (data.accountId !== undefined) {
3017
+ changes.set('accountId', data.accountId);
2754
3018
  }
2755
- if (typeof data.accountId !== 'undefined') {
2756
- await this._payloadStore.miscellaneous?.set('accountId', data.accountId);
3019
+ if (data.lastEventSeq !== undefined) {
3020
+ changes.set('lastEventSeq', data.lastEventSeq);
2757
3021
  }
3022
+ await this.withEventCheckpoint(async () => this.writeEventMetadata(changes, epoch));
2758
3023
  }
2759
- async resetMiscellaneousStoreData() {
3024
+ async resetMiscellaneousStoreData(epoch) {
2760
3025
  if (envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
2761
3026
  return;
2762
3027
  }
2763
- await this._payloadStore.miscellaneous?.delete('lastEventSeq');
2764
- await this._payloadStore.miscellaneous?.delete('lastEventTimestamp');
2765
- await this._payloadStore.miscellaneous?.delete('accountId');
3028
+ await this.withEventCheckpoint(async () => this.writeEventMetadata(new Map([
3029
+ ['lastEventSeq', undefined], ['lastEventTimestamp', undefined], ['accountId', undefined],
3030
+ ]), epoch));
2766
3031
  }
2767
3032
  }
2768
3033
  export { PuppetService, };