mirror of
https://github.com/HeyPuter/puter
synced 2024-11-14 14:03:42 +00:00
dev: add generic pub/sub system for use anywhere
Dispatching and listening to events is non-trivial. The apparent triviality is in implementing a list of listeners and calling them. The non-triviality is in the nature of what happens to a system when it has multiple different interfaces to register listeners and publish events. This commit adds TopicsFeature, which allows any class extending AdvancedBase to declare topics. A topic is a simple pub/sub channel. TopicsFeature will manage the state of listeners so the class doesn't need to. A GC-friendly mechanism for detaching listeners is also provided.
This commit is contained in:
parent
c5ad3a8362
commit
da613af9ea
@ -16,6 +16,9 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// DEPRECATED: use putility.libs.listener instead
|
||||
|
||||
class MultiDetachable {
|
||||
constructor() {
|
||||
this.delegates = [];
|
||||
|
@ -19,6 +19,7 @@
|
||||
const { AdvancedBase } = require('./src/AdvancedBase');
|
||||
const { Service } = require('./src/concepts/Service');
|
||||
const { ServiceManager } = require('./src/system/ServiceManager');
|
||||
const { TTopics } = require('./src/traits/traits');
|
||||
|
||||
module.exports = {
|
||||
AdvancedBase,
|
||||
@ -28,8 +29,12 @@ module.exports = {
|
||||
libs: {
|
||||
promise: require('./src/libs/promise'),
|
||||
context: require('./src/libs/context'),
|
||||
listener: require('./src/libs/listener'),
|
||||
},
|
||||
concepts: {
|
||||
Service,
|
||||
},
|
||||
traits: {
|
||||
TTopics,
|
||||
},
|
||||
};
|
||||
|
@ -27,6 +27,7 @@ class AdvancedBase extends FeatureBase {
|
||||
require('./features/PropertiesFeature'),
|
||||
require('./features/TraitsFeature'),
|
||||
require('./features/NariMethodsFeature'),
|
||||
require('./features/TopicsFeature'),
|
||||
]
|
||||
}
|
||||
|
||||
|
40
src/putility/src/features/TopicsFeature.js
Normal file
40
src/putility/src/features/TopicsFeature.js
Normal file
@ -0,0 +1,40 @@
|
||||
const { RemoveFromArrayDetachable } = require("../libs/listener");
|
||||
const { TTopics } = require("../traits/traits");
|
||||
const { install_in_instance } = require("./NodeModuleDIFeature");
|
||||
|
||||
module.exports = {
|
||||
install_in_instance: (instance, { parameters }) => {
|
||||
const topics = instance._get_merged_static_array('TOPICS');
|
||||
|
||||
instance._.topics = {};
|
||||
|
||||
for ( const name of topics ) {
|
||||
instance._.topics[name] = {
|
||||
listeners_: [],
|
||||
};
|
||||
}
|
||||
|
||||
instance.mixin(TTopics, {
|
||||
pub: (k, v) => {
|
||||
const topic = instance._.topics[k];
|
||||
if ( ! topic ) {
|
||||
console.warn('missing topic: ' + topic);
|
||||
return;
|
||||
}
|
||||
for ( const lis of topic.listeners_ ) {
|
||||
lis();
|
||||
}
|
||||
},
|
||||
sub: (k, fn) => {
|
||||
const topic = instance._.topics[k];
|
||||
if ( ! topic ) {
|
||||
console.warn('missing topic: ' + topic);
|
||||
return;
|
||||
}
|
||||
topic.listeners_.push(fn);
|
||||
return new RemoveFromArrayDetachable(topic.listeners_, fn);
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
};
|
@ -26,6 +26,7 @@ module.exports = {
|
||||
|
||||
instance.as = trait_name => instance._.impls[trait_name];
|
||||
instance.list_traits = () => Object.keys(instance._.impls);
|
||||
instance.mixin = (name, impl) => instance._.impls[name] = impl;
|
||||
|
||||
for ( const cls of chain ) {
|
||||
const cls_traits = cls.IMPLEMENTS;
|
||||
|
103
src/putility/src/libs/listener.js
Normal file
103
src/putility/src/libs/listener.js
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { FeatureBase } = require("../bases/FeatureBase");
|
||||
const { TDetachable } = require("../traits/traits");
|
||||
|
||||
// NOTE: copied from src/backend/src/util/listenerutil.js,
|
||||
// which is now deprecated.
|
||||
|
||||
class MultiDetachable extends FeatureBase {
|
||||
static FEATURES = [
|
||||
require('../features/TraitsFeature'),
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.delegates = [];
|
||||
this.detached_ = false;
|
||||
}
|
||||
|
||||
add (delegate) {
|
||||
if ( this.detached_ ) {
|
||||
delegate.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
this.delegates.push(delegate);
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
[TDetachable]: {
|
||||
detach () {
|
||||
this.detached_ = true;
|
||||
for ( const delegate of this.delegates ) {
|
||||
delegate.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AlsoDetachable extends FeatureBase {
|
||||
static FEATURES = [
|
||||
require('../features/TraitsFeature'),
|
||||
];
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.also = () => {};
|
||||
}
|
||||
|
||||
also (also) {
|
||||
this.also = also;
|
||||
return this;
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
[TDetachable]: {
|
||||
detach () {
|
||||
this.detach_();
|
||||
this.also();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this doesn't work, but I don't know why yet.
|
||||
class RemoveFromArrayDetachable extends AlsoDetachable {
|
||||
constructor (array, element) {
|
||||
super();
|
||||
this.array = new WeakRef(array);
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
detach_ () {
|
||||
const array = this.array.deref();
|
||||
if ( ! array ) return;
|
||||
const index = array.indexOf(this.element);
|
||||
if ( index !== -1 ) {
|
||||
array.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MultiDetachable,
|
||||
RemoveFromArrayDetachable,
|
||||
};
|
4
src/putility/src/traits/traits.js
Normal file
4
src/putility/src/traits/traits.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
TTopics: Symbol('TTopics'),
|
||||
TDetachable: Symbol('TDetachable'),
|
||||
};
|
24
src/putility/test/listener.test.js
Normal file
24
src/putility/test/listener.test.js
Normal file
@ -0,0 +1,24 @@
|
||||
const { RemoveFromArrayDetachable } = require("../src/libs/listener");
|
||||
const { expect } = require('chai');
|
||||
const { TDetachable } = require("../src/traits/traits");
|
||||
|
||||
describe('RemoveFromArrayDetachable', () => {
|
||||
it ('does the thing', () => {
|
||||
const someArray = [];
|
||||
|
||||
const add_listener = (key, lis) => {
|
||||
someArray.push(lis);
|
||||
return new RemoveFromArrayDetachable(someArray, lis);
|
||||
}
|
||||
|
||||
const det = add_listener('test', () => {
|
||||
console.log('i am test func');
|
||||
});
|
||||
|
||||
expect(someArray.length).to.equal(1);
|
||||
|
||||
det.as(TDetachable).detach();
|
||||
|
||||
expect(someArray.length).to.equal(0);
|
||||
})
|
||||
})
|
48
src/putility/test/topics.test.js
Normal file
48
src/putility/test/topics.test.js
Normal file
@ -0,0 +1,48 @@
|
||||
const { AdvancedBase } = require("../src/AdvancedBase");
|
||||
const { TTopics, TDetachable } = require("../src/traits/traits");
|
||||
|
||||
describe('topics', () => {
|
||||
it ('works', () => {
|
||||
// A trait for something that's "punchable"
|
||||
const TPunchable = Symbol('punchable');
|
||||
|
||||
class SomeClassWithTopics extends AdvancedBase {
|
||||
// We can "listen on punched"
|
||||
static TOPICS = ['punched']
|
||||
|
||||
// Punchable trait implementation
|
||||
static IMPLEMENTS = {
|
||||
[TPunchable]: {
|
||||
punch () {
|
||||
this.as(TTopics).pub('punched!', {
|
||||
information: 'about the punch',
|
||||
in_whatever: 'format you desire',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const thingy = new SomeClassWithTopics();
|
||||
|
||||
// Register the first listener, which we expect to be called both times
|
||||
let first_listener_called = false;
|
||||
thingy.as(TTopics).sub('punched', () => {
|
||||
first_listener_called = true;
|
||||
});
|
||||
|
||||
// Register the second listener, which we expect to be called once,
|
||||
// and then we're gonna detach it and make sure detach works
|
||||
let second_listener_call_count = 0;
|
||||
const det = thingy.as(TTopics).sub('punched', () => {
|
||||
second_listener_call_count++;
|
||||
});
|
||||
|
||||
thingy.as(TPunchable).punch();
|
||||
det.as(TDetachable).detach();
|
||||
thingy.as(TPunchable).punch();
|
||||
|
||||
expect(first_listener_called).to.equal(true);
|
||||
expect(second_listener_call_count).to.equal(1);
|
||||
})
|
||||
});
|
Loading…
Reference in New Issue
Block a user