@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 +47 -0
- package/dist/forum_store.d.ts +18 -0
- package/dist/forum_store.d.ts.map +1 -0
- package/dist/forum_store.js +80 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +17 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|