@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.
- package/dist/cjs/src/auth/call-cred.d.ts +2 -2
- package/dist/cjs/src/auth/call-cred.d.ts.map +1 -1
- package/dist/cjs/src/auth/call-cred.js.map +1 -1
- package/dist/cjs/src/client/puppet-service.d.ts +27 -2
- package/dist/cjs/src/client/puppet-service.d.ts.map +1 -1
- package/dist/cjs/src/client/puppet-service.js +431 -124
- package/dist/cjs/src/client/puppet-service.js.map +1 -1
- package/dist/cjs/src/client/puppet-service.spec.js +107 -0
- package/dist/cjs/src/client/puppet-service.spec.js.map +1 -1
- package/dist/cjs/src/client/room-cache-coordinator.d.ts +14 -0
- package/dist/cjs/src/client/room-cache-coordinator.d.ts.map +1 -0
- package/dist/cjs/src/client/room-cache-coordinator.js +53 -0
- package/dist/cjs/src/client/room-cache-coordinator.js.map +1 -0
- package/dist/cjs/src/client/room-member-cache.spec.d.ts +2 -0
- package/dist/cjs/src/client/room-member-cache.spec.d.ts.map +1 -0
- package/dist/cjs/src/client/room-member-cache.spec.js +417 -0
- package/dist/cjs/src/client/room-member-cache.spec.js.map +1 -0
- package/dist/cjs/src/client/room-member-load.spec.d.ts +2 -0
- package/dist/cjs/src/client/room-member-load.spec.d.ts.map +1 -0
- package/dist/cjs/src/client/room-member-load.spec.js +324 -0
- package/dist/cjs/src/client/room-member-load.spec.js.map +1 -0
- package/dist/cjs/src/package-json.js +4 -4
- package/dist/cjs/src/server/event-stream-manager.d.ts.map +1 -1
- package/dist/cjs/src/server/event-stream-manager.js +30 -0
- package/dist/cjs/src/server/event-stream-manager.js.map +1 -1
- package/dist/cjs/src/server/event-stream-manager.spec.d.ts +3 -0
- package/dist/cjs/src/server/event-stream-manager.spec.d.ts.map +1 -0
- package/dist/cjs/src/server/event-stream-manager.spec.js +22 -0
- package/dist/cjs/src/server/event-stream-manager.spec.js.map +1 -0
- package/dist/cjs/src/server/puppet-implementation.d.ts.map +1 -1
- package/dist/cjs/src/server/puppet-implementation.js +135 -28
- package/dist/cjs/src/server/puppet-implementation.js.map +1 -1
- package/dist/cjs/src/server/puppet-implementation.spec.js +98 -0
- package/dist/cjs/src/server/puppet-implementation.spec.js.map +1 -1
- package/dist/cjs/src/utils/pb-payload-helper.d.ts +10 -0
- package/dist/cjs/src/utils/pb-payload-helper.d.ts.map +1 -1
- package/dist/cjs/src/utils/pb-payload-helper.js +180 -1
- package/dist/cjs/src/utils/pb-payload-helper.js.map +1 -1
- package/dist/cjs/src/utils/room-member-load.spec.d.ts +2 -0
- package/dist/cjs/src/utils/room-member-load.spec.d.ts.map +1 -0
- package/dist/cjs/src/utils/room-member-load.spec.js +83 -0
- package/dist/cjs/src/utils/room-member-load.spec.js.map +1 -0
- package/dist/esm/src/auth/call-cred.d.ts +2 -2
- package/dist/esm/src/auth/call-cred.d.ts.map +1 -1
- package/dist/esm/src/auth/call-cred.js.map +1 -1
- package/dist/esm/src/client/puppet-service.d.ts +27 -2
- package/dist/esm/src/client/puppet-service.d.ts.map +1 -1
- package/dist/esm/src/client/puppet-service.js +432 -125
- package/dist/esm/src/client/puppet-service.js.map +1 -1
- package/dist/esm/src/client/puppet-service.spec.js +107 -0
- package/dist/esm/src/client/puppet-service.spec.js.map +1 -1
- package/dist/esm/src/client/room-cache-coordinator.d.ts +14 -0
- package/dist/esm/src/client/room-cache-coordinator.d.ts.map +1 -0
- package/dist/esm/src/client/room-cache-coordinator.js +49 -0
- package/dist/esm/src/client/room-cache-coordinator.js.map +1 -0
- package/dist/esm/src/client/room-member-cache.spec.d.ts +2 -0
- package/dist/esm/src/client/room-member-cache.spec.d.ts.map +1 -0
- package/dist/esm/src/client/room-member-cache.spec.js +415 -0
- package/dist/esm/src/client/room-member-cache.spec.js.map +1 -0
- package/dist/esm/src/client/room-member-load.spec.d.ts +2 -0
- package/dist/esm/src/client/room-member-load.spec.d.ts.map +1 -0
- package/dist/esm/src/client/room-member-load.spec.js +296 -0
- package/dist/esm/src/client/room-member-load.spec.js.map +1 -0
- package/dist/esm/src/package-json.js +4 -4
- package/dist/esm/src/server/event-stream-manager.d.ts.map +1 -1
- package/dist/esm/src/server/event-stream-manager.js +30 -0
- package/dist/esm/src/server/event-stream-manager.js.map +1 -1
- package/dist/esm/src/server/event-stream-manager.spec.d.ts +3 -0
- package/dist/esm/src/server/event-stream-manager.spec.d.ts.map +1 -0
- package/dist/esm/src/server/event-stream-manager.spec.js +20 -0
- package/dist/esm/src/server/event-stream-manager.spec.js.map +1 -0
- package/dist/esm/src/server/puppet-implementation.d.ts.map +1 -1
- package/dist/esm/src/server/puppet-implementation.js +136 -29
- package/dist/esm/src/server/puppet-implementation.js.map +1 -1
- package/dist/esm/src/server/puppet-implementation.spec.js +98 -0
- package/dist/esm/src/server/puppet-implementation.spec.js.map +1 -1
- package/dist/esm/src/utils/pb-payload-helper.d.ts +10 -0
- package/dist/esm/src/utils/pb-payload-helper.d.ts.map +1 -1
- package/dist/esm/src/utils/pb-payload-helper.js +169 -0
- package/dist/esm/src/utils/pb-payload-helper.js.map +1 -1
- package/dist/esm/src/utils/room-member-load.spec.d.ts +2 -0
- package/dist/esm/src/utils/room-member-load.spec.d.ts.map +1 -0
- package/dist/esm/src/utils/room-member-load.spec.js +58 -0
- package/dist/esm/src/utils/room-member-load.spec.js.map +1 -0
- package/package.json +4 -4
- package/src/auth/call-cred.ts +1 -1
- package/src/client/puppet-service.spec.ts +147 -0
- package/src/client/puppet-service.ts +378 -138
- package/src/client/room-cache-coordinator.ts +50 -0
- package/src/client/room-member-cache.spec.ts +448 -0
- package/src/client/room-member-load.spec.ts +319 -0
- package/src/package-json.ts +4 -4
- package/src/server/event-stream-manager.spec.ts +30 -0
- package/src/server/event-stream-manager.ts +29 -0
- package/src/server/puppet-implementation.spec.ts +127 -0
- package/src/server/puppet-implementation.ts +144 -29
- package/src/utils/pb-payload-helper.ts +150 -0
- package/src/utils/room-member-load.spec.ts +67 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class RoomCacheOperationError extends Error {
|
|
2
|
+
|
|
3
|
+
constructor (roomId: string, readonly original: unknown) {
|
|
4
|
+
super(`Room cache ${roomId} failed: ${original instanceof Error ? original.message : String(original)}`)
|
|
5
|
+
this.name = 'RoomCacheOperationError'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class RoomCacheCoordinator {
|
|
11
|
+
|
|
12
|
+
private readonly generations = new Map<string, number>()
|
|
13
|
+
private readonly pending = new Map<string, Promise<void>>()
|
|
14
|
+
|
|
15
|
+
generation (roomId: string): number {
|
|
16
|
+
return this.generations.get(roomId) ?? 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
invalidate (roomId: string): void {
|
|
20
|
+
this.generations.set(roomId, this.generation(roomId) + 1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async run<Result> (roomId: string, operation: () => Promise<Result>): Promise<Result> {
|
|
24
|
+
const preceding = this.pending.get(roomId) ?? Promise.resolve()
|
|
25
|
+
const result = (async (): Promise<Result> => {
|
|
26
|
+
await preceding
|
|
27
|
+
return operation()
|
|
28
|
+
})()
|
|
29
|
+
// Only the queue barrier settles errors; the caller still receives the rejected result.
|
|
30
|
+
const barrier = result.then((): undefined => undefined, (): undefined => undefined)
|
|
31
|
+
this.pending.set(roomId, barrier)
|
|
32
|
+
try {
|
|
33
|
+
return await result
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw error instanceof RoomCacheOperationError ? error : new RoomCacheOperationError(roomId, error)
|
|
36
|
+
} finally {
|
|
37
|
+
if (this.pending.get(roomId) === barrier) { this.pending.delete(roomId) }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async commit (roomId: string, generation: number, operation: () => Promise<unknown>): Promise<void> {
|
|
42
|
+
await this.run(roomId, async (): Promise<void> => {
|
|
43
|
+
if (this.generation(roomId) !== generation) { return }
|
|
44
|
+
await operation()
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { RoomCacheCoordinator, RoomCacheOperationError }
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { test, sinon } from 'tstest'
|
|
2
|
+
import { puppet as grpcPuppet } from '@juzi/wechaty-grpc'
|
|
3
|
+
import type * as PUPPET from '@juzi/wechaty-puppet'
|
|
4
|
+
|
|
5
|
+
import { PuppetService } from './puppet-service.js'
|
|
6
|
+
import { GrpcManager } from './grpc-manager.js'
|
|
7
|
+
import { roomPayloadToPb, roomMemberPayloadToPb } from '../utils/pb-payload-helper.js'
|
|
8
|
+
|
|
9
|
+
interface Deferred<Value> {
|
|
10
|
+
promise: Promise<Value>
|
|
11
|
+
resolve: (value: Value) => void
|
|
12
|
+
reject: (reason: unknown) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ResetFixture {
|
|
16
|
+
service: PuppetService
|
|
17
|
+
manager: GrpcManager
|
|
18
|
+
metadata: Map<string, string>
|
|
19
|
+
started: Deferred<void>
|
|
20
|
+
release: Deferred<void>
|
|
21
|
+
stop: { readonly callCount: number }
|
|
22
|
+
start: { readonly callCount: number, readonly args: readonly unknown[][] }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function deferred<Value> (): Deferred<Value> {
|
|
26
|
+
let finish: (value: Value) => void = (): void => { throw new Error('Uninitialized resolver') }
|
|
27
|
+
let fail: (reason: unknown) => void = (): void => { throw new Error('Uninitialized rejection') }
|
|
28
|
+
const promise = new Promise<Value>((resolve, reject) => { finish = resolve; fail = reject })
|
|
29
|
+
return { promise, resolve: finish, reject: fail }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class BridgedService extends PuppetService {
|
|
33
|
+
|
|
34
|
+
connect (manager: GrpcManager): void { this._grpcManager = manager; this.bridgeGrpcEventStream(manager) }
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let fixtureId = 0
|
|
39
|
+
function serviceFixture (): BridgedService {
|
|
40
|
+
return new BridgedService({ token: `insecure_cache-${process.pid}-${fixtureId++}` })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function event (payload: unknown, seq = 2): grpcPuppet.EventResponse {
|
|
44
|
+
const response = new grpcPuppet.EventResponse()
|
|
45
|
+
response.setType(grpcPuppet.EventType.EVENT_TYPE_ROOM_MEMBER_LOAD)
|
|
46
|
+
response.setSeq(seq)
|
|
47
|
+
response.setPayload(JSON.stringify(payload))
|
|
48
|
+
return response
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const state = { roomId: 'room', taskId: 'task', status: 3, revision: '1' }
|
|
52
|
+
|
|
53
|
+
async function drain (): Promise<void> {
|
|
54
|
+
await new Promise<void>(resolve => setImmediate(resolve))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resetFixture (remove: () => Promise<void> = async (): Promise<void> => {}): ResetFixture {
|
|
58
|
+
const service = serviceFixture()
|
|
59
|
+
const manager = new GrpcManager({ token: 'insecure_token' })
|
|
60
|
+
const metadata = new Map([ [ 'lastEventSeq', '1' ], [ 'lastEventTimestamp', String(Date.now()) ], [ 'accountId', 'account' ] ])
|
|
61
|
+
Object.defineProperty(service, '_payloadStore', {
|
|
62
|
+
value: {
|
|
63
|
+
room: { delete: remove },
|
|
64
|
+
roomMember: { delete: async (): Promise<void> => {} },
|
|
65
|
+
miscellaneous: {
|
|
66
|
+
get: async (key: string): Promise<string | undefined> => metadata.get(key),
|
|
67
|
+
set: async (key: string, value: string): Promise<void> => { metadata.set(key, value) },
|
|
68
|
+
delete: async (key: string): Promise<void> => { metadata.delete(key) },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
Object.defineProperty(service, 'isLoggedIn', { get: (): boolean => true })
|
|
73
|
+
sinon.stub(service, 'login').resolves()
|
|
74
|
+
const stop = sinon.stub(manager, 'stopStream')
|
|
75
|
+
const started = deferred<void>()
|
|
76
|
+
const release = deferred<void>()
|
|
77
|
+
const start = sinon.stub(manager, 'startStream').callsFake(async (): Promise<void> => {
|
|
78
|
+
started.resolve()
|
|
79
|
+
await release.promise
|
|
80
|
+
})
|
|
81
|
+
service.connect(manager)
|
|
82
|
+
return { service, manager, metadata, stop, start, started, release }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function finishResetHandshake (fixture: ResetFixture): Promise<void> {
|
|
86
|
+
fixture.release.resolve()
|
|
87
|
+
await drain()
|
|
88
|
+
const login = new grpcPuppet.EventResponse()
|
|
89
|
+
login.setType(grpcPuppet.EventType.EVENT_TYPE_LOGIN)
|
|
90
|
+
login.setPayload(JSON.stringify({ contactId: 'account' }))
|
|
91
|
+
fixture.manager.emit('data', login)
|
|
92
|
+
const ready = new grpcPuppet.EventResponse()
|
|
93
|
+
ready.setType(grpcPuppet.EventType.EVENT_TYPE_READY)
|
|
94
|
+
ready.setPayload(JSON.stringify({ data: 'ready' }))
|
|
95
|
+
fixture.manager.emit('data', ready)
|
|
96
|
+
await drain()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
test('bridge cache failure stops the stream and fences later seq until explicit reset', async t => {
|
|
100
|
+
const service = serviceFixture()
|
|
101
|
+
const manager = new GrpcManager({ token: 'insecure_token' })
|
|
102
|
+
const metadata = new Map<string, string>([ [ 'lastEventSeq', '1' ], [ 'lastEventTimestamp', String(Date.now()) ] ])
|
|
103
|
+
const deleteRoom = sinon.stub().rejects(new Error('persistent cache unavailable'))
|
|
104
|
+
Object.defineProperty(service, '_payloadStore', {
|
|
105
|
+
value: {
|
|
106
|
+
room: { delete: deleteRoom },
|
|
107
|
+
roomMember: { delete: sinon.stub().resolves() },
|
|
108
|
+
miscellaneous: {
|
|
109
|
+
get: async (key: string): Promise<string | undefined> => metadata.get(key),
|
|
110
|
+
set: async (key: string, value: string): Promise<void> => { metadata.set(key, value) },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
const stop = sinon.stub(manager, 'stopStream')
|
|
115
|
+
const start = sinon.stub(manager, 'startStream').resolves()
|
|
116
|
+
const errors = sinon.spy()
|
|
117
|
+
const completed = sinon.spy()
|
|
118
|
+
const unhandled = sinon.spy()
|
|
119
|
+
process.on('unhandledRejection', unhandled)
|
|
120
|
+
t.teardown((): void => { process.off('unhandledRejection', unhandled) })
|
|
121
|
+
service.on('error', errors)
|
|
122
|
+
service.on('room-member-load', completed)
|
|
123
|
+
service.connect(manager)
|
|
124
|
+
manager.emit('data', event(state))
|
|
125
|
+
manager.emit('data', event({ ...state, revision: '2' }, 3))
|
|
126
|
+
await drain()
|
|
127
|
+
t.equal(unhandled.callCount, 0)
|
|
128
|
+
t.equal(errors.callCount, 1)
|
|
129
|
+
t.equal(completed.callCount, 0)
|
|
130
|
+
t.equal(metadata.get('lastEventSeq'), '1')
|
|
131
|
+
t.equal(stop.callCount, 1)
|
|
132
|
+
t.equal(start.callCount, 0)
|
|
133
|
+
deleteRoom.resolves()
|
|
134
|
+
manager.emit('data', event(state))
|
|
135
|
+
await drain()
|
|
136
|
+
t.equal(completed.callCount, 0)
|
|
137
|
+
t.equal(metadata.get('lastEventSeq'), '1')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('invalid event payloads never reach persistent cache or emit completion', async t => {
|
|
141
|
+
for (const payload of [
|
|
142
|
+
{ ...state, revision: 1 }, { ...state, revision: '01' }, { ...state, status: 99 },
|
|
143
|
+
{ ...state, roomId: '' }, { ...state, roomId: undefined }, { ...state, taskId: 1 },
|
|
144
|
+
]) {
|
|
145
|
+
const service = serviceFixture()
|
|
146
|
+
const manager = new GrpcManager({ token: 'insecure_token' })
|
|
147
|
+
const remove = sinon.stub().resolves()
|
|
148
|
+
Object.defineProperty(service, '_payloadStore', { value: { room: { delete: remove }, roomMember: { delete: remove } } })
|
|
149
|
+
sinon.stub(manager, 'stopStream')
|
|
150
|
+
sinon.stub(manager, 'startStream').resolves()
|
|
151
|
+
const errors = sinon.spy()
|
|
152
|
+
const completed = sinon.spy()
|
|
153
|
+
service.on('error', errors)
|
|
154
|
+
service.on('room-member-load', completed)
|
|
155
|
+
service.connect(manager)
|
|
156
|
+
manager.emit('data', event(payload))
|
|
157
|
+
await drain()
|
|
158
|
+
t.equal(remove.callCount, 0)
|
|
159
|
+
t.equal(completed.callCount, 0)
|
|
160
|
+
t.equal(errors.callCount, 1)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('all four raw RPC reads begun before invalidation cannot refill persistent caches', async t => {
|
|
165
|
+
const room: PUPPET.payloads.Room = { id: 'room', topic: 'old', adminIdList: [], memberIdList: [] }
|
|
166
|
+
const member: PUPPET.payloads.RoomMember = { id: 'member', name: 'old', avatar: '' }
|
|
167
|
+
for (const method of [ 'room', 'batchRoom', 'member', 'batchMember' ]) {
|
|
168
|
+
const responseReady = deferred<void>()
|
|
169
|
+
const requested = deferred<void>()
|
|
170
|
+
const service = serviceFixture()
|
|
171
|
+
const manager = new GrpcManager({ token: 'insecure_token' })
|
|
172
|
+
const rooms = new Map<string, PUPPET.payloads.Room>()
|
|
173
|
+
const members = new Map<string, Record<string, PUPPET.payloads.RoomMember>>()
|
|
174
|
+
Object.defineProperty(service, '_payloadStore', {
|
|
175
|
+
value: {
|
|
176
|
+
room: {
|
|
177
|
+
get: async (id: string): Promise<PUPPET.payloads.Room | undefined> => rooms.get(id),
|
|
178
|
+
set: async (id: string, value: PUPPET.payloads.Room): Promise<void> => { rooms.set(id, value) },
|
|
179
|
+
delete: async (id: string): Promise<void> => { rooms.delete(id) },
|
|
180
|
+
},
|
|
181
|
+
roomMember: {
|
|
182
|
+
get: async (id: string): Promise<Record<string, PUPPET.payloads.RoomMember> | undefined> => members.get(id),
|
|
183
|
+
set: async (id: string, value: Record<string, PUPPET.payloads.RoomMember>): Promise<void> => { members.set(id, value) },
|
|
184
|
+
delete: async (id: string): Promise<void> => { members.delete(id) },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
const wait = async (): Promise<void> => { requested.resolve(); await responseReady.promise }
|
|
189
|
+
const respond = async <Response>(response: Response, callback: (error: Error | null, value: Response) => void): Promise<void> => {
|
|
190
|
+
await wait()
|
|
191
|
+
callback(null, response)
|
|
192
|
+
}
|
|
193
|
+
const client = {
|
|
194
|
+
roomPayload: (_request: grpcPuppet.RoomPayloadRequest, callback: (error: Error | null, response: grpcPuppet.RoomPayloadResponse) => void): void => {
|
|
195
|
+
void respond(roomPayloadToPb(room), callback)
|
|
196
|
+
},
|
|
197
|
+
batchRoomPayload: (_request: grpcPuppet.BatchRoomPayloadRequest, callback: (error: Error | null, response: grpcPuppet.BatchRoomPayloadResponse) => void): void => {
|
|
198
|
+
const response = new grpcPuppet.BatchRoomPayloadResponse()
|
|
199
|
+
response.setRoomPayloadsList([ roomPayloadToPb(room) ])
|
|
200
|
+
void respond(response, callback)
|
|
201
|
+
},
|
|
202
|
+
roomMemberPayload: (_request: grpcPuppet.RoomMemberPayloadRequest, callback: (error: Error | null, response: grpcPuppet.RoomMemberPayloadResponse) => void): void => {
|
|
203
|
+
void respond(roomMemberPayloadToPb(member), callback)
|
|
204
|
+
},
|
|
205
|
+
batchRoomMemberPayload: (_request: grpcPuppet.BatchRoomMemberPayloadRequest, callback: (error: Error | null, response: grpcPuppet.BatchRoomMemberPayloadResponse) => void): void => {
|
|
206
|
+
const response = new grpcPuppet.BatchRoomMemberPayloadResponse()
|
|
207
|
+
response.setMemberPayloadsList([ roomMemberPayloadToPb(member) ])
|
|
208
|
+
void respond(response, callback)
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
Object.defineProperty(service, 'grpcManager', { value: { client } })
|
|
212
|
+
service.on('error', (error: unknown): void => { t.fail(String(error)) })
|
|
213
|
+
service.connect(manager)
|
|
214
|
+
const pending = method === 'room' ? service.roomRawPayload('room')
|
|
215
|
+
: method === 'batchRoom' ? service.batchRoomRawPayload([ 'room' ])
|
|
216
|
+
: method === 'member' ? service.roomMemberRawPayload('room', 'member')
|
|
217
|
+
: service.batchRoomMemberRawPayload('room', [ 'member' ])
|
|
218
|
+
await requested.promise
|
|
219
|
+
manager.emit('data', event(state, 0))
|
|
220
|
+
await drain()
|
|
221
|
+
responseReady.resolve()
|
|
222
|
+
await pending
|
|
223
|
+
t.equal(rooms.size, 0, method)
|
|
224
|
+
t.equal(members.size, 0, method)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('invalidation waits for an already-started asynchronous cache write', async t => {
|
|
229
|
+
const service = serviceFixture()
|
|
230
|
+
const manager = new GrpcManager({ token: 'insecure_token' })
|
|
231
|
+
const writing = deferred<void>()
|
|
232
|
+
const finishWrite = deferred<void>()
|
|
233
|
+
let stored: PUPPET.payloads.Room | undefined
|
|
234
|
+
const room: PUPPET.payloads.Room = { id: 'room', topic: 'old', adminIdList: [], memberIdList: [] }
|
|
235
|
+
const remove = sinon.stub().callsFake(async (): Promise<void> => { stored = undefined })
|
|
236
|
+
Object.defineProperty(service, '_payloadStore', {
|
|
237
|
+
value: {
|
|
238
|
+
room: {
|
|
239
|
+
get: async (): Promise<PUPPET.payloads.Room | undefined> => stored,
|
|
240
|
+
set: async (_id: string, value: PUPPET.payloads.Room): Promise<void> => {
|
|
241
|
+
writing.resolve()
|
|
242
|
+
await finishWrite.promise
|
|
243
|
+
stored = value
|
|
244
|
+
},
|
|
245
|
+
delete: remove,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
Object.defineProperty(service, 'grpcManager', {
|
|
250
|
+
value: {
|
|
251
|
+
client: {
|
|
252
|
+
roomPayload: (_request: grpcPuppet.RoomPayloadRequest, callback: (error: Error | null, response: grpcPuppet.RoomPayloadResponse) => void): void => callback(null, roomPayloadToPb(room)),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
})
|
|
256
|
+
service.connect(manager)
|
|
257
|
+
const loaded = sinon.spy()
|
|
258
|
+
service.on('room-member-load', loaded)
|
|
259
|
+
const pending = service.roomRawPayload('room')
|
|
260
|
+
await writing.promise
|
|
261
|
+
manager.emit('data', event(state, 0))
|
|
262
|
+
await drain()
|
|
263
|
+
t.equal(remove.callCount, 0)
|
|
264
|
+
t.equal(loaded.callCount, 0)
|
|
265
|
+
finishWrite.resolve()
|
|
266
|
+
await pending
|
|
267
|
+
await drain()
|
|
268
|
+
t.equal(stored, undefined)
|
|
269
|
+
t.equal(loaded.callCount, 1)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('later events cannot bypass a halted failure and never cause automatic restart', async t => {
|
|
273
|
+
for (const skipped of [ false, true ]) {
|
|
274
|
+
const service = serviceFixture()
|
|
275
|
+
const manager = new GrpcManager({ token: 'insecure_token' })
|
|
276
|
+
const metadata = new Map<string, string>([ [ 'lastEventSeq', '1' ] ])
|
|
277
|
+
Object.defineProperty(service, '_payloadStore', {
|
|
278
|
+
value: {
|
|
279
|
+
room: { delete: sinon.stub().rejects(new Error('persistent cache unavailable')) },
|
|
280
|
+
miscellaneous: {
|
|
281
|
+
get: async (key: string): Promise<string | undefined> => metadata.get(key),
|
|
282
|
+
set: async (key: string, value: string): Promise<void> => { metadata.set(key, value) },
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
sinon.stub(manager, 'stopStream')
|
|
287
|
+
const start = sinon.stub(manager, 'startStream').resolves()
|
|
288
|
+
const errors = sinon.spy()
|
|
289
|
+
const completed = sinon.spy()
|
|
290
|
+
service.on('error', errors)
|
|
291
|
+
service.on('room-member-load', completed)
|
|
292
|
+
service.connect(manager)
|
|
293
|
+
manager.emit('data', event(state))
|
|
294
|
+
await drain()
|
|
295
|
+
manager.emit('data', event(state, skipped ? 3 : 2))
|
|
296
|
+
manager.emit('data', event(state, 4))
|
|
297
|
+
await drain()
|
|
298
|
+
t.equal(start.callCount, 0)
|
|
299
|
+
t.equal(errors.callCount, 1)
|
|
300
|
+
t.equal(completed.callCount, 0)
|
|
301
|
+
t.equal(metadata.get('lastEventSeq'), '1')
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('duplicate reset does not invalidate events queued by the owner reset', async t => {
|
|
306
|
+
const fixture = resetFixture()
|
|
307
|
+
const completed = sinon.spy()
|
|
308
|
+
fixture.service.on('room-member-load', completed)
|
|
309
|
+
const resetting = fixture.service.reset()
|
|
310
|
+
await fixture.started.promise
|
|
311
|
+
fixture.release.resolve()
|
|
312
|
+
await drain()
|
|
313
|
+
const login = new grpcPuppet.EventResponse()
|
|
314
|
+
login.setType(grpcPuppet.EventType.EVENT_TYPE_LOGIN)
|
|
315
|
+
login.setPayload(JSON.stringify({ contactId: 'account' }))
|
|
316
|
+
fixture.manager.emit('data', login)
|
|
317
|
+
await drain()
|
|
318
|
+
fixture.manager.emit('data', event(state))
|
|
319
|
+
await fixture.service.reset()
|
|
320
|
+
await drain()
|
|
321
|
+
t.equal(completed.callCount, 1)
|
|
322
|
+
t.equal(fixture.metadata.get('lastEventSeq'), '2')
|
|
323
|
+
t.equal(fixture.start.callCount, 1)
|
|
324
|
+
await finishResetHandshake(fixture)
|
|
325
|
+
await resetting
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('superseded delete rejection cannot stop the stream established by reset', async t => {
|
|
329
|
+
const removing = deferred<void>()
|
|
330
|
+
const began = deferred<void>()
|
|
331
|
+
const remove = sinon.stub().onFirstCall().callsFake(async (): Promise<void> => { began.resolve(); await removing.promise })
|
|
332
|
+
remove.onSecondCall().resolves()
|
|
333
|
+
const fixture = resetFixture(remove)
|
|
334
|
+
const errors = sinon.spy()
|
|
335
|
+
const completed = sinon.spy()
|
|
336
|
+
fixture.service.on('error', errors)
|
|
337
|
+
fixture.service.on('room-member-load', completed)
|
|
338
|
+
fixture.manager.emit('data', event(state))
|
|
339
|
+
await began.promise
|
|
340
|
+
const resetting = fixture.service.reset()
|
|
341
|
+
await fixture.started.promise
|
|
342
|
+
await finishResetHandshake(fixture)
|
|
343
|
+
await resetting
|
|
344
|
+
removing.reject(new Error('old delete rejected after reset'))
|
|
345
|
+
await drain()
|
|
346
|
+
t.equal(fixture.stop.callCount, 1)
|
|
347
|
+
t.equal(fixture.start.callCount, 1)
|
|
348
|
+
t.equal(errors.callCount, 0)
|
|
349
|
+
t.equal(completed.callCount, 0)
|
|
350
|
+
t.equal(fixture.metadata.get('lastEventSeq'), '1')
|
|
351
|
+
fixture.manager.emit('data', event(state))
|
|
352
|
+
await drain()
|
|
353
|
+
t.equal(completed.callCount, 1)
|
|
354
|
+
t.equal(fixture.metadata.get('lastEventSeq'), '2')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('failure stops before notifying an error listener that immediately performs actual reset', async t => {
|
|
358
|
+
const remove = sinon.stub().onFirstCall().rejects(new Error('persistent cache unavailable'))
|
|
359
|
+
remove.onSecondCall().resolves()
|
|
360
|
+
const fixture = resetFixture(remove)
|
|
361
|
+
const resetting = deferred<Promise<void>>()
|
|
362
|
+
const stopsAtError: number[] = []
|
|
363
|
+
fixture.service.on('error', (): void => {
|
|
364
|
+
stopsAtError.push(fixture.stop.callCount)
|
|
365
|
+
resetting.resolve(fixture.service.reset())
|
|
366
|
+
})
|
|
367
|
+
fixture.manager.emit('data', event(state))
|
|
368
|
+
await fixture.started.promise
|
|
369
|
+
t.same(stopsAtError, [ 1 ])
|
|
370
|
+
t.equal(fixture.start.callCount, 1)
|
|
371
|
+
t.same(fixture.start.args[0], [ '1', 'account' ])
|
|
372
|
+
await finishResetHandshake(fixture)
|
|
373
|
+
await resetting.promise
|
|
374
|
+
await drain()
|
|
375
|
+
t.equal(fixture.stop.callCount, 2)
|
|
376
|
+
t.equal(fixture.start.callCount, 1)
|
|
377
|
+
fixture.manager.emit('data', event(state))
|
|
378
|
+
await drain()
|
|
379
|
+
t.equal(fixture.metadata.get('lastEventSeq'), '2')
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test('reset waits for and rolls back a superseded asynchronous cursor commit before resuming', async t => {
|
|
383
|
+
const fixture = resetFixture()
|
|
384
|
+
const writing = deferred<void>()
|
|
385
|
+
const finishWrite = deferred<void>()
|
|
386
|
+
Object.defineProperty(fixture.service, '_payloadStore', {
|
|
387
|
+
value: {
|
|
388
|
+
room: { delete: async (): Promise<void> => {} },
|
|
389
|
+
roomMember: { delete: async (): Promise<void> => {} },
|
|
390
|
+
miscellaneous: {
|
|
391
|
+
get: async (key: string): Promise<string | undefined> => fixture.metadata.get(key),
|
|
392
|
+
set: async (key: string, value: string): Promise<void> => {
|
|
393
|
+
if (key === 'lastEventSeq' && value === '2') { writing.resolve(); await finishWrite.promise }
|
|
394
|
+
fixture.metadata.set(key, value)
|
|
395
|
+
},
|
|
396
|
+
delete: async (key: string): Promise<void> => { fixture.metadata.delete(key) },
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
})
|
|
400
|
+
fixture.manager.emit('data', event(state))
|
|
401
|
+
await writing.promise
|
|
402
|
+
const resetting = fixture.service.reset()
|
|
403
|
+
await drain()
|
|
404
|
+
t.equal(fixture.start.callCount, 0)
|
|
405
|
+
finishWrite.resolve()
|
|
406
|
+
await fixture.started.promise
|
|
407
|
+
t.same(fixture.start.args[0], [ '1', 'account' ])
|
|
408
|
+
t.equal(fixture.metadata.get('lastEventSeq'), '1')
|
|
409
|
+
await finishResetHandshake(fixture)
|
|
410
|
+
await resetting
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('explicit reset preserves the failed seq barrier if the new stream skips that event', async t => {
|
|
414
|
+
const remove = sinon.stub().onFirstCall().rejects(new Error('persistent cache unavailable'))
|
|
415
|
+
remove.onSecondCall().resolves()
|
|
416
|
+
const fixture = resetFixture(remove)
|
|
417
|
+
const errors = sinon.spy()
|
|
418
|
+
const completed = sinon.spy()
|
|
419
|
+
fixture.service.on('error', errors)
|
|
420
|
+
fixture.service.on('room-member-load', completed)
|
|
421
|
+
fixture.manager.emit('data', event(state))
|
|
422
|
+
await drain()
|
|
423
|
+
t.equal(fixture.start.callCount, 0)
|
|
424
|
+
const resetting = fixture.service.reset()
|
|
425
|
+
await fixture.started.promise
|
|
426
|
+
await finishResetHandshake(fixture)
|
|
427
|
+
await resetting
|
|
428
|
+
fixture.manager.emit('data', event(state, 3))
|
|
429
|
+
await drain()
|
|
430
|
+
t.equal(errors.callCount, 2)
|
|
431
|
+
t.equal(completed.callCount, 0)
|
|
432
|
+
t.equal(fixture.metadata.get('lastEventSeq'), '1')
|
|
433
|
+
t.equal(fixture.start.callCount, 1)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
test('explicit reset uses the actual durable cursor without age fallback or invented zero', async t => {
|
|
437
|
+
for (const absent of [ false, true ]) {
|
|
438
|
+
const fixture = resetFixture()
|
|
439
|
+
fixture.metadata.set('lastEventTimestamp', '0')
|
|
440
|
+
if (absent) { fixture.metadata.delete('lastEventSeq') }
|
|
441
|
+
const resetting = fixture.service.reset()
|
|
442
|
+
await fixture.started.promise
|
|
443
|
+
t.same(fixture.start.args[0], [ absent ? undefined : '1', 'account' ])
|
|
444
|
+
await finishResetHandshake(fixture)
|
|
445
|
+
await resetting
|
|
446
|
+
t.equal(fixture.metadata.get('lastEventSeq'), absent ? undefined : '1')
|
|
447
|
+
}
|
|
448
|
+
})
|