@juzi/wechaty-puppet-service 1.0.122 → 1.0.124

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 (47) hide show
  1. package/dist/cjs/src/client/grpc-manager.d.ts +1 -0
  2. package/dist/cjs/src/client/grpc-manager.d.ts.map +1 -1
  3. package/dist/cjs/src/client/grpc-manager.js +49 -47
  4. package/dist/cjs/src/client/grpc-manager.js.map +1 -1
  5. package/dist/cjs/src/client/payload-store.d.ts +3 -0
  6. package/dist/cjs/src/client/payload-store.d.ts.map +1 -1
  7. package/dist/cjs/src/client/payload-store.js +8 -6
  8. package/dist/cjs/src/client/payload-store.js.map +1 -1
  9. package/dist/cjs/src/client/puppet-service.d.ts +14 -0
  10. package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
  11. package/dist/cjs/src/client/puppet-service.js +284 -219
  12. package/dist/cjs/src/client/puppet-service.js.map +1 -1
  13. package/dist/cjs/src/package-json.d.ts.map +1 -1
  14. package/dist/cjs/src/package-json.js +4 -3
  15. package/dist/cjs/src/package-json.js.map +1 -1
  16. package/dist/cjs/tests/fast-dirty-room-member.spec.js +123 -21
  17. package/dist/cjs/tests/fast-dirty-room-member.spec.js.map +1 -1
  18. package/dist/cjs/tests/grpc-stream-dirty-order.spec.d.ts +3 -0
  19. package/dist/cjs/tests/grpc-stream-dirty-order.spec.d.ts.map +1 -0
  20. package/dist/cjs/tests/grpc-stream-dirty-order.spec.js +135 -0
  21. package/dist/cjs/tests/grpc-stream-dirty-order.spec.js.map +1 -0
  22. package/dist/esm/src/client/grpc-manager.d.ts +1 -0
  23. package/dist/esm/src/client/grpc-manager.d.ts.map +1 -1
  24. package/dist/esm/src/client/grpc-manager.js +50 -48
  25. package/dist/esm/src/client/grpc-manager.js.map +1 -1
  26. package/dist/esm/src/client/payload-store.d.ts +3 -0
  27. package/dist/esm/src/client/payload-store.d.ts.map +1 -1
  28. package/dist/esm/src/client/payload-store.js +8 -6
  29. package/dist/esm/src/client/payload-store.js.map +1 -1
  30. package/dist/esm/src/client/puppet-service.d.ts +14 -0
  31. package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
  32. package/dist/esm/src/client/puppet-service.js +284 -219
  33. package/dist/esm/src/client/puppet-service.js.map +1 -1
  34. package/dist/esm/src/package-json.d.ts.map +1 -1
  35. package/dist/esm/src/package-json.js +4 -3
  36. package/dist/esm/src/package-json.js.map +1 -1
  37. package/dist/esm/tests/fast-dirty-room-member.spec.js +123 -21
  38. package/dist/esm/tests/fast-dirty-room-member.spec.js.map +1 -1
  39. package/dist/esm/tests/grpc-stream-dirty-order.spec.d.ts +3 -0
  40. package/dist/esm/tests/grpc-stream-dirty-order.spec.d.ts.map +1 -0
  41. package/dist/esm/tests/grpc-stream-dirty-order.spec.js +110 -0
  42. package/dist/esm/tests/grpc-stream-dirty-order.spec.js.map +1 -0
  43. package/package.json +4 -3
  44. package/src/client/grpc-manager.ts +54 -48
  45. package/src/client/payload-store.ts +13 -7
  46. package/src/client/puppet-service.ts +283 -220
  47. package/src/package-json.ts +4 -3
@@ -1,19 +1,24 @@
1
1
  #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
2
  /**
3
- * Regression test for the RoomMember persistent-store dirty bug.
3
+ * Regression tests for the RoomMember persistent-store dirty semantics.
4
4
  *
5
- * The puppet-side cache mixin already knows that RoomMember dirty ids are
6
- * sometimes a compound `"<roomId><memberId>"` (using STRING_SPLITTER)
7
- * and splits the id before deleting from the in-memory LRU
8
- * (wechaty-puppet/src/mixins/cache-mixin.ts).
5
+ * The persistent `PayloadStore.roomMember` is a
6
+ * `FlashStore<roomId, {[memberId]: RoomMember}>` -- a per-room record of
7
+ * members. Dirty ids arrive in two shapes:
9
8
  *
10
- * However the client-side `fastDirty` here passes the raw id straight to
11
- * `PayloadStore.roomMember.delete`. The persistent FlashStore is keyed by
12
- * roomId only, so the delete silently no-ops and the stale entry survives
13
- * across process restarts.
9
+ * 1. A compound `"<roomId><STRING_SPLITTER><memberId>"` -- a single
10
+ * member's payload went stale (e.g. one member renamed).
11
+ * 2. A bare `"<roomId>"` -- the whole room's member set went stale.
14
12
  *
15
- * This test populates the store, fires the dirty path with a compound id,
16
- * and asserts the entry is gone.
13
+ * The previous handler treated both shapes the same by calling
14
+ * `roomMember.delete(roomId)`, which threw away the entire room's
15
+ * cache even when only one member was dirty. That thrash forces the
16
+ * next N-1 members to re-fetch over gRPC.
17
+ *
18
+ * The new handler must:
19
+ * - On compound id: drop only the named member, keep the rest.
20
+ * - On compound id whose split leaves the record empty: delete the row.
21
+ * - On bare roomId: delete the entire row (as before).
17
22
  */
