@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 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"]}
@@ -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 };
@@ -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
+ }