@juzi/wechaty-puppet-service 1.0.129 → 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 (98) 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 +27 -2
  5. package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
  6. package/dist/cjs/src/client/puppet-service.js +431 -124
  7. package/dist/cjs/src/client/puppet-service.js.map +1 -1
  8. package/dist/cjs/src/client/puppet-service.spec.js +107 -0
  9. package/dist/cjs/src/client/puppet-service.spec.js.map +1 -1
  10. package/dist/cjs/src/client/room-cache-coordinator.d.ts +14 -0
  11. package/dist/cjs/src/client/room-cache-coordinator.d.ts.map +1 -0
  12. package/dist/cjs/src/client/room-cache-coordinator.js +53 -0
  13. package/dist/cjs/src/client/room-cache-coordinator.js.map +1 -0
  14. package/dist/cjs/src/client/room-member-cache.spec.d.ts +2 -0
  15. package/dist/cjs/src/client/room-member-cache.spec.d.ts.map +1 -0
  16. package/dist/cjs/src/client/room-member-cache.spec.js +417 -0
  17. package/dist/cjs/src/client/room-member-cache.spec.js.map +1 -0
  18. package/dist/cjs/src/client/room-member-load.spec.d.ts +2 -0
  19. package/dist/cjs/src/client/room-member-load.spec.d.ts.map +1 -0
  20. package/dist/cjs/src/client/room-member-load.spec.js +324 -0
  21. package/dist/cjs/src/client/room-member-load.spec.js.map +1 -0
  22. package/dist/cjs/src/package-json.js +4 -4
  23. package/dist/cjs/src/server/event-stream-manager.d.ts.map +1 -1
  24. package/dist/cjs/src/server/event-stream-manager.js +30 -0
  25. package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
  26. package/dist/cjs/src/server/event-stream-manager.spec.d.ts +3 -0
  27. package/dist/cjs/src/server/event-stream-manager.spec.d.ts.map +1 -0
  28. package/dist/cjs/src/server/event-stream-manager.spec.js +22 -0
  29. package/dist/cjs/src/server/event-stream-manager.spec.js.map +1 -0
  30. package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
  31. package/dist/cjs/src/server/puppet-implementation.js +135 -28
  32. package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
  33. package/dist/cjs/src/server/puppet-implementation.spec.js +98 -0
  34. package/dist/cjs/src/server/puppet-implementation.spec.js.map +1 -1
  35. package/dist/cjs/src/utils/pb-payload-helper.d.ts +10 -0
  36. package/dist/cjs/src/utils/pb-payload-helper.d.ts.map +1 -1
  37. package/dist/cjs/src/utils/pb-payload-helper.js +180 -1
  38. package/dist/cjs/src/utils/pb-payload-helper.js.map +1 -1
  39. package/dist/cjs/src/utils/room-member-load.spec.d.ts +2 -0
  40. package/dist/cjs/src/utils/room-member-load.spec.d.ts.map +1 -0
  41. package/dist/cjs/src/utils/room-member-load.spec.js +83 -0
  42. package/dist/cjs/src/utils/room-member-load.spec.js.map +1 -0
  43. package/dist/esm/src/auth/call-cred.d.ts +2 -2
  44. package/dist/esm/src/auth/call-cred.d.ts.map +1 -1
  45. package/dist/esm/src/auth/call-cred.js.map +1 -1
  46. package/dist/esm/src/client/puppet-service.d.ts +27 -2
  47. package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
  48. package/dist/esm/src/client/puppet-service.js +432 -125
  49. package/dist/esm/src/client/puppet-service.js.map +1 -1
  50. package/dist/esm/src/client/puppet-service.spec.js +107 -0
  51. package/dist/esm/src/client/puppet-service.spec.js.map +1 -1
  52. package/dist/esm/src/client/room-cache-coordinator.d.ts +14 -0
  53. package/dist/esm/src/client/room-cache-coordinator.d.ts.map +1 -0
  54. package/dist/esm/src/client/room-cache-coordinator.js +49 -0
  55. package/dist/esm/src/client/room-cache-coordinator.js.map +1 -0
  56. package/dist/esm/src/client/room-member-cache.spec.d.ts +2 -0
  57. package/dist/esm/src/client/room-member-cache.spec.d.ts.map +1 -0
  58. package/dist/esm/src/client/room-member-cache.spec.js +415 -0
  59. package/dist/esm/src/client/room-member-cache.spec.js.map +1 -0
  60. package/dist/esm/src/client/room-member-load.spec.d.ts +2 -0
  61. package/dist/esm/src/client/room-member-load.spec.d.ts.map +1 -0
  62. package/dist/esm/src/client/room-member-load.spec.js +296 -0
  63. package/dist/esm/src/client/room-member-load.spec.js.map +1 -0
  64. package/dist/esm/src/package-json.js +4 -4
  65. package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
  66. package/dist/esm/src/server/event-stream-manager.js +30 -0
  67. package/dist/esm/src/server/event-stream-manager.js.map +1 -1
  68. package/dist/esm/src/server/event-stream-manager.spec.d.ts +3 -0
  69. package/dist/esm/src/server/event-stream-manager.spec.d.ts.map +1 -0
  70. package/dist/esm/src/server/event-stream-manager.spec.js +20 -0
  71. package/dist/esm/src/server/event-stream-manager.spec.js.map +1 -0
  72. package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
  73. package/dist/esm/src/server/puppet-implementation.js +136 -29
  74. package/dist/esm/src/server/puppet-implementation.js.map +1 -1
  75. package/dist/esm/src/server/puppet-implementation.spec.js +98 -0
  76. package/dist/esm/src/server/puppet-implementation.spec.js.map +1 -1
  77. package/dist/esm/src/utils/pb-payload-helper.d.ts +10 -0
  78. package/dist/esm/src/utils/pb-payload-helper.d.ts.map +1 -1
  79. package/dist/esm/src/utils/pb-payload-helper.js +169 -0
  80. package/dist/esm/src/utils/pb-payload-helper.js.map +1 -1
  81. package/dist/esm/src/utils/room-member-load.spec.d.ts +2 -0
  82. package/dist/esm/src/utils/room-member-load.spec.d.ts.map +1 -0
  83. package/dist/esm/src/utils/room-member-load.spec.js +58 -0
  84. package/dist/esm/src/utils/room-member-load.spec.js.map +1 -0
  85. package/package.json +4 -4
  86. package/src/auth/call-cred.ts +1 -1
  87. package/src/client/puppet-service.spec.ts +147 -0
  88. package/src/client/puppet-service.ts +378 -138
  89. package/src/client/room-cache-coordinator.ts +50 -0
  90. package/src/client/room-member-cache.spec.ts +448 -0
  91. package/src/client/room-member-load.spec.ts +319 -0
  92. package/src/package-json.ts +4 -4
  93. package/src/server/event-stream-manager.spec.ts +30 -0
  94. package/src/server/event-stream-manager.ts +29 -0
  95. package/src/server/puppet-implementation.spec.ts +127 -0
  96. package/src/server/puppet-implementation.ts +144 -29
  97. package/src/utils/pb-payload-helper.ts +150 -0
  98. 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;
