platform/lib/src/core/hooked_service.dart

571 lines
19 KiB
Dart
Raw Normal View History

2018-08-20 20:58:37 +00:00
library angel_framework.core.hooked_service;
import 'dart:async';
import '../util.dart';
2017-01-20 22:11:20 +00:00
import 'request_context.dart';
import 'response_context.dart';
import 'routable.dart';
import 'metadata.dart';
2018-08-21 02:44:32 +00:00
import 'server.dart';
import 'service.dart';
2016-06-19 05:02:41 +00:00
/// Wraps another service in a service that broadcasts events on actions.
class HookedService extends Service {
2017-04-04 08:35:36 +00:00
final List<StreamController<HookedServiceEvent>> _ctrl = [];
2016-06-21 22:56:04 +00:00
/// Tbe service that is proxied by this hooked one.
final Service inner;
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher beforeIndexed =
new HookedServiceEventDispatcher();
final HookedServiceEventDispatcher beforeRead =
new HookedServiceEventDispatcher();
final HookedServiceEventDispatcher beforeCreated =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher beforeModified =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher beforeUpdated =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher beforeRemoved =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher afterIndexed =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher afterRead =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher afterCreated =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher afterModified =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher afterUpdated =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2017-04-04 08:35:36 +00:00
final HookedServiceEventDispatcher afterRemoved =
2016-10-22 20:41:36 +00:00
new HookedServiceEventDispatcher();
2016-06-21 04:19:43 +00:00
HookedService(Service this.inner) {
2016-06-24 19:19:02 +00:00
// Clone app instance
2016-10-22 20:41:36 +00:00
if (inner.app != null) this.app = inner.app;
2016-11-23 09:10:47 +00:00
}
2016-06-24 19:19:02 +00:00
2017-01-20 22:40:48 +00:00
RequestContext _getRequest(Map params) {
if (params == null) return null;
2018-06-23 03:29:38 +00:00
return params['__requestctx'] as RequestContext;
2017-01-20 22:40:48 +00:00
}
ResponseContext _getResponse(Map params) {
if (params == null) return null;
2018-06-23 03:29:38 +00:00
return params['__responsectx'] as ResponseContext;
2017-01-20 22:40:48 +00:00
}
2017-01-20 22:11:20 +00:00
Map _stripReq(Map params) {
if (params == null)
return params;
else
2017-01-28 03:47:00 +00:00
return params.keys
.where((key) => key != '__requestctx' && key != '__responsectx')
.fold({}, (map, key) => map..[key] = params[key]);
2017-01-20 22:11:20 +00:00
}
2017-04-04 08:35:36 +00:00
/// Closes any open [StreamController]s on this instance. **Internal use only**.
2017-04-25 02:44:22 +00:00
@override
2018-06-08 07:06:26 +00:00
Future close() {
2017-04-04 08:35:36 +00:00
_ctrl.forEach((c) => c.close());
beforeIndexed._close();
beforeRead._close();
beforeCreated._close();
beforeModified._close();
beforeUpdated._close();
beforeRemoved._close();
afterIndexed._close();
afterRead._close();
afterCreated._close();
afterModified._close();
afterUpdated._close();
afterRemoved._close();
2018-06-08 07:06:26 +00:00
inner.close();
return new Future.value();
2017-04-04 08:35:36 +00:00
}
2017-01-20 22:11:20 +00:00
/// Adds hooks to this instance.
2018-08-21 02:44:32 +00:00
void addHooks(Angel app) {
Hooks hooks = getAnnotation(inner, Hooks, app.container.reflector);
2018-06-23 03:29:38 +00:00
List<HookedServiceEventListener> before = [], after = [];
2016-12-10 14:05:40 +00:00
if (hooks != null) {
before.addAll(hooks.before);
after.addAll(hooks.after);
}
void applyListeners(Function fn, HookedServiceEventDispatcher dispatcher,
[bool isAfter]) {
2018-08-21 02:44:32 +00:00
Hooks hooks = getAnnotation(fn, Hooks, app.container.reflector);
2018-06-23 03:29:38 +00:00
final listeners = <HookedServiceEventListener>[]
..addAll(isAfter == true ? after : before);
2016-12-10 14:05:40 +00:00
if (hooks != null)
listeners.addAll(isAfter == true ? hooks.after : hooks.before);
listeners.forEach(dispatcher.listen);
}
applyListeners(inner.index, beforeIndexed);
applyListeners(inner.read, beforeRead);
applyListeners(inner.create, beforeCreated);
2016-12-10 14:05:40 +00:00
applyListeners(inner.modify, beforeModified);
applyListeners(inner.update, beforeUpdated);
applyListeners(inner.remove, beforeRemoved);
2016-12-10 14:05:40 +00:00
applyListeners(inner.index, afterIndexed, true);
applyListeners(inner.read, afterRead, true);
applyListeners(inner.create, afterCreated, true);
2016-12-10 14:05:40 +00:00
applyListeners(inner.modify, afterModified, true);
applyListeners(inner.update, afterUpdated, true);
applyListeners(inner.remove, afterRemoved, true);
2016-12-10 14:05:40 +00:00
}
2018-08-21 18:50:43 +00:00
List<RequestHandler> get bootstrappers =>
new List<RequestHandler>.from(super.bootstrappers)
..add((RequestContext req, ResponseContext res) {
req.serviceParams
..['__requestctx'] = req
..['__responsectx'] = res;
return true;
});
2018-06-08 07:06:26 +00:00
void addRoutes([Service s]) {
super.addRoutes(s ?? inner);
}
2017-01-28 21:08:07 +00:00
/// Runs the [listener] before every service method specified.
void before(
Iterable<String> eventNames, HookedServiceEventListener listener) {
eventNames.map((name) {
switch (name) {
case HookedServiceEvent.indexed:
2017-01-28 21:08:07 +00:00
return beforeIndexed;
case HookedServiceEvent.read:
2017-01-28 21:08:07 +00:00
return beforeRead;
case HookedServiceEvent.created:
2017-01-28 21:08:07 +00:00
return beforeCreated;
case HookedServiceEvent.modified:
2017-01-28 21:08:07 +00:00
return beforeModified;
case HookedServiceEvent.updated:
2017-01-28 21:08:07 +00:00
return beforeUpdated;
case HookedServiceEvent.removed:
2017-01-28 21:08:07 +00:00
return beforeRemoved;
default:
throw new ArgumentError('Invalid service method: ${name}');
}
}).forEach((HookedServiceEventDispatcher dispatcher) =>
dispatcher.listen(listener));
}
/// Runs the [listener] after every service method specified.
void after(Iterable<String> eventNames, HookedServiceEventListener listener) {
eventNames.map((name) {
switch (name) {
case HookedServiceEvent.indexed:
2017-01-28 21:08:07 +00:00
return afterIndexed;
case HookedServiceEvent.read:
2017-01-28 21:08:07 +00:00
return afterRead;
case HookedServiceEvent.created:
2017-01-28 21:08:07 +00:00
return afterCreated;
case HookedServiceEvent.modified:
2017-01-28 21:08:07 +00:00
return afterModified;
case HookedServiceEvent.updated:
2017-01-28 21:08:07 +00:00
return afterUpdated;
case HookedServiceEvent.removed:
2017-01-28 21:08:07 +00:00
return afterRemoved;
default:
throw new ArgumentError('Invalid service method: ${name}');
}
}).forEach((HookedServiceEventDispatcher dispatcher) =>
dispatcher.listen(listener));
}
/// Runs the [listener] before every service method.
2017-01-28 03:47:00 +00:00
void beforeAll(HookedServiceEventListener listener) {
beforeIndexed.listen(listener);
beforeRead.listen(listener);
beforeCreated.listen(listener);
beforeModified.listen(listener);
beforeUpdated.listen(listener);
beforeRemoved.listen(listener);
}
/// Runs the [listener] after every service method.
void afterAll(HookedServiceEventListener listener) {
afterIndexed.listen(listener);
afterRead.listen(listener);
afterCreated.listen(listener);
afterModified.listen(listener);
afterUpdated.listen(listener);
afterRemoved.listen(listener);
}
2017-04-04 08:35:36 +00:00
/// Returns a [Stream] of all events fired before every service method.
///
/// *NOTE*: Only use this if you do not plan to modify events. There is no guarantee
/// that events coming out of this [Stream] will see changes you make within the [Stream]
/// callback.
Stream<HookedServiceEvent> beforeAllStream() {
var ctrl = new StreamController<HookedServiceEvent>();
_ctrl.add(ctrl);
before(HookedServiceEvent.all, ctrl.add);
2017-04-04 08:35:36 +00:00
return ctrl.stream;
}
/// Returns a [Stream] of all events fired after every service method.
///
/// *NOTE*: Only use this if you do not plan to modify events. There is no guarantee
/// that events coming out of this [Stream] will see changes you make within the [Stream]
/// callback.
Stream<HookedServiceEvent> afterAllStream() {
var ctrl = new StreamController<HookedServiceEvent>();
_ctrl.add(ctrl);
before(HookedServiceEvent.all, ctrl.add);
2017-04-04 08:35:36 +00:00
return ctrl.stream;
}
/// Returns a [Stream] of all events fired before every service method specified.
///
/// *NOTE*: Only use this if you do not plan to modify events. There is no guarantee
/// that events coming out of this [Stream] will see changes you make within the [Stream]
/// callback.
Stream<HookedServiceEvent> beforeStream(Iterable<String> eventNames) {
var ctrl = new StreamController<HookedServiceEvent>();
_ctrl.add(ctrl);
before(eventNames, ctrl.add);
return ctrl.stream;
}
/// Returns a [Stream] of all events fired AFTER every service method specified.
///
/// *NOTE*: Only use this if you do not plan to modify events. There is no guarantee
/// that events coming out of this [Stream] will see changes you make within the [Stream]
/// callback.
Stream<HookedServiceEvent> afterStream(Iterable<String> eventNames) {
var ctrl = new StreamController<HookedServiceEvent>();
_ctrl.add(ctrl);
after(eventNames, ctrl.add);
return ctrl.stream;
}
2017-01-28 03:47:00 +00:00
/// Runs the [listener] before [create], [modify] and [update].
void beforeModify(HookedServiceEventListener listener) {
beforeCreated.listen(listener);
beforeModified.listen(listener);
beforeUpdated.listen(listener);
}
@override
2018-06-08 07:06:26 +00:00
Future index([Map _params]) {
2017-01-20 22:11:20 +00:00
var params = _stripReq(_params);
2018-06-08 07:06:26 +00:00
return beforeIndexed
._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.indexed,
2018-06-08 07:06:26 +00:00
params: params))
.then((before) {
if (before._canceled) {
return beforeIndexed
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.indexed,
params: params, result: before.result))
.then((after) => after.result);
}
2016-06-19 05:02:41 +00:00
2018-06-08 07:06:26 +00:00
return inner.index(params).then((result) {
return afterIndexed
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.indexed,
params: params, result: result))
.then((after) => after.result);
});
});
}
@override
2018-06-08 07:06:26 +00:00
Future read(id, [Map _params]) {
2017-01-20 22:11:20 +00:00
var params = _stripReq(_params);
2018-06-08 07:06:26 +00:00
return beforeRead
._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.read,
id: id, params: params))
.then((before) {
if (before._canceled) {
return beforeRead
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.read,
id: id, params: params, result: before.result))
.then((after) => after.result);
}
2016-06-21 04:19:43 +00:00
2018-06-08 07:06:26 +00:00
return inner.read(id, params).then((result) {
return afterRead
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.read,
id: id, params: params, result: result))
.then((after) => after.result);
});
});
2016-06-21 04:19:43 +00:00
}
@override
2018-06-08 07:06:26 +00:00
Future create(data, [Map _params]) {
2017-01-20 22:11:20 +00:00
var params = _stripReq(_params);
2018-06-08 07:06:26 +00:00
return beforeCreated
._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created,
2018-06-08 07:06:26 +00:00
data: data, params: params))
.then((before) {
if (before._canceled) {
return beforeCreated
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created,
data: data, params: params, result: before.result))
.then((after) => after.result);
}
2016-06-21 04:19:43 +00:00
2018-06-08 07:06:26 +00:00
return inner.create(data, params).then((result) {
return afterCreated
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created,
data: data, params: params, result: result))
.then((after) => after.result);
});
});
}
@override
2018-06-08 07:06:26 +00:00
Future modify(id, data, [Map _params]) {
2017-01-20 22:11:20 +00:00
var params = _stripReq(_params);
2018-06-08 07:06:26 +00:00
return beforeModified
._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.modified,
2018-06-08 07:06:26 +00:00
id: id, data: data, params: params))
.then((before) {
if (before._canceled) {
return beforeModified
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.modified,
id: id, data: data, params: params, result: before.result))
.then((after) => after.result);
}
2016-06-21 04:19:43 +00:00
2018-06-08 07:06:26 +00:00
return inner.modify(id, data, params).then((result) {
return afterModified
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created,
id: id, data: data, params: params, result: result))
.then((after) => after.result);
});
});
2016-06-21 04:19:43 +00:00
}
@override
2018-06-08 07:06:26 +00:00
Future update(id, data, [Map _params]) {
2017-01-20 22:11:20 +00:00
var params = _stripReq(_params);
2018-06-08 07:06:26 +00:00
return beforeUpdated
._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.updated,
2018-06-08 07:06:26 +00:00
id: id, data: data, params: params))
.then((before) {
if (before._canceled) {
return beforeUpdated
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.updated,
id: id, data: data, params: params, result: before.result))
.then((after) => after.result);
}
2016-06-21 04:19:43 +00:00
2018-06-08 07:06:26 +00:00
return inner.update(id, data, params).then((result) {
return afterUpdated
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.updated,
id: id, data: data, params: params, result: result))
.then((after) => after.result);
});
});
}
@override
2018-06-08 07:06:26 +00:00
Future remove(id, [Map _params]) {
2017-01-20 22:11:20 +00:00
var params = _stripReq(_params);
2018-06-08 07:06:26 +00:00
return beforeRemoved
._emit(new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.removed,
2018-06-08 07:06:26 +00:00
id: id, params: params))
.then((before) {
if (before._canceled) {
return beforeRemoved
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params, result: before.result))
.then((after) => after.result);
}
2016-06-21 04:19:43 +00:00
2018-06-08 07:06:26 +00:00
return inner.remove(id, params).then((result) {
return afterRemoved
._emit(new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params, result: result))
.then((after) => after.result);
});
});
}
2017-01-20 22:11:20 +00:00
/// Fires an `after` event. This will not be propagated to clients,
/// but will be broadcasted to WebSockets, etc.
2017-01-20 22:40:48 +00:00
Future<HookedServiceEvent> fire(String eventName, result,
2018-06-08 07:06:26 +00:00
[HookedServiceEventListener callback]) {
2017-01-20 22:11:20 +00:00
HookedServiceEventDispatcher dispatcher;
switch (eventName) {
case HookedServiceEvent.indexed:
2017-01-20 22:11:20 +00:00
dispatcher = afterIndexed;
break;
case HookedServiceEvent.read:
2017-01-20 22:11:20 +00:00
dispatcher = afterRead;
break;
case HookedServiceEvent.created:
2017-01-20 22:11:20 +00:00
dispatcher = afterCreated;
break;
case HookedServiceEvent.modified:
2017-01-20 22:11:20 +00:00
dispatcher = afterModified;
break;
case HookedServiceEvent.updated:
2017-01-20 22:11:20 +00:00
dispatcher = afterUpdated;
break;
case HookedServiceEvent.removed:
2017-01-20 22:11:20 +00:00
dispatcher = afterRemoved;
break;
default:
throw new ArgumentError("Invalid service event name: '$eventName'");
}
2017-02-23 00:37:15 +00:00
var ev = new HookedServiceEvent(true, null, null, this, eventName);
2018-06-08 07:06:26 +00:00
return fireEvent(dispatcher, ev, callback);
2017-02-23 00:37:15 +00:00
}
/// Sends an arbitrary event down the hook chain.
Future<HookedServiceEvent> fireEvent(
HookedServiceEventDispatcher dispatcher, HookedServiceEvent event,
2018-06-08 07:06:26 +00:00
[HookedServiceEventListener callback]) {
2018-06-20 19:44:44 +00:00
Future f;
if (callback != null && event?._canceled != true)
f = new Future.sync(() => callback(event));
f ??= new Future.value();
return f.then((_) => dispatcher._emit(event));
2017-01-20 22:11:20 +00:00
}
2016-06-19 05:02:41 +00:00
}
/// Fired when a hooked service is invoked.
class HookedServiceEvent {
static const String indexed = 'indexed';
static const String read = 'read';
static const String created = 'created';
static const String modified = 'modified';
static const String updated = 'updated';
static const String removed = 'removed';
static const List<String> all = const [
2017-09-22 14:03:23 +00:00
indexed,
read,
created,
modified,
updated,
removed
2017-04-04 08:35:36 +00:00
];
2016-06-21 04:19:43 +00:00
2016-06-19 05:02:41 +00:00
/// Use this to end processing of an event.
2017-01-28 21:25:02 +00:00
void cancel([result]) {
2016-06-19 05:02:41 +00:00
_canceled = true;
2017-01-28 21:25:02 +00:00
this.result = result ?? this.result;
2016-06-19 05:02:41 +00:00
}
2017-02-26 21:31:09 +00:00
/// Resolves a service from the application.
2017-03-28 23:29:22 +00:00
///
2017-02-26 21:31:09 +00:00
/// Shorthand for `e.service.app.service(...)`.
Service getService(Pattern path) => service.app.service(path);
2016-06-19 05:02:41 +00:00
bool _canceled = false;
2016-06-21 04:19:43 +00:00
String _eventName;
var _id;
2017-01-28 21:08:07 +00:00
bool _isAfter;
2016-06-19 05:02:41 +00:00
var data;
2016-06-21 04:19:43 +00:00
Map _params;
2017-01-20 22:11:20 +00:00
RequestContext _request;
ResponseContext _response;
2017-01-28 21:25:02 +00:00
var result;
2016-06-21 04:19:43 +00:00
String get eventName => _eventName;
get id => _id;
2017-01-28 21:08:07 +00:00
bool get isAfter => _isAfter == true;
bool get isBefore => !isAfter;
2016-06-21 04:19:43 +00:00
Map get params => _params;
2017-01-20 22:11:20 +00:00
RequestContext get request => _request;
ResponseContext get response => _response;
2016-06-19 05:02:41 +00:00
/// The inner service whose method was hooked.
Service service;
2017-02-23 00:37:15 +00:00
HookedServiceEvent(this._isAfter, this._request, this._response,
2017-01-28 21:08:07 +00:00
Service this.service, String this._eventName,
2017-01-28 21:25:02 +00:00
{id, this.data, Map params, this.result}) {
2016-06-21 04:19:43 +00:00
_id = id;
_params = params ?? {};
2016-06-19 05:02:41 +00:00
}
2016-06-21 04:19:43 +00:00
}
/// Triggered on a hooked service event.
2017-01-20 22:11:20 +00:00
typedef HookedServiceEventListener(HookedServiceEvent event);
2016-06-21 04:19:43 +00:00
/// Can be listened to, but events may be canceled.
class HookedServiceEventDispatcher {
2017-04-04 08:35:36 +00:00
final List<StreamController<HookedServiceEvent>> _ctrl = [];
final List<HookedServiceEventListener> listeners = [];
void _close() {
_ctrl.forEach((c) => c.close());
2017-10-28 08:50:16 +00:00
listeners.clear();
2017-04-04 08:35:36 +00:00
}
2016-06-21 04:19:43 +00:00
/// Fires an event, and returns it once it is either canceled, or all listeners have run.
2018-06-08 07:06:26 +00:00
Future<HookedServiceEvent> _emit(HookedServiceEvent event) {
2018-06-20 19:44:44 +00:00
if (event?._canceled == true || event == null || listeners.isEmpty)
return new Future.value(event);
2016-06-21 04:19:43 +00:00
2018-06-20 19:44:44 +00:00
var f = new Future<HookedServiceEvent>.value(event);
for (var listener in listeners) {
f = f.then((event) {
if (event._canceled) return event;
return new Future.sync(() => listener(event)).then((_) => event);
});
2016-06-21 04:19:43 +00:00
}
2018-06-20 19:44:44 +00:00
return f;
2016-06-21 04:19:43 +00:00
}
2017-04-04 08:35:36 +00:00
/// Returns a [Stream] containing all events fired by this dispatcher.
///
/// *NOTE*: Callbacks on the returned [Stream] cannot be guaranteed to run before other [listeners].
/// Use this only if you need a read-only stream of events.
Stream<HookedServiceEvent> asStream() {
2018-08-20 03:18:19 +00:00
// TODO: Close StreamController
2017-04-04 08:35:36 +00:00
var ctrl = new StreamController<HookedServiceEvent>();
_ctrl.add(ctrl);
listen(ctrl.add);
return ctrl.stream;
}
2016-06-21 04:19:43 +00:00
/// Registers the listener to be called whenever an event is triggered.
void listen(HookedServiceEventListener listener) {
listeners.add(listener);
}
}