diff --git a/src/backend/src/util/listenerutil.js b/src/backend/src/util/listenerutil.js
index 711758a7..2a9416c8 100644
--- a/src/backend/src/util/listenerutil.js
+++ b/src/backend/src/util/listenerutil.js
@@ -16,6 +16,9 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
+
+// DEPRECATED: use putility.libs.listener instead
+
class MultiDetachable {
constructor() {
this.delegates = [];
diff --git a/src/putility/index.js b/src/putility/index.js
index 1acc8221..17f98549 100644
--- a/src/putility/index.js
+++ b/src/putility/index.js
@@ -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,
+ },
};
diff --git a/src/putility/src/AdvancedBase.js b/src/putility/src/AdvancedBase.js
index 51b59fb2..1a48ada7 100644
--- a/src/putility/src/AdvancedBase.js
+++ b/src/putility/src/AdvancedBase.js
@@ -27,6 +27,7 @@ class AdvancedBase extends FeatureBase {
require('./features/PropertiesFeature'),
require('./features/TraitsFeature'),
require('./features/NariMethodsFeature'),
+ require('./features/TopicsFeature'),
]
}
diff --git a/src/putility/src/features/TopicsFeature.js b/src/putility/src/features/TopicsFeature.js
new file mode 100644
index 00000000..365679d6
--- /dev/null
+++ b/src/putility/src/features/TopicsFeature.js
@@ -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);
+ }
+ })
+
+ }
+};
diff --git a/src/putility/src/features/TraitsFeature.js b/src/putility/src/features/TraitsFeature.js
index ba597251..157aa7fd 100644
--- a/src/putility/src/features/TraitsFeature.js
+++ b/src/putility/src/features/TraitsFeature.js
@@ -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;
diff --git a/src/putility/src/libs/listener.js b/src/putility/src/libs/listener.js
new file mode 100644
index 00000000..1d54b93d
--- /dev/null
+++ b/src/putility/src/libs/listener.js
@@ -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 .
+ */
+
+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,
+};
diff --git a/src/putility/src/traits/traits.js b/src/putility/src/traits/traits.js
new file mode 100644
index 00000000..2416e6d3
--- /dev/null
+++ b/src/putility/src/traits/traits.js
@@ -0,0 +1,4 @@
+module.exports = {
+ TTopics: Symbol('TTopics'),
+ TDetachable: Symbol('TDetachable'),
+};
diff --git a/src/putility/test/listener.test.js b/src/putility/test/listener.test.js
new file mode 100644
index 00000000..399a8f66
--- /dev/null
+++ b/src/putility/test/listener.test.js
@@ -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);
+ })
+})
diff --git a/src/putility/test/topics.test.js b/src/putility/test/topics.test.js
new file mode 100644
index 00000000..18b8702e
--- /dev/null
+++ b/src/putility/test/topics.test.js
@@ -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);
+ })
+});