@@ -316,10 +496,19 @@ class PuppetService extends PUPPET.Puppet {
316
496
  case grpcPuppet.EventType.EVENT_TYPE_CALL:
317
497
  this.emit('call', JSON.parse(payload));
318
498
  break;
499
+ case grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_CREATED:
500
+ this.emit('org-broadcast-created', JSON.parse(payload));
501
+ break;
502
+ case grpcPuppet.EventType.EVENT_TYPE_ORG_BROADCAST_SENT:
503
+ this.emit('org-broadcast-sent', JSON.parse(payload));
504
+ break;
319
505
  default:
320
506
  // Huan(202003): in default, the `type` type should be `never`, please check.
321
507
  this.log.error(`eventType ${type} unsupported! data: ${payload}`);
322
508
  }
509
+ if (seq && this.isEventEpochCurrent(epoch) && !envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
510
+ await this.commitEventCheckpoint(seq, timestamp, epoch);
511
+ }
323
512
  }
324
513
  async logout(reason) {
325
514
  this.log.verbose('PuppetService', 'logout(%s)', reason ? `"${reason}"` : '');
@@ -375,8 +564,14 @@ class PuppetService extends PUPPET.Puppet {
375
564
  [PUPPET.types.Dirty.Friendship]: async (_) => { },
376
565
  [PUPPET.types.Dirty.Message]: async (_) => { },
377
566
  [PUPPET.types.Dirty.Post]: async (_) => { },
378
- [PUPPET.types.Dirty.Room]: async (id) => this._payloadStore.room?.delete(id),
379
- [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
+ },
380
575
  [PUPPET.types.Dirty.Tag]: async (id) => this._payloadStore.tag?.delete(id),
381
576
  [PUPPET.types.Dirty.TagGroup]: async (id) => this._payloadStore.tagGroup?.delete(id),
382
577
  [PUPPET.types.Dirty.Unspecified]: async (id) => { throw new Error('Unspecified type with id: ' + id); },
@@ -1545,8 +1740,9 @@ class PuppetService extends PUPPET.Puppet {
1545
1740
  */
1546
1741
  async roomRawPayload(id) {
1547
1742
  this.log.verbose('PuppetService', 'roomRawPayload(%s)', id);
1548
- const cachedPayload = await this._payloadStore.room?.get(id);
1549
- 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)) {
1550
1746
  this.log.silly('PuppetService', 'roomRawPayload(%s) cache HIT', id);
1551
1747
  return cachedPayload;
1552
1748
  }
@@ -1554,26 +1750,52 @@ class PuppetService extends PUPPET.Puppet {
1554
1750
  request.setId(id);
1555
1751
  const response = await util.promisify(this.grpcManager.client.roomPayload
1556
1752
  .bind(this.grpcManager.client))(request);
1557
- const payload = {
1558
- adminIdList: response.getAdminIdsList(),
1559
- avatar: response.getAvatar(),
1560
- handle: response.getHandle(),
1561
- id: response.getId(),
1562
- memberIdList: response.getMemberIdsList(),
1563
- ownerId: response.getOwnerId(),
1564
- topic: response.getTopic(),
1565
- additionalInfo: response.getAdditionalInfo(),
1566
- remark: response.getRoomRemark(),
1567
- external: response.getExternal(),
1568
- };
1569
- const createTime = response.getCreateTime();
1570
- if (createTime) {
1571
- payload.createTime = millisecondsFromTimestamp(createTime);
1572
- }
1573
- 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));
1574
1755
  this.log.silly('PuppetService', 'roomRawPayload(%s) cache SET', id);
1575
1756
  return payload;
1576
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
+ }
1577
1799
  async roomRawPayloadParser(payload) {
1578
1800
  // this.log.silly('PuppetService', 'roomRawPayloadParser({id:%s})', payload.id)
1579
1801
  // passthrough
@@ -1583,10 +1805,11 @@ class PuppetService extends PUPPET.Puppet {
1583
1805
  this.log.verbose('PuppetService', 'batchRoomRawPayload(%s)', roomIdList);
1584
1806
  const result = new Map();
1585
1807
  const roomIdSet = new Set(roomIdList);
1808
+ const generations = new Map(Array.from(roomIdSet, id => [id, this.roomCache.generation(id)]));
1586
1809
  const needGetSet = new Set();
1587
1810
  for (const roomId of roomIdSet) {
1588
- const cachedRoomPayload = await this._payloadStore.room?.get(roomId);
1589
- 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)) {
1590
1813
  result.set(roomId, cachedRoomPayload);
1591
1814
  }
1592
1815
  else {
@@ -1602,27 +1825,18 @@ class PuppetService extends PUPPET.Puppet {
1602
1825
  const payloads = response.getRoomPayloadsList();
1603
1826
  for (const payload of payloads) {
1604
1827
  const roomId = payload.getId();
1605
- const puppetPayload = {
1606
- adminIdList: payload.getAdminIdsList(),
1607
- avatar: payload.getAvatar(),
1608
- handle: payload.getHandle(),
1609
- id: payload.getId(),
1610
- memberIdList: payload.getMemberIdsList(),
1611
- ownerId: payload.getOwnerId(),
1612
- topic: payload.getTopic(),
1613
- additionalInfo: payload.getAdditionalInfo(),
1614
- remark: payload.getRoomRemark(),
1615
- external: payload.getExternal(),
1616
- };
1617
- const createTime = payload.getCreateTime();
1618
- if (createTime) {
1619
- puppetPayload.createTime = millisecondsFromTimestamp(createTime);
1620
- }
1828
+ const puppetPayload = roomPbToPayload(payload);
1621
1829
  result.set(roomId, puppetPayload);
1622
- 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
+ }
1623
1834
  }
1624
1835
  }
1625
1836
  catch (e) {
1837
+ if (e instanceof RoomCacheOperationError) {
1838
+ throw e;
1839
+ }
1626
1840
  this.log.error('PuppetService', 'batchRoomRawPayload(%s, %s) error: %s, use one by one method', roomIdList, needGetSet, e);
1627
1841
  let fallbackSuccessCount = 0;
1628
1842
  for (const roomId of needGetSet) {
@@ -1808,9 +2022,10 @@ class PuppetService extends PUPPET.Puppet {
1808
2022
  }
1809
2023
  async roomMemberRawPayload(roomId, contactId) {
1810
2024
  this.log.verbose('PuppetService', 'roomMemberRawPayload(%s, %s)', roomId, contactId);
1811
- 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));
1812
2027
  const cachedRoomMemberPayload = cachedPayload && cachedPayload[contactId];
1813
- if (cachedRoomMemberPayload) {
2028
+ if (cachedRoomMemberPayload && generation === this.roomCache.generation(roomId)) {
1814
2029
  this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache HIT', roomId, contactId);
1815
2030
  return cachedRoomMemberPayload;
1816
2031
  }
@@ -1829,9 +2044,9 @@ class PuppetService extends PUPPET.Puppet {
1829
2044
  joinScene: response.getJoinScene(),
1830
2045
  joinTime: response.getJoinTime(),
1831
2046
  };
1832
- await this._payloadStore.roomMember?.set(roomId, {
1833
- ...cachedPayload,
1834
- [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 });
1835
2050
  });
1836
2051
  this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache SET', roomId, contactId);
1837
2052
  return payload;
@@ -1843,10 +2058,12 @@ class PuppetService extends PUPPET.Puppet {
1843
2058
  }
1844
2059
  async batchRoomMemberRawPayload(roomId, contactIdList) {
1845
2060
  this.log.verbose('PuppetService', 'batchRoomMemberRawPayload(%s, %s)', roomId, contactIdList);
2061
+ const generation = this.roomCache.generation(roomId);
1846
2062
  const result = new Map();
1847
2063
  const contactIdSet = new Set(contactIdList);
1848
2064
  let needGetSet = new Set();
1849
- 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 || {} : {};
1850
2067
  if (Object.keys(cachedPayload).length > 0) {
1851
2068
  for (const contactId of contactIdSet) {
1852
2069
  const cachedRoomMemberPayload = cachedPayload[contactId];
@@ -1869,15 +2086,22 @@ class PuppetService extends PUPPET.Puppet {
1869
2086
  const response = await util.promisify(this.grpcManager.client.batchRoomMemberPayload
1870
2087
  .bind(this.grpcManager.client))(request);
1871
2088
  const payloads = response.getMemberPayloadsList();
2089
+ const fetched = {};
1872
2090
  for (const payload of payloads) {
1873
2091
  const contactId = payload.getId();
1874
2092
  const puppetPayload = roomMemberPbToPayload(payload);
1875
2093
  result.set(contactId, puppetPayload);
1876
- cachedPayload[contactId] = puppetPayload;
2094
+ fetched[contactId] = puppetPayload;
1877
2095
  }
1878
- 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
+ });
1879
2100
  }
1880
2101
  catch (e) {
2102
+ if (e instanceof RoomCacheOperationError) {
2103
+ throw e;
2104
+ }
1881
2105
  this.log.error('PuppetService', 'batchRoomMemberRawPayload(%s, %s) error: %s, use one by one method', roomId, needGetSet, e);
1882
2106
  for (const contactId of needGetSet) {
1883
2107
  const payload = await this.roomMemberRawPayload(roomId, contactId);
@@ -2568,6 +2792,42 @@ class PuppetService extends PUPPET.Puppet {
2568
2792
  }
2569
2793
  return result;
2570
2794
  }
2795
+ async orgBroadcastPayload(orgBroadcastId) {
2796
+ this.log.verbose('PuppetService', 'orgBroadcastPayload(%s)', orgBroadcastId);
2797
+ const request = new grpcPuppet.OrgBroadcastPayloadRequest();
2798
+ request.setId(orgBroadcastId);
2799
+ const response = await util.promisify(this.grpcManager.client.orgBroadcastPayload.bind(this.grpcManager.client))(request);
2800
+ return {
2801
+ id: response.getId(),
2802
+ sendType: response.getSendType(),
2803
+ conversationType: response.getConversationType(),
2804
+ creatorId: response.getCreatorId() || undefined,
2805
+ execTime: response.getExecTime(),
2806
+ status: response.getStatus(),
2807
+ canCancel: response.getCanCancel(),
2808
+ allowSelect: response.getAllowSelect(),
2809
+ sent: response.getSent(),
2810
+ totalCount: response.getTotalCount(),
2811
+ sentCount: response.getSentCount(),
2812
+ contentListJson: response.getContentListJson(),
2813
+ targets: response.getTargetsList().map(targetPb => ({
2814
+ contactId: targetPb.getContactId() || undefined,
2815
+ roomId: targetPb.getRoomId() || undefined,
2816
+ status: targetPb.getStatus(),
2817
+ })),
2818
+ };
2819
+ }
2820
+ async orgBroadcastExecute(orgBroadcastId, targetIds) {
2821
+ this.log.verbose('PuppetService', 'orgBroadcastExecute(%s, %s)', orgBroadcastId, targetIds?.length);
2822
+ // on the wire an empty target_ids means all targets, so an explicit empty list must not pass through
2823
+ if (targetIds && targetIds.length === 0) {
2824
+ throw new Error('targetIds is empty, omit it to send to all targets of the org broadcast');
2825
+ }
2826
+ const request = new grpcPuppet.OrgBroadcastExecuteRequest();
2827
+ request.setId(orgBroadcastId);
2828
+ request.setTargetIdsList(targetIds ?? []);
2829
+ await util.promisify(this.grpcManager.client.orgBroadcastExecute.bind(this.grpcManager.client))(request);
2830
+ }
2571
2831
  healthCheckInterval;
2572
2832
  startHealthCheck() {
2573
2833
  this.healthCheckInterval = setInterval(() => {
@@ -2583,23 +2843,40 @@ class PuppetService extends PUPPET.Puppet {
2583
2843
  waitingForReady = false;
2584
2844
  reconnectIndicator;
2585
2845
  async reset() {
2586
- if (!this._grpcManager) {
2587
- this.log.warn('PuppetService', 'grpc manager not constructed, perform regular reset');
2588
- return super.reset();
2589
- }
2590
- if (!this.isLoggedIn) {
2591
- this.log.warn('PuppetService', 'puppet not logged in, perform regular reset');
2592
- return super.reset();
2593
- }
2594
2846
  if (this.reconnectIndicator.value()) {
2595
2847
  this.log.warn('PuppetService', 'already trying to reconnect, pass this one');
2596
2848
  return;
2597
2849
  }
2598
2850
  this.reconnectIndicator.value(true);
2599
- this.grpcManager.stopStream();
2600
- 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
+ }
2601
2875
  const onLoginResolve = (resolve) => {
2602
2876
  const onLogin = (event) => {
2877
+ if (!this.ownsEventWorker(manager, epoch)) {
2878
+ return;
2879
+ }
2603
2880
  const type = event.getType();
2604
2881
  const payload = event.getPayload();
2605
2882
  if (this.waitingForLogin && type === grpcPuppet.EventType.EVENT_TYPE_LOGIN) {
@@ -2615,6 +2892,9 @@ class PuppetService extends PUPPET.Puppet {
2615
2892
  };
2616
2893
  const onReadyResolve = (resolve) => {
2617
2894
  const onReady = (event) => {
2895
+ if (!this.ownsEventWorker(manager, epoch)) {
2896
+ return;
2897
+ }
2618
2898
  const type = event.getType();
2619
2899
  if (this.waitingForReady && type === grpcPuppet.EventType.EVENT_TYPE_READY) {
2620
2900
  this.waitingForReady = false;
@@ -2623,60 +2903,85 @@ class PuppetService extends PUPPET.Puppet {
2623
2903
  };
2624
2904
  return onReady;
2625
2905
  };
2626
- let onLogin;
2627
- let onReady;
2906
+ const removeListeners = [];
2628
2907
  const loginFuture = new Promise(resolve => {
2629
- onLogin = onLoginResolve(resolve);
2630
- this.grpcManager.on('data', onLogin);
2908
+ const onLogin = onLoginResolve(resolve);
2909
+ manager.on('data', onLogin);
2910
+ removeListeners.push(() => { manager.off('data', onLogin); });
2631
2911
  });
2632
2912
  const readyFuture = new Promise(resolve => {
2633
- onReady = onReadyResolve(resolve);
2634
- this.grpcManager.on('data', onReady);
2913
+ const onReady = onReadyResolve(resolve);
2914
+ manager.on('data', onReady);
2915
+ removeListeners.push(() => { manager.off('data', onReady); });
2635
2916
  });
2636
2917
  const startTime = Date.now();
2637
2918
  const timeoutMilliseconds = this.timeoutMilliseconds / 10; // 2 min default, 4 min xiaoju-bot
2638
- 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
+ }
2639
2950
  try {
2640
- await timeoutPromise(this.grpcManager.startStream(lastEventSeq, accountId), 30 * 1000);
2641
- break;
2951
+ await timeoutPromise(loginFuture, ResetLoginTimeout)
2952
+ .finally(() => {
2953
+ if (epoch === this.eventEpoch) {
2954
+ this.waitingForLogin = false;
2955
+ }
2956
+ });
2642
2957
  }
2643
2958
  catch (e) {
2644
- if (Date.now() - startTime < timeoutMilliseconds) {
2645
- this.log.warn('failed to start stream, will try again in 15 seconds');
2646
- await new Promise(resolve => {
2647
- setTimeout(resolve, 5000);
2648
- });
2649
- }
2650
- else {
2651
- this.log.warn('failed to start stream and reaches timeout, will perform regular reset');
2652
- this.reconnectIndicator.value(false);
2653
- return super.reset();
2959
+ if (!this.ownsEventWorker(manager, epoch)) {
2960
+ return;
2654
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');
2655
2975
  }
2656
2976
  }
2657
- this.waitingForLogin = true;
2658
- this.waitingForReady = true;
2659
- try {
2660
- await timeoutPromise(loginFuture, ResetLoginTimeout)
2661
- .finally(() => {
2977
+ finally {
2978
+ for (const removeListener of removeListeners) {
2979
+ removeListener();
2980
+ }
2981
+ if (epoch === this.eventEpoch) {
2662
2982
  this.waitingForLogin = false;
2663
- this.reconnectIndicator.value(false);
2664
- this.grpcManager.off('data', onLogin);
2665
- });
2666
- }
2667
- catch (e) {
2668
- this.log.warn('PuppetService', 'waiting for event reset login error, will perform regular reset');
2669
- return super.reset();
2670
- }
2671
- try {
2672
- await timeoutPromise(readyFuture, ResetReadyTimeout)
2673
- .finally(() => {
2674
2983
  this.waitingForReady = false;
2675
- this.grpcManager.off('data', onReady);
2676
- });
2677
- }
2678
- catch (e) {
2679
- this.log.warn('PuppetService', 'waiting for event reset ready error, will do nothing');
2984
+ }
2680
2985
  }
2681
2986
  }
2682
2987
  async getMiscellaneousStoreData() {
@@ -2700,27 +3005,29 @@ class PuppetService extends PUPPET.Puppet {
2700
3005
  accountId,
2701
3006
  };
2702
3007
  }
2703
- async setMiscellaneousStoreData(data) {
3008
+ async setMiscellaneousStoreData(data, epoch) {
2704
3009
  if (envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
2705
3010
  return;
2706
3011
  }
2707
- if (typeof data.lastEventSeq !== 'undefined') {
2708
- await this._payloadStore.miscellaneous?.set('lastEventSeq', data.lastEventSeq);
3012
+ const changes = new Map();
3013
+ if (data.lastEventTimestamp !== undefined) {
3014
+ changes.set('lastEventTimestamp', data.lastEventTimestamp);
2709
3015
  }
2710
- if (typeof data.lastEventTimestamp !== 'undefined') {
2711
- await this._payloadStore.miscellaneous?.set('lastEventTimestamp', data.lastEventTimestamp);
3016
+ if (data.accountId !== undefined) {
3017
+ changes.set('accountId', data.accountId);
2712
3018
  }
2713
- if (typeof data.accountId !== 'undefined') {
2714
- await this._payloadStore.miscellaneous?.set('accountId', data.accountId);
3019
+ if (data.lastEventSeq !== undefined) {
3020
+ changes.set('lastEventSeq', data.lastEventSeq);
2715
3021
  }
3022
+ await this.withEventCheckpoint(async () => this.writeEventMetadata(changes, epoch));
2716
3023
  }
2717
- async resetMiscellaneousStoreData() {
3024
+ async resetMiscellaneousStoreData(epoch) {
2718
3025
  if (envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
2719
3026
  return;
2720
3027
  }
2721
- await this._payloadStore.miscellaneous?.delete('lastEventSeq');
2722
- await this._payloadStore.miscellaneous?.delete('lastEventTimestamp');
2723
- await this._payloadStore.miscellaneous?.delete('accountId');
3028
+ await this.withEventCheckpoint(async () => this.writeEventMetadata(new Map([
3029
+ ['lastEventSeq', undefined], ['lastEventTimestamp', undefined], ['accountId', undefined],
3030
+ ]), epoch));
2724
3031
  }
2725
3032
  }
2726
3033
  export { PuppetService, };