-
Notifications
You must be signed in to change notification settings - Fork 19
/
thing.js
230 lines (188 loc) · 5.15 KB
/
thing.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
'use strict';
const { Class, Mixin, toExtendable, mix } = require('foibles');
const { EventEmitter } = require('./events');
const debug = require('debug');
const collectMetadata = require('./utils/collectMetadata');
const merge = require('./utils/merge');
const id = Symbol('id');
const debugProperty = Symbol('debug');
const eventQueue = Symbol('eventQueue');
const eventEmitter = Symbol('eventEmitter');
const isInitialized = Symbol('isInitialized');
const isDestroyed = Symbol('isDestroyed');
module.exports = toExtendable(class Thing {
constructor() {
this.metadata = collectMetadata(Thing, this);
this[eventQueue] = [];
this[eventEmitter] = new EventEmitter({
context: this
});
}
get id() {
return this[id] || null;
}
set id(identifier) {
// Make sure the identifier is not changed after device init
if(this[isInitialized]) {
throw new Error('Identifier can not be changed after initialization has been done');
}
// Do a simple check to ensure the identifier is a string
if(typeof identifier !== 'string') {
throw new Error('Identifier should be a string');
}
/*
* Set the identifier without further validation. This allows it to be
* set to something invalid and then enhanced via initCallbacks
*/
this[id] = identifier;
}
init() {
if(this[isInitialized]) return Promise.resolve(this);
this[isDestroyed] = false;
return Promise.resolve(this.initCallback())
.then(() => {
/*
* Validate the identifier after initialization to ensure that
* it is correctly set.
*/
if(typeof this.id !== 'string') {
throw new Error('Identifier needs to be set either during thing construction or during init');
}
if(this.id.indexOf(':') <= 0) {
throw new Error('Identifier does not contain a namespace, currently: `' + this.id + '`');
}
this[isInitialized] = true;
this.emitEvent('thing:initialized');
return this;
});
}
initCallback() {
return Promise.resolve();
}
/**
* Destroy this appliance, freeing any resources that it is using.
*/
destroy() {
if(! this[isInitialized] || this[isDestroyed]) return Promise.resolve();
this[isDestroyed] = true;
this[isInitialized] = false;
return Promise.resolve(this.destroyCallback())
.then(() => {
this.emitEvent('thing:destroyed');
});
}
destroyCallback() {
return Promise.resolve();
}
/**
* Emit an event with the given name and data.
*
* @param {string} event
* @param {*} data
*/
emitEvent(event, data, options) {
const queue = this[eventQueue];
// Metadata may emit events before the queue is availabe, skip them
if(! queue) return;
const shouldQueueEmit = queue.length === 0;
const multiple = options ? options.multiple : false;
if(! multiple) {
// Check if there is already an even scheduled
const idx = queue.findIndex(e => e[0] === event);
if(idx >= 0) {
// Remove the event - only a single event can is emitted per tick
queue.splice(idx, 1);
}
} else if(typeof multiple === 'function') {
// More advanced matching using a function
for(let i=0; i<queue.length; i++) {
const e = queue[i];
if(e[0] === event && multiple(e[1])) {
// This event matches, remove it
queue.splice(i, 1);
break;
}
}
}
// Add the event to the queue
queue.push([ event, data ]);
if(shouldQueueEmit) {
// Schedule emittal of the events
setImmediate(() => {
const emitter = this[eventEmitter];
for(const e of queue) {
emitter.emit(e[0], e[1], this);
}
this[eventQueue] = [];
});
}
}
on(event, listener) {
return this[eventEmitter].on(event, listener);
}
off(event, listener) {
return this[eventEmitter].off(event, listener);
}
onAny(listener) {
return this[eventEmitter].onAny(listener);
}
offAny(listener) {
return this[eventEmitter].offAny(listener);
}
debug() {
if(! this[debugProperty]) {
this[debugProperty] = debug('thing:' + this.id);
}
this[debugProperty].apply(this[debugProperty], arguments);
}
/**
* Check if this appliance matches all of the given tags.
*/
matches(...tags) {
return this.metadata.matches(...tags);
}
/**
* Create a new type that can be mixed in with Appliance.
*
* @param {function} func
*/
static type(func) {
return Class(Thing, func);
}
/**
* Create a new capability that can be mixed in with a Appliance.
*
* @param {function} func
*/
static mixin(func) {
return Mixin(func);
}
/**
* Mixin the given mixins to the specified object.
*
* @param {*} obj
* @param {array} mixins
*/
static mixinDynamic(obj, ...mixins) {
const direct = Object.getPrototypeOf(obj);
const parent = Object.getPrototypeOf(direct);
const proto = {};
for(let name of Object.getOwnPropertyNames(direct)) {
proto[name] = direct[name];
}
const base = mix(parent.constructor, ...mixins);
Object.setPrototypeOf(proto, base.prototype);
Object.setPrototypeOf(obj, proto);
const data = new base();
merge(obj, data);
}
/**
* Extend this appliance with the given mixin. Used to dynamically apply
* capabilities during instance construction.
*
* @param {array} mixins
*/
extendWith(...mixins) {
Thing.mixinDynamic(this, ...mixins);
}
});