@femtomc/mu-forum 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/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @femtomc/mu-forum
2
+
3
+ Forum/message store backed by a JSONL store, with helpers for posting, reading, and listing topics.
4
+
5
+ ## Install
6
+
7
+ After publishing:
8
+
9
+ ```bash
10
+ npm install @femtomc/mu-forum
11
+ # or: bun add @femtomc/mu-forum
12
+ ```
13
+
14
+ From this repo:
15
+
16
+ ```bash
17
+ cd mu
18
+ bun install
19
+ bun run build
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { InMemoryJsonlStore } from "@femtomc/mu-core";
26
+ import { ForumStore } from "@femtomc/mu-forum";
27
+
28
+ const forum = new ForumStore(new InMemoryJsonlStore());
29
+
30
+ await forum.post("issue:demo", "hello", "worker");
31
+ console.log(await forum.read("issue:demo"));
32
+ console.log(await forum.topics("issue:"));
33
+ ```
34
+
35
+ ## Tests / Typecheck
36
+
37
+ From the `mu/` repo root:
38
+
39
+ ```bash
40
+ bun test packages/forum
41
+ bun run typecheck
42
+ ```
43
+
44
+ ## Runtime
45
+
46
+ - Runtime-agnostic: works in Node or the browser.
47
+ - You provide a `JsonlStore` implementation (see `@femtomc/mu-core/node` for `FsJsonlStore`, or `@femtomc/mu-core/browser` for IndexedDB/localStorage stores).
@@ -0,0 +1,18 @@
1
+ import type { ForumMessage, JsonlStore } from "@femtomc/mu-core";
2
+ import { EventLog } from "@femtomc/mu-core";
3
+ export type ForumTopicSummary = {
4
+ topic: string;
5
+ messages: number;
6
+ last_at: number;
7
+ };
8
+ export declare class ForumStore {
9
+ #private;
10
+ readonly events: EventLog;
11
+ constructor(forum: JsonlStore<unknown>, opts?: {
12
+ events?: EventLog;
13
+ });
14
+ post(topic: string, body: string, author?: string): Promise<ForumMessage>;
15
+ read(topic: string, limit?: number): Promise<ForumMessage[]>;
16
+ topics(prefix?: string | null): Promise<ForumTopicSummary[]>;
17
+ }
18
+ //# sourceMappingURL=forum_store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forum_store.d.ts","sourceRoot":"","sources":["../src/forum_store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,QAAQ,EAA4C,MAAM,kBAAkB,CAAC;AAEtF,MAAM,MAAM,iBAAiB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,qBAAa,UAAU;;IACtB,SAAgB,MAAM,EAAE,QAAQ,CAAC;gBAGd,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,QAAQ,CAAA;KAAO;IAoBlE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAiB,GAAG,OAAO,CAAC,YAAY,CAAC;IA6BnF,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAMhE,MAAM,CAAC,MAAM,GAAE,MAAM,GAAG,IAAW,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAgC/E"}
@@ -0,0 +1,80 @@
1
+ import { EventLog, ForumMessageSchema, NullEventSink, nowTs } from "@femtomc/mu-core";
2
+ export class ForumStore {
3
+ events;
4
+ #forum;
5
+ constructor(forum, opts = {}) {
6
+ this.#forum = forum;
7
+ this.events = opts.events ?? new EventLog(new NullEventSink());
8
+ }
9
+ async #load() {
10
+ const rows = await this.#forum.read();
11
+ return rows.map((row, idx) => {
12
+ const parsed = ForumMessageSchema.safeParse(row);
13
+ if (!parsed.success) {
14
+ throw new Error(`invalid forum row ${idx}: ${parsed.error.message}`);
15
+ }
16
+ return parsed.data;
17
+ });
18
+ }
19
+ async #save(rows) {
20
+ await this.#forum.write(rows);
21
+ }
22
+ async post(topic, body, author = "system") {
23
+ let issueId;
24
+ if (topic.startsWith("issue:")) {
25
+ const candidate = topic.slice("issue:".length).trim();
26
+ if (candidate.length > 0) {
27
+ issueId = candidate;
28
+ }
29
+ }
30
+ const msg = ForumMessageSchema.parse({
31
+ topic,
32
+ body,
33
+ author,
34
+ created_at: nowTs(),
35
+ });
36
+ const rows = await this.#load();
37
+ rows.push(msg);
38
+ await this.#save(rows);
39
+ await this.events.emit("forum.post", {
40
+ source: "forum_store",
41
+ issueId,
42
+ payload: { message: msg },
43
+ });
44
+ return msg;
45
+ }
46
+ async read(topic, limit = 50) {
47
+ const rows = await this.#load();
48
+ const matching = rows.filter((row) => row.topic === topic);
49
+ return matching.slice(-limit);
50
+ }
51
+ async topics(prefix = null) {
52
+ const rows = await this.#load();
53
+ const byTopic = new Map();
54
+ for (const row of rows) {
55
+ const topic = row.topic;
56
+ if (prefix && !topic.startsWith(prefix)) {
57
+ continue;
58
+ }
59
+ const entry = byTopic.get(topic) ?? { topic, messages: 0, last_at: 0 };
60
+ entry.messages += 1;
61
+ entry.last_at = Math.max(entry.last_at, Math.trunc(row.created_at ?? 0));
62
+ byTopic.set(topic, entry);
63
+ }
64
+ // Match Python's `sorted(..., key=(last_at, topic), reverse=True)`.
65
+ const out = [...byTopic.values()];
66
+ out.sort((a, b) => {
67
+ if (a.last_at !== b.last_at) {
68
+ return b.last_at - a.last_at;
69
+ }
70
+ if (a.topic < b.topic) {
71
+ return 1;
72
+ }
73
+ if (a.topic > b.topic) {
74
+ return -1;
75
+ }
76
+ return 0;
77
+ });
78
+ return out;
79
+ }
80
+ }
@@ -0,0 +1,3 @@
1
+ export type { ForumTopicSummary } from "./forum_store.js";
2
+ export { ForumStore } from "./forum_store.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { ForumStore } from "./forum_store.js";
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@femtomc/mu-forum",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": ["dist/**"],
14
+ "dependencies": {
15
+ "@femtomc/mu-core": "0.1.0"
16
+ }
17
+ }