@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
@@ -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;
@@ -345,10 +525,19 @@ class PuppetService extends PUPPET.Puppet {
345
525
  case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_CALL:
346
526
  this.emit('call', JSON.parse(payload));
347
527
  break;
528
+ case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_ORG_BROADCAST_CREATED:
529
+ this.emit('org-broadcast-created', JSON.parse(payload));
530
+ break;
531
+ case wechaty_grpc_1.puppet.EventType.EVENT_TYPE_ORG_BROADCAST_SENT:
532
+ this.emit('org-broadcast-sent', JSON.parse(payload));
533
+ break;
348
534
  default:
349
535
  // Huan(202003): in default, the `type` type should be `never`, please check.
350
536
  this.log.error(`eventType ${type} unsupported! data: ${payload}`);
351
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
+ }
352
541
  }
353
542
  async logout(reason) {
354
543
  this.log.verbose('PuppetService', 'logout(%s)', reason ? `"${reason}"` : '');
@@ -404,8 +593,14 @@ class PuppetService extends PUPPET.Puppet {
404
593
  [PUPPET.types.Dirty.Friendship]: async (_) => { },
405
594
  [PUPPET.types.Dirty.Message]: async (_) => { },
406
595
  [PUPPET.types.Dirty.Post]: async (_) => { },
407
- [PUPPET.types.Dirty.Room]: async (id) => this._payloadStore.room?.delete(id),
408
- [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
+ },
409
604
  [PUPPET.types.Dirty.Tag]: async (id) => this._payloadStore.tag?.delete(id),
410
605
  [PUPPET.types.Dirty.TagGroup]: async (id) => this._payloadStore.tagGroup?.delete(id),
411
606
  [PUPPET.types.Dirty.Unspecified]: async (id) => { throw new Error('Unspecified type with id: ' + id); },
@@ -1574,8 +1769,9 @@ class PuppetService extends PUPPET.Puppet {
1574
1769
  */
1575
1770
  async roomRawPayload(id) {
1576
1771
  this.log.verbose('PuppetService', 'roomRawPayload(%s)', id);
1577
- const cachedPayload = await this._payloadStore.room?.get(id);
1578
- 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)) {
1579
1775
  this.log.silly('PuppetService', 'roomRawPayload(%s) cache HIT', id);
1580
1776
  return cachedPayload;
1581
1777
  }
@@ -1583,26 +1779,52 @@ class PuppetService extends PUPPET.Puppet {
1583
1779
  request.setId(id);
1584
1780
  const response = await util_1.default.promisify(this.grpcManager.client.roomPayload
1585
1781
  .bind(this.grpcManager.client))(request);
1586
- const payload = {
1587
- adminIdList: response.getAdminIdsList(),
1588
- avatar: response.getAvatar(),
1589
- handle: response.getHandle(),
1590
- id: response.getId(),
1591
- memberIdList: response.getMemberIdsList(),
1592
- ownerId: response.getOwnerId(),
1593
- topic: response.getTopic(),
1594
- additionalInfo: response.getAdditionalInfo(),
1595
- remark: response.getRoomRemark(),
1596
- external: response.getExternal(),
1597
- };
1598
- const createTime = response.getCreateTime();
1599
- if (createTime) {
1600
- payload.createTime = (0, timestamp_js_1.millisecondsFromTimestamp)(createTime);
1601
- }
1602
- 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));
1603
1784
  this.log.silly('PuppetService', 'roomRawPayload(%s) cache SET', id);
1604
1785
  return payload;
1605
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
+ }
1606
1828
  async roomRawPayloadParser(payload) {
1607
1829
  // this.log.silly('PuppetService', 'roomRawPayloadParser({id:%s})', payload.id)
1608
1830
  // passthrough
@@ -1612,10 +1834,11 @@ class PuppetService extends PUPPET.Puppet {
1612
1834
  this.log.verbose('PuppetService', 'batchRoomRawPayload(%s)', roomIdList);
1613
1835
  const result = new Map();
1614
1836
  const roomIdSet = new Set(roomIdList);
1837
+ const generations = new Map(Array.from(roomIdSet, id => [id, this.roomCache.generation(id)]));
1615
1838
  const needGetSet = new Set();
1616
1839
  for (const roomId of roomIdSet) {
1617
- const cachedRoomPayload = await this._payloadStore.room?.get(roomId);
1618
- 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)) {
1619
1842
  result.set(roomId, cachedRoomPayload);
1620
1843
  }
1621
1844
  else {
@@ -1631,27 +1854,18 @@ class PuppetService extends PUPPET.Puppet {
1631
1854
  const payloads = response.getRoomPayloadsList();
1632
1855
  for (const payload of payloads) {
1633
1856
  const roomId = payload.getId();
1634
- const puppetPayload = {
1635
- adminIdList: payload.getAdminIdsList(),
1636
- avatar: payload.getAvatar(),
1637
- handle: payload.getHandle(),
1638
- id: payload.getId(),
1639
- memberIdList: payload.getMemberIdsList(),
1640
- ownerId: payload.getOwnerId(),
1641
- topic: payload.getTopic(),
1642
- additionalInfo: payload.getAdditionalInfo(),
1643
- remark: payload.getRoomRemark(),
1644
- external: payload.getExternal(),
1645
- };
1646
- const createTime = payload.getCreateTime();
1647
- if (createTime) {
1648
- puppetPayload.createTime = (0, timestamp_js_1.millisecondsFromTimestamp)(createTime);
1649
- }
1857
+ const puppetPayload = (0, pb_payload_helper_js_1.roomPbToPayload)(payload);
1650
1858
  result.set(roomId, puppetPayload);
1651
- 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
+ }
1652
1863
  }
1653
1864
  }
1654
1865
  catch (e) {
1866
+ if (e instanceof room_cache_coordinator_js_1.RoomCacheOperationError) {
1867
+ throw e;
1868
+ }
1655
1869
  this.log.error('PuppetService', 'batchRoomRawPayload(%s, %s) error: %s, use one by one method', roomIdList, needGetSet, e);
1656
1870
  let fallbackSuccessCount = 0;
1657
1871
  for (const roomId of needGetSet) {
@@ -1837,9 +2051,10 @@ class PuppetService extends PUPPET.Puppet {
1837
2051
  }
1838
2052
  async roomMemberRawPayload(roomId, contactId) {
1839
2053
  this.log.verbose('PuppetService', 'roomMemberRawPayload(%s, %s)', roomId, contactId);
1840
- 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));
1841
2056
  const cachedRoomMemberPayload = cachedPayload && cachedPayload[contactId];
1842
- if (cachedRoomMemberPayload) {
2057
+ if (cachedRoomMemberPayload && generation === this.roomCache.generation(roomId)) {
1843
2058
  this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache HIT', roomId, contactId);
1844
2059
  return cachedRoomMemberPayload;
1845
2060
  }
@@ -1858,9 +2073,9 @@ class PuppetService extends PUPPET.Puppet {
1858
2073
  joinScene: response.getJoinScene(),
1859
2074
  joinTime: response.getJoinTime(),
1860
2075
  };
1861
- await this._payloadStore.roomMember?.set(roomId, {
1862
- ...cachedPayload,
1863
- [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 });
1864
2079
  });
1865
2080
  this.log.silly('PuppetService', 'roomMemberRawPayload(%s, %s) cache SET', roomId, contactId);
1866
2081
  return payload;
@@ -1872,10 +2087,12 @@ class PuppetService extends PUPPET.Puppet {
1872
2087
  }
1873
2088
  async batchRoomMemberRawPayload(roomId, contactIdList) {
1874
2089
  this.log.verbose('PuppetService', 'batchRoomMemberRawPayload(%s, %s)', roomId, contactIdList);
2090
+ const generation = this.roomCache.generation(roomId);
1875
2091
  const result = new Map();
1876
2092
  const contactIdSet = new Set(contactIdList);
1877
2093
  let needGetSet = new Set();
1878
- 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 || {} : {};
1879
2096
  if (Object.keys(cachedPayload).length > 0) {
1880
2097
  for (const contactId of contactIdSet) {
1881
2098
  const cachedRoomMemberPayload = cachedPayload[contactId];
@@ -1898,15 +2115,22 @@ class PuppetService extends PUPPET.Puppet {
1898
2115
  const response = await util_1.default.promisify(this.grpcManager.client.batchRoomMemberPayload
1899
2116
  .bind(this.grpcManager.client))(request);
1900
2117
  const payloads = response.getMemberPayloadsList();
2118
+ const fetched = {};
1901
2119
  for (const payload of payloads) {
1902
2120
  const contactId = payload.getId();
1903
2121
  const puppetPayload = (0, pb_payload_helper_js_1.roomMemberPbToPayload)(payload);
1904
2122
  result.set(contactId, puppetPayload);
1905
- cachedPayload[contactId] = puppetPayload;
2123
+ fetched[contactId] = puppetPayload;
1906
2124
  }
1907
- 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
+ });
1908
2129
  }
1909
2130
  catch (e) {
2131
+ if (e instanceof room_cache_coordinator_js_1.RoomCacheOperationError) {
2132
+ throw e;
2133
+ }
1910
2134
  this.log.error('PuppetService', 'batchRoomMemberRawPayload(%s, %s) error: %s, use one by one method', roomId, needGetSet, e);
1911
2135
  for (const contactId of needGetSet) {
1912
2136
  const payload = await this.roomMemberRawPayload(roomId, contactId);
@@ -2597,6 +2821,42 @@ class PuppetService extends PUPPET.Puppet {
2597
2821
  }
2598
2822
  return result;
2599
2823
  }
2824
+ async orgBroadcastPayload(orgBroadcastId) {
2825
+ this.log.verbose('PuppetService', 'orgBroadcastPayload(%s)', orgBroadcastId);
2826
+ const request = new wechaty_grpc_1.puppet.OrgBroadcastPayloadRequest();
2827
+ request.setId(orgBroadcastId);
2828
+ const response = await util_1.default.promisify(this.grpcManager.client.orgBroadcastPayload.bind(this.grpcManager.client))(request);
2829
+ return {
2830
+ id: response.getId(),
2831
+ sendType: response.getSendType(),
2832
+ conversationType: response.getConversationType(),
2833
+ creatorId: response.getCreatorId() || undefined,
2834
+ execTime: response.getExecTime(),
2835
+ status: response.getStatus(),
2836
+ canCancel: response.getCanCancel(),
2837
+ allowSelect: response.getAllowSelect(),
2838
+ sent: response.getSent(),
2839
+ totalCount: response.getTotalCount(),
2840
+ sentCount: response.getSentCount(),
2841
+ contentListJson: response.getContentListJson(),
2842
+ targets: response.getTargetsList().map(targetPb => ({
2843
+ contactId: targetPb.getContactId() || undefined,
2844
+ roomId: targetPb.getRoomId() || undefined,
2845
+ status: targetPb.getStatus(),
2846
+ })),
2847
+ };
2848
+ }
2849
+ async orgBroadcastExecute(orgBroadcastId, targetIds) {
2850
+ this.log.verbose('PuppetService', 'orgBroadcastExecute(%s, %s)', orgBroadcastId, targetIds?.length);
2851
+ // on the wire an empty target_ids means all targets, so an explicit empty list must not pass through
2852
+ if (targetIds && targetIds.length === 0) {
2853
+ throw new Error('targetIds is empty, omit it to send to all targets of the org broadcast');
2854
+ }
2855
+ const request = new wechaty_grpc_1.puppet.OrgBroadcastExecuteRequest();
2856
+ request.setId(orgBroadcastId);
2857
+ request.setTargetIdsList(targetIds ?? []);
2858
+ await util_1.default.promisify(this.grpcManager.client.orgBroadcastExecute.bind(this.grpcManager.client))(request);
2859
+ }
2600
2860
  healthCheckInterval;
2601
2861
  startHealthCheck() {
2602
2862
  this.healthCheckInterval = setInterval(() => {
@@ -2612,23 +2872,40 @@ class PuppetService extends PUPPET.Puppet {
2612
2872
  waitingForReady = false;
2613
2873
  reconnectIndicator;
2614
2874
  async reset() {
2615
- if (!this._grpcManager) {
2616
- this.log.warn('PuppetService', 'grpc manager not constructed, perform regular reset');
2617
- return super.reset();
2618
- }
2619
- if (!this.isLoggedIn) {
2620
- this.log.warn('PuppetService', 'puppet not logged in, perform regular reset');
2621
- return super.reset();
2622
- }
2623
2875
  if (this.reconnectIndicator.value()) {
2624
2876
  this.log.warn('PuppetService', 'already trying to reconnect, pass this one');
2625
2877
  return;
2626
2878
  }
2627
2879
  this.reconnectIndicator.value(true);
2628
- this.grpcManager.stopStream();
2629
- 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
+ }
2630
2904
  const onLoginResolve = (resolve) => {
2631
2905
  const onLogin = (event) => {
2906
+ if (!this.ownsEventWorker(manager, epoch)) {
2907
+ return;
2908
+ }
2632
2909
  const type = event.getType();
2633
2910
  const payload = event.getPayload();
2634
2911
  if (this.waitingForLogin && type === wechaty_grpc_1.puppet.EventType.EVENT_TYPE_LOGIN) {
@@ -2644,6 +2921,9 @@ class PuppetService extends PUPPET.Puppet {
2644
2921
  };
2645
2922
  const onReadyResolve = (resolve) => {
2646
2923
  const onReady = (event) => {
2924
+ if (!this.ownsEventWorker(manager, epoch)) {
2925
+ return;
2926
+ }
2647
2927
  const type = event.getType();
2648
2928
  if (this.waitingForReady && type === wechaty_grpc_1.puppet.EventType.EVENT_TYPE_READY) {
2649
2929
  this.waitingForReady = false;
@@ -2652,60 +2932,85 @@ class PuppetService extends PUPPET.Puppet {
2652
2932
  };
2653
2933
  return onReady;
2654
2934
  };
2655
- let onLogin;
2656
- let onReady;
2935
+ const removeListeners = [];
2657
2936
  const loginFuture = new Promise(resolve => {
2658
- onLogin = onLoginResolve(resolve);
2659
- this.grpcManager.on('data', onLogin);
2937
+ const onLogin = onLoginResolve(resolve);
2938
+ manager.on('data', onLogin);
2939
+ removeListeners.push(() => { manager.off('data', onLogin); });
2660
2940
  });
2661
2941
  const readyFuture = new Promise(resolve => {
2662
- onReady = onReadyResolve(resolve);
2663
- this.grpcManager.on('data', onReady);
2942
+ const onReady = onReadyResolve(resolve);
2943
+ manager.on('data', onReady);
2944
+ removeListeners.push(() => { manager.off('data', onReady); });
2664
2945
  });
2665
2946
  const startTime = Date.now();
2666
2947
  const timeoutMilliseconds = this.timeoutMilliseconds / 10; // 2 min default, 4 min xiaoju-bot
2667
- 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
+ }
2668
2979
  try {
2669
- await (0, gerror_1.timeoutPromise)(this.grpcManager.startStream(lastEventSeq, accountId), 30 * 1000);
2670
- break;
2980
+ await (0, gerror_1.timeoutPromise)(loginFuture, ResetLoginTimeout)
2981
+ .finally(() => {
2982
+ if (epoch === this.eventEpoch) {
2983
+ this.waitingForLogin = false;
2984
+ }
2985
+ });
2671
2986
  }
2672
2987
  catch (e) {
2673
- if (Date.now() - startTime < timeoutMilliseconds) {
2674
- this.log.warn('failed to start stream, will try again in 15 seconds');
2675
- await new Promise(resolve => {
2676
- setTimeout(resolve, 5000);
2677
- });
2678
- }
2679
- else {
2680
- this.log.warn('failed to start stream and reaches timeout, will perform regular reset');
2681
- this.reconnectIndicator.value(false);
2682
- return super.reset();
2988
+ if (!this.ownsEventWorker(manager, epoch)) {
2989
+ return;
2683
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');
2684
3004
  }
2685
3005
  }
2686
- this.waitingForLogin = true;
2687
- this.waitingForReady = true;
2688
- try {
2689
- await (0, gerror_1.timeoutPromise)(loginFuture, ResetLoginTimeout)
2690
- .finally(() => {
3006
+ finally {
3007
+ for (const removeListener of removeListeners) {
3008
+ removeListener();
3009
+ }
3010
+ if (epoch === this.eventEpoch) {
2691
3011
  this.waitingForLogin = false;
2692
- this.reconnectIndicator.value(false);
2693
- this.grpcManager.off('data', onLogin);
2694
- });
2695
- }
2696
- catch (e) {
2697
- this.log.warn('PuppetService', 'waiting for event reset login error, will perform regular reset');
2698
- return super.reset();
2699
- }
2700
- try {
2701
- await (0, gerror_1.timeoutPromise)(readyFuture, ResetReadyTimeout)
2702
- .finally(() => {
2703
3012
  this.waitingForReady = false;
2704
- this.grpcManager.off('data', onReady);
2705
- });
2706
- }
2707
- catch (e) {
2708
- this.log.warn('PuppetService', 'waiting for event reset ready error, will do nothing');
3013
+ }
2709
3014
  }
2710
3015
  }
2711
3016
  async getMiscellaneousStoreData() {
@@ -2729,27 +3034,29 @@ class PuppetService extends PUPPET.Puppet {
2729
3034
  accountId,
2730
3035
  };
2731
3036
  }
2732
- async setMiscellaneousStoreData(data) {
3037
+ async setMiscellaneousStoreData(data, epoch) {
2733
3038
  if (config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
2734
3039
  return;
2735
3040
  }
2736
- if (typeof data.lastEventSeq !== 'undefined') {
2737
- await this._payloadStore.miscellaneous?.set('lastEventSeq', data.lastEventSeq);
3041
+ const changes = new Map();
3042
+ if (data.lastEventTimestamp !== undefined) {
3043
+ changes.set('lastEventTimestamp', data.lastEventTimestamp);
2738
3044
  }
2739
- if (typeof data.lastEventTimestamp !== 'undefined') {
2740
- await this._payloadStore.miscellaneous?.set('lastEventTimestamp', data.lastEventTimestamp);
3045
+ if (data.accountId !== undefined) {
3046
+ changes.set('accountId', data.accountId);
2741
3047
  }
2742
- if (typeof data.accountId !== 'undefined') {
2743
- await this._payloadStore.miscellaneous?.set('accountId', data.accountId);
3048
+ if (data.lastEventSeq !== undefined) {
3049
+ changes.set('lastEventSeq', data.lastEventSeq);
2744
3050
  }
3051
+ await this.withEventCheckpoint(async () => this.writeEventMetadata(changes, epoch));
2745
3052
  }
2746
- async resetMiscellaneousStoreData() {
3053
+ async resetMiscellaneousStoreData(epoch) {
2747
3054
  if (config_js_1.envVars.WECHATY_PUPPET_SERVICE_DISABLE_EVENT_CACHE()) {
2748
3055
  return;
2749
3056
  }
2750
- await this._payloadStore.miscellaneous?.delete('lastEventSeq');
2751
- await this._payloadStore.miscellaneous?.delete('lastEventTimestamp');
2752
- await this._payloadStore.miscellaneous?.delete('accountId');
3057
+ await this.withEventCheckpoint(async () => this.writeEventMetadata(new Map([
3058
+ ['lastEventSeq', undefined], ['lastEventTimestamp', undefined], ['accountId', undefined],
3059
+ ]), epoch));
2753
3060
  }
2754
3061
  }
2755
3062
  exports.PuppetService = PuppetService;