Skip to content

Commit b02c052

Browse files
committed
simplify things: remove lowclass/Mixin and use #private fields such that we can output declaration files
lowclass/Mixin and TypeScript "private" or "protected" access modifiers cause declaration output not to be possible, thus forcing us to point package.json types at src/index.ts instead of dist/index.d.ts, which can cause type errors in downstream consumers if they use different tsconfig.json settings when they type-check their applications (coupled with the fact that TypeScript will type-check library code if the library code types field does not point to declaration files, *even if* the user set `skipLibCheck` to `true`). Outputting declaration files and pointing `types` to them is the way to go for publishing libraries, but certain features of TypeScript do not work when declaration output is enabled, which is why we had to refactor. microsoft/TypeScript#35822 Now that lume/cli is updated, npm test fails. Needs a fix.
1 parent 92e2e32 commit b02c052

File tree

5 files changed

+70
-32
lines changed

5 files changed

+70
-32
lines changed

.prettierrc.js renamed to .prettierrc.cjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
module.exports = {
2-
tabWidth: 4,
32
useTabs: true,
43
semi: false,
54
singleQuote: true,

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"homepage": "http://github.com/lume/eventful#readme",
88
"type": "module",
99
"main": "dist/index.js",
10-
"types": "src/index.ts",
10+
"types": "dist/index.d.ts",
1111
"exports COMMENT:": "This removes 'dist' from import statements, as well as replaces the 'main' field. See https://github.com/nodejs/node/issues/14970#issuecomment-571887546",
1212
"exports": {
1313
".": "./dist/index.js",
@@ -35,8 +35,8 @@
3535
"lowclass": "^4.9.0"
3636
},
3737
"devDependencies": {
38-
"@lume/cli": "^0.3.0",
39-
"prettier": "^1.19.1"
38+
"@lume/cli": "^0.5.0",
39+
"prettier": "^2.0.0"
4040
},
4141
"repository": {
4242
"type": "git",

src/Eventful.test.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import Eventful from './Eventful.js'
1+
import {Eventful} from './Eventful.js'
22

33
let eventCount = 0
44
let eventCount2 = 0
55

66
describe('Eventful', () => {
77
describe('provides an event pattern', () => {
88
it('triggers an event handler based on event names', () => {
9-
const o = new Eventful()
9+
const MyClass = Eventful()
10+
const o = new MyClass()
1011

1112
const eventHandler = () => {
1213
eventCount += 1
@@ -92,7 +93,8 @@ describe('Eventful', () => {
9293
})
9394

9495
it('passes event payloads to event handlers', () => {
95-
const o = new Eventful()
96+
class MyClass extends Eventful() {}
97+
const o = new MyClass()
9698

9799
let thePayload: string | number | undefined
98100
let thePayload2: number | undefined
@@ -128,7 +130,8 @@ describe('Eventful', () => {
128130
})
129131

130132
it('allows callbacks to be paired with contexts with which to be called', () => {
131-
const o = new Eventful()
133+
class MyClass extends Eventful() {}
134+
const o = new MyClass()
132135

133136
let obj = {n: 0}
134137
const o1 = {}
@@ -175,5 +178,32 @@ describe('Eventful', () => {
175178

176179
expect(obj.n).toBe(4)
177180
})
181+
182+
it('allows us to check objects are instances of the mixin class or composed classes', () => {
183+
const MyClass = Eventful()
184+
class OtherClass extends Eventful() {}
185+
class AnotherClass extends Eventful(Array) {}
186+
187+
let o = new MyClass
188+
expect(o).toBeInstanceOf(Eventful)
189+
expect(o).toBeInstanceOf(MyClass)
190+
expect(o).not.toBeInstanceOf(OtherClass)
191+
expect(o).not.toBeInstanceOf(AnotherClass)
192+
expect(o).not.toBeInstanceOf(Array)
193+
194+
o = new OtherClass
195+
expect(o).toBeInstanceOf(Eventful)
196+
expect(o).not.toBeInstanceOf(MyClass)
197+
expect(o).toBeInstanceOf(OtherClass)
198+
expect(o).not.toBeInstanceOf(AnotherClass)
199+
expect(o).not.toBeInstanceOf(Array)
200+
201+
o = new AnotherClass
202+
expect(o).toBeInstanceOf(Eventful)
203+
expect(o).not.toBeInstanceOf(MyClass)
204+
expect(o).not.toBeInstanceOf(OtherClass)
205+
expect(o).toBeInstanceOf(AnotherClass)
206+
expect(o).toBeInstanceOf(Array)
207+
})
178208
})
179209
})

src/Eventful.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import {Mixin, MixinResult, Constructor} from 'lowclass'
1+
import type {Constructor} from 'lowclass'
22

3-
// TODO @trusktr, make strongly typed event args. Combine with stuff in Events.ts (or similar).
3+
// TODO, make strongly typed event args. Combine with stuff in Events.ts (or similar).
44

5-
// TODO @trusktr, Make sure emit will not attempt to call event handlers removed
5+
// TODO, Make sure emit will not attempt to call event handlers removed
66
// during emit (in place modification of listeners array during emit iteration
77
// will try to access undefined after the end of the array). Possibly use
88
// for..of with a Set instead, otherwise modify the iteration index manually.
99

10-
// TODO @trusktr, an option to defer events, and batch them (so that 3 of the
10+
// TODO, an option to defer events, and batch them (so that 3 of the
1111
// same event and payload triggers only one event instead of three)
1212

1313
/**
14+
* @mixin
1415
* @class Eventful - An instance of Eventful emits events that code can
1516
* subscribe to with callbacks. Events may optionally pass a payload to the
1617
* callbacks.
@@ -41,12 +42,10 @@ import {Mixin, MixinResult, Constructor} from 'lowclass'
4142
* rectangle.off("resize", onResize)
4243
* ```
4344
*/
44-
export const Eventful = Mixin(EventfulMixin)
45-
export interface Eventful extends InstanceType<typeof Eventful> {}
46-
export default Eventful
45+
export function Eventful<T extends Constructor>(Base: T = Object as any) {
46+
class Eventful extends Base {
47+
[isEventful] = isEventful
4748

48-
export function EventfulMixin<T extends Constructor>(Base: T) {
49-
class Eventful extends Constructor(Base) {
5049
/**
5150
* @method on - Register a `callback` to be executed any
5251
* time an event with name `eventName` is triggered by an instance of
@@ -60,13 +59,13 @@ export function EventfulMixin<T extends Constructor>(Base: T) {
6059
* @param {any} context - An optional context to call the callback with. Passing no context is the same as subscribing `callback` for a `context` of `undefined`.
6160
*/
6261
on(eventName: string, callback: Function, context?: any) {
63-
let eventMap = this.__eventMap
62+
let eventMap = this.#eventMap
6463

6564
// @prod-prune
6665
if (typeof callback !== 'function')
6766
throw new Error('Expected a function in callback argument of Eventful#on.')
6867

69-
if (!eventMap) eventMap = this.__eventMap = new Map()
68+
if (!eventMap) eventMap = this.#eventMap = new Map()
7069

7170
let callbacks = eventMap.get(eventName)
7271

@@ -85,7 +84,7 @@ export function EventfulMixin<T extends Constructor>(Base: T) {
8584
* @param {any} context - A context associated with `callback`. Not passing a `context` arg is equivalent to unsubscribing the `callback` for `context` of `undefined`.
8685
*/
8786
off(eventName: string, callback?: Function, context?: any) {
88-
const eventMap = this.__eventMap
87+
const eventMap = this.#eventMap
8988

9089
if (!eventMap) return
9190

@@ -101,7 +100,7 @@ export function EventfulMixin<T extends Constructor>(Base: T) {
101100

102101
if (callbacks.length === 0) eventMap.delete(eventName)
103102

104-
if (eventMap.size === 0) this.__eventMap = null
103+
if (eventMap.size === 0) this.#eventMap = null
105104
}
106105

107106
/**
@@ -120,7 +119,7 @@ export function EventfulMixin<T extends Constructor>(Base: T) {
120119
* @param {data} any - The data that is passed to each callback subscribed to the event.
121120
*/
122121
emit(eventName: string, data?: any) {
123-
const eventMap = this.__eventMap
122+
const eventMap = this.#eventMap
124123

125124
if (!eventMap) return
126125

@@ -140,12 +139,24 @@ export function EventfulMixin<T extends Constructor>(Base: T) {
140139
}
141140
}
142141

143-
private __eventMap: Map<string, Array<[Function, any]>> | null = null
142+
#eventMap: Map<string, Array<[Function, any]>> | null = null
144143
}
145144

146-
return Eventful as MixinResult<typeof Eventful, T>
145+
Eventful.prototype[isEventful] = isEventful
146+
147+
return Eventful
147148
}
148149

150+
const isEventful = Symbol('isEventful')
151+
152+
Object.defineProperty(Eventful, Symbol.hasInstance, {
153+
value(obj: any): boolean {
154+
if (!obj || typeof obj !== 'object') return false
155+
if (!(isEventful in obj)) return false
156+
return true
157+
},
158+
})
159+
149160
/**
150161
* @decorator
151162
* @function emits - This is a decorator that when used on a property in a
@@ -171,7 +182,7 @@ function _emits(prototype: any, propName: string, descriptor: PropertyDescriptor
171182
if (!(prototype instanceof Eventful))
172183
throw new TypeError('The @emits decorator in only for use on properties of classes that extend from Eventful.')
173184

174-
const vName = 'emits_' + propName
185+
const vName = Symbol('@emits: ' + propName)
175186

176187
// property decorators are not passed a descriptor (unlike decorators on accessors or methods)
177188
let calledAsPropertyDecorator = false
@@ -226,7 +237,7 @@ function _emits(prototype: any, propName: string, descriptor: PropertyDescriptor
226237
}
227238

228239
let initialValueIsSet = false
229-
function emitEvent(this: Eventful) {
240+
function emitEvent(this: EventfulInstance) {
230241
this.emit(eventName, propName)
231242
}
232243

@@ -257,15 +268,15 @@ function _emits(prototype: any, propName: string, descriptor: PropertyDescriptor
257268
originalSet!.call(this, newValue)
258269

259270
// TODO should we defer the event, or not? Perhaps provide an option, and defer by default.
260-
Promise.resolve().then(emitEvent.bind(this as Eventful))
271+
Promise.resolve().then(emitEvent.bind(this as EventfulInstance))
261272
// emitEvent.call(this as Eventful)
262273
},
263274
}
264275
: {
265276
set(newValue: any) {
266277
if (!initialValueIsSet) initialValueIsSet = true
267278
;(this as any)[vName] = newValue
268-
Promise.resolve().then(emitEvent.bind(this as Eventful))
279+
Promise.resolve().then(emitEvent.bind(this as EventfulInstance))
269280
},
270281
}),
271282
}
@@ -279,3 +290,5 @@ function _emits(prototype: any, propName: string, descriptor: PropertyDescriptor
279290
// Weird, huh?
280291
// This will all change with updates to the ES decorators proposal, https://github.com/tc39/proposal-decorators
281292
}
293+
294+
type EventfulInstance = InstanceType<ReturnType<typeof Eventful>>

tsconfig.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
{
22
"extends": "./node_modules/@lume/cli/config/ts.config.json",
3-
"compilerOptions": {
4-
"declaration": false, // TODO, set back to true after removing class-factory mixins
5-
"declarationMap": false
6-
}
73
}

0 commit comments

Comments
 (0)