Skip to content

Commit 7922314

Browse files
committed
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.
1 parent 9b1f181 commit 7922314

File tree

9 files changed

+229
-0
lines changed

9 files changed

+229
-0
lines changed

src/backend/src/util/listenerutil.js

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
* You should have received a copy of the GNU Affero General Public License
1717
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1818
*/
19+
20+
// DEPRECATED: use putility.libs.listener instead
21+
1922
class MultiDetachable {
2023
constructor() {
2124
this.delegates = [];

src/putility/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
const { AdvancedBase } = require('./src/AdvancedBase');
2020
const { Service } = require('./src/concepts/Service');
2121
const { ServiceManager } = require('./src/system/ServiceManager');
22+
const { TTopics } = require('./src/traits/traits');
2223

2324
module.exports = {
2425
AdvancedBase,
@@ -28,8 +29,12 @@ module.exports = {
2829
libs: {
2930
promise: require('./src/libs/promise'),
3031
context: require('./src/libs/context'),
32+
listener: require('./src/libs/listener'),
3133
},
3234
concepts: {
3335
Service,
3436
},
37+
traits: {
38+
TTopics,
39+
},
3540
};

src/putility/src/AdvancedBase.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class AdvancedBase extends FeatureBase {
2727
require('./features/PropertiesFeature'),
2828
require('./features/TraitsFeature'),
2929
require('./features/NariMethodsFeature'),
30+
require('./features/TopicsFeature'),
3031
]
3132
}
3233

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const { RemoveFromArrayDetachable } = require("../libs/listener");
2+
const { TTopics } = require("../traits/traits");
3+
const { install_in_instance } = require("./NodeModuleDIFeature");
4+
5+
module.exports = {
6+
install_in_instance: (instance, { parameters }) => {
7+
const topics = instance._get_merged_static_array('TOPICS');
8+
9+
instance._.topics = {};
10+
11+
for ( const name of topics ) {
12+
instance._.topics[name] = {
13+
listeners_: [],
14+
};
15+
}
16+
17+
instance.mixin(TTopics, {
18+
pub: (k, v) => {
19+
const topic = instance._.topics[k];
20+
if ( ! topic ) {
21+
console.warn('missing topic: ' + topic);
22+
return;
23+
}
24+
for ( const lis of topic.listeners_ ) {
25+
lis();
26+
}
27+
},
28+
sub: (k, fn) => {
29+
const topic = instance._.topics[k];
30+
if ( ! topic ) {
31+
console.warn('missing topic: ' + topic);
32+
return;
33+
}
34+
topic.listeners_.push(fn);
35+
return new RemoveFromArrayDetachable(topic.listeners_, fn);
36+
}
37+
})
38+
39+
}
40+
};

src/putility/src/features/TraitsFeature.js

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626

2727
instance.as = trait_name => instance._.impls[trait_name];
2828
instance.list_traits = () => Object.keys(instance._.impls);
29+
instance.mixin = (name, impl) => instance._.impls[name] = impl;
2930

3031
for ( const cls of chain ) {
3132
const cls_traits = cls.IMPLEMENTS;

src/putility/src/libs/listener.js

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (C) 2024 Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
const { FeatureBase } = require("../bases/FeatureBase");
21+
const { TDetachable } = require("../traits/traits");
22+
23+
// NOTE: copied from src/backend/src/util/listenerutil.js,
24+
// which is now deprecated.
25+
26+
class MultiDetachable extends FeatureBase {
27+
static FEATURES = [
28+
require('../features/TraitsFeature'),
29+
];
30+
31+
constructor() {
32+
this.delegates = [];
33+
this.detached_ = false;
34+
}
35+
36+
add (delegate) {
37+
if ( this.detached_ ) {
38+
delegate.detach();
39+
return;
40+
}
41+
42+
this.delegates.push(delegate);
43+
}
44+
45+
static IMPLEMENTS = {
46+
[TDetachable]: {
47+
detach () {
48+
this.detached_ = true;
49+
for ( const delegate of this.delegates ) {
50+
delegate.detach();
51+
}
52+
}
53+
}
54+
}
55+
}
56+
57+
class AlsoDetachable extends FeatureBase {
58+
static FEATURES = [
59+
require('../features/TraitsFeature'),
60+
];
61+
62+
constructor () {
63+
super();
64+
this.also = () => {};
65+
}
66+
67+
also (also) {
68+
this.also = also;
69+
return this;
70+
}
71+
72+
static IMPLEMENTS = {
73+
[TDetachable]: {
74+
detach () {
75+
this.detach_();
76+
this.also();
77+
}
78+
}
79+
}
80+
}
81+
82+
// TODO: this doesn't work, but I don't know why yet.
83+
class RemoveFromArrayDetachable extends AlsoDetachable {
84+
constructor (array, element) {
85+
super();
86+
this.array = new WeakRef(array);
87+
this.element = element;
88+
}
89+
90+
detach_ () {
91+
const array = this.array.deref();
92+
if ( ! array ) return;
93+
const index = array.indexOf(this.element);
94+
if ( index !== -1 ) {
95+
array.splice(index, 1);
96+
}
97+
}
98+
}
99+
100+
module.exports = {
101+
MultiDetachable,
102+
RemoveFromArrayDetachable,
103+
};

src/putility/src/traits/traits.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
TTopics: Symbol('TTopics'),
3+
TDetachable: Symbol('TDetachable'),
4+
};

src/putility/test/listener.test.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const { RemoveFromArrayDetachable } = require("../src/libs/listener");
2+
const { expect } = require('chai');
3+
const { TDetachable } = require("../src/traits/traits");
4+
5+
describe('RemoveFromArrayDetachable', () => {
6+
it ('does the thing', () => {
7+
const someArray = [];
8+
9+
const add_listener = (key, lis) => {
10+
someArray.push(lis);
11+
return new RemoveFromArrayDetachable(someArray, lis);
12+
}
13+
14+
const det = add_listener('test', () => {
15+
console.log('i am test func');
16+
});
17+
18+
expect(someArray.length).to.equal(1);
19+
20+
det.as(TDetachable).detach();
21+
22+
expect(someArray.length).to.equal(0);
23+
})
24+
})

src/putility/test/topics.test.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const { AdvancedBase } = require("../src/AdvancedBase");
2+
const { TTopics, TDetachable } = require("../src/traits/traits");
3+
4+
describe('topics', () => {
5+
it ('works', () => {
6+
// A trait for something that's "punchable"
7+
const TPunchable = Symbol('punchable');
8+
9+
class SomeClassWithTopics extends AdvancedBase {
10+
// We can "listen on punched"
11+
static TOPICS = ['punched']
12+
13+
// Punchable trait implementation
14+
static IMPLEMENTS = {
15+
[TPunchable]: {
16+
punch () {
17+
this.as(TTopics).pub('punched!', {
18+
information: 'about the punch',
19+
in_whatever: 'format you desire',
20+
});
21+
}
22+
}
23+
}
24+
}
25+
26+
const thingy = new SomeClassWithTopics();
27+
28+
// Register the first listener, which we expect to be called both times
29+
let first_listener_called = false;
30+
thingy.as(TTopics).sub('punched', () => {
31+
first_listener_called = true;
32+
});
33+
34+
// Register the second listener, which we expect to be called once,
35+
// and then we're gonna detach it and make sure detach works
36+
let second_listener_call_count = 0;
37+
const det = thingy.as(TTopics).sub('punched', () => {
38+
second_listener_call_count++;
39+
});
40+
41+
thingy.as(TPunchable).punch();
42+
det.as(TDetachable).detach();
43+
thingy.as(TPunchable).punch();
44+
45+
expect(first_listener_called).to.equal(true);
46+
expect(second_listener_call_count).to.equal(1);
47+
})
48+
});

0 commit comments

Comments
 (0)