@juzi/wechaty 1.0.155 → 1.0.157
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.
- package/dist/cjs/src/package-json.js +4 -4
- package/dist/cjs/src/user-modules/call.spec.js +5 -1
- package/dist/cjs/src/user-modules/call.spec.js.map +1 -1
- package/dist/cjs/src/user-modules/contact-alias-polling.spec.d.ts +3 -0
- package/dist/cjs/src/user-modules/contact-alias-polling.spec.d.ts.map +1 -0
- package/dist/cjs/src/user-modules/contact-alias-polling.spec.js +58 -0
- package/dist/cjs/src/user-modules/contact-alias-polling.spec.js.map +1 -0
- package/dist/cjs/src/user-modules/contact.d.ts +24 -0
- package/dist/cjs/src/user-modules/contact.d.ts.map +1 -1
- package/dist/cjs/src/user-modules/contact.js +69 -5
- package/dist/cjs/src/user-modules/contact.js.map +1 -1
- package/dist/cjs/src/user-modules/contact.spec.js +156 -0
- package/dist/cjs/src/user-modules/contact.spec.js.map +1 -1
- package/dist/cjs/src/user-modules/room.d.ts +23 -0
- package/dist/cjs/src/user-modules/room.d.ts.map +1 -1
- package/dist/cjs/src/user-modules/room.js +52 -0
- package/dist/cjs/src/user-modules/room.js.map +1 -1
- package/dist/cjs/src/user-modules/room.spec.js +156 -0
- package/dist/cjs/src/user-modules/room.spec.js.map +1 -1
- package/dist/cjs/src/wechaty-mixins/puppet-mixin-dirty-call.spec.d.ts +3 -0
- package/dist/cjs/src/wechaty-mixins/puppet-mixin-dirty-call.spec.d.ts.map +1 -0
- package/dist/cjs/src/wechaty-mixins/puppet-mixin-dirty-call.spec.js +68 -0
- package/dist/cjs/src/wechaty-mixins/puppet-mixin-dirty-call.spec.js.map +1 -0
- package/dist/cjs/src/wechaty-mixins/puppet-mixin.d.ts.map +1 -1
- package/dist/cjs/src/wechaty-mixins/puppet-mixin.js +14 -9
- package/dist/cjs/src/wechaty-mixins/puppet-mixin.js.map +1 -1
- package/dist/esm/src/package-json.js +4 -4
- package/dist/esm/src/user-modules/call.spec.js +5 -1
- package/dist/esm/src/user-modules/call.spec.js.map +1 -1
- package/dist/esm/src/user-modules/contact-alias-polling.spec.d.ts +3 -0
- package/dist/esm/src/user-modules/contact-alias-polling.spec.d.ts.map +1 -0
- package/dist/esm/src/user-modules/contact-alias-polling.spec.js +56 -0
- package/dist/esm/src/user-modules/contact-alias-polling.spec.js.map +1 -0
- package/dist/esm/src/user-modules/contact.d.ts +24 -0
- package/dist/esm/src/user-modules/contact.d.ts.map +1 -1
- package/dist/esm/src/user-modules/contact.js +69 -5
- package/dist/esm/src/user-modules/contact.js.map +1 -1
- package/dist/esm/src/user-modules/contact.spec.js +157 -1
- package/dist/esm/src/user-modules/contact.spec.js.map +1 -1
- package/dist/esm/src/user-modules/room.d.ts +23 -0
- package/dist/esm/src/user-modules/room.d.ts.map +1 -1
- package/dist/esm/src/user-modules/room.js +52 -0
- package/dist/esm/src/user-modules/room.js.map +1 -1
- package/dist/esm/src/user-modules/room.spec.js +156 -0
- package/dist/esm/src/user-modules/room.spec.js.map +1 -1
- package/dist/esm/src/wechaty-mixins/puppet-mixin-dirty-call.spec.d.ts +3 -0
- package/dist/esm/src/wechaty-mixins/puppet-mixin-dirty-call.spec.d.ts.map +1 -0
- package/dist/esm/src/wechaty-mixins/puppet-mixin-dirty-call.spec.js +43 -0
- package/dist/esm/src/wechaty-mixins/puppet-mixin-dirty-call.spec.js.map +1 -0
- package/dist/esm/src/wechaty-mixins/puppet-mixin.d.ts.map +1 -1
- package/dist/esm/src/wechaty-mixins/puppet-mixin.js +14 -9
- package/dist/esm/src/wechaty-mixins/puppet-mixin.js.map +1 -1
- package/package.json +3 -3
- package/src/package-json.ts +4 -4
- package/src/user-modules/call.spec.ts +5 -1
- package/src/user-modules/contact-alias-polling.spec.ts +70 -0
- package/src/user-modules/contact.spec.ts +212 -0
- package/src/user-modules/contact.ts +81 -5
- package/src/user-modules/room.spec.ts +210 -0
- package/src/user-modules/room.ts +63 -0
- package/src/wechaty-mixins/puppet-mixin-dirty-call.spec.ts +55 -0
- package/src/wechaty-mixins/puppet-mixin.ts +14 -9
package/src/package-json.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { PackageJsonWechaty } from './config'
|
|
6
6
|
|
|
7
|
-
export const GIT_COMMIT_HASH: string = '
|
|
7
|
+
export const GIT_COMMIT_HASH: string = 'a69947004cdd32876cf204ef4344e10443d444a3'
|
|
8
8
|
export const packageJson: PackageJsonWechaty = {
|
|
9
9
|
"name": "@juzi/wechaty",
|
|
10
|
-
"version": "1.0.
|
|
10
|
+
"version": "1.0.157",
|
|
11
11
|
"description": "Wechaty is a RPA SDK for Chatbot Makers.",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"exports": {
|
|
@@ -116,7 +116,7 @@ export const packageJson: PackageJsonWechaty = {
|
|
|
116
116
|
},
|
|
117
117
|
"homepage": "https://github.com/wechaty/",
|
|
118
118
|
"dependencies": {
|
|
119
|
-
"@juzi/wechaty-puppet-service": "1.0.
|
|
119
|
+
"@juzi/wechaty-puppet-service": "1.0.121",
|
|
120
120
|
"clone-class": "^1.1.1",
|
|
121
121
|
"cmd-ts": "^0.10.0",
|
|
122
122
|
"cockatiel": "^2.0.2",
|
|
@@ -139,7 +139,7 @@ export const packageJson: PackageJsonWechaty = {
|
|
|
139
139
|
"@chatie/eslint-config": "^1.0.4",
|
|
140
140
|
"@chatie/semver": "^0.4.7",
|
|
141
141
|
"@chatie/tsconfig": "^4.6.3",
|
|
142
|
-
"@juzi/wechaty-puppet": "^1.0.
|
|
142
|
+
"@juzi/wechaty-puppet": "^1.0.145",
|
|
143
143
|
"@juzi/wechaty-puppet-mock": "^1.0.1",
|
|
144
144
|
"@swc/core": "1.3.44",
|
|
145
145
|
"@swc/helpers": "^0.3.6",
|
|
@@ -922,7 +922,11 @@ test('dirty(Call) refreshes user-layer payload so getters see new value', async
|
|
|
922
922
|
await flush()
|
|
923
923
|
|
|
924
924
|
t.equal(incoming!.media(), PUPPET.types.CallMediaType.Video, 'media should reflect refreshed payload after dirty')
|
|
925
|
-
|
|
925
|
+
// Note: the handler intentionally does NOT call puppet.callPayloadDirty;
|
|
926
|
+
// doing so would form a gRPC loop under puppet-service (server bounces the
|
|
927
|
+
// dirty back through the event stream). Cache invalidation is handled by
|
|
928
|
+
// the cache-mixin onDirty listener; the user-visible contract is that
|
|
929
|
+
// .media() above reflects the fresh server value.
|
|
926
930
|
|
|
927
931
|
await wechaty.stop()
|
|
928
932
|
sandbox.restore()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
|
|
2
|
+
|
|
3
|
+
import { test } from 'tstest'
|
|
4
|
+
import { PuppetMock } from '@juzi/wechaty-puppet-mock'
|
|
5
|
+
|
|
6
|
+
import { WechatyBuilder } from '../wechaty-builder.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Regression: `Contact.alias(newAlias)` invokes
|
|
10
|
+
* 1. puppet.contactAlias(id, newAlias) // gRPC write
|
|
11
|
+
* 2. puppet.contactPayloadDirty(id) // ONCE, outside the loop ← BUG
|
|
12
|
+
* 3. loop 10× { sleep 300ms; puppet.contactPayload(id); check }
|
|
13
|
+
*
|
|
14
|
+
* Under puppet-service the first contactPayload() call after a dirty
|
|
15
|
+
* clears both the LRU and the FlashStore, then refills both with the
|
|
16
|
+
* fresh fetch. LRU TTL is 15 minutes, so iterations 2..10 hit the LRU
|
|
17
|
+
* and return iteration-1's payload forever. If the underlying protocol
|
|
18
|
+
* (puppet-phoenix etc.) was still propagating the alias change at the
|
|
19
|
+
* moment of iteration 1, the 9 follow-up retries become no-ops and
|
|
20
|
+
* `alias()` throws "still got old alias after 10 tries".
|
|
21
|
+
*
|
|
22
|
+
* The fix moves contactPayloadDirty INTO the loop so every retry
|
|
23
|
+
* forces a fresh fetch. This test pins that contract: under a forced
|
|
24
|
+
* stale-read scenario, contactPayloadDirty must be called more than
|
|
25
|
+
* once.
|
|
26
|
+
*/
|
|
27
|
+
test('alias() must call contactPayloadDirty on every poll iteration', async t => {
|
|
28
|
+
const puppet = new PuppetMock() as any
|
|
29
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
30
|
+
|
|
31
|
+
// Swallow the wechaty-level error the alias() impl re-emits after
|
|
32
|
+
// its 10 poll iterations all fail to confirm the new alias; without
|
|
33
|
+
// this, the unhandled 'error' event tears down the test.
|
|
34
|
+
wechaty.on('error', () => {})
|
|
35
|
+
|
|
36
|
+
const mockMe = puppet.mocker.createContact({ name: 'me' })
|
|
37
|
+
const mockUser = puppet.mocker.createContact({ name: 'Alice' })
|
|
38
|
+
|
|
39
|
+
await wechaty.start()
|
|
40
|
+
await puppet.mocker.login(mockMe)
|
|
41
|
+
|
|
42
|
+
let dirtyCount = 0
|
|
43
|
+
puppet.contactPayloadDirty = async (_: string) => {
|
|
44
|
+
dirtyCount++
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Simulate the protocol-layer "alias write accepted" but
|
|
48
|
+
// payload reads keep returning the OLD alias — the exact stuck
|
|
49
|
+
// scenario the polling loop is supposed to wait out.
|
|
50
|
+
puppet.contactAlias = async (_id: string, _newAlias: string) => {}
|
|
51
|
+
puppet.contactPayload = async (id: string) => ({
|
|
52
|
+
id,
|
|
53
|
+
alias: 'OLD',
|
|
54
|
+
name: 'Alice',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const contact = await wechaty.Contact.find({ id: mockUser.id })
|
|
58
|
+
t.ok(contact, 'sanity: find returned a contact')
|
|
59
|
+
|
|
60
|
+
// alias() traps internally and emits through wechaty error channel.
|
|
61
|
+
// We do NOT assert on the throw — only on dirty-call cadence.
|
|
62
|
+
await contact!.alias('NEW')
|
|
63
|
+
|
|
64
|
+
t.ok(
|
|
65
|
+
dirtyCount > 1,
|
|
66
|
+
`contactPayloadDirty must be called inside the retry loop, not just once before it; got ${dirtyCount}`,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
await wechaty.stop()
|
|
70
|
+
})
|
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import {
|
|
22
22
|
test,
|
|
23
|
+
sinon,
|
|
23
24
|
} from 'tstest'
|
|
24
25
|
|
|
26
|
+
import type * as PUPPET from '@juzi/wechaty-puppet'
|
|
25
27
|
import { PuppetMock } from '@juzi/wechaty-puppet-mock'
|
|
26
28
|
import { WechatyBuilder } from '../wechaty-builder.js'
|
|
27
29
|
|
|
@@ -88,6 +90,216 @@ test('should throw when instanciate the global class', async t => {
|
|
|
88
90
|
}, 'should throw when we instanciate a global class')
|
|
89
91
|
})
|
|
90
92
|
|
|
93
|
+
test('findAllIter() yields every contact returned by contactSearch', async t => {
|
|
94
|
+
const TOTAL = 7
|
|
95
|
+
|
|
96
|
+
const sandbox = sinon.createSandbox()
|
|
97
|
+
const puppet = new PuppetMock() as any
|
|
98
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
99
|
+
|
|
100
|
+
const idList = Array.from({ length: TOTAL }, (_, idx) => `c-${idx}`)
|
|
101
|
+
const buildPayload = (id: string) => ({ id, name: `name-${id}` } as PUPPET.payloads.Contact)
|
|
102
|
+
|
|
103
|
+
sandbox.stub(puppet, 'contactSearch').resolves(idList)
|
|
104
|
+
sandbox.stub(puppet, 'batchContactPayload').callsFake(async (...args: any[]) => {
|
|
105
|
+
const ids = args[0] as string[]
|
|
106
|
+
return new Map(ids.map(id => [ id, buildPayload(id) ]))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
await wechaty.start()
|
|
110
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
111
|
+
await puppet.mocker.login(bot)
|
|
112
|
+
|
|
113
|
+
const seen: string[] = []
|
|
114
|
+
for await (const chunk of wechaty.Contact.findAllIter()) {
|
|
115
|
+
for (const contact of chunk) {
|
|
116
|
+
seen.push(contact.id)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
t.equal(seen.length, TOTAL, `should yield ${TOTAL} contacts`)
|
|
121
|
+
t.same(seen, idList, 'should yield contacts in search order')
|
|
122
|
+
|
|
123
|
+
await wechaty.stop()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('findAllIter() chunks 101 contacts into [100, 1] at default batch=100', async t => {
|
|
127
|
+
const TOTAL = 101
|
|
128
|
+
|
|
129
|
+
const sandbox = sinon.createSandbox()
|
|
130
|
+
const puppet = new PuppetMock() as any
|
|
131
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
132
|
+
|
|
133
|
+
const idList = Array.from({ length: TOTAL }, (_, idx) => `c-${idx}`)
|
|
134
|
+
sandbox.stub(puppet, 'contactSearch').resolves(idList)
|
|
135
|
+
sandbox.stub(puppet, 'batchContactPayload').callsFake(async (...args: any[]) => {
|
|
136
|
+
const ids = args[0] as string[]
|
|
137
|
+
return new Map(ids.map(id => [ id, { id, name: id } as PUPPET.payloads.Contact ]))
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await wechaty.start()
|
|
141
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
142
|
+
await puppet.mocker.login(bot)
|
|
143
|
+
|
|
144
|
+
const sizes: number[] = []
|
|
145
|
+
for await (const chunk of wechaty.Contact.findAllIter()) {
|
|
146
|
+
sizes.push(chunk.length)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
t.same(sizes, [ 100, 1 ], 'should split into two batches: 100 and 1')
|
|
150
|
+
|
|
151
|
+
await wechaty.stop()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('findAllIter() honors early break without throwing', async t => {
|
|
155
|
+
const TOTAL = 250
|
|
156
|
+
|
|
157
|
+
const sandbox = sinon.createSandbox()
|
|
158
|
+
const puppet = new PuppetMock() as any
|
|
159
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
160
|
+
|
|
161
|
+
const idList = Array.from({ length: TOTAL }, (_, idx) => `c-${idx}`)
|
|
162
|
+
sandbox.stub(puppet, 'contactSearch').resolves(idList)
|
|
163
|
+
const batchSpy = sandbox.stub(puppet, 'batchContactPayload').callsFake(async (...args: any[]) => {
|
|
164
|
+
const ids = args[0] as string[]
|
|
165
|
+
return new Map(ids.map(id => [ id, { id, name: id } as PUPPET.payloads.Contact ]))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await wechaty.start()
|
|
169
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
170
|
+
await puppet.mocker.login(bot)
|
|
171
|
+
|
|
172
|
+
let received = 0
|
|
173
|
+
await t.resolves((async () => {
|
|
174
|
+
for await (const chunk of wechaty.Contact.findAllIter()) {
|
|
175
|
+
received += chunk.length
|
|
176
|
+
if (received >= 100) {
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})(), 'breaking the loop should not throw')
|
|
181
|
+
|
|
182
|
+
t.equal(received, 100, 'should stop after the first batch')
|
|
183
|
+
t.equal(batchSpy.callCount, 1, 'should not invoke batchContactPayload again after break')
|
|
184
|
+
|
|
185
|
+
await wechaty.stop()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('findAllIter() invokes batchContactPayload ceil(N/batch) times, not N times', async t => {
|
|
189
|
+
const TOTAL = 100
|
|
190
|
+
const BATCH = 25
|
|
191
|
+
|
|
192
|
+
const sandbox = sinon.createSandbox()
|
|
193
|
+
const puppet = new PuppetMock() as any
|
|
194
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
195
|
+
|
|
196
|
+
const idList = Array.from({ length: TOTAL }, (_, idx) => `c-${idx}`)
|
|
197
|
+
sandbox.stub(puppet, 'contactSearch').resolves(idList)
|
|
198
|
+
|
|
199
|
+
const batchSpy = sandbox.stub(puppet, 'batchContactPayload').callsFake(async (...args: any[]) => {
|
|
200
|
+
const ids = args[0] as string[]
|
|
201
|
+
return new Map(ids.map(id => [ id, { id, name: id } as PUPPET.payloads.Contact ]))
|
|
202
|
+
})
|
|
203
|
+
const rawSpy = sandbox.spy(puppet, 'contactRawPayload')
|
|
204
|
+
|
|
205
|
+
await wechaty.start()
|
|
206
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
207
|
+
await puppet.mocker.login(bot)
|
|
208
|
+
|
|
209
|
+
const rawCountBefore = rawSpy.callCount
|
|
210
|
+
|
|
211
|
+
let received = 0
|
|
212
|
+
for await (const chunk of wechaty.Contact.findAllIter(undefined, { batch: BATCH })) {
|
|
213
|
+
received += chunk.length
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
t.equal(received, TOTAL, 'should iterate all contacts')
|
|
217
|
+
t.equal(batchSpy.callCount, Math.ceil(TOTAL / BATCH), `should invoke batchContactPayload ${Math.ceil(TOTAL / BATCH)} times`)
|
|
218
|
+
t.equal(rawSpy.callCount, rawCountBefore, 'should never fall back to per-id contactRawPayload during iteration')
|
|
219
|
+
|
|
220
|
+
await wechaty.stop()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('findAllIter() with empty contactSearch yields nothing and does not throw', async t => {
|
|
224
|
+
const sandbox = sinon.createSandbox()
|
|
225
|
+
const puppet = new PuppetMock() as any
|
|
226
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
227
|
+
|
|
228
|
+
sandbox.stub(puppet, 'contactSearch').resolves([])
|
|
229
|
+
const batchSpy = sandbox.stub(puppet, 'batchContactPayload').resolves(new Map())
|
|
230
|
+
|
|
231
|
+
await wechaty.start()
|
|
232
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
233
|
+
await puppet.mocker.login(bot)
|
|
234
|
+
|
|
235
|
+
let chunkCount = 0
|
|
236
|
+
await t.resolves((async () => {
|
|
237
|
+
for await (const _chunk of wechaty.Contact.findAllIter()) {
|
|
238
|
+
chunkCount++
|
|
239
|
+
}
|
|
240
|
+
})(), 'empty iteration should not throw')
|
|
241
|
+
|
|
242
|
+
t.equal(chunkCount, 0, 'should yield zero chunks')
|
|
243
|
+
t.equal(batchSpy.callCount, 0, 'should not call batchContactPayload when no ids')
|
|
244
|
+
|
|
245
|
+
await wechaty.stop()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('findAllIter() throws when batch <= 0', async t => {
|
|
249
|
+
const sandbox = sinon.createSandbox()
|
|
250
|
+
const puppet = new PuppetMock() as any
|
|
251
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
252
|
+
|
|
253
|
+
sandbox.stub(puppet, 'contactSearch').resolves([])
|
|
254
|
+
sandbox.stub(puppet, 'batchContactPayload').resolves(new Map())
|
|
255
|
+
|
|
256
|
+
await wechaty.start()
|
|
257
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
258
|
+
await puppet.mocker.login(bot)
|
|
259
|
+
|
|
260
|
+
for (const badBatch of [ 0, -1 ]) {
|
|
261
|
+
await t.rejects(
|
|
262
|
+
(async () => {
|
|
263
|
+
for await (const _chunk of wechaty.Contact.findAllIter(undefined, { batch: badBatch })) {
|
|
264
|
+
// should never reach here
|
|
265
|
+
}
|
|
266
|
+
})(),
|
|
267
|
+
/batch must be positive/,
|
|
268
|
+
`should throw when batch=${badBatch}`,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await wechaty.stop()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('findAllIter() skips ids missing from payloadMap', async t => {
|
|
276
|
+
const sandbox = sinon.createSandbox()
|
|
277
|
+
const puppet = new PuppetMock() as any
|
|
278
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
279
|
+
|
|
280
|
+
sandbox.stub(puppet, 'contactSearch').resolves([ 'c1', 'c2', 'c3' ])
|
|
281
|
+
sandbox.stub(puppet, 'batchContactPayload').resolves(
|
|
282
|
+
new Map([ [ 'c1', { id: 'c1', name: 'name-c1' } as PUPPET.payloads.Contact ] ]),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
await wechaty.start()
|
|
286
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
287
|
+
await puppet.mocker.login(bot)
|
|
288
|
+
|
|
289
|
+
const seen: string[] = []
|
|
290
|
+
await t.resolves((async () => {
|
|
291
|
+
for await (const chunk of wechaty.Contact.findAllIter()) {
|
|
292
|
+
for (const contact of chunk) {
|
|
293
|
+
seen.push(contact.id)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
})(), 'missing ids should not throw or stall iteration')
|
|
297
|
+
|
|
298
|
+
t.same(seen, [ 'c1' ], 'should yield only contacts whose payload was returned')
|
|
299
|
+
|
|
300
|
+
await wechaty.stop()
|
|
301
|
+
})
|
|
302
|
+
|
|
91
303
|
test('ProtectedProperties', async t => {
|
|
92
304
|
type NotExistInWechaty = Exclude<ContactProtectedProperty, keyof ContactImpl>
|
|
93
305
|
type NotExistTest = NotExistInWechaty extends never ? true : false
|
|
@@ -216,6 +216,73 @@ class ContactMixin extends MixinBase implements SayableSayer {
|
|
|
216
216
|
return contactList
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Async iterator variant of {@link findAll} that yields contacts in batches.
|
|
221
|
+
*
|
|
222
|
+
* Each batch is materialised with a single `puppet.batchContactPayload` RPC,
|
|
223
|
+
* so iterating N contacts costs roughly `ceil(N / batch)` round trips instead
|
|
224
|
+
* of N. Uses `puppet.batchContactPayload` directly (required on the puppet
|
|
225
|
+
* abstract), without holding the whole list in memory or blocking the caller
|
|
226
|
+
* until every contact has been hydrated. Callers may `break` between batches
|
|
227
|
+
* to short-circuit.
|
|
228
|
+
*
|
|
229
|
+
* @static
|
|
230
|
+
* @param {string | PUPPET.filters.Contact} [query]
|
|
231
|
+
* @param {{ batch?: number }} [opts] batch size, default 100
|
|
232
|
+
* @returns {AsyncIterable<ContactInterface[]>}
|
|
233
|
+
* @example
|
|
234
|
+
* for await (const chunk of bot.Contact.findAllIter()) {
|
|
235
|
+
* for (const contact of chunk) {
|
|
236
|
+
* console.log(contact.name())
|
|
237
|
+
* }
|
|
238
|
+
* }
|
|
239
|
+
*/
|
|
240
|
+
static async * findAllIter (
|
|
241
|
+
query? : string | PUPPET.filters.Contact,
|
|
242
|
+
opts? : { batch?: number },
|
|
243
|
+
): AsyncIterable<ContactInterface[]> {
|
|
244
|
+
const batch = opts?.batch ?? 100
|
|
245
|
+
log.verbose('Contact', 'findAllIter(%s, batch=%d)', JSON.stringify(query, stringifyFilter) || '', batch)
|
|
246
|
+
|
|
247
|
+
if (batch <= 0) {
|
|
248
|
+
throw new Error(`Contact.findAllIter() batch must be positive, got ${batch}`)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const contactIdList: string[] = await this.wechaty.puppet.contactSearch(query)
|
|
252
|
+
|
|
253
|
+
if (!this.wechaty.isLoggedIn) {
|
|
254
|
+
throw new Error('Contact.findAllIter() requires logged in')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < contactIdList.length; i += batch) {
|
|
258
|
+
const idChunk = contactIdList.slice(i, i + batch)
|
|
259
|
+
|
|
260
|
+
const payloadMap = await this.wechaty.puppet.batchContactPayload(idChunk)
|
|
261
|
+
|
|
262
|
+
const contactChunk: ContactInterface[] = []
|
|
263
|
+
for (const id of idChunk) {
|
|
264
|
+
const payload = payloadMap.get(id)
|
|
265
|
+
if (!payload) {
|
|
266
|
+
log.silly('Contact', 'findAllIter() payload missing for id=%s, skip', id)
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const contact: ContactImpl = this.wechaty.puppet.currentUserId === id
|
|
271
|
+
? (this.wechaty.ContactSelf as any as typeof ContactSelfImpl).load(id)
|
|
272
|
+
: (this.wechaty.Contact as any as typeof ContactImpl).load(id)
|
|
273
|
+
|
|
274
|
+
contact.payload = payload
|
|
275
|
+
contactChunk.push(contact)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (contactChunk.length > 0) {
|
|
279
|
+
yield contactChunk
|
|
280
|
+
} else {
|
|
281
|
+
log.warn('Contact', 'findAllIter() batch all missing from payloadMap, idChunk=%j', idChunk)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
219
286
|
static async batchLoadContacts (contactIdList: string[]) {
|
|
220
287
|
if (typeof this.wechaty.puppet.batchContactPayload === 'function') {
|
|
221
288
|
const contactList: ContactInterface[] = contactIdList.map(id => {
|
|
@@ -496,22 +563,31 @@ class ContactMixin extends MixinBase implements SayableSayer {
|
|
|
496
563
|
|
|
497
564
|
try {
|
|
498
565
|
await this.wechaty.puppet.contactAlias(this.id, newAlias)
|
|
499
|
-
await this.wechaty.puppet.contactPayloadDirty(this.id)
|
|
500
566
|
|
|
501
567
|
/**
|
|
502
568
|
* In normal puppet, the dirty event handler, onDirty, is a sync function, so the contactPayload will get the new payload
|
|
503
569
|
* However for wechaty-puppet-service, it uses flashstore to cache payloads, and deleting a cache is an async function
|
|
504
|
-
* So there is a chance contactPayload will still get old contact
|
|
570
|
+
* So there is a chance contactPayload will still get old contact.
|
|
571
|
+
*
|
|
572
|
+
* The puppet LRU TTL (15min) is far longer than the 300ms retry
|
|
573
|
+
* tick, so a dirty fired ONCE before the loop only invalidates
|
|
574
|
+
* the cache for iteration 1 — the iteration-1 fetch immediately
|
|
575
|
+
* refills both LRU and FlashStore, and iterations 2..10 hit the
|
|
576
|
+
* LRU and return iteration-1's payload forever. Dirty must fire
|
|
577
|
+
* on every retry so every read goes to source.
|
|
505
578
|
*/
|
|
506
579
|
let maxCheck = 10
|
|
507
580
|
let changed = false
|
|
508
581
|
while (maxCheck-- > 0 && !changed) {
|
|
509
|
-
await
|
|
510
|
-
setTimeout(resolve, 300)
|
|
511
|
-
})
|
|
582
|
+
await this.wechaty.puppet.contactPayloadDirty(this.id)
|
|
512
583
|
this.payload = await this.wechaty.puppet.contactPayload(this.id)
|
|
513
584
|
const payloadAlias = this.payload.alias || ''
|
|
514
585
|
changed = newAlias === payloadAlias
|
|
586
|
+
if (!changed) {
|
|
587
|
+
await new Promise(resolve => {
|
|
588
|
+
setTimeout(resolve, 300)
|
|
589
|
+
})
|
|
590
|
+
}
|
|
515
591
|
}
|
|
516
592
|
if (!changed) {
|
|
517
593
|
throw new Error('failed to modify clias, still got old alias after 10 tries')
|
|
@@ -232,6 +232,216 @@ test('room.say() smoke testing', async () => {
|
|
|
232
232
|
await wechaty.stop()
|
|
233
233
|
})
|
|
234
234
|
|
|
235
|
+
test('Room.findAllIter() yields every room returned by roomSearch', async t => {
|
|
236
|
+
const TOTAL = 6
|
|
237
|
+
|
|
238
|
+
const sandbox = sinon.createSandbox()
|
|
239
|
+
const puppet = new PuppetMock() as any
|
|
240
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
241
|
+
|
|
242
|
+
const idList = Array.from({ length: TOTAL }, (_, idx) => `r-${idx}`)
|
|
243
|
+
const buildPayload = (id: string) => ({ id, topic: `topic-${id}`, memberIdList: [], adminIdList: [] } as PUPPET.payloads.Room)
|
|
244
|
+
|
|
245
|
+
sandbox.stub(puppet, 'roomSearch').resolves(idList)
|
|
246
|
+
sandbox.stub(puppet, 'batchRoomPayload').callsFake(async (...args: any[]) => {
|
|
247
|
+
const ids = args[0] as string[]
|
|
248
|
+
return new Map(ids.map(id => [ id, buildPayload(id) ]))
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
await wechaty.start()
|
|
252
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
253
|
+
await puppet.mocker.login(bot)
|
|
254
|
+
|
|
255
|
+
const seen: string[] = []
|
|
256
|
+
for await (const chunk of wechaty.Room.findAllIter()) {
|
|
257
|
+
for (const room of chunk) {
|
|
258
|
+
seen.push(room.id)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
t.equal(seen.length, TOTAL, `should yield ${TOTAL} rooms`)
|
|
263
|
+
t.same(seen, idList, 'should yield rooms in search order')
|
|
264
|
+
|
|
265
|
+
await wechaty.stop()
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('Room.findAllIter() chunks 101 rooms into [100, 1] at default batch=100', async t => {
|
|
269
|
+
const TOTAL = 101
|
|
270
|
+
|
|
271
|
+
const sandbox = sinon.createSandbox()
|
|
272
|
+
const puppet = new PuppetMock() as any
|
|
273
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
274
|
+
|
|
275
|
+
const idList = Array.from({ length: TOTAL }, (_, idx) => `r-${idx}`)
|
|
276
|
+
sandbox.stub(puppet, 'roomSearch').resolves(idList)
|
|
277
|
+
sandbox.stub(puppet, 'batchRoomPayload').callsFake(async (...args: any[]) => {
|
|
278
|
+
const ids = args[0] as string[]
|
|
279
|
+
return new Map(ids.map(id => [ id, { id, topic: id, memberIdList: [], adminIdList: [] } as PUPPET.payloads.Room ]))
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
await wechaty.start()
|
|
283
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
284
|
+
await puppet.mocker.login(bot)
|
|
285
|
+
|
|
286
|
+
const sizes: number[] = []
|
|
287
|
+
for await (const chunk of wechaty.Room.findAllIter()) {
|
|
288
|
+
sizes.push(chunk.length)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
t.same(sizes, [ 100, 1 ], 'should split into two batches: 100 and 1')
|
|
292
|
+
|
|
293
|
+
await wechaty.stop()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('Room.findAllIter() honors early break without throwing', async t => {
|
|
297
|
+
const TOTAL = 250
|
|
298
|
+
|
|
299
|
+
const sandbox = sinon.createSandbox()
|
|
300
|
+
const puppet = new PuppetMock() as any
|
|
301
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
302
|
+
|
|
303
|
+
const idList = Array.from({ length: TOTAL }, (_, idx) => `r-${idx}`)
|
|
304
|
+
sandbox.stub(puppet, 'roomSearch').resolves(idList)
|
|
305
|
+
const batchSpy = sandbox.stub(puppet, 'batchRoomPayload').callsFake(async (...args: any[]) => {
|
|
306
|
+
const ids = args[0] as string[]
|
|
307
|
+
return new Map(ids.map(id => [ id, { id, topic: id, memberIdList: [], adminIdList: [] } as PUPPET.payloads.Room ]))
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
await wechaty.start()
|
|
311
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
312
|
+
await puppet.mocker.login(bot)
|
|
313
|
+
|
|
314
|
+
let received = 0
|
|
315
|
+
await t.resolves((async () => {
|
|
316
|
+
for await (const chunk of wechaty.Room.findAllIter()) {
|
|
317
|
+
received += chunk.length
|
|
318
|
+
if (received >= 100) {
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
})(), 'breaking the loop should not throw')
|
|
323
|
+
|
|
324
|
+
t.equal(received, 100, 'should stop after the first batch')
|
|
325
|
+
t.equal(batchSpy.callCount, 1, 'should not invoke batchRoomPayload again after break')
|
|
326
|
+
|
|
327
|
+
await wechaty.stop()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('Room.findAllIter() invokes batchRoomPayload ceil(N/batch) times, not N times', async t => {
|
|
331
|
+
const TOTAL = 100
|
|
332
|
+
const BATCH = 25
|
|
333
|
+
|
|
334
|
+
const sandbox = sinon.createSandbox()
|
|
335
|
+
const puppet = new PuppetMock() as any
|
|
336
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
337
|
+
|
|
338
|
+
const idList = Array.from({ length: TOTAL }, (_, idx) => `r-${idx}`)
|
|
339
|
+
sandbox.stub(puppet, 'roomSearch').resolves(idList)
|
|
340
|
+
|
|
341
|
+
const batchSpy = sandbox.stub(puppet, 'batchRoomPayload').callsFake(async (...args: any[]) => {
|
|
342
|
+
const ids = args[0] as string[]
|
|
343
|
+
return new Map(ids.map(id => [ id, { id, topic: id, memberIdList: [], adminIdList: [] } as PUPPET.payloads.Room ]))
|
|
344
|
+
})
|
|
345
|
+
const rawSpy = sandbox.spy(puppet, 'roomRawPayload')
|
|
346
|
+
|
|
347
|
+
await wechaty.start()
|
|
348
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
349
|
+
await puppet.mocker.login(bot)
|
|
350
|
+
|
|
351
|
+
const rawCountBefore = rawSpy.callCount
|
|
352
|
+
|
|
353
|
+
let received = 0
|
|
354
|
+
for await (const chunk of wechaty.Room.findAllIter(undefined, { batch: BATCH })) {
|
|
355
|
+
received += chunk.length
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
t.equal(received, TOTAL, 'should iterate all rooms')
|
|
359
|
+
t.equal(batchSpy.callCount, Math.ceil(TOTAL / BATCH), `should invoke batchRoomPayload ${Math.ceil(TOTAL / BATCH)} times`)
|
|
360
|
+
t.equal(rawSpy.callCount, rawCountBefore, 'should never fall back to per-id roomRawPayload during iteration')
|
|
361
|
+
|
|
362
|
+
await wechaty.stop()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('Room.findAllIter() with empty roomSearch yields nothing and does not throw', async t => {
|
|
366
|
+
const sandbox = sinon.createSandbox()
|
|
367
|
+
const puppet = new PuppetMock() as any
|
|
368
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
369
|
+
|
|
370
|
+
sandbox.stub(puppet, 'roomSearch').resolves([])
|
|
371
|
+
const batchSpy = sandbox.stub(puppet, 'batchRoomPayload').resolves(new Map())
|
|
372
|
+
|
|
373
|
+
await wechaty.start()
|
|
374
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
375
|
+
await puppet.mocker.login(bot)
|
|
376
|
+
|
|
377
|
+
let chunkCount = 0
|
|
378
|
+
await t.resolves((async () => {
|
|
379
|
+
for await (const _chunk of wechaty.Room.findAllIter()) {
|
|
380
|
+
chunkCount++
|
|
381
|
+
}
|
|
382
|
+
})(), 'empty iteration should not throw')
|
|
383
|
+
|
|
384
|
+
t.equal(chunkCount, 0, 'should yield zero chunks')
|
|
385
|
+
t.equal(batchSpy.callCount, 0, 'should not call batchRoomPayload when no ids')
|
|
386
|
+
|
|
387
|
+
await wechaty.stop()
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test('Room.findAllIter() throws when batch <= 0', async t => {
|
|
391
|
+
const sandbox = sinon.createSandbox()
|
|
392
|
+
const puppet = new PuppetMock() as any
|
|
393
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
394
|
+
|
|
395
|
+
sandbox.stub(puppet, 'roomSearch').resolves([])
|
|
396
|
+
sandbox.stub(puppet, 'batchRoomPayload').resolves(new Map())
|
|
397
|
+
|
|
398
|
+
await wechaty.start()
|
|
399
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
400
|
+
await puppet.mocker.login(bot)
|
|
401
|
+
|
|
402
|
+
for (const badBatch of [ 0, -1 ]) {
|
|
403
|
+
await t.rejects(
|
|
404
|
+
(async () => {
|
|
405
|
+
for await (const _chunk of wechaty.Room.findAllIter(undefined, { batch: badBatch })) {
|
|
406
|
+
// should never reach here
|
|
407
|
+
}
|
|
408
|
+
})(),
|
|
409
|
+
/batch must be positive/,
|
|
410
|
+
`should throw when batch=${badBatch}`,
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await wechaty.stop()
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test('Room.findAllIter() skips ids missing from payloadMap', async t => {
|
|
418
|
+
const sandbox = sinon.createSandbox()
|
|
419
|
+
const puppet = new PuppetMock() as any
|
|
420
|
+
const wechaty = WechatyBuilder.build({ puppet })
|
|
421
|
+
|
|
422
|
+
sandbox.stub(puppet, 'roomSearch').resolves([ 'r1', 'r2', 'r3' ])
|
|
423
|
+
sandbox.stub(puppet, 'batchRoomPayload').resolves(
|
|
424
|
+
new Map([ [ 'r1', { id: 'r1', topic: 'topic-r1', memberIdList: [], adminIdList: [] } as PUPPET.payloads.Room ] ]),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
await wechaty.start()
|
|
428
|
+
const bot = puppet.mocker.createContact({ name: 'bot' })
|
|
429
|
+
await puppet.mocker.login(bot)
|
|
430
|
+
|
|
431
|
+
const seen: string[] = []
|
|
432
|
+
await t.resolves((async () => {
|
|
433
|
+
for await (const chunk of wechaty.Room.findAllIter()) {
|
|
434
|
+
for (const room of chunk) {
|
|
435
|
+
seen.push(room.id)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
})(), 'missing ids should not throw or stall iteration')
|
|
439
|
+
|
|
440
|
+
t.same(seen, [ 'r1' ], 'should yield only rooms whose payload was returned')
|
|
441
|
+
|
|
442
|
+
await wechaty.stop()
|
|
443
|
+
})
|
|
444
|
+
|
|
235
445
|
test('ProtectedProperties', async t => {
|
|
236
446
|
type NotExistInWechaty = Exclude<RoomProtectedProperty, keyof RoomImpl>
|
|
237
447
|
type NotExistTest = NotExistInWechaty extends never ? true : false
|