-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplugin-base.js
More file actions
229 lines (196 loc) · 8.29 KB
/
Copy pathplugin-base.js
File metadata and controls
229 lines (196 loc) · 8.29 KB
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
// A base class for plugins, and a registry of them -- shipped with Rectify,
// but not built into it.
//
// This is an ordinary plugin. It consumes "app" and provides "Plugin" and
// "ext", so an app that wants them puts it in the config array and the plugins
// that use them say so in consumes, like anything else:
//
// var plugins = [rectify.PluginBase, ...the rest];
//
// setup.consumes = ["Plugin", "server"];
//
// It stays out of the "app" service on purpose. A plugin built on this has a
// real dependency on it, and the config should say so -- otherwise nothing can
// swap it, for a test or anything else, and Rectify would own this design
// forever rather than being able to leave it here.
//
// What it adds to a plain service object:
//
// an emitter so other plugins can watch it, not only call it
// sticky events so "it already happened" is not the same as "you missed it"
// own() so teardown is a note you make when you take something
// api() so the public surface is a list, not whatever was returned
// disable() a flag and two events, for a plugin that can be switched off
//
// and "ext" is the registry: what instances exist, and -- from the graph
// Rectify sorted the load with -- who depends on what.
//
// None of it is required. A service is still allowed to be a plain object.
function objHas(obj, name) { return Object.prototype.hasOwnProperty.call(obj, name); }
function setup(imports, register) {
var app = imports.app;
var EventEmitter = app.EventEmitter;
// One listener for the app, rather than one per instance, and remembered
// so that an instance made after the app was up still hears about it.
var started = null;
var waiting = [];
app.on("ready", function (readyApp) {
started = readyApp;
waiting.splice(0).forEach(function (plugin) {
plugin.announce("ready", readyApp);
});
});
function Plugin(name) {
var events = new EventEmitter();
var disposers = [];
var sticky = {};
var unloaded = false;
var disabled = false;
var self = this;
this.name = name || "";
// Late subscribers are the normal case in a plugin system: whoever
// wanted to hear about it may not have loaded yet. An event announced
// rather than emitted is replayed to anyone who asks afterwards.
this.announce = function (type, data) {
sticky[type] = data;
events.emit(type, data);
return self;
};
this.emit = function (type, data) {
events.emit(type, data);
return self;
};
this.on = function (type, listener) {
if (objHas(sticky, type)) { listener(sticky[type]); }
events.on(type, listener);
return self;
};
this.once = function (type, listener) {
if (objHas(sticky, type)) { return listener(sticky[type]); }
events.once(type, listener);
return self;
};
this.off = function (type, listener) {
events.removeListener(type, listener);
return self;
};
// Say what to undo at the moment you do the thing, rather than keeping
// a teardown function in step with a setup function by hand.
this.own = function (disposer) {
if (typeof disposer != "function") {
throw new Error("own() takes a function to undo something: " + self.name);
}
disposers.push(disposer);
return self;
};
// Reverse order, and one that throws does not strand the rest -- the
// same rule Rectify uses for onDestroy, for the same reason.
this.unload = async function (reason) {
if (unloaded) { return; }
unloaded = true;
while (disposers.length) {
try {
await disposers.pop()();
} catch (err) {
app.emit("error", err);
}
}
events.emit("unload", reason);
};
this.unloaded = function () { return unloaded; };
// A flag and two events. Nothing enforces it -- Rectify has no idea a
// plugin can be switched off, and a consumer holding the service can
// still call it. What "disabled" means is between the plugin and the
// plugins that watch for it.
this.disable = function (reason) {
if (disabled) { return self; }
disabled = true;
events.emit("disable", reason);
return self;
};
this.enable = function (reason) {
if (!disabled) { return self; }
disabled = false;
events.emit("enable", reason);
return self;
};
this.disabled = function () { return disabled; };
// The surface, stated rather than implied. Copied onto the instance so
// the service a plugin registers is the plugin itself, events and all.
this.api = function (surface) {
Object.keys(surface).forEach(function (key) {
Object.defineProperty(self, key, Object.getOwnPropertyDescriptor(surface, key));
});
Object.freeze(self);
return self;
};
// Every instance hears about the app being up, whether it was made
// before that happened or after. A plugin cannot know when the load has
// finished, so this is the answer to "do this once everything is up".
if (started) { this.announce("ready", started); }
else { waiting.push(this); }
made.push(this);
}
// Every instance, in the order they were made. Private: being handed one
// of these is being handed the service.
var made = [];
// What the registry actually holds: the instances that became services,
// learned from the event Rectify emits when one is registered.
//
// This is what keeps `allowed` meaning something. A restricted service is
// deliberately never announced through "service" -- so it never lands here,
// and ext cannot hand out what app.services already refuses to. Registering
// instances as they are constructed would have quietly undone that.
var listed = [];
app.on("service", function (name, service) {
if (made.indexOf(service) !== -1) {
listed.push({ name: name, plugin: service });
}
});
// The registry. c9 called this "ext" and used it to load and unload IDE
// plugins while the editor ran; Rectify cannot do that, since build() takes
// a fixed array and start() runs once. What is left is still worth having:
// knowing what is in the app, and who is relying on what.
var ext = {
get plugins() {
return listed.map(function (entry) { return entry.plugin; });
},
// By the name the service was registered under, not the name the
// instance was given -- that is the one everything else refers to.
get named() {
var byName = {};
listed.forEach(function (entry) {
byName[entry.name] = entry.plugin;
});
return byName;
},
get: function (name) {
return ext.named[name];
},
// From the graph Rectify built to sort the load, so this answers for
// every plugin in the app -- not only the ones built on Plugin.
// Frozen { name, provides, consumes } records; .map(e => e.name) if
// that is all you wanted.
dependents: function (service) {
return app.plugins.filter(function (entry) {
return entry.consumes.indexOf(service) !== -1;
});
},
// Reverse order, so a consumer lets go before what it consumed. Safe to
// call after app.destroy(), which will have unloaded most of these
// already -- unload happens once however often it is asked for.
unloadAll: async function (reason) {
var order = ext.plugins.reverse();
for (var i = 0; i < order.length; i++) {
await order[i].unload(reason);
}
}
};
register(null, {
Plugin: Plugin,
ext: ext
});
}
setup.consumes = ["app"];
setup.provides = ["Plugin", "ext"];
module.exports = setup;