18
23
  import { test } from 'tstest';
19
24
  import os from 'os';
@@ -21,29 +26,126 @@ import path from 'path';
21
26
  import fs from 'fs';
22
27
  import * as PUPPET from '@juzi/wechaty-puppet';
23
28
  import { PuppetService } from '../src/mod.js';
24
- test('fastDirty(RoomMember, "<roomId>\\u001F<memberId>") must clear the room entry', async (t) => {
29
+ const makePuppet = async () => {
25
30
  const token = `puppet_service_test_${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
26
31
  const puppet = new PuppetService({ token });
27
32
  const accountId = 'acct-test';
28
33
  await puppet._payloadStore.start(accountId);
34
+ return { puppet, token };
35
+ };
36
+ const cleanup = async (puppet, token) => {
37
+ await puppet._payloadStore.stop();
38
+ try {
39
+ await fs.promises.rm(path.join(os.homedir(), '.wechaty', 'wechaty-puppet-service', token), { recursive: true, force: true });
40
+ }
41
+ catch (_) { /* ignore */ }
42
+ };
43
+ test('fastDirty(RoomMember, "<roomId>\\u001F<memberId>") drops only the named member', async (t) => {
44
+ const { puppet, token } = await makePuppet();
29
45
  const roomId = 'room-test-id';
30
46
  await puppet._payloadStore.roomMember.set(roomId, {
31
47
  'member-A': { id: 'member-A', name: 'A' },
32
48
  'member-B': { id: 'member-B', name: 'B' },
33
49
  });
34
- const before = await puppet._payloadStore.roomMember.get(roomId);
35
- t.ok(before, 'sanity: roomMember entry exists before dirty');
36
50
  await puppet.fastDirty({
37
51
  payloadType: PUPPET.types.Dirty.RoomMember,
38
52
  payloadId: `${roomId}${PUPPET.STRING_SPLITTER}member-A`,
39
53
  });
40
54
  const after = await puppet._payloadStore.roomMember.get(roomId);
41
- t.notOk(after, 'roomMember entry must be cleared after compound-id dirty');
42
- await puppet._payloadStore.stop();
43
- // best-effort cleanup of the on-disk store dir
44
- try {
45
- await fs.promises.rm(path.join(os.homedir(), '.wechaty', 'wechaty-puppet-service', token), { recursive: true, force: true });
46
- }
47
- catch (_) { /* ignore */ }
55
+ t.ok(after, 'room entry must survive when only one member is dirtied');
56
+ t.notOk(after && after['member-A'], 'dirtied member-A must be gone');
57
+ t.ok(after && after['member-B'], 'unrelated member-B must be preserved');
58
+ await cleanup(puppet, token);
59
+ });
60
+ test('fastDirty(RoomMember, "<roomId>\\u001F<lastMember>") deletes the row when empty', async (t) => {
61
+ const { puppet, token } = await makePuppet();
62
+ const roomId = 'room-test-id-lone';
63
+ await puppet._payloadStore.roomMember.set(roomId, {
64
+ 'only-member': { id: 'only-member', name: 'Solo' },
65
+ });
66
+ await puppet.fastDirty({
67
+ payloadType: PUPPET.types.Dirty.RoomMember,
68
+ payloadId: `${roomId}${PUPPET.STRING_SPLITTER}only-member`,
69
+ });
70
+ const after = await puppet._payloadStore.roomMember.get(roomId);
71
+ t.notOk(after, 'row must be deleted once the record has no members left');
72
+ await cleanup(puppet, token);
73
+ });
74
+ test('fastDirty(RoomMember, "<roomId>") clears the whole room entry', async (t) => {
75
+ const { puppet, token } = await makePuppet();
76
+ const roomId = 'room-test-id-full';
77
+ await puppet._payloadStore.roomMember.set(roomId, {
78
+ 'member-A': { id: 'member-A', name: 'A' },
79
+ 'member-B': { id: 'member-B', name: 'B' },
80
+ });
81
+ await puppet.fastDirty({
82
+ payloadType: PUPPET.types.Dirty.RoomMember,
83
+ payloadId: roomId,
84
+ });
85
+ const after = await puppet._payloadStore.roomMember.get(roomId);
86
+ t.notOk(after, 'bare roomId dirty must clear the whole row');
87
+ await cleanup(puppet, token);
88
+ });
89
+ test('fastDirty(RoomMember) on unknown member is a no-op', async (t) => {
90
+ const { puppet, token } = await makePuppet();
91
+ const roomId = 'room-test-id-noop';
92
+ const before = {
93
+ 'member-A': { id: 'member-A', name: 'A' },
94
+ 'member-B': { id: 'member-B', name: 'B' },
95
+ };
96
+ await puppet._payloadStore.roomMember.set(roomId, before);
97
+ await puppet.fastDirty({
98
+ payloadType: PUPPET.types.Dirty.RoomMember,
99
+ payloadId: `${roomId}${PUPPET.STRING_SPLITTER}ghost-member`,
100
+ });
101
+ const after = await puppet._payloadStore.roomMember.get(roomId);
102
+ t.same(after, before, 'unrelated dirty must not disturb the record');
103
+ await cleanup(puppet, token);
104
+ });
105
+ test('fastDirty(RoomMember) concurrent compound dirties on same roomId drop every named member', async (t) => {
106
+ const { puppet, token } = await makePuppet();
107
+ const roomId = 'room-test-id-race';
108
+ await puppet._payloadStore.roomMember.set(roomId, {
109
+ 'member-A': { id: 'member-A', name: 'A' },
110
+ 'member-B': { id: 'member-B', name: 'B' },
111
+ });
112
+ // Fire both compound dirties without awaiting between them so their
113
+ // `get → mutate → set` cycles interleave. Without per-roomId
114
+ // serialization, both reads would see {A, B}, both writes would keep
115
+ // one sibling, and one of the two deletes would be lost.
116
+ await Promise.all([
117
+ puppet.fastDirty({
118
+ payloadType: PUPPET.types.Dirty.RoomMember,
119
+ payloadId: `${roomId}${PUPPET.STRING_SPLITTER}member-A`,
120
+ }),
121
+ puppet.fastDirty({
122
+ payloadType: PUPPET.types.Dirty.RoomMember,
123
+ payloadId: `${roomId}${PUPPET.STRING_SPLITTER}member-B`,
124
+ }),
125
+ ]);
126
+ const after = await puppet._payloadStore.roomMember.get(roomId);
127
+ // Either the whole row is gone (last shrink deleted it) or the record
128
+ // is empty. Both are acceptable "no members left" observations.
129
+ const noMembersLeft = !after || Object.keys(after).length === 0;
130
+ t.ok(noMembersLeft, `both dirtied members must be evicted, got: ${JSON.stringify(after)}`);
131
+ await cleanup(puppet, token);
132
+ });
133
+ test('fastDirty(RoomMember) prototype-key member id must not match a real member', async (t) => {
134
+ const { puppet, token } = await makePuppet();
135
+ const roomId = 'room-test-id-proto';
136
+ const before = {
137
+ 'member-A': { id: 'member-A', name: 'A' },
138
+ };
139
+ await puppet._payloadStore.roomMember.set(roomId, before);
140
+ // `toString` exists on Object.prototype; a naive `memberId in current`
141
+ // check would match it and take the mutate branch. The correct guard
142
+ // uses hasOwnProperty and treats this as an unknown member.
143
+ await puppet.fastDirty({
144
+ payloadType: PUPPET.types.Dirty.RoomMember,
145
+ payloadId: `${roomId}${PUPPET.STRING_SPLITTER}toString`,
146
+ });
147
+ const after = await puppet._payloadStore.roomMember.get(roomId);
148
+ t.same(after, before, 'prototype-key dirty must be a no-op');
149
+ await cleanup(puppet, token);
48
150
  });
49
151
  //# sourceMappingURL=fast-dirty-room-member.spec.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fast-dirty-room-member.spec.js","sourceRoot":"","sources":["../../../tests/fast-dirty-room-member.spec.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC7B,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,EAAE,MAAM,IAAI,CAAA;AAEnB,OAAO,KAAK,MAAM,MAAM,sBAAsB,CAAA;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAE7C,IAAI,CAAC,8EAA8E,EAAE,KAAK,EAAC,CAAC,EAAC,EAAE;IAC7F,MAAM,KAAK,GAAG,uBAAuB,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,CAAA;IACpF,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,KAAK,EAAE,CAAQ,CAAA;IAElD,MAAM,SAAS,GAAG,WAAW,CAAA;IAC7B,MAAM,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAE3C,MAAM,MAAM,GAAG,cAAc,CAAA;IAC7B,MAAM,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE;QAChD,UAAU,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAS;QAChD,UAAU,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAS;KACjD,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAChE,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,8CAA8C,CAAC,CAAA;IAE5D,MAAM,MAAM,CAAC,SAAS,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU;QAC1C,SAAS,EAAI,GAAG,MAAM,GAAG,MAAM,CAAC,eAAe,UAAU;KAC1D,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/D,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,0DAA0D,CAAC,CAAA;IAE1E,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,EAAE,CAAA;IAEjC,+CAA+C;IAC/C,IAAI;QACF,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAClB,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,wBAAwB,EAAE,KAAK,CAAC,EACpE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CACjC,CAAA;KACF;IAAC,OAAO,CAAC,EAAE,EAAE,YAAY,EAAE;AAC9B,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"fast-dirty-room-member.spec.js","sourceRoot":"","sources":["../../../tests/fast-dirty-room-member.spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
+ export {};
3
+ //# sourceMappingURL=grpc-stream-dirty-order.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grpc-stream-dirty-order.spec.d.ts","sourceRoot":"","sources":["../../../tests/grpc-stream-dirty-order.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2
+ /**
3
+ * Regression / hardening test for the EVENT_TYPE_DIRTY dispatch order.
4
+ *
5
+ * Client-side dirty handling touches two independent caches:
6
+ *
7
+ * - `fastDirty()` clears `_payloadStore` (FlashStore, on disk, async).
8
+ * - `emit('dirty')` triggers the puppet-side CacheMixin which clears
9
+ * the in-memory LRU synchronously in the same tick.
10
+ *
11
+ * If `emit('dirty')` runs before `fastDirty()` resolves, a listener that
12
+ * immediately reads back the payload -- as CacheMixin.onDirty does when
13
+ * a caller races with the dirty event -- can:
14
+ *
15
+ * 1. See an LRU miss (already cleared),
16
+ * 2. Refetch via `contactRawPayload` etc.,
17
+ * 3. Hit the still-un-cleared `_payloadStore` entry,
18
+ * 4. Repopulate the LRU with the stale row that fastDirty is about
19
+ * to delete a microtask later,
20
+ * 5. And the fresh fastDirty delete no-ops against a now-cold key.
21
+ *
22
+ * The current implementation `await`s fastDirty before emitting. This
23
+ * test pins that invariant: fastDirty must fully resolve before any
24
+ * `dirty` listener observes the event.
25
+ */
26
+ import { test, sinon } from 'tstest';
27
+ import * as PUPPET from '@juzi/wechaty-puppet';
28
+ import { puppet as grpcPuppet } from '@juzi/wechaty-grpc';
29
+ import { PuppetService } from '../src/mod.js';
30
+ test('onGrpcStreamEvent awaits fastDirty before emitting dirty', async (t) => {
31
+ const token = `puppet_service_test_${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
32
+ const puppet = new PuppetService({ token });
33
+ let fastDirtyResolvedAt;
34
+ let dirtyEmittedAt;
35
+ const FASTDIRTY_DELAY_MS = 50;
36
+ puppet.fastDirty = async (_payload) => {
37
+ await new Promise(resolve => setTimeout(resolve, FASTDIRTY_DELAY_MS));
38
+ fastDirtyResolvedAt = Date.now();
39
+ };
40
+ puppet.on('dirty', () => {
41
+ dirtyEmittedAt = Date.now();
42
+ });
43
+ const dirtyJson = JSON.stringify({
44
+ payloadType: PUPPET.types.Dirty.Contact,
45
+ payloadId: 'contact-x',
46
+ });
47
+ const fakeEvent = {
48
+ getType: () => grpcPuppet.EventType.EVENT_TYPE_DIRTY,
49
+ getPayload: () => dirtyJson,
50
+ getSeq: () => 0,
51
+ };
52
+ await puppet.onGrpcStreamEvent(fakeEvent);
53
+ t.ok(fastDirtyResolvedAt, 'fastDirty must have been invoked');
54
+ t.ok(dirtyEmittedAt, 'dirty listener must have been invoked');
55
+ t.ok(fastDirtyResolvedAt <= dirtyEmittedAt, `dirty emit (${dirtyEmittedAt}) must not precede fastDirty resolve (${fastDirtyResolvedAt})`);
56
+ });
57
+ test('onGrpcStreamEvent: dirty emit sequence-number is strictly after every fastDirty resolution', async (t) => {
58
+ // Wall-clock ordering (the baseline test above) is fine as a smoke
59
+ // signal but can collapse to a tie when both boundaries fall inside
60
+ // the same millisecond. The invariant we actually care about is
61
+ // *causal*: each fastDirty must have fully resolved before the
62
+ // corresponding dirty emit fires. Record call ordinals from a shared
63
+ // monotonic counter to pin that -- ordinals cannot tie.
64
+ const token = `puppet_service_test_${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
65
+ const puppet = new PuppetService({ token });
66
+ let sequence = 0;
67
+ const nextOrdinal = () => ++sequence;
68
+ // Order of interest per event: fastDirty enter -> fastDirty resolve -> dirty emit.
69
+ const fastDirtyEnterOrdinals = [];
70
+ const fastDirtyResolveOrdinals = [];
71
+ const dirtyEmitOrdinals = [];
72
+ const fastDirtyStub = sinon.stub().callsFake(async (_payload) => {
73
+ fastDirtyEnterOrdinals.push(nextOrdinal());
74
+ // Yield the microtask queue so a broken ordering would surface: if
75
+ // emit ran ahead of the await point below, its ordinal would slot
76
+ // between our enter and resolve records.
77
+ await new Promise(resolve => setImmediate(resolve));
78
+ fastDirtyResolveOrdinals.push(nextOrdinal());
79
+ });
80
+ puppet.fastDirty = fastDirtyStub;
81
+ const dirtySpy = sinon.spy(() => {
82
+ dirtyEmitOrdinals.push(nextOrdinal());
83
+ });
84
+ puppet.on('dirty', dirtySpy);
85
+ const makeEvent = (payloadId) => {
86
+ const dirtyJson = JSON.stringify({
87
+ payloadType: PUPPET.types.Dirty.Contact,
88
+ payloadId,
89
+ });
90
+ return {
91
+ getType: () => grpcPuppet.EventType.EVENT_TYPE_DIRTY,
92
+ getPayload: () => dirtyJson,
93
+ getSeq: () => 0,
94
+ };
95
+ };
96
+ // Two back-to-back dirty events: this is the scenario Fix #6 pins,
97
+ // sequential await must hold across both.
98
+ await puppet.onGrpcStreamEvent(makeEvent('contact-1'));
99
+ await puppet.onGrpcStreamEvent(makeEvent('contact-2'));
100
+ t.equal(fastDirtyStub.callCount, 2, 'fastDirty invoked once per dirty event');
101
+ t.equal(dirtySpy.callCount, 2, 'dirty listener invoked once per dirty event');
102
+ t.equal(fastDirtyEnterOrdinals.length, 2, 'both fastDirty invocations recorded an entry ordinal');
103
+ t.equal(fastDirtyResolveOrdinals.length, 2, 'both fastDirty invocations recorded a resolve ordinal');
104
+ t.equal(dirtyEmitOrdinals.length, 2, 'both dirty emits recorded an ordinal');
105
+ for (let i = 0; i < 2; i++) {
106
+ t.ok(fastDirtyEnterOrdinals[i] < fastDirtyResolveOrdinals[i], `event #${i}: fastDirty must enter (ord=${fastDirtyEnterOrdinals[i]}) before it resolves (ord=${fastDirtyResolveOrdinals[i]})`);
107
+ t.ok(fastDirtyResolveOrdinals[i] < dirtyEmitOrdinals[i], `event #${i}: dirty emit ordinal (${dirtyEmitOrdinals[i]}) must strictly follow fastDirty resolve ordinal (${fastDirtyResolveOrdinals[i]})`);
108
+ }
109
+ });
110
+ //# sourceMappingURL=grpc-stream-dirty-order.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grpc-stream-dirty-order.spec.js","sourceRoot":"","sources":["../../../tests/grpc-stream-dirty-order.spec.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juzi/wechaty-puppet-service",
3
- "version": "1.0.122",
3
+ "version": "1.0.124",
4
4
  "description": "Puppet Service for Wechaty",
5
5
  "type": "module",
6
6
  "exports": {
@@ -51,11 +51,12 @@
51
51
  "@chatie/eslint-config": "^1.0.4",
52
52
  "@chatie/semver": "^0.4.7",
53
53
  "@chatie/tsconfig": "^4.6.3",
54
- "@juzi/wechaty-puppet": "^1.0.146",
54
+ "@juzi/wechaty-puppet": "^1.0.148",
55
55
  "@juzi/wechaty-puppet-mock": "^1.0.1",
56
56
  "@swc/core": "1.3.39",
57
57
  "@types/google-protobuf": "^3.15.5",
58
58
  "@types/lru-cache": "^7.5.0",
59
+ "@types/node": "^20",
59
60
  "@types/semver": "^7.3.9",
60
61
  "@types/temp": "^0.9.1",
61
62
  "@types/uuid": "^8.3.4",
@@ -70,7 +71,7 @@
70
71
  "why-is-node-running": "^2.2.1"
71
72
  },
72
73
  "peerDependencies": {
73
- "@juzi/wechaty-puppet": "^1.0.143"
74
+ "@juzi/wechaty-puppet": "^1.0.148"
74
75
  },
75
76
  "dependencies": {
76
77
  "@juzi/wechaty-grpc": "^1.0.106",
@@ -21,7 +21,10 @@ import util from 'util'
21
21
  import EventEmitter from 'events'
22
22
  import crypto from 'crypto'
23
23
 
24
- import { log } from '@juzi/wechaty-puppet'
24
+ import {
25
+ log,
26
+ type LoggerLike,
27
+ } from '@juzi/wechaty-puppet'
25
28
  import {
26
29
  grpc,
27
30
  puppet,
@@ -78,14 +81,17 @@ class GrpcManager extends EventEmitter {
78
81
  serverName : string
79
82
  token : WechatyToken
80
83
 
84
+ private readonly _log: LoggerLike
85
+
81
86
  constructor (private options: PuppetServiceOptions) {
82
87
  super()
83
- log.verbose('GrpcManager', 'constructor(%s)', JSON.stringify(options))
88
+ this._log = options.logger ?? log
89
+ this._log.verbose('GrpcManager', 'constructor(%s)', JSON.stringify(options))
84
90
 
85
91
  this.caCert = Buffer.from(
86
92
  envVars.WECHATY_PUPPET_SERVICE_TLS_CA_CERT(this.options.tls?.caCert) || TLS_CA_CERT,
87
93
  )
88
- log.verbose('GrpcManager', 'constructor() tlsRootCert(hash): "%s"',
94
+ this._log.verbose('GrpcManager', 'constructor() tlsRootCert(hash): "%s"',
89
95
  crypto.createHash('sha256')
90
96
  .update(this.caCert)
91
97
  .digest('hex'),
@@ -97,7 +103,7 @@ class GrpcManager extends EventEmitter {
97
103
  this.token = new WechatyToken(
98
104
  envVars.WECHATY_PUPPET_SERVICE_TOKEN(this.options.token),
99
105
  )
100
- log.verbose('GrpcManager', 'constructor() token: "%s"', this.token)
106
+ this._log.verbose('GrpcManager', 'constructor() token: "%s"', this.token)
101
107
 
102
108
  this.endpoint = envVars.WECHATY_PUPPET_SERVICE_ENDPOINT(this.options.endpoint)
103
109
  /**
@@ -110,13 +116,13 @@ class GrpcManager extends EventEmitter {
110
116
  '/',
111
117
  this.token,
112
118
  ].join('')
113
- log.verbose('GrpcManager', 'constructor() endpoint: "%s"', this.endpoint)
119
+ this._log.verbose('GrpcManager', 'constructor() endpoint: "%s"', this.endpoint)
114
120
 
115
121
  /**
116
122
  * Disable TLS
117
123
  */
118
124
  this.disableTls = envVars.WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT(this.options.tls?.disable)
119
- log.verbose('GrpcManager', 'constructor() disableTls: "%s"', this.disableTls)
125
+ this._log.verbose('GrpcManager', 'constructor() disableTls: "%s"', this.disableTls)
120
126
 
121
127
  /**
122
128
  * for Node.js TLS SNI
@@ -141,59 +147,59 @@ class GrpcManager extends EventEmitter {
141
147
  }
142
148
 
143
149
  this.serverName = serverNameIndication
144
- log.verbose('GrpcManager', 'constructor() serverName(SNI): "%s"', this.serverName)
150
+ this._log.verbose('GrpcManager', 'constructor() serverName(SNI): "%s"', this.serverName)
145
151
  }
146
152
 
147
153
  async start (lastEventSeq?: string, accountId?: string): Promise<void> {
148
- log.verbose('GrpcManager', 'start()')
154
+ this._log.verbose('GrpcManager', 'start()')
149
155
 
150
156
  /**
151
157
  * 1. Init grpc client
152
158
  */
153
- log.verbose('GrpcManager', 'start() initializing client ...')
159
+ this._log.verbose('GrpcManager', 'start() initializing client ...')
154
160
  await this.initClient()
155
- log.verbose('GrpcManager', 'start() initializing client ... done')
161
+ this._log.verbose('GrpcManager', 'start() initializing client ... done')
156
162
 
157
163
  /**
158
164
  * 2. Connect to stream
159
165
  */
160
- log.verbose('GrpcManager', 'start() starting stream ...')
166
+ this._log.verbose('GrpcManager', 'start() starting stream ...')
161
167
  await this.startStream(lastEventSeq, accountId)
162
- log.verbose('GrpcManager', 'start() starting stream ... done')
168
+ this._log.verbose('GrpcManager', 'start() starting stream ... done')
163
169
 
164
170
  /**
165
171
  * 3. Start the puppet
166
172
  */
167
- log.verbose('GrpcManager', 'start() calling grpc server: start() ...')
173
+ this._log.verbose('GrpcManager', 'start() calling grpc server: start() ...')
168
174
  await util.promisify(
169
175
  this.client.start
170
176
  .bind(this.client),
171
177
  )(new puppet.StartRequest())
172
- log.verbose('GrpcManager', 'start() calling grpc server: start() ... done')
178
+ this._log.verbose('GrpcManager', 'start() calling grpc server: start() ... done')
173
179
 
174
- log.verbose('GrpcManager', 'start() ... done')
180
+ this._log.verbose('GrpcManager', 'start() ... done')
175
181
  }
176
182
 
177
183
  async stop (): Promise<void> {
178
- log.verbose('GrpcManager', 'stop()')
184
+ this._log.verbose('GrpcManager', 'stop()')
179
185
 
180
186
  /**
181
187
  * 1. Disconnect from stream
182
188
  */
183
- log.verbose('GrpcManager', 'stop() stop stream ...')
189
+ this._log.verbose('GrpcManager', 'stop() stop stream ...')
184
190
  this.stopStream()
185
- log.verbose('GrpcManager', 'stop() stop stream ... done')
191
+ this._log.verbose('GrpcManager', 'stop() stop stream ... done')
186
192
 
187
193
  /**
188
194
  * 2. Stop the puppet
189
195
  */
190
196
  try {
191
- log.verbose('GrpcManager', 'stop() stop client ...')
197
+ this._log.verbose('GrpcManager', 'stop() stop client ...')
192
198
  await util.promisify(
193
199
  this.client.stop
194
200
  .bind(this.client),
195
201
  )(new puppet.StopRequest())
196
- log.verbose('GrpcManager', 'stop() stop client ... done')
202
+ this._log.verbose('GrpcManager', 'stop() stop client ... done')
197
203
  } catch (e) {
198
204
  this.emit('error', e)
199
205
  }
@@ -202,18 +208,18 @@ class GrpcManager extends EventEmitter {
202
208
  * 3. Destroy grpc client
203
209
  */
204
210
  try {
205
- log.verbose('GrpcManager', 'stop() destroy client ...')
211
+ this._log.verbose('GrpcManager', 'stop() destroy client ...')
206
212
  this.destroyClient()
207
- log.verbose('GrpcManager', 'stop() destroy client ... done')
213
+ this._log.verbose('GrpcManager', 'stop() destroy client ... done')
208
214
  } catch (e) {
209
215
  this.emit('error', e)
210
216
  }
211
217
 
212
- log.verbose('GrpcManager', 'stop() ... done')
218
+ this._log.verbose('GrpcManager', 'stop() ... done')
213
219
  }
214
220
 
215
221
  protected async initClient (): Promise<void> {
216
- log.verbose('GrpcManager', 'initClient()')
222
+ this._log.verbose('GrpcManager', 'initClient()')
217
223
 
218
224
  /**
219
225
  * Huan(202108): for maximum compatible with the non-tls community servers/clients,
@@ -222,10 +228,10 @@ class GrpcManager extends EventEmitter {
222
228
  */
223
229
  let credential
224
230
  if (this.disableTls) {
225
- log.warn('GrpcManager', 'initClient() TLS: disabled (INSECURE)')
231
+ this._log.warn('GrpcManager', 'initClient() TLS: disabled (INSECURE)')
226
232
  credential = grpc.credentials.createInsecure()
227
233
  } else {
228
- log.verbose('GrpcManager', 'initClient() TLS: enabled')
234
+ this._log.verbose('GrpcManager', 'initClient() TLS: enabled')
229
235
  const callCred = callCredToken(this.token.token)
230
236
  const channelCred = grpc.credentials.createSsl(this.caCert)
231
237
  const combCreds = grpc.credentials.combineChannelCredentials(channelCred, callCred)
@@ -252,7 +258,7 @@ class GrpcManager extends EventEmitter {
252
258
  }
253
259
 
254
260
  if (this._client) {
255
- log.warn('GrpcManager', 'initClient() this.#client exists? Old client has been dropped.')
261
+ this._log.warn('GrpcManager', 'initClient() this.#client exists? Old client has been dropped.')
256
262
  this._client = undefined
257
263
  }
258
264
 
@@ -262,14 +268,14 @@ class GrpcManager extends EventEmitter {
262
268
  clientOptions,
263
269
  )
264
270
 
265
- log.verbose('GrpcManager', 'initClient() ... done')
271
+ this._log.verbose('GrpcManager', 'initClient() ... done')
266
272
  }
267
273
 
268
274
  protected destroyClient (): void {
269
- log.verbose('GrpcManager', 'destroyClient()')
275
+ this._log.verbose('GrpcManager', 'destroyClient()')
270
276
 
271
277
  if (!this._client) {
272
- log.warn('GrpcManager', 'destroyClient() this.#client not exist')
278
+ this._log.warn('GrpcManager', 'destroyClient() this.#client not exist')
273
279
  return
274
280
  }
275
281
 
@@ -283,19 +289,19 @@ class GrpcManager extends EventEmitter {
283
289
  try {
284
290
  client.close()
285
291
  } catch (e) {
286
- log.error('GrpcManager', 'destroyClient() client.close() rejection: %s\n%s', e && (e as Error).message, (e as Error).stack)
292
+ this._log.error('GrpcManager', 'destroyClient() client.close() rejection: %s\n%s', e && (e as Error).message, (e as Error).stack)
287
293
  }
288
294
  }
289
295
 
290
296
  async startStream (lastEventSeq?: string, accountId?: string): Promise<void> {
291
- log.verbose('GrpcManager', 'startStream()')
297
+ this._log.verbose('GrpcManager', 'startStream()')
292
298
 
293
299
  if (this.eventStream) {
294
- log.verbose('GrpcManager', 'startStream() this.eventStream exists, dropped.')
300
+ this._log.verbose('GrpcManager', 'startStream() this.eventStream exists, dropped.')
295
301
  this.eventStream = undefined
296
302
  }
297
303
 
298
- log.verbose('GrpcManager', 'startStream() grpc -> event() ...')
304
+ this._log.verbose('GrpcManager', 'startStream() grpc -> event() ...')
299
305
 
300
306
  const eventRequest = new puppet.EventRequest()
301
307
  if (lastEventSeq) {
@@ -304,10 +310,10 @@ class GrpcManager extends EventEmitter {
304
310
  if (accountId) {
305
311
  eventRequest.setAccountId(accountId)
306
312
  }
307
- log.info('GrpcManager', `startStream() connecting event stream with account ${accountId} and seq ${lastEventSeq}`)
313
+ this._log.info('GrpcManager', `startStream() connecting event stream with account ${accountId} and seq ${lastEventSeq}`)
308
314
 
309
315
  const eventStream = this.client.event(eventRequest)
310
- log.verbose('GrpcManager', 'startStream() grpc -> event() ... done')
316
+ this._log.verbose('GrpcManager', 'startStream() grpc -> event() ... done')
311
317
 
312
318
  /**
313
319
  * Store the event data from the stream when we test connection,
@@ -374,13 +380,13 @@ class GrpcManager extends EventEmitter {
374
380
  * if a puppet service provider is coming from the community, it might not follow the protocol specification.
375
381
  * So we need a timeout for compatible with those providers
376
382
  */
377
- log.verbose('GrpcManager', 'startStream() grpc -> event peeking data or timeout ...')
383
+ this._log.verbose('GrpcManager', 'startStream() grpc -> event peeking data or timeout ...')
378
384
  try {
379
385
  await timeoutPromise(future, 5 * 1000) // 5 seconds
380
386
  } catch (_) {
381
- log.verbose('GrpcManager', 'startStream() grpc -> event peeking data or timeout ... timeout')
387
+ this._log.verbose('GrpcManager', 'startStream() grpc -> event peeking data or timeout ... timeout')
382
388
  }
383
- log.verbose('GrpcManager', 'startStream() grpc -> event peeking data or timeout ... data peeked')
389
+ this._log.verbose('GrpcManager', 'startStream() grpc -> event peeking data or timeout ... data peeked')
384
390
 
385
391
  /**
386
392
  * Bridge the events
@@ -389,7 +395,7 @@ class GrpcManager extends EventEmitter {
389
395
  * so that if there's any `error` event,
390
396
  * it will be triggered already.
391
397
  */
392
- log.verbose('GrpcManager', 'startStream() initializing event stream ...')
398
+ this._log.verbose('GrpcManager', 'startStream() initializing event stream ...')
393
399
  eventStream
394
400
  .on('cancel', (...args) => this.emit('cancel', ...args))
395
401
  .on('data', (...args) => this.emit('data', ...args))
@@ -399,26 +405,26 @@ class GrpcManager extends EventEmitter {
399
405
  .on('status', (...args) => this.emit('status', ...args))
400
406
 
401
407
  this.eventStream = eventStream
402
- log.verbose('GrpcManager', 'startStream() initializing event stream ... done')
408
+ this._log.verbose('GrpcManager', 'startStream() initializing event stream ... done')
403
409
 
404
410
  /**
405
411
  * Re-emit the peeked data if there' s any
406
412
  */
407
413
  if (peekedData) {
408
- log.verbose('GrpcManager', 'startStream() sending back the peeked data ...')
414
+ this._log.verbose('GrpcManager', 'startStream() sending back the peeked data ...')
409
415
  this.emit('data', peekedData)
410
416
  peekedData = undefined
411
- log.verbose('GrpcManager', 'startStream() sending back the peeked data ... done')
417
+ this._log.verbose('GrpcManager', 'startStream() sending back the peeked data ... done')
412
418
  }
413
419
 
414
- log.verbose('GrpcManager', 'startStream() ... done')
420
+ this._log.verbose('GrpcManager', 'startStream() ... done')
415
421
  }
416
422
 
417
423
  stopStream (): void {
418
- log.verbose('GrpcManager', 'stopStream()')
424
+ this._log.verbose('GrpcManager', 'stopStream()')
419
425
 
420
426
  if (!this.eventStream) {
421
- log.verbose('GrpcManager', 'stopStream() no eventStream when stop, skip destroy.')
427
+ this._log.verbose('GrpcManager', 'stopStream() no eventStream when stop, skip destroy.')
422
428
  return
423
429
  }
424
430
  /**
@@ -436,9 +442,9 @@ class GrpcManager extends EventEmitter {
436
442
  // this.eventStream.cancel()
437
443
 
438
444
  try {
439
- log.verbose('GrpcManager', 'stopStream() destroying event stream ...')
445
+ this._log.verbose('GrpcManager', 'stopStream() destroying event stream ...')
440
446
  eventStream.destroy()
441
- log.verbose('GrpcManager', 'stopStream() destroying event stream ... done')
447
+ this._log.verbose('GrpcManager', 'stopStream() destroying event stream ... done')
442
448
  } catch (e) {
443
449
  this.emit('error', e)
444
450
  }