@pubber-subber/memory 0.0.1
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 +21 -0
- package/README.md +82 -0
- package/dist/index.cjs +78 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/index.ts +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sami Mishal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @pubber-subber/memory
|
|
2
|
+
|
|
3
|
+
In-process pub/sub adapter for [`@pubber-subber/core`](https://www.npmjs.com/package/@pubber-subber/core). EventEmitter-style fan-out, glob pattern subscriptions, payload cloning, zero external dependencies.
|
|
4
|
+
|
|
5
|
+
Use it in tests, in single-process apps, and as the default when no real broker is configured.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @pubber-subber/core @pubber-subber/memory
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { PubSub } from '@pubber-subber/core';
|
|
17
|
+
import { memory } from '@pubber-subber/memory';
|
|
18
|
+
|
|
19
|
+
const pubsub = new PubSub({ adapter: memory() });
|
|
20
|
+
|
|
21
|
+
await pubsub.subscribe('users.*', (msg) => {
|
|
22
|
+
console.log(msg.topic, msg.payload);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await pubsub.publish('users.created', { id: 1, name: 'Alice' });
|
|
26
|
+
await pubsub.publish('users.updated', { id: 1, name: 'Alice (edited)' });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Options
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
memory({ rawPayloads?: boolean })
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Option | Default | Notes |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `rawPayloads` | `false` | If `true`, subscribers receive the publisher's exact object reference. Default is to `structuredClone()` so subscribers can't accidentally mutate the publisher's data. Set `true` when your payloads are immutable, expensive to clone, or contain non-cloneable values (functions, class instances). |
|
|
38
|
+
|
|
39
|
+
## Capabilities
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
{ publish: true, subscribe: true, patternSubscribe: true, ack: false }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Pattern matching
|
|
46
|
+
|
|
47
|
+
Built-in glob matcher (no external dependency):
|
|
48
|
+
|
|
49
|
+
| Pattern | Matches | Notes |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `*` | any run of characters within a topic segment | won't cross `.` |
|
|
52
|
+
| `**` | any run of characters across segments | greedy |
|
|
53
|
+
| `?` | exactly one character within a segment | won't match `.` |
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
await pubsub.subscribe('users.*', handler); // users.created, users.updated; NOT users.created.email
|
|
57
|
+
await pubsub.subscribe('users.**', handler); // users.created, users.created.email, ...
|
|
58
|
+
await pubsub.subscribe('user?', handler); // user1, users
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Metadata
|
|
62
|
+
|
|
63
|
+
The memory adapter forwards any object as `AdapterMessage.meta` and as the second handler argument, but doesn't interpret it. Use it for tracing, tenant IDs, correlation IDs, or anything else you'd like to thread through.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
await pubsub.subscribe('t', (msg, meta) => {
|
|
67
|
+
// msg.meta === { traceId: 'abc' }
|
|
68
|
+
// meta === undefined
|
|
69
|
+
}, /* no subscribe meta */);
|
|
70
|
+
|
|
71
|
+
await pubsub.publish('t', payload, { traceId: 'abc' });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Behavior
|
|
75
|
+
|
|
76
|
+
- **Fire-and-forget but synchronous-ish.** `publish()` awaits each subscriber's handler in order before resolving. Memory has no in-flight buffer or backpressure — when `publish()` resolves, every live subscriber has been called.
|
|
77
|
+
- **No replay.** Subscribers added *after* a publish do not see backfill (consistent with pub/sub semantics across the ecosystem).
|
|
78
|
+
- **Handler errors don't break delivery.** If one subscriber throws, the others still receive the message. Errors surface via the facade's `onError` callback if set.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@pubber-subber/core');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
function memory(opts = {}) {
|
|
7
|
+
const exactSubs = /* @__PURE__ */ new Map();
|
|
8
|
+
const patternSubs = /* @__PURE__ */ new Map();
|
|
9
|
+
let idCounter = 0;
|
|
10
|
+
const cloneIfNeeded = (value) => {
|
|
11
|
+
if (opts.rawPayloads) return value;
|
|
12
|
+
try {
|
|
13
|
+
return structuredClone(value);
|
|
14
|
+
} catch {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
name: "memory",
|
|
20
|
+
capabilities: {
|
|
21
|
+
publish: true,
|
|
22
|
+
subscribe: true,
|
|
23
|
+
patternSubscribe: true,
|
|
24
|
+
ack: false
|
|
25
|
+
},
|
|
26
|
+
async publish(topic, payload, meta) {
|
|
27
|
+
const buildMessage = () => ({
|
|
28
|
+
topic,
|
|
29
|
+
payload: cloneIfNeeded(payload),
|
|
30
|
+
raw: payload,
|
|
31
|
+
meta
|
|
32
|
+
});
|
|
33
|
+
const exact = exactSubs.get(topic);
|
|
34
|
+
if (exact && exact.size > 0) {
|
|
35
|
+
for (const handler of [...exact]) {
|
|
36
|
+
await safeDeliver(handler, buildMessage());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const [pattern, handlers] of patternSubs) {
|
|
40
|
+
if (core.matchTopic(pattern, topic)) {
|
|
41
|
+
for (const handler of [...handlers]) {
|
|
42
|
+
await safeDeliver(handler, buildMessage());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
async subscribe(topic, handler) {
|
|
48
|
+
const map = core.isPattern(topic) ? patternSubs : exactSubs;
|
|
49
|
+
let set = map.get(topic);
|
|
50
|
+
if (!set) {
|
|
51
|
+
set = /* @__PURE__ */ new Set();
|
|
52
|
+
map.set(topic, set);
|
|
53
|
+
}
|
|
54
|
+
set.add(handler);
|
|
55
|
+
idCounter += 1;
|
|
56
|
+
const id = `mem-${idCounter}`;
|
|
57
|
+
const ownSet = set;
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
topic,
|
|
61
|
+
unsubscribe: async () => {
|
|
62
|
+
ownSet.delete(handler);
|
|
63
|
+
if (ownSet.size === 0) map.delete(topic);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function safeDeliver(handler, message) {
|
|
70
|
+
try {
|
|
71
|
+
await handler(message);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
exports.memory = memory;
|
|
77
|
+
//# sourceMappingURL=index.cjs.map
|
|
78
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["matchTopic","isPattern"],"mappings":";;;;;AAoBO,SAAS,MAAA,CACd,IAAA,GAA6B,EAAC,EACqB;AACnD,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAiC;AACvD,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAiC;AACzD,EAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,EAAA,MAAM,aAAA,GAAgB,CAAC,KAAA,KAA4B;AACjD,IAAA,IAAI,IAAA,CAAK,aAAa,OAAO,KAAA;AAC7B,IAAA,IAAI;AACF,MAAA,OAAO,gBAAgB,KAAK,CAAA;AAAA,IAC9B,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,QAAA;AAAA,IACN,YAAA,EAAc;AAAA,MACZ,OAAA,EAAS,IAAA;AAAA,MACT,SAAA,EAAW,IAAA;AAAA,MACX,gBAAA,EAAkB,IAAA;AAAA,MAClB,GAAA,EAAK;AAAA,KACP;AAAA,IAEA,MAAM,OAAA,CAAQ,KAAA,EAAO,OAAA,EAAS,IAAA,EAAM;AAClC,MAAA,MAAM,eAAe,OAAuB;AAAA,QAC1C,KAAA;AAAA,QACA,OAAA,EAAS,cAAc,OAAO,CAAA;AAAA,QAC9B,GAAA,EAAK,OAAA;AAAA,QACL;AAAA,OACF,CAAA;AAEA,MAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,GAAA,CAAI,KAAK,CAAA;AACjC,MAAA,IAAI,KAAA,IAAS,KAAA,CAAM,IAAA,GAAO,CAAA,EAAG;AAC3B,QAAA,KAAA,MAAW,OAAA,IAAW,CAAC,GAAG,KAAK,CAAA,EAAG;AAChC,UAAA,MAAM,WAAA,CAAY,OAAA,EAAS,YAAA,EAAc,CAAA;AAAA,QAC3C;AAAA,MACF;AAEA,MAAA,KAAA,MAAW,CAAC,OAAA,EAAS,QAAQ,CAAA,IAAK,WAAA,EAAa;AAC7C,QAAA,IAAIA,eAAA,CAAW,OAAA,EAAS,KAAK,CAAA,EAAG;AAC9B,UAAA,KAAA,MAAW,OAAA,IAAW,CAAC,GAAG,QAAQ,CAAA,EAAG;AACnC,YAAA,MAAM,WAAA,CAAY,OAAA,EAAS,YAAA,EAAc,CAAA;AAAA,UAC3C;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,SAAA,CAAU,KAAA,EAAO,OAAA,EAAS;AAC9B,MAAA,MAAM,GAAA,GAAMC,cAAA,CAAU,KAAK,CAAA,GAAI,WAAA,GAAc,SAAA;AAC7C,MAAA,IAAI,GAAA,GAAM,GAAA,CAAI,GAAA,CAAI,KAAK,CAAA;AACvB,MAAA,IAAI,CAAC,GAAA,EAAK;AACR,QAAA,GAAA,uBAAU,GAAA,EAAI;AACd,QAAA,GAAA,CAAI,GAAA,CAAI,OAAO,GAAG,CAAA;AAAA,MACpB;AACA,MAAA,GAAA,CAAI,IAAI,OAAO,CAAA;AACf,MAAA,SAAA,IAAa,CAAA;AACb,MAAA,MAAM,EAAA,GAAK,OAAO,SAAS,CAAA,CAAA;AAC3B,MAAA,MAAM,MAAA,GAAS,GAAA;AACf,MAAA,OAAO;AAAA,QACL,EAAA;AAAA,QACA,KAAA;AAAA,QACA,aAAa,YAAY;AACvB,UAAA,MAAA,CAAO,OAAO,OAAO,CAAA;AACrB,UAAA,IAAI,MAAA,CAAO,IAAA,KAAS,CAAA,EAAG,GAAA,CAAI,OAAO,KAAK,CAAA;AAAA,QACzC;AAAA,OACF;AAAA,IACF;AAAA,GACF;AACF;AAEA,eAAe,WAAA,CAAY,SAAyB,OAAA,EAAwC;AAC1F,EAAA,IAAI;AACF,IAAA,MAAM,QAAQ,OAAO,CAAA;AAAA,EACvB,CAAA,CAAA,MAAQ;AAAA,EAGR;AACF","file":"index.cjs","sourcesContent":["import {\n type AdapterMessage,\n type MessageHandler,\n type PubSubAdapter,\n isPattern,\n matchTopic,\n} from '@pubber-subber/core';\n\nexport interface MemoryAdapterOptions {\n /**\n * Skip cloning published payloads. By default the adapter passes a\n * `structuredClone`d copy to subscribers so handlers can't mutate the\n * publisher's object. Set this if your payloads are immutable or contain\n * non-cloneable values (functions, class instances) and you accept the risk.\n */\n rawPayloads?: boolean;\n}\n\nexport type MemoryMeta = Record<string, unknown>;\n\nexport function memory(\n opts: MemoryAdapterOptions = {},\n): PubSubAdapter<MemoryMeta, MemoryMeta, MemoryMeta> {\n const exactSubs = new Map<string, Set<MessageHandler>>();\n const patternSubs = new Map<string, Set<MessageHandler>>();\n let idCounter = 0;\n\n const cloneIfNeeded = (value: unknown): unknown => {\n if (opts.rawPayloads) return value;\n try {\n return structuredClone(value);\n } catch {\n return value;\n }\n };\n\n return {\n name: 'memory',\n capabilities: {\n publish: true,\n subscribe: true,\n patternSubscribe: true,\n ack: false,\n },\n\n async publish(topic, payload, meta) {\n const buildMessage = (): AdapterMessage => ({\n topic,\n payload: cloneIfNeeded(payload),\n raw: payload,\n meta,\n });\n\n const exact = exactSubs.get(topic);\n if (exact && exact.size > 0) {\n for (const handler of [...exact]) {\n await safeDeliver(handler, buildMessage());\n }\n }\n\n for (const [pattern, handlers] of patternSubs) {\n if (matchTopic(pattern, topic)) {\n for (const handler of [...handlers]) {\n await safeDeliver(handler, buildMessage());\n }\n }\n }\n },\n\n async subscribe(topic, handler) {\n const map = isPattern(topic) ? patternSubs : exactSubs;\n let set = map.get(topic);\n if (!set) {\n set = new Set();\n map.set(topic, set);\n }\n set.add(handler);\n idCounter += 1;\n const id = `mem-${idCounter}`;\n const ownSet = set;\n return {\n id,\n topic,\n unsubscribe: async () => {\n ownSet.delete(handler);\n if (ownSet.size === 0) map.delete(topic);\n },\n };\n },\n };\n}\n\nasync function safeDeliver(handler: MessageHandler, message: AdapterMessage): Promise<void> {\n try {\n await handler(message);\n } catch {\n // Handler errors are surfaced by the PubSub facade via onError; we\n // swallow here so one bad subscriber doesn't break delivery to the rest.\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PubSubAdapter } from '@pubber-subber/core';
|
|
2
|
+
|
|
3
|
+
interface MemoryAdapterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Skip cloning published payloads. By default the adapter passes a
|
|
6
|
+
* `structuredClone`d copy to subscribers so handlers can't mutate the
|
|
7
|
+
* publisher's object. Set this if your payloads are immutable or contain
|
|
8
|
+
* non-cloneable values (functions, class instances) and you accept the risk.
|
|
9
|
+
*/
|
|
10
|
+
rawPayloads?: boolean;
|
|
11
|
+
}
|
|
12
|
+
type MemoryMeta = Record<string, unknown>;
|
|
13
|
+
declare function memory(opts?: MemoryAdapterOptions): PubSubAdapter<MemoryMeta, MemoryMeta, MemoryMeta>;
|
|
14
|
+
|
|
15
|
+
export { type MemoryAdapterOptions, type MemoryMeta, memory };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PubSubAdapter } from '@pubber-subber/core';
|
|
2
|
+
|
|
3
|
+
interface MemoryAdapterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Skip cloning published payloads. By default the adapter passes a
|
|
6
|
+
* `structuredClone`d copy to subscribers so handlers can't mutate the
|
|
7
|
+
* publisher's object. Set this if your payloads are immutable or contain
|
|
8
|
+
* non-cloneable values (functions, class instances) and you accept the risk.
|
|
9
|
+
*/
|
|
10
|
+
rawPayloads?: boolean;
|
|
11
|
+
}
|
|
12
|
+
type MemoryMeta = Record<string, unknown>;
|
|
13
|
+
declare function memory(opts?: MemoryAdapterOptions): PubSubAdapter<MemoryMeta, MemoryMeta, MemoryMeta>;
|
|
14
|
+
|
|
15
|
+
export { type MemoryAdapterOptions, type MemoryMeta, memory };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isPattern, matchTopic } from '@pubber-subber/core';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
function memory(opts = {}) {
|
|
5
|
+
const exactSubs = /* @__PURE__ */ new Map();
|
|
6
|
+
const patternSubs = /* @__PURE__ */ new Map();
|
|
7
|
+
let idCounter = 0;
|
|
8
|
+
const cloneIfNeeded = (value) => {
|
|
9
|
+
if (opts.rawPayloads) return value;
|
|
10
|
+
try {
|
|
11
|
+
return structuredClone(value);
|
|
12
|
+
} catch {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
name: "memory",
|
|
18
|
+
capabilities: {
|
|
19
|
+
publish: true,
|
|
20
|
+
subscribe: true,
|
|
21
|
+
patternSubscribe: true,
|
|
22
|
+
ack: false
|
|
23
|
+
},
|
|
24
|
+
async publish(topic, payload, meta) {
|
|
25
|
+
const buildMessage = () => ({
|
|
26
|
+
topic,
|
|
27
|
+
payload: cloneIfNeeded(payload),
|
|
28
|
+
raw: payload,
|
|
29
|
+
meta
|
|
30
|
+
});
|
|
31
|
+
const exact = exactSubs.get(topic);
|
|
32
|
+
if (exact && exact.size > 0) {
|
|
33
|
+
for (const handler of [...exact]) {
|
|
34
|
+
await safeDeliver(handler, buildMessage());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const [pattern, handlers] of patternSubs) {
|
|
38
|
+
if (matchTopic(pattern, topic)) {
|
|
39
|
+
for (const handler of [...handlers]) {
|
|
40
|
+
await safeDeliver(handler, buildMessage());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async subscribe(topic, handler) {
|
|
46
|
+
const map = isPattern(topic) ? patternSubs : exactSubs;
|
|
47
|
+
let set = map.get(topic);
|
|
48
|
+
if (!set) {
|
|
49
|
+
set = /* @__PURE__ */ new Set();
|
|
50
|
+
map.set(topic, set);
|
|
51
|
+
}
|
|
52
|
+
set.add(handler);
|
|
53
|
+
idCounter += 1;
|
|
54
|
+
const id = `mem-${idCounter}`;
|
|
55
|
+
const ownSet = set;
|
|
56
|
+
return {
|
|
57
|
+
id,
|
|
58
|
+
topic,
|
|
59
|
+
unsubscribe: async () => {
|
|
60
|
+
ownSet.delete(handler);
|
|
61
|
+
if (ownSet.size === 0) map.delete(topic);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function safeDeliver(handler, message) {
|
|
68
|
+
try {
|
|
69
|
+
await handler(message);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { memory };
|
|
75
|
+
//# sourceMappingURL=index.js.map
|
|
76
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAoBO,SAAS,MAAA,CACd,IAAA,GAA6B,EAAC,EACqB;AACnD,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAiC;AACvD,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAiC;AACzD,EAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,EAAA,MAAM,aAAA,GAAgB,CAAC,KAAA,KAA4B;AACjD,IAAA,IAAI,IAAA,CAAK,aAAa,OAAO,KAAA;AAC7B,IAAA,IAAI;AACF,MAAA,OAAO,gBAAgB,KAAK,CAAA;AAAA,IAC9B,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,QAAA;AAAA,IACN,YAAA,EAAc;AAAA,MACZ,OAAA,EAAS,IAAA;AAAA,MACT,SAAA,EAAW,IAAA;AAAA,MACX,gBAAA,EAAkB,IAAA;AAAA,MAClB,GAAA,EAAK;AAAA,KACP;AAAA,IAEA,MAAM,OAAA,CAAQ,KAAA,EAAO,OAAA,EAAS,IAAA,EAAM;AAClC,MAAA,MAAM,eAAe,OAAuB;AAAA,QAC1C,KAAA;AAAA,QACA,OAAA,EAAS,cAAc,OAAO,CAAA;AAAA,QAC9B,GAAA,EAAK,OAAA;AAAA,QACL;AAAA,OACF,CAAA;AAEA,MAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,GAAA,CAAI,KAAK,CAAA;AACjC,MAAA,IAAI,KAAA,IAAS,KAAA,CAAM,IAAA,GAAO,CAAA,EAAG;AAC3B,QAAA,KAAA,MAAW,OAAA,IAAW,CAAC,GAAG,KAAK,CAAA,EAAG;AAChC,UAAA,MAAM,WAAA,CAAY,OAAA,EAAS,YAAA,EAAc,CAAA;AAAA,QAC3C;AAAA,MACF;AAEA,MAAA,KAAA,MAAW,CAAC,OAAA,EAAS,QAAQ,CAAA,IAAK,WAAA,EAAa;AAC7C,QAAA,IAAI,UAAA,CAAW,OAAA,EAAS,KAAK,CAAA,EAAG;AAC9B,UAAA,KAAA,MAAW,OAAA,IAAW,CAAC,GAAG,QAAQ,CAAA,EAAG;AACnC,YAAA,MAAM,WAAA,CAAY,OAAA,EAAS,YAAA,EAAc,CAAA;AAAA,UAC3C;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,SAAA,CAAU,KAAA,EAAO,OAAA,EAAS;AAC9B,MAAA,MAAM,GAAA,GAAM,SAAA,CAAU,KAAK,CAAA,GAAI,WAAA,GAAc,SAAA;AAC7C,MAAA,IAAI,GAAA,GAAM,GAAA,CAAI,GAAA,CAAI,KAAK,CAAA;AACvB,MAAA,IAAI,CAAC,GAAA,EAAK;AACR,QAAA,GAAA,uBAAU,GAAA,EAAI;AACd,QAAA,GAAA,CAAI,GAAA,CAAI,OAAO,GAAG,CAAA;AAAA,MACpB;AACA,MAAA,GAAA,CAAI,IAAI,OAAO,CAAA;AACf,MAAA,SAAA,IAAa,CAAA;AACb,MAAA,MAAM,EAAA,GAAK,OAAO,SAAS,CAAA,CAAA;AAC3B,MAAA,MAAM,MAAA,GAAS,GAAA;AACf,MAAA,OAAO;AAAA,QACL,EAAA;AAAA,QACA,KAAA;AAAA,QACA,aAAa,YAAY;AACvB,UAAA,MAAA,CAAO,OAAO,OAAO,CAAA;AACrB,UAAA,IAAI,MAAA,CAAO,IAAA,KAAS,CAAA,EAAG,GAAA,CAAI,OAAO,KAAK,CAAA;AAAA,QACzC;AAAA,OACF;AAAA,IACF;AAAA,GACF;AACF;AAEA,eAAe,WAAA,CAAY,SAAyB,OAAA,EAAwC;AAC1F,EAAA,IAAI;AACF,IAAA,MAAM,QAAQ,OAAO,CAAA;AAAA,EACvB,CAAA,CAAA,MAAQ;AAAA,EAGR;AACF","file":"index.js","sourcesContent":["import {\n type AdapterMessage,\n type MessageHandler,\n type PubSubAdapter,\n isPattern,\n matchTopic,\n} from '@pubber-subber/core';\n\nexport interface MemoryAdapterOptions {\n /**\n * Skip cloning published payloads. By default the adapter passes a\n * `structuredClone`d copy to subscribers so handlers can't mutate the\n * publisher's object. Set this if your payloads are immutable or contain\n * non-cloneable values (functions, class instances) and you accept the risk.\n */\n rawPayloads?: boolean;\n}\n\nexport type MemoryMeta = Record<string, unknown>;\n\nexport function memory(\n opts: MemoryAdapterOptions = {},\n): PubSubAdapter<MemoryMeta, MemoryMeta, MemoryMeta> {\n const exactSubs = new Map<string, Set<MessageHandler>>();\n const patternSubs = new Map<string, Set<MessageHandler>>();\n let idCounter = 0;\n\n const cloneIfNeeded = (value: unknown): unknown => {\n if (opts.rawPayloads) return value;\n try {\n return structuredClone(value);\n } catch {\n return value;\n }\n };\n\n return {\n name: 'memory',\n capabilities: {\n publish: true,\n subscribe: true,\n patternSubscribe: true,\n ack: false,\n },\n\n async publish(topic, payload, meta) {\n const buildMessage = (): AdapterMessage => ({\n topic,\n payload: cloneIfNeeded(payload),\n raw: payload,\n meta,\n });\n\n const exact = exactSubs.get(topic);\n if (exact && exact.size > 0) {\n for (const handler of [...exact]) {\n await safeDeliver(handler, buildMessage());\n }\n }\n\n for (const [pattern, handlers] of patternSubs) {\n if (matchTopic(pattern, topic)) {\n for (const handler of [...handlers]) {\n await safeDeliver(handler, buildMessage());\n }\n }\n }\n },\n\n async subscribe(topic, handler) {\n const map = isPattern(topic) ? patternSubs : exactSubs;\n let set = map.get(topic);\n if (!set) {\n set = new Set();\n map.set(topic, set);\n }\n set.add(handler);\n idCounter += 1;\n const id = `mem-${idCounter}`;\n const ownSet = set;\n return {\n id,\n topic,\n unsubscribe: async () => {\n ownSet.delete(handler);\n if (ownSet.size === 0) map.delete(topic);\n },\n };\n },\n };\n}\n\nasync function safeDeliver(handler: MessageHandler, message: AdapterMessage): Promise<void> {\n try {\n await handler(message);\n } catch {\n // Handler errors are surfaced by the PubSub facade via onError; we\n // swallow here so one bad subscriber doesn't break delivery to the rest.\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pubber-subber/memory",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "In-memory pub/sub adapter for @pubber-subber. Use in tests, dev, and single-process apps.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pubsub",
|
|
7
|
+
"pub-sub",
|
|
8
|
+
"messaging",
|
|
9
|
+
"events",
|
|
10
|
+
"adapter",
|
|
11
|
+
"in-memory",
|
|
12
|
+
"eventemitter",
|
|
13
|
+
"in-process",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/samishal1998/pubber-subber/tree/main/packages/memory#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/samishal1998/pubber-subber.git",
|
|
20
|
+
"directory": "packages/memory"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/samishal1998/pubber-subber/issues"
|
|
24
|
+
},
|
|
25
|
+
"author": "Sami Mishal",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/index.d.cts",
|
|
37
|
+
"default": "./dist/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"main": "./dist/index.cjs",
|
|
43
|
+
"module": "./dist/index.js",
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"prebuild": "node ../../scripts/swap-package-json.mjs build",
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"prepublishOnly": "node ../../scripts/swap-package-json.mjs publish",
|
|
53
|
+
"postpublish": "node ../../scripts/swap-package-json.mjs build"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@pubber-subber/core": "^0.0.1"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@pubber-subber/core": "workspace:*"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AdapterMessage,
|
|
3
|
+
type MessageHandler,
|
|
4
|
+
type PubSubAdapter,
|
|
5
|
+
isPattern,
|
|
6
|
+
matchTopic,
|
|
7
|
+
} from '@pubber-subber/core';
|
|
8
|
+
|
|
9
|
+
export interface MemoryAdapterOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Skip cloning published payloads. By default the adapter passes a
|
|
12
|
+
* `structuredClone`d copy to subscribers so handlers can't mutate the
|
|
13
|
+
* publisher's object. Set this if your payloads are immutable or contain
|
|
14
|
+
* non-cloneable values (functions, class instances) and you accept the risk.
|
|
15
|
+
*/
|
|
16
|
+
rawPayloads?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type MemoryMeta = Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
export function memory(
|
|
22
|
+
opts: MemoryAdapterOptions = {},
|
|
23
|
+
): PubSubAdapter<MemoryMeta, MemoryMeta, MemoryMeta> {
|
|
24
|
+
const exactSubs = new Map<string, Set<MessageHandler>>();
|
|
25
|
+
const patternSubs = new Map<string, Set<MessageHandler>>();
|
|
26
|
+
let idCounter = 0;
|
|
27
|
+
|
|
28
|
+
const cloneIfNeeded = (value: unknown): unknown => {
|
|
29
|
+
if (opts.rawPayloads) return value;
|
|
30
|
+
try {
|
|
31
|
+
return structuredClone(value);
|
|
32
|
+
} catch {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
name: 'memory',
|
|
39
|
+
capabilities: {
|
|
40
|
+
publish: true,
|
|
41
|
+
subscribe: true,
|
|
42
|
+
patternSubscribe: true,
|
|
43
|
+
ack: false,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async publish(topic, payload, meta) {
|
|
47
|
+
const buildMessage = (): AdapterMessage => ({
|
|
48
|
+
topic,
|
|
49
|
+
payload: cloneIfNeeded(payload),
|
|
50
|
+
raw: payload,
|
|
51
|
+
meta,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const exact = exactSubs.get(topic);
|
|
55
|
+
if (exact && exact.size > 0) {
|
|
56
|
+
for (const handler of [...exact]) {
|
|
57
|
+
await safeDeliver(handler, buildMessage());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const [pattern, handlers] of patternSubs) {
|
|
62
|
+
if (matchTopic(pattern, topic)) {
|
|
63
|
+
for (const handler of [...handlers]) {
|
|
64
|
+
await safeDeliver(handler, buildMessage());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async subscribe(topic, handler) {
|
|
71
|
+
const map = isPattern(topic) ? patternSubs : exactSubs;
|
|
72
|
+
let set = map.get(topic);
|
|
73
|
+
if (!set) {
|
|
74
|
+
set = new Set();
|
|
75
|
+
map.set(topic, set);
|
|
76
|
+
}
|
|
77
|
+
set.add(handler);
|
|
78
|
+
idCounter += 1;
|
|
79
|
+
const id = `mem-${idCounter}`;
|
|
80
|
+
const ownSet = set;
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
topic,
|
|
84
|
+
unsubscribe: async () => {
|
|
85
|
+
ownSet.delete(handler);
|
|
86
|
+
if (ownSet.size === 0) map.delete(topic);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function safeDeliver(handler: MessageHandler, message: AdapterMessage): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
await handler(message);
|
|
96
|
+
} catch {
|
|
97
|
+
// Handler errors are surfaced by the PubSub facade via onError; we
|
|
98
|
+
// swallow here so one bad subscriber doesn't break delivery to the rest.
|
|
99
|
+
}
|
|
100
|
+
}
|