@lazyapps/mqemitter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +13 -0
- package/commandProcessorEventBusMqEmitter.js +32 -0
- package/commandReceiverMqEmitter.js +76 -0
- package/commandSenderMqEmitter.js +26 -0
- package/index.js +10 -0
- package/mqEmitterRegistry.js +51 -0
- package/package.json +46 -0
- package/readModelEventBusMqEmitter.js +44 -0
- package/readModelListenerMqEmitter.js +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Oliver Sturm
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @lazyapps/mqemitter
|
|
2
|
+
|
|
3
|
+
In-process message queue with role-specific sub-modules. Provides lightweight inter-component communication for monolithic LazyApps deployments using MQEmitter.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @lazyapps/mqemitter
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Part of LazyApps
|
|
12
|
+
|
|
13
|
+
This package is part of the [LazyApps](https://github.com/oliversturm/lazyapps-libs) event-sourcing and CQRS framework.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getLogger } from '@lazyapps/logger';
|
|
2
|
+
import { getSharedMqEmitter } from './mqEmitterRegistry.js';
|
|
3
|
+
|
|
4
|
+
export const commandProcessorEventBusMqEmitter =
|
|
5
|
+
({ mqName }) =>
|
|
6
|
+
() => {
|
|
7
|
+
const initLog = getLogger('CP/EB/MQE', 'INIT');
|
|
8
|
+
|
|
9
|
+
return Promise.resolve(getSharedMqEmitter('INIT', mqName))
|
|
10
|
+
.then((mq) => ({
|
|
11
|
+
publishEvent: (correlationId) => (event) => {
|
|
12
|
+
const log = getLogger('CP/EB/MQE', correlationId);
|
|
13
|
+
log.debug(`Publishing event timestamp ${event.timestamp}`);
|
|
14
|
+
event.correlationId = correlationId;
|
|
15
|
+
mq.emit({ topic: 'events', payload: event });
|
|
16
|
+
return event;
|
|
17
|
+
},
|
|
18
|
+
publishReplayState: (correlationId) => (state) => {
|
|
19
|
+
const log = getLogger('CP/EB/MQE', correlationId);
|
|
20
|
+
log.debug(`Publishing replay state ${state}`);
|
|
21
|
+
mq.emit({
|
|
22
|
+
topic: '__system',
|
|
23
|
+
payload: { type: 'SET_REPLAY_STATE', state, correlationId },
|
|
24
|
+
});
|
|
25
|
+
return state;
|
|
26
|
+
},
|
|
27
|
+
}))
|
|
28
|
+
.then((res) => {
|
|
29
|
+
initLog.debug('Event bus ready');
|
|
30
|
+
return res;
|
|
31
|
+
});
|
|
32
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getLogger } from '@lazyapps/logger';
|
|
2
|
+
import { getSharedMqEmitter } from './mqEmitterRegistry.js';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
|
|
5
|
+
// The terminology is not optimal here. In the case of other
|
|
6
|
+
// transports, for instance the project eventbus-rabbitmq,
|
|
7
|
+
// there are "command-receiver" implementations which apply
|
|
8
|
+
// to the command receiver component of the architecture in that
|
|
9
|
+
// they allow that received to send events to the event bus.
|
|
10
|
+
// For this mqemitter implementation, the command receiver
|
|
11
|
+
// is the component that receives commands and handles them --
|
|
12
|
+
// in the case of rabbit etc such an implementation does not
|
|
13
|
+
// currently exist, so the inconsistency has existed for a while.
|
|
14
|
+
// For mqemitter, the publishEvent stuff is in the
|
|
15
|
+
// commandProcessorEventBusMqEmitter.js file.
|
|
16
|
+
//
|
|
17
|
+
export const commandReceiverMqEmitter =
|
|
18
|
+
({ mqName }) =>
|
|
19
|
+
({
|
|
20
|
+
aggregateStore,
|
|
21
|
+
eventStore,
|
|
22
|
+
eventBus,
|
|
23
|
+
aggregates,
|
|
24
|
+
handleCommand,
|
|
25
|
+
correlationConfig,
|
|
26
|
+
}) => {
|
|
27
|
+
const initLog = getLogger('CR/MQ', 'INIT');
|
|
28
|
+
|
|
29
|
+
return Promise.resolve(getSharedMqEmitter('INIT', mqName))
|
|
30
|
+
.then((mq) => {
|
|
31
|
+
mq.on('command', ({ payload: messagePayload }, cb) => {
|
|
32
|
+
let { correlationId } = messagePayload;
|
|
33
|
+
if (!correlationId) {
|
|
34
|
+
correlationId = `${
|
|
35
|
+
correlationConfig?.serviceId || 'UNK'
|
|
36
|
+
}-${nanoid()}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const log = getLogger('CR/MQ', correlationId);
|
|
40
|
+
log.debug(`Received command: ${JSON.stringify(messagePayload)}`);
|
|
41
|
+
const { command, aggregateName, aggregateId, payload } =
|
|
42
|
+
messagePayload;
|
|
43
|
+
const aggregate = aggregates[aggregateName];
|
|
44
|
+
const commandHandler =
|
|
45
|
+
aggregate?.commands && aggregate.commands[command];
|
|
46
|
+
// Minimal input tests here -- this receiver
|
|
47
|
+
// will be used in-process, not a public
|
|
48
|
+
// endpoint.
|
|
49
|
+
if (commandHandler) {
|
|
50
|
+
handleCommand(
|
|
51
|
+
aggregateStore,
|
|
52
|
+
eventStore,
|
|
53
|
+
eventBus,
|
|
54
|
+
command,
|
|
55
|
+
aggregateName,
|
|
56
|
+
aggregateId,
|
|
57
|
+
payload,
|
|
58
|
+
commandHandler,
|
|
59
|
+
undefined /* auth */,
|
|
60
|
+
undefined /* timestamp */,
|
|
61
|
+
correlationId,
|
|
62
|
+
).catch((err) => {
|
|
63
|
+
log.error(`An error occurred handling command ${command} for aggregate ${aggregateName}(${aggregateId}) with payload:
|
|
64
|
+
|
|
65
|
+
${JSON.stringify(payload)}
|
|
66
|
+
|
|
67
|
+
${err}`);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
cb();
|
|
71
|
+
});
|
|
72
|
+
})
|
|
73
|
+
.then(() => {
|
|
74
|
+
initLog.debug('Command receiver active');
|
|
75
|
+
});
|
|
76
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getLogger } from '@lazyapps/logger';
|
|
2
|
+
import { getSharedMqEmitter } from './mqEmitterRegistry.js';
|
|
3
|
+
|
|
4
|
+
export const commandSenderMqEmitter = ({ mqName }) => {
|
|
5
|
+
return {
|
|
6
|
+
sendCommand: (correlationId, cmd) => {
|
|
7
|
+
const log = getLogger('RM/CS', correlationId);
|
|
8
|
+
cmd.correlationId = correlationId;
|
|
9
|
+
return Promise.resolve(getSharedMqEmitter(correlationId, mqName))
|
|
10
|
+
.then((mq) =>
|
|
11
|
+
mq.emit({
|
|
12
|
+
topic: 'command',
|
|
13
|
+
payload: cmd,
|
|
14
|
+
}),
|
|
15
|
+
)
|
|
16
|
+
.then(() => {
|
|
17
|
+
log.debug(`Sending command ${JSON.stringify(cmd)}`);
|
|
18
|
+
})
|
|
19
|
+
.catch((e) => {
|
|
20
|
+
log.error(
|
|
21
|
+
`Error occurred sending command ${JSON.stringify(cmd)}: ${e}`,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { commandProcessorEventBusMqEmitter } from './commandProcessorEventBusMqEmitter.js';
|
|
2
|
+
export { commandReceiverMqEmitter } from './commandReceiverMqEmitter.js';
|
|
3
|
+
export { commandSenderMqEmitter } from './commandSenderMqEmitter.js';
|
|
4
|
+
export { readModelEventBusMqEmitter } from './readModelEventBusMqEmitter.js';
|
|
5
|
+
export { readModelListenerMqEmitter } from './readModelListenerMqEmitter.js';
|
|
6
|
+
export {
|
|
7
|
+
registerSharedMqEmitter,
|
|
8
|
+
getSharedMqEmitter,
|
|
9
|
+
getPublishedMqEmitter,
|
|
10
|
+
} from './mqEmitterRegistry.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getLogger } from '@lazyapps/logger';
|
|
2
|
+
import cs from 'mqemitter-cs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
|
|
5
|
+
// These emitters are process bounded and they must be shared
|
|
6
|
+
// between different parts of the process. In the case of
|
|
7
|
+
// the frontend processes that use vite (or whatever bundler)
|
|
8
|
+
// this is not easily done by way of injection. I considered
|
|
9
|
+
// using a bundler plugin to inject the emitters, but that
|
|
10
|
+
// is quite specific to the bundler. After some research, it
|
|
11
|
+
// also appears to be impossible to inject a specific object
|
|
12
|
+
// reference into the vite process. It cleans up in such a strange
|
|
13
|
+
// way that all imports in the vite context are separate from
|
|
14
|
+
// those outside it. I created a registry of emitters so they
|
|
15
|
+
// can be shared efficiently between the parts of the monolith
|
|
16
|
+
// process that don't have a problem with it, and for other
|
|
17
|
+
// parts there is a simple network server instead.
|
|
18
|
+
|
|
19
|
+
const emitters = new Map();
|
|
20
|
+
|
|
21
|
+
export const registerSharedMqEmitter = (name, emitter, port = undefined) => {
|
|
22
|
+
const log = getLogger('MQ/Reg', 'INIT');
|
|
23
|
+
|
|
24
|
+
log.debug(`Registering shared MQ emitter: ${name}`);
|
|
25
|
+
emitters.set(name, emitter);
|
|
26
|
+
if (port) {
|
|
27
|
+
log.debug(`Publishing shared MQ emitter ${name} on port ${port}`);
|
|
28
|
+
const server = net.createServer(cs.server(emitter));
|
|
29
|
+
server.on('listening', () => {
|
|
30
|
+
log.debug(`MQ emitter ${name} listening on port ${port}`);
|
|
31
|
+
});
|
|
32
|
+
server.listen(port);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getSharedMqEmitter = (correlationId, name) => {
|
|
37
|
+
const log = getLogger('MQ/Reg', correlationId);
|
|
38
|
+
if (!emitters.has(name)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`[${correlationId}] No shared MQ emitter registered for name: ${name}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
log.debug(`Getting shared MQ emitter: ${name}`);
|
|
44
|
+
return emitters.get(name);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const getPublishedMqEmitter = (correlationId, port) => {
|
|
48
|
+
const log = getLogger('MQ/Reg', correlationId);
|
|
49
|
+
log.debug(`Getting published MQ emitter on port ${port}`);
|
|
50
|
+
return cs.client(net.connect(port));
|
|
51
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lazyapps/mqemitter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "In-process message queue with role-specific sub-modules for LazyApps",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"*.js"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"event-sourcing",
|
|
14
|
+
"cqrs",
|
|
15
|
+
"lazyapps",
|
|
16
|
+
"message-queue",
|
|
17
|
+
"mqemitter"
|
|
18
|
+
],
|
|
19
|
+
"author": "Oliver Sturm",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/oliversturm/lazyapps-libs.git",
|
|
24
|
+
"directory": "packages/mqemitter"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/oliversturm/lazyapps-libs/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/oliversturm/lazyapps-libs/tree/main/packages/mqemitter#readme",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.20.3 || >=20.18.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"eslint": "^8.46.0",
|
|
35
|
+
"vitest": "^4.0.18"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"mqemitter-cs": "^1.0.1",
|
|
39
|
+
"nanoid": "^5.0.7",
|
|
40
|
+
"@lazyapps/logger": "^0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"type": "module",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "vitest"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getLogger } from '@lazyapps/logger';
|
|
2
|
+
import { getSharedMqEmitter } from './mqEmitterRegistry.js';
|
|
3
|
+
|
|
4
|
+
export const readModelEventBusMqEmitter =
|
|
5
|
+
({ mqName }) =>
|
|
6
|
+
(context) => {
|
|
7
|
+
let inReplay = false;
|
|
8
|
+
|
|
9
|
+
const handleSysMessage = (msg) => {
|
|
10
|
+
switch (msg.type) {
|
|
11
|
+
case 'SET_REPLAY_STATE':
|
|
12
|
+
inReplay = msg.state;
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const initLog = getLogger('RM/EB', 'INIT');
|
|
18
|
+
|
|
19
|
+
return Promise.resolve(getSharedMqEmitter('INIT', mqName))
|
|
20
|
+
.then((mq) => {
|
|
21
|
+
mq.on('events', ({ payload }, cb) => {
|
|
22
|
+
const { correlationId } = payload;
|
|
23
|
+
const log = getLogger('RM/EB', correlationId);
|
|
24
|
+
log.debug(`Received event: ${JSON.stringify(payload)}`);
|
|
25
|
+
context.projectionHandler.projectEvent(correlationId)(
|
|
26
|
+
payload,
|
|
27
|
+
inReplay,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
cb();
|
|
31
|
+
});
|
|
32
|
+
mq.on('__system', ({ payload }, cb) => {
|
|
33
|
+
const { correlationId, event } = payload;
|
|
34
|
+
const log = getLogger('RM/EB', correlationId);
|
|
35
|
+
log.debug(`Received '__system' event: ${JSON.stringify(event)}`);
|
|
36
|
+
|
|
37
|
+
handleSysMessage(event);
|
|
38
|
+
cb();
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
.then(() => {
|
|
42
|
+
initLog.debug(`Event bus receiving`);
|
|
43
|
+
});
|
|
44
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getLogger } from '@lazyapps/logger';
|
|
2
|
+
import { getSharedMqEmitter } from './mqEmitterRegistry.js';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
|
|
5
|
+
export const readModelListenerMqEmitter =
|
|
6
|
+
({ mqName }) =>
|
|
7
|
+
(context) => {
|
|
8
|
+
const initLog = getLogger('RM/LS', 'INIT');
|
|
9
|
+
return Promise.resolve(getSharedMqEmitter('INIT', mqName))
|
|
10
|
+
.then((mq) => {
|
|
11
|
+
mq.on('query', ({ payload }, cb) => {
|
|
12
|
+
const { readModelName, resolverName, args, replyTopic } = payload;
|
|
13
|
+
let { correlationId } = payload;
|
|
14
|
+
if (!correlationId) {
|
|
15
|
+
correlationId = `${
|
|
16
|
+
context.correlationConfig?.serviceId || 'UNK'
|
|
17
|
+
}-${nanoid()}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const log = getLogger('RM/LS', correlationId);
|
|
21
|
+
log.debug(
|
|
22
|
+
`Query received for ${readModelName}/${resolverName} (reply ${replyTopic}) with args ${JSON.stringify(
|
|
23
|
+
args,
|
|
24
|
+
)}`,
|
|
25
|
+
);
|
|
26
|
+
const readModel = context.readModels[readModelName];
|
|
27
|
+
if (!readModel) {
|
|
28
|
+
log.error(
|
|
29
|
+
`Read model ${readModelName} not found during query for ${readModelName}/${resolverName} (reply ${replyTopic}) with args ${JSON.stringify(
|
|
30
|
+
args,
|
|
31
|
+
)}`,
|
|
32
|
+
);
|
|
33
|
+
cb();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const resolver = readModel.resolvers[resolverName];
|
|
37
|
+
if (!resolver) {
|
|
38
|
+
log.error(
|
|
39
|
+
`Resolver ${resolverName} not found in read model ${readModelName} during query for ${readModelName}/${resolverName} (reply ${replyTopic}) with args ${JSON.stringify(
|
|
40
|
+
args,
|
|
41
|
+
)}`,
|
|
42
|
+
);
|
|
43
|
+
cb();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Run the promise, then call cb and return
|
|
47
|
+
// The promise will send the reply
|
|
48
|
+
// asynchronously
|
|
49
|
+
Promise.resolve(
|
|
50
|
+
resolver(context.storage.perRequest(correlationId), args),
|
|
51
|
+
)
|
|
52
|
+
.then((result) => {
|
|
53
|
+
const payload = {
|
|
54
|
+
correlationId,
|
|
55
|
+
result,
|
|
56
|
+
};
|
|
57
|
+
mq.emit({ topic: replyTopic, payload });
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
log.error(
|
|
61
|
+
`An error occurred handling query for ${readModelName}/${resolverName} (reply ${replyTopic}) with args ${JSON.stringify(
|
|
62
|
+
args,
|
|
63
|
+
)}: ${err}`,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
cb();
|
|
68
|
+
});
|
|
69
|
+
})
|
|
70
|
+
.then((res) => {
|
|
71
|
+
initLog.debug(`Read model listener active`);
|
|
72
|
+
return res;
|
|
73
|
+
});
|
|
74
|
+
};
|