Skip to content

Commit 9d4a8dd

Browse files
authored
Init Mixins (#154)
* create PayloadCache class * init mixins * add mixin docs * code clean * change protected to jsdoc (#155) * add CacheMixin & WatchdogMin with PuppetSkelton * add CacheMixin & WatchdogMin with PuppetSkelton * clean * follow mixin design (#156) * Create PuppetError classes (#159) * split all code into mixins * fix memory * fix * fix memory * 0.49.4 * clean * 0.49.5 * rename PuppetError -> GError * rename PuppetError -> GError * 0.49.6 * clean code * 0.49.7
1 parent d53e748 commit 9d4a8dd

32 files changed

+2443
-1531
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,17 @@ Set the max size for wechaty entities in LRU Cache.
112112
* [What Are Pure Functions And Why Use Them?](https://medium.com/@jamesjefferyuk/javascript-what-are-pure-functions-4d4d5392d49c)
113113
* [Master the JavaScript Interview: What is a Pure Function?](https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976)
114114

115+
### Mixin
116+
117+
* [The mixin pattern in TypeScript – all you need to know](https://www.bryntum.com/blog/the-mixin-pattern-in-typescript-all-you-need-to-know/)
118+
* [Mixin Classes in TypeScript](https://mariusschulz.com/blog/mixin-classes-in-typescript)
119+
115120
## History
116121

117-
### master v0.47
122+
### master v0.49
118123

119124
1. Add `Error` interface to `EventErrorPayload`, and make `.data` optional
125+
2. Using [Mixin](https://en.wikipedia.org/wiki/Mixin) to extend `Puppet`
120126

121127
### v0.43 (Aug 28, 2021)
122128

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wechaty-puppet",
3-
"version": "0.49.3",
3+
"version": "0.49.7",
44
"description": "Abstract Puppet for Wechaty",
55
"type": "module",
66
"exports": {

src/payload-cache.spec.ts renamed to src/agents/cache-agent.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import { test } from 'tstest'
44

5-
import { PayloadCache } from './payload-cache.js'
5+
import { CacheAgent } from './cache-agent.js'
66

7-
test('PayloadCache roomMemberId() restart', async t => {
8-
const payloadCache = new PayloadCache()
7+
test('CacheAgent roomMemberId() restart', async t => {
8+
const payloadCache = new CacheAgent()
99
const roomMemberId = payloadCache.roomMemberId('roomId', 'userId')
1010
t.equal(roomMemberId, 'roomId-userId', 'should get right id')
1111
})

src/payload-cache.ts renamed to src/agents/cache-agent.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,28 @@ import QuickLru from '@alloc/quick-lru'
66
import {
77
envVars,
88
log,
9-
} from './config.js'
9+
} from '../config.js'
1010
import type {
1111
ContactPayload,
12-
} from './schemas/contact.js'
12+
} from '../schemas/contact.js'
1313
import type {
1414
FriendshipPayload,
15-
} from './schemas/friendship.js'
15+
} from '../schemas/friendship.js'
1616
import type {
1717
MessagePayload,
18-
} from './schemas/message.js'
18+
} from '../schemas/message.js'
1919
import type {
2020
RoomMemberPayload,
2121
RoomPayload,
22-
} from './schemas/room.js'
22+
} from '../schemas/room.js'
2323
import type {
2424
RoomInvitationPayload,
25-
} from './schemas/room-invitation.js'
25+
} from '../schemas/room-invitation.js'
26+
import type { PuppetOptions } from '../schemas/puppet.js'
2627

27-
interface PayloadCacheOptions {
28-
contact? : number
29-
friendship? : number
30-
message? : number
31-
room? : number
32-
roomInvitation? : number
33-
roomMember? : number
34-
}
28+
type PayloadCacheOptions = Required<PuppetOptions>['cache']
3529

36-
class PayloadCache {
30+
class CacheAgent {
3731

3832
readonly contact : QuickLru<string, ContactPayload>
3933
readonly friendship : QuickLru<string, FriendshipPayload>
@@ -45,7 +39,7 @@ class PayloadCache {
4539
constructor (
4640
protected options: PayloadCacheOptions = {},
4741
) {
48-
log.verbose('PayloadCache', 'constructor(%s)', JSON.stringify(options))
42+
log.verbose('CacheAgent', 'constructor("%s")', JSON.stringify(options))
4943

5044
/**
5145
* Setup LRU Caches
@@ -77,12 +71,12 @@ class PayloadCache {
7771
}
7872

7973
start (): void {
80-
log.verbose('PayloadCache', 'start()')
74+
log.verbose('CacheAgent', 'start()')
8175
this.clear()
8276
}
8377

8478
stop (): void {
85-
log.verbose('PayloadCache', 'stop()')
79+
log.verbose('CacheAgent', 'stop()')
8680
this.clear()
8781
}
8882

@@ -97,7 +91,7 @@ class PayloadCache {
9791
* Huan(2021-08-28): clear the cache when stop
9892
*/
9993
clear (): void {
100-
log.verbose('PayloadCache', 'clear()')
94+
log.verbose('CacheAgent', 'clear()')
10195

10296
this.contact.clear()
10397
this.friendship.clear()
@@ -119,4 +113,5 @@ class PayloadCache {
119113

120114
}
121115

122-
export { PayloadCache }
116+
export type { PayloadCacheOptions }
117+
export { CacheAgent }

src/agents/mod.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { CacheAgent } from './cache-agent.js'
2+
import { WatchdogAgent } from './watchdog-agent.js'
3+
4+
export {
5+
CacheAgent,
6+
WatchdogAgent,
7+
}

src/agents/watchdog-agent.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
Watchdog,
3+
WatchdogFood,
4+
} from 'watchdog'
5+
6+
import type { PuppetSkelton } from '../puppet/skelton.js'
7+
import {
8+
log,
9+
} from '../config.js'
10+
11+
const DEFAULT_WATCHDOG_TIMEOUT_SECONDS = 60
12+
13+
class WatchdogAgent {
14+
15+
protected readonly watchdog : Watchdog
16+
17+
private cleanCallbackList: Function[]
18+
19+
/**
20+
* Throttle Reset Events
21+
*
22+
* @private
23+
*/
24+
// protected readonly resetThrottleQueue: ThrottleQueue<string>
25+
26+
constructor (
27+
protected readonly puppet: PuppetSkelton,
28+
) {
29+
log.verbose('WatchdogAgent', 'constructor(%s)', puppet)
30+
31+
this.cleanCallbackList = []
32+
33+
/**
34+
* 1. Setup Watchdog
35+
* puppet implementation class only need to do one thing:
36+
* feed the watchdog by `this.emit('heartbeat', ...)`
37+
*/
38+
const timeoutSeconds = puppet.options.timeoutSeconds || DEFAULT_WATCHDOG_TIMEOUT_SECONDS
39+
log.verbose('WatchdogAgent', 'constructor() watchdog timeout set to %d seconds', timeoutSeconds)
40+
this.watchdog = new Watchdog(1000 * timeoutSeconds, 'Puppet')
41+
42+
// /**
43+
// * 2. Setup `reset` Event via a 1 second Throttle Queue:
44+
// */
45+
// this.resetThrottleQueue = new ThrottleQueue<string>(1000)
46+
// this.resetThrottleQueue.subscribe(reason => {
47+
// log.silly('WatchdogAgent', 'constructor() resetThrottleQueue.subscribe() reason: "%s"', reason)
48+
// puppet.reset(reason)
49+
// })
50+
}
51+
52+
/**
53+
* @private
54+
*
55+
* For used by Wechaty internal ONLY.
56+
*/
57+
start (): void {
58+
/**
59+
* puppet event `heartbeat` to feed() watchdog
60+
*/
61+
const feed = (food: WatchdogFood) => { this.watchdog.feed(food) }
62+
this.puppet.on('heartbeat', feed)
63+
this.cleanCallbackList.push(() => this.puppet.off('heartbeat', feed))
64+
65+
/**
66+
* watchdog event `reset` to reset() puppet
67+
*/
68+
const reset = (lastFood: WatchdogFood) => {
69+
log.warn('WatchdogAgent', 'start() reset() reason: %s', JSON.stringify(lastFood))
70+
this.puppet
71+
.reset(`WatchdogAgent reset: lastFood: "${JSON.stringify(lastFood)}"`)
72+
.catch(e => log.error('WatchdogAgent', 'start() reset() rejection: %s', e.message))
73+
}
74+
this.watchdog.on('reset', reset)
75+
this.cleanCallbackList.push(() => this.puppet.off('reset', reset))
76+
77+
// this.puppet.on('reset', this.throttleReset)
78+
}
79+
80+
stop (): void {
81+
while (this.cleanCallbackList.length) {
82+
const callback = this.cleanCallbackList.shift()
83+
if (callback) {
84+
callback()
85+
}
86+
}
87+
this.watchdog.sleep()
88+
}
89+
90+
}
91+
92+
export { WatchdogAgent }

src/events.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ interface PuppetEvents {
5353
scan : PuppetScanListener
5454
}
5555

56-
export const PuppetEventEmitter = EventEmitter as new () => TypedEventEmitter<
56+
const PuppetEventEmitter = EventEmitter as new () => TypedEventEmitter<
5757
PuppetEvents
5858
>
59+
60+
export type {
61+
PuppetEvents,
62+
}
63+
export {
64+
PuppetEventEmitter,
65+
}

src/gerror/ecma.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* JavaScript Error Interface:
3+
*
4+
* interface Error {
5+
* name: string;
6+
* message: string;
7+
* stack?: string;
8+
* }
9+
*/
10+
interface EcmaError extends Error {}
11+
12+
const isEcmaError = (payload: any): payload is EcmaError => payload instanceof Object
13+
&& typeof payload.name === 'string'
14+
&& typeof payload.message === 'string'
15+
16+
export type { EcmaError }
17+
export { isEcmaError }

src/gerror/gerror.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm
2+
3+
import { test } from 'tstest'
4+
5+
import { GError } from './gerror.js'
6+
import { Code } from './grpc.js'
7+
8+
test('GError class smoke testing', async t => {
9+
const MESSAGE = 'test'
10+
11+
const gerror = GError.fromJSON(new Error(MESSAGE))
12+
const obj = gerror.toJSON()
13+
14+
t.equal(obj.code, Code.UNKNOWN, 'should be default code UNKNOWN')
15+
t.equal(obj.message, MESSAGE, 'should set message')
16+
})

src/gerror/gerror.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* [RFC] We need a better error system #159
3+
* https://github.com/wechaty/puppet/issues/159
4+
*/
5+
import { log } from '../config.js'
6+
7+
import type { EcmaError } from './ecma.js'
8+
import { isEcmaError } from './ecma.js'
9+
import type { GrpcStatus } from './grpc.js'
10+
import {
11+
isGrpcStatus,
12+
Code,
13+
} from './grpc.js'
14+
15+
const isGError = (payload: any): payload is GError => payload instanceof Object
16+
&& isEcmaError(payload)
17+
&& isGrpcStatus(payload)
18+
19+
class GError extends Error implements GrpcStatus, EcmaError {
20+
21+
code : number
22+
details? : any[]
23+
24+
/**
25+
* From a gRPC standard error
26+
* Protobuf
27+
*/
28+
public static fromJSON (payload: string | GrpcStatus | EcmaError) {
29+
log.verbose('PuppetError', 'from("%s")', JSON.stringify(payload))
30+
31+
if (typeof payload === 'string') {
32+
payload = JSON.parse(payload)
33+
}
34+
35+
if (!isEcmaError(payload) && !isGrpcStatus(payload)) {
36+
throw new Error('payload is neither EcmaError nor GrpcStatus')
37+
}
38+
39+
const e = new this(payload)
40+
return e
41+
}
42+
43+
protected constructor (
44+
payload: GrpcStatus | EcmaError,
45+
) {
46+
super()
47+
log.verbose('PuppetError', 'constructor("%s")', JSON.stringify(payload))
48+
49+
/**
50+
* Common properties
51+
*/
52+
this.message = payload.message
53+
54+
if (isGError(payload)) {
55+
this.code = payload.code
56+
this.details = payload.details
57+
this.name = payload.name
58+
this.stack = payload.stack
59+
60+
} else if (isGrpcStatus(payload)) {
61+
this.code = payload.code
62+
this.details = payload.details
63+
/**
64+
* Convert gRPC error to EcmaError
65+
*/
66+
this.name = Array.isArray(payload.details) && payload.details.length > 0
67+
? payload.details[0]
68+
: Code[this.code] || String(this.code)
69+
70+
} else if (isEcmaError(payload)) {
71+
this.name = payload.name
72+
this.stack = payload.stack
73+
/**
74+
* Convert EcmaError to gRPC error
75+
*/
76+
this.code = Code.UNKNOWN
77+
this.details = [
78+
payload.name,
79+
...payload.stack?.split('\n') ?? [],
80+
]
81+
82+
} else {
83+
throw new Error('payload is neither EcmaError nor GrpcStatus')
84+
}
85+
}
86+
87+
public toJSON (): GrpcStatus & EcmaError {
88+
return {
89+
code : this.code,
90+
details : this.details,
91+
message : this.message,
92+
name : this.name,
93+
stack : this.stack,
94+
}
95+
}
96+
97+
public toGrpcStatus (): GrpcStatus {
98+
return {
99+
code : this.code,
100+
details : this.details,
101+
message : this.message,
102+
}
103+
}
104+
105+
public toEcmaError (): EcmaError {
106+
return {
107+
message : this.message,
108+
name : this.name,
109+
stack : this.stack,
110+
}
111+
}
112+
113+
}
114+
115+
export {
116+
GError,
117+
isGError,
118+
isGrpcStatus,
119+
isEcmaError,
120+
}

0 commit comments

Comments
 (0)