@better-state/server 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/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +50 -0
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +4 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +55 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +241 -0
- package/dist/index.js.map +1 -0
- package/dist/seed.d.ts +2 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +28 -0
- package/dist/seed.js.map +1 -0
- package/dist/state-engine.d.ts +57 -0
- package/dist/state-engine.d.ts.map +1 -0
- package/dist/state-engine.js +141 -0
- package/dist/state-engine.js.map +1 -0
- package/package.json +54 -0
- package/public/playground.html +301 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomBytes, createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const dataDir = path.join(process.cwd(), ".better-state");
|
|
6
|
+
mkdirSync(dataDir, { recursive: true });
|
|
7
|
+
process.env.DATABASE_PATH = process.env.DATABASE_PATH || path.join(dataDir, "state.db");
|
|
8
|
+
process.env.DEV_KEY_PATH = process.env.DEV_KEY_PATH || path.join(dataDir, ".dev-key");
|
|
9
|
+
const { createDatabase, runMigrations } = await import("./db.js");
|
|
10
|
+
const PORT = parseInt(process.env.PORT || "3001", 10);
|
|
11
|
+
function autoSeed() {
|
|
12
|
+
const db = createDatabase();
|
|
13
|
+
runMigrations(db);
|
|
14
|
+
const devKeyPath = process.env.DEV_KEY_PATH;
|
|
15
|
+
const existing = db.prepare("SELECT id FROM namespaces WHERE name = ?").get("default");
|
|
16
|
+
if (existing) {
|
|
17
|
+
db.close();
|
|
18
|
+
if (existsSync(devKeyPath)) {
|
|
19
|
+
return readFileSync(devKeyPath, "utf-8").trim();
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
const rawKey = randomBytes(32).toString("hex");
|
|
24
|
+
const hashedKey = createHash("sha256").update(rawKey).digest("hex");
|
|
25
|
+
const id = randomUUID();
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
db.prepare("INSERT INTO namespaces (id, name, api_key, created_at) VALUES (?, ?, ?, ?)").run(id, "default", hashedKey, now);
|
|
28
|
+
writeFileSync(devKeyPath, rawKey, "utf-8");
|
|
29
|
+
db.close();
|
|
30
|
+
return rawKey;
|
|
31
|
+
}
|
|
32
|
+
const apiKey = autoSeed();
|
|
33
|
+
const box = [
|
|
34
|
+
"",
|
|
35
|
+
" ┌─────────────────────────────────────────────────────┐",
|
|
36
|
+
" │ │",
|
|
37
|
+
" │ Better-State Server │",
|
|
38
|
+
" │ │",
|
|
39
|
+
` │ Local: http://localhost:${PORT} │`,
|
|
40
|
+
` │ Playground: http://localhost:${PORT}/playground │`,
|
|
41
|
+
` │ Data: ${dataDir} │`.slice(0, 57) + "│",
|
|
42
|
+
" │ │",
|
|
43
|
+
];
|
|
44
|
+
if (apiKey) {
|
|
45
|
+
box.push(" │ API Key (save this): │", ` │ ${apiKey} │`, " │ │", " │ Usage in your app: │", " │ │", " │ import { createClient } │", " │ from '@better-state/client' │", " │ │", " │ const bs = createClient( │", ` │ 'http://localhost:${PORT}', │`, " │ { apiKey: '<your-key>' } │", " │ ) │", " │ const counter = bs.state('counter', 0) │", " │ counter.set(1) │", " │ │");
|
|
46
|
+
}
|
|
47
|
+
box.push(" └─────────────────────────────────────────────────────┘", "");
|
|
48
|
+
console.log(box.join("\n"));
|
|
49
|
+
await import("./index.js");
|
|
50
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/db.d.ts
ADDED
package/dist/db.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAQtC,wBAAgB,cAAc,IAAI,QAAQ,CAAC,QAAQ,CAUlD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAuCzD"}
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, "..", "data", "state.db");
|
|
7
|
+
export function createDatabase() {
|
|
8
|
+
const dir = path.dirname(DB_PATH);
|
|
9
|
+
mkdirSync(dir, { recursive: true });
|
|
10
|
+
const db = new Database(DB_PATH);
|
|
11
|
+
db.pragma("journal_mode = WAL");
|
|
12
|
+
db.pragma("foreign_keys = ON");
|
|
13
|
+
return db;
|
|
14
|
+
}
|
|
15
|
+
export function runMigrations(db) {
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS namespaces (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
name TEXT NOT NULL,
|
|
20
|
+
api_key TEXT NOT NULL UNIQUE,
|
|
21
|
+
created_at INTEGER NOT NULL
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS states (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
namespace TEXT NOT NULL REFERENCES namespaces(id),
|
|
27
|
+
key TEXT NOT NULL,
|
|
28
|
+
initial TEXT NOT NULL,
|
|
29
|
+
snapshot TEXT NOT NULL,
|
|
30
|
+
version INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
created_at INTEGER NOT NULL,
|
|
32
|
+
updated_at INTEGER NOT NULL,
|
|
33
|
+
UNIQUE(namespace, key)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS event_log (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
state_id TEXT NOT NULL REFERENCES states(id),
|
|
39
|
+
client_id TEXT NOT NULL,
|
|
40
|
+
client_ts INTEGER NOT NULL,
|
|
41
|
+
server_ts INTEGER NOT NULL,
|
|
42
|
+
seq INTEGER NOT NULL,
|
|
43
|
+
mutation TEXT NOT NULL,
|
|
44
|
+
meta TEXT,
|
|
45
|
+
created_at INTEGER NOT NULL
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_event_log_state_ts
|
|
49
|
+
ON event_log(state_id, server_ts, seq);
|
|
50
|
+
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_states_namespace_key
|
|
52
|
+
ON states(namespace, key);
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=db.js.map
|
package/dist/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;AAE5F,MAAM,UAAU,cAAc;IAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAClC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEpC,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC;IAEjC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAE/B,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAqB;IACjD,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCP,CAAC,CAAC;AACL,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { Server } from "socket.io";
|
|
3
|
+
declare const app: ReturnType<typeof express>;
|
|
4
|
+
declare const io: Server<import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, any>;
|
|
5
|
+
export { app, io };
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAO9B,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAYnC,QAAA,MAAM,GAAG,EAAE,UAAU,CAAC,OAAO,OAAO,CAAa,CAAC;AAKlD,QAAA,MAAM,EAAE,+HAEN,CAAC;AAgRH,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { randomBytes, createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { Server } from "socket.io";
|
|
9
|
+
import { createDatabase, runMigrations } from "./db.js";
|
|
10
|
+
import { ensureState, processMutations, getHistory } from "./state-engine.js";
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PORT = parseInt(process.env.PORT || "3001", 10);
|
|
13
|
+
const CORS_ORIGIN = process.env.CORS_ORIGIN || "*";
|
|
14
|
+
const db = createDatabase();
|
|
15
|
+
runMigrations(db);
|
|
16
|
+
const app = express();
|
|
17
|
+
app.use(cors({ origin: CORS_ORIGIN }));
|
|
18
|
+
app.use(express.json());
|
|
19
|
+
const httpServer = createServer(app);
|
|
20
|
+
const io = new Server(httpServer, {
|
|
21
|
+
cors: { origin: CORS_ORIGIN, methods: ["GET", "POST"] },
|
|
22
|
+
});
|
|
23
|
+
// ─── REST API (Dashboard) ────────────────────────────────────────────────────
|
|
24
|
+
app.get("/api/v1/health", (_req, res) => {
|
|
25
|
+
res.json({ status: "ok", uptime: process.uptime() });
|
|
26
|
+
});
|
|
27
|
+
app.get("/api/v1/namespaces", (_req, res) => {
|
|
28
|
+
const rows = db.prepare("SELECT id, name, created_at FROM namespaces").all();
|
|
29
|
+
res.json(rows);
|
|
30
|
+
});
|
|
31
|
+
app.post("/api/v1/namespaces", (req, res) => {
|
|
32
|
+
const { name } = req.body;
|
|
33
|
+
if (!name || typeof name !== "string") {
|
|
34
|
+
res.status(400).json({ error: "name is required" });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const rawKey = randomBytes(32).toString("hex");
|
|
38
|
+
const hashedKey = createHash("sha256").update(rawKey).digest("hex");
|
|
39
|
+
const id = randomUUID();
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
db.prepare("INSERT INTO namespaces (id, name, api_key, created_at) VALUES (?, ?, ?, ?)").run(id, name, hashedKey, now);
|
|
42
|
+
res.status(201).json({ id, name, apiKey: rawKey, created_at: now });
|
|
43
|
+
});
|
|
44
|
+
app.get("/api/v1/states", (req, res) => {
|
|
45
|
+
const namespace = req.query.namespace;
|
|
46
|
+
if (!namespace) {
|
|
47
|
+
res.status(400).json({ error: "namespace query param is required" });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const rows = db
|
|
51
|
+
.prepare("SELECT id, key, snapshot, version, created_at, updated_at FROM states WHERE namespace = ?")
|
|
52
|
+
.all(namespace);
|
|
53
|
+
res.json(rows);
|
|
54
|
+
});
|
|
55
|
+
app.get("/api/v1/states/:key/history", (req, res) => {
|
|
56
|
+
const namespace = req.query.namespace;
|
|
57
|
+
if (!namespace) {
|
|
58
|
+
res.status(400).json({ error: "namespace query param is required" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const stateRow = db
|
|
62
|
+
.prepare("SELECT * FROM states WHERE namespace = ? AND key = ?")
|
|
63
|
+
.get(namespace, req.params.key);
|
|
64
|
+
if (!stateRow) {
|
|
65
|
+
res.status(404).json({ error: "state not found" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const limit = parseInt(req.query.limit, 10) || 50;
|
|
69
|
+
const cursor = req.query.cursor;
|
|
70
|
+
const { events, hasMore } = getHistory(db, stateRow.id, limit, cursor);
|
|
71
|
+
res.json({
|
|
72
|
+
key: stateRow.key,
|
|
73
|
+
version: stateRow.version,
|
|
74
|
+
events,
|
|
75
|
+
cursor: events.length > 0 ? events[events.length - 1].id : null,
|
|
76
|
+
hasMore,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
// ─── Playground & SDK Serving ─────────────────────────────────────────────────
|
|
80
|
+
import { existsSync } from "node:fs";
|
|
81
|
+
const CLIENT_BUNDLE = path.resolve(__dirname, "../../client/dist/browser.mjs");
|
|
82
|
+
const CLIENT_SOURCEMAP = path.resolve(__dirname, "../../client/dist/browser.mjs.map");
|
|
83
|
+
const PLAYGROUND_HTML = path.resolve(__dirname, "../public/playground.html");
|
|
84
|
+
app.get("/sdk/browser.mjs", (_req, res) => {
|
|
85
|
+
if (existsSync(CLIENT_BUNDLE)) {
|
|
86
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
87
|
+
res.sendFile(CLIENT_BUNDLE);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
res.redirect("https://unpkg.com/@better-state/client/dist/browser.mjs");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
app.get("/sdk/browser.mjs.map", (_req, res) => {
|
|
94
|
+
if (existsSync(CLIENT_SOURCEMAP)) {
|
|
95
|
+
res.setHeader("Content-Type", "application/json");
|
|
96
|
+
res.sendFile(CLIENT_SOURCEMAP);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
res.redirect("https://unpkg.com/@better-state/client/dist/browser.mjs.map");
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
app.get("/playground", (_req, res) => {
|
|
103
|
+
const ns = db.prepare("SELECT api_key FROM namespaces WHERE name = ?").get("default");
|
|
104
|
+
if (!ns) {
|
|
105
|
+
res.status(500).send("No default namespace. Run the server via CLI to auto-seed.");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const devKeyPath = process.env.DEV_KEY_PATH || path.resolve(__dirname, "../data/.dev-key");
|
|
109
|
+
let apiKey;
|
|
110
|
+
try {
|
|
111
|
+
apiKey = readFileSync(devKeyPath, "utf-8").trim();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
res.status(500).send("Dev API key not found. Delete the .better-state directory and restart the server.");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!existsSync(PLAYGROUND_HTML)) {
|
|
118
|
+
const inline = getInlinePlayground(apiKey);
|
|
119
|
+
res.setHeader("Content-Type", "text/html");
|
|
120
|
+
res.send(inline);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const html = readFileSync(PLAYGROUND_HTML, "utf-8").replace("__API_KEY__", apiKey);
|
|
124
|
+
res.setHeader("Content-Type", "text/html");
|
|
125
|
+
res.send(html);
|
|
126
|
+
});
|
|
127
|
+
function getInlinePlayground(apiKey) {
|
|
128
|
+
return `<!DOCTYPE html>
|
|
129
|
+
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
130
|
+
<title>Better-State Playground</title>
|
|
131
|
+
<style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:1.5rem}
|
|
132
|
+
h1{font-size:1.5rem;font-weight:600}#counter{font-size:4rem;font-weight:700;color:#38bdf8}
|
|
133
|
+
.row{display:flex;gap:.75rem}button{background:#1e293b;color:#e2e8f0;border:1px solid #334155;padding:.5rem 1.25rem;border-radius:.5rem;font-size:1rem;cursor:pointer;transition:background .15s}
|
|
134
|
+
button:hover{background:#334155}#status{font-size:.875rem;color:#94a3b8}</style></head>
|
|
135
|
+
<body><h1>Better-State Playground</h1><div id="counter">0</div>
|
|
136
|
+
<div class="row"><button onclick="dec()">-1</button><button onclick="reset()">Reset</button><button onclick="inc()">+1</button></div>
|
|
137
|
+
<div id="status">connecting...</div>
|
|
138
|
+
<script type="module">
|
|
139
|
+
import{createClient}from"/sdk/browser.mjs";
|
|
140
|
+
const bs=createClient(location.origin,{apiKey:"${apiKey}",debug:false});
|
|
141
|
+
bs.onStatusChange(s=>document.getElementById("status").textContent=s);
|
|
142
|
+
const counter=bs.state("counter",0);
|
|
143
|
+
counter.subscribe(v=>document.getElementById("counter").textContent=String(v));
|
|
144
|
+
window.inc=()=>counter.update(n=>n+1);
|
|
145
|
+
window.dec=()=>counter.update(n=>n-1);
|
|
146
|
+
window.reset=()=>counter.set(0);
|
|
147
|
+
</script></body></html>`;
|
|
148
|
+
}
|
|
149
|
+
// ─── WebSocket (Real-time Sync) ──────────────────────────────────────────────
|
|
150
|
+
const namespaceCache = new Map(); // apiKey hash → namespace id
|
|
151
|
+
function authenticateApiKey(rawKey) {
|
|
152
|
+
const hash = createHash("sha256").update(rawKey).digest("hex");
|
|
153
|
+
if (namespaceCache.has(hash))
|
|
154
|
+
return namespaceCache.get(hash);
|
|
155
|
+
const row = db
|
|
156
|
+
.prepare("SELECT id FROM namespaces WHERE api_key = ?")
|
|
157
|
+
.get(hash);
|
|
158
|
+
if (!row)
|
|
159
|
+
return null;
|
|
160
|
+
namespaceCache.set(hash, row.id);
|
|
161
|
+
return row.id;
|
|
162
|
+
}
|
|
163
|
+
io.on("connection", (socket) => {
|
|
164
|
+
let authedNamespace = null;
|
|
165
|
+
let clientId = null;
|
|
166
|
+
console.log(`[ws] new connection: ${socket.id}`);
|
|
167
|
+
socket.on("auth", (payload, ack) => {
|
|
168
|
+
const nsId = authenticateApiKey(payload.apiKey);
|
|
169
|
+
if (!nsId) {
|
|
170
|
+
socket.emit("auth:error", { message: "Invalid API key" });
|
|
171
|
+
if (typeof ack === "function")
|
|
172
|
+
ack({ ok: false });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
authedNamespace = nsId;
|
|
176
|
+
clientId = payload.clientId;
|
|
177
|
+
console.log(`[ws] auth ok: socket=${socket.id} client=${clientId}`);
|
|
178
|
+
socket.emit("auth:ok", { sessionId: socket.id });
|
|
179
|
+
if (typeof ack === "function")
|
|
180
|
+
ack({ ok: true });
|
|
181
|
+
});
|
|
182
|
+
socket.on("subscribe", (payload) => {
|
|
183
|
+
if (!authedNamespace) {
|
|
184
|
+
socket.emit("auth:error", { message: "Not authenticated" });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (const entry of payload.keys) {
|
|
188
|
+
const key = typeof entry === "string" ? entry : entry.key;
|
|
189
|
+
const initialValue = typeof entry === "object" && entry !== null ? entry.initialValue : null;
|
|
190
|
+
const room = `${authedNamespace}:${key}`;
|
|
191
|
+
socket.join(room);
|
|
192
|
+
const state = ensureState(db, authedNamespace, key, initialValue);
|
|
193
|
+
socket.emit("state:init", {
|
|
194
|
+
key,
|
|
195
|
+
value: JSON.parse(state.snapshot),
|
|
196
|
+
version: state.version,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
socket.on("unsubscribe", (payload) => {
|
|
201
|
+
if (!authedNamespace)
|
|
202
|
+
return;
|
|
203
|
+
for (const key of payload.keys) {
|
|
204
|
+
socket.leave(`${authedNamespace}:${key}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
socket.on("mutate", (payload) => {
|
|
208
|
+
if (!authedNamespace || !clientId) {
|
|
209
|
+
socket.emit("auth:error", { message: "Not authenticated" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
console.log(`[ws] mutate: key=${payload.key} client=${clientId} count=${payload.mutations.length}`);
|
|
214
|
+
const { value, version, mutationIds } = processMutations(db, authedNamespace, payload.key, clientId, payload.mutations);
|
|
215
|
+
socket.emit("mutate:ack", { mutationIds });
|
|
216
|
+
const room = `${authedNamespace}:${payload.key}`;
|
|
217
|
+
io.to(room).emit("state:update", {
|
|
218
|
+
key: payload.key,
|
|
219
|
+
value,
|
|
220
|
+
version,
|
|
221
|
+
});
|
|
222
|
+
console.log(`[ws] broadcast: key=${payload.key} version=${version}`);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
socket.emit("mutate:error", {
|
|
226
|
+
message: err.message,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
socket.on("disconnect", (reason) => {
|
|
231
|
+
console.log(`[ws] disconnect: socket=${socket.id} client=${clientId} reason=${reason}`);
|
|
232
|
+
authedNamespace = null;
|
|
233
|
+
clientId = null;
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
237
|
+
httpServer.listen(PORT, () => {
|
|
238
|
+
console.log(`[better-state] server running on http://localhost:${PORT}`);
|
|
239
|
+
});
|
|
240
|
+
export { app, io };
|
|
241
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/seed.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":""}
|
package/dist/seed.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createDatabase, runMigrations } from "./db.js";
|
|
2
|
+
import { randomBytes, createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const db = createDatabase();
|
|
8
|
+
runMigrations(db);
|
|
9
|
+
const existing = db.prepare("SELECT id FROM namespaces WHERE name = ?").get("default");
|
|
10
|
+
if (existing) {
|
|
11
|
+
console.log("[seed] Default namespace already exists. Skipping.");
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
const rawKey = randomBytes(32).toString("hex");
|
|
15
|
+
const hashedKey = createHash("sha256").update(rawKey).digest("hex");
|
|
16
|
+
const id = randomUUID();
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
db.prepare("INSERT INTO namespaces (id, name, api_key, created_at) VALUES (?, ?, ?, ?)").run(id, "default", hashedKey, now);
|
|
19
|
+
const devKeyPath = path.resolve(__dirname, "../data/.dev-key");
|
|
20
|
+
writeFileSync(devKeyPath, rawKey, "utf-8");
|
|
21
|
+
console.log("[seed] Created default namespace:");
|
|
22
|
+
console.log(` Namespace ID : ${id}`);
|
|
23
|
+
console.log(` API Key : ${rawKey}`);
|
|
24
|
+
console.log(` Dev key file : ${devKeyPath}`);
|
|
25
|
+
console.log("");
|
|
26
|
+
console.log("Use it in your client: createClient({ apiKey: '...' })");
|
|
27
|
+
console.log("Or open http://localhost:3001/playground to test interactively.");
|
|
28
|
+
//# sourceMappingURL=seed.js.map
|
package/dist/seed.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/D,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;AAC5B,aAAa,CAAC,EAAE,CAAC,CAAC;AAElB,MAAM,QAAQ,GAAG,EAAE,CAAC,OAAO,CAAC,0CAA0C,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AACvF,IAAI,QAAQ,EAAE,CAAC;IACb,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;IAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC/C,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACpE,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;AACxB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;AAEvB,EAAE,CAAC,OAAO,CACR,4EAA4E,CAC7E,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;AAErC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;AAC/D,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAE3C,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACjD,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;AACtC,OAAO,CAAC,GAAG,CAAC,oBAAoB,MAAM,EAAE,CAAC,CAAC;AAC1C,OAAO,CAAC,GAAG,CAAC,oBAAoB,UAAU,EAAE,CAAC,CAAC;AAC9C,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAChB,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;AACtE,OAAO,CAAC,GAAG,CAAC,iEAAiE,CAAC,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
export interface MutationInput {
|
|
3
|
+
id: string;
|
|
4
|
+
clientTs: number;
|
|
5
|
+
fn: string;
|
|
6
|
+
value?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface EventRecord {
|
|
9
|
+
id: string;
|
|
10
|
+
state_id: string;
|
|
11
|
+
client_id: string;
|
|
12
|
+
client_ts: number;
|
|
13
|
+
server_ts: number;
|
|
14
|
+
seq: number;
|
|
15
|
+
mutation: string;
|
|
16
|
+
meta: string | null;
|
|
17
|
+
created_at: number;
|
|
18
|
+
}
|
|
19
|
+
export interface StateRecord {
|
|
20
|
+
id: string;
|
|
21
|
+
namespace: string;
|
|
22
|
+
key: string;
|
|
23
|
+
initial: string;
|
|
24
|
+
snapshot: string;
|
|
25
|
+
version: number;
|
|
26
|
+
created_at: number;
|
|
27
|
+
updated_at: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ensures a state row exists for the given namespace + key.
|
|
31
|
+
* If it doesn't exist, creates one with the provided initial value.
|
|
32
|
+
* Returns the state row.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ensureState(db: Database.Database, namespace: string, key: string, initialValue?: unknown): StateRecord;
|
|
35
|
+
/**
|
|
36
|
+
* Processes a batch of mutations for a given state key.
|
|
37
|
+
* Appends events to the log, replays to compute new snapshot,
|
|
38
|
+
* persists the snapshot, and returns the new value + version.
|
|
39
|
+
*/
|
|
40
|
+
export declare function processMutations(db: Database.Database, namespace: string, key: string, clientId: string, mutations: MutationInput[]): {
|
|
41
|
+
value: unknown;
|
|
42
|
+
version: number;
|
|
43
|
+
mutationIds: string[];
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Replays the full event log for a state to compute the current value.
|
|
47
|
+
* This is the authoritative state computation.
|
|
48
|
+
*/
|
|
49
|
+
export declare function replayState(db: Database.Database, stateId: string, initialJson: string): unknown;
|
|
50
|
+
/**
|
|
51
|
+
* Returns the event history for a state key (paginated).
|
|
52
|
+
*/
|
|
53
|
+
export declare function getHistory(db: Database.Database, stateId: string, limit?: number, cursor?: string): {
|
|
54
|
+
events: EventRecord[];
|
|
55
|
+
hasMore: boolean;
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=state-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-engine.d.ts","sourceRoot":"","sources":["../src/state-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAG3C,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,YAAY,GAAE,OAAc,GAC3B,WAAW,CAmBb;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,aAAa,EAAE,GACzB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAmC5D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,OAAO,CAoCT;AAoCD;;GAEG;AACH,wBAAgB,UAAU,CACxB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,OAAO,EAAE,MAAM,EACf,KAAK,SAAK,EACV,MAAM,CAAC,EAAE,MAAM,GACd;IAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CA0B7C"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { v4 as uuid } from "uuid";
|
|
2
|
+
/**
|
|
3
|
+
* Ensures a state row exists for the given namespace + key.
|
|
4
|
+
* If it doesn't exist, creates one with the provided initial value.
|
|
5
|
+
* Returns the state row.
|
|
6
|
+
*/
|
|
7
|
+
export function ensureState(db, namespace, key, initialValue = null) {
|
|
8
|
+
const existing = db
|
|
9
|
+
.prepare("SELECT * FROM states WHERE namespace = ? AND key = ?")
|
|
10
|
+
.get(namespace, key);
|
|
11
|
+
if (existing)
|
|
12
|
+
return existing;
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const serialized = JSON.stringify(initialValue);
|
|
15
|
+
const id = uuid();
|
|
16
|
+
db.prepare(`INSERT INTO states (id, namespace, key, initial, snapshot, version, created_at, updated_at)
|
|
17
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, ?)`).run(id, namespace, key, serialized, serialized, now, now);
|
|
18
|
+
return db
|
|
19
|
+
.prepare("SELECT * FROM states WHERE id = ?")
|
|
20
|
+
.get(id);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Processes a batch of mutations for a given state key.
|
|
24
|
+
* Appends events to the log, replays to compute new snapshot,
|
|
25
|
+
* persists the snapshot, and returns the new value + version.
|
|
26
|
+
*/
|
|
27
|
+
export function processMutations(db, namespace, key, clientId, mutations) {
|
|
28
|
+
const state = ensureState(db, namespace, key);
|
|
29
|
+
const maxSeqRow = db
|
|
30
|
+
.prepare("SELECT MAX(seq) as max_seq FROM event_log WHERE state_id = ?")
|
|
31
|
+
.get(state.id);
|
|
32
|
+
let seq = (maxSeqRow.max_seq ?? -1) + 1;
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const insertEvent = db.prepare(`INSERT INTO event_log (id, state_id, client_id, client_ts, server_ts, seq, mutation, meta, created_at)
|
|
35
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
36
|
+
const mutationIds = [];
|
|
37
|
+
const insertAll = db.transaction(() => {
|
|
38
|
+
for (const mut of mutations) {
|
|
39
|
+
const eventId = mut.id || uuid();
|
|
40
|
+
const meta = mut.value !== undefined ? JSON.stringify({ fallbackValue: mut.value }) : null;
|
|
41
|
+
insertEvent.run(eventId, state.id, clientId, mut.clientTs, now, seq, mut.fn, meta, now);
|
|
42
|
+
mutationIds.push(eventId);
|
|
43
|
+
seq++;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
insertAll();
|
|
47
|
+
const newValue = replayState(db, state.id, state.initial);
|
|
48
|
+
const newVersion = state.version + mutations.length;
|
|
49
|
+
db.prepare("UPDATE states SET snapshot = ?, version = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(newValue), newVersion, Date.now(), state.id);
|
|
50
|
+
return { value: newValue, version: newVersion, mutationIds };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Replays the full event log for a state to compute the current value.
|
|
54
|
+
* This is the authoritative state computation.
|
|
55
|
+
*/
|
|
56
|
+
export function replayState(db, stateId, initialJson) {
|
|
57
|
+
const events = db
|
|
58
|
+
.prepare("SELECT mutation, meta FROM event_log WHERE state_id = ? ORDER BY server_ts ASC, seq ASC")
|
|
59
|
+
.all(stateId);
|
|
60
|
+
let state = JSON.parse(initialJson);
|
|
61
|
+
for (const event of events) {
|
|
62
|
+
if (event.mutation === "__SET__") {
|
|
63
|
+
if (event.meta) {
|
|
64
|
+
try {
|
|
65
|
+
state = JSON.parse(event.meta).fallbackValue;
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
state = applyMutation(state, event.mutation);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (event.meta) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(event.meta);
|
|
78
|
+
if (parsed.fallbackValue !== undefined) {
|
|
79
|
+
state = parsed.fallbackValue;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
}
|
|
85
|
+
console.error(`Skipping faulting mutation: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
const SANDBOX_KEYS = [
|
|
91
|
+
"crypto", "JSON", "Math", "Date", "Array", "Object", "String",
|
|
92
|
+
"Number", "Boolean", "parseInt", "parseFloat", "isNaN", "isFinite",
|
|
93
|
+
"structuredClone", "undefined", "NaN", "Infinity",
|
|
94
|
+
];
|
|
95
|
+
const SANDBOX_VALUES = [
|
|
96
|
+
globalThis.crypto, JSON, Math, Date, Array, Object, String,
|
|
97
|
+
Number, Boolean, parseInt, parseFloat, isNaN, isFinite,
|
|
98
|
+
structuredClone, undefined, NaN, Infinity,
|
|
99
|
+
];
|
|
100
|
+
/**
|
|
101
|
+
* Applies a serialized mutation function to a state value.
|
|
102
|
+
* The mutation must be a pure arrow function: (old) => newValue
|
|
103
|
+
*
|
|
104
|
+
* Runs in a sandbox with a curated set of globals. Mutation functions
|
|
105
|
+
* should ideally be deterministic, but we provide crypto etc. as a
|
|
106
|
+
* safety net for common patterns.
|
|
107
|
+
*/
|
|
108
|
+
function applyMutation(state, mutationBody) {
|
|
109
|
+
const ARROW_FN = /^\s*\(?\s*\w+\s*\)?\s*=>/;
|
|
110
|
+
if (!ARROW_FN.test(mutationBody)) {
|
|
111
|
+
throw new Error(`Invalid mutation format: must be an arrow function`);
|
|
112
|
+
}
|
|
113
|
+
const fn = new Function("__state__", ...SANDBOX_KEYS, `"use strict"; const __fn__ = ${mutationBody}; return __fn__(__state__);`);
|
|
114
|
+
return fn(structuredClone(state), ...SANDBOX_VALUES);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns the event history for a state key (paginated).
|
|
118
|
+
*/
|
|
119
|
+
export function getHistory(db, stateId, limit = 50, cursor) {
|
|
120
|
+
let events;
|
|
121
|
+
if (cursor) {
|
|
122
|
+
events = db
|
|
123
|
+
.prepare(`SELECT * FROM event_log
|
|
124
|
+
WHERE state_id = ? AND (server_ts, seq) > (
|
|
125
|
+
SELECT server_ts, seq FROM event_log WHERE id = ?
|
|
126
|
+
)
|
|
127
|
+
ORDER BY server_ts ASC, seq ASC
|
|
128
|
+
LIMIT ?`)
|
|
129
|
+
.all(stateId, cursor, limit + 1);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
events = db
|
|
133
|
+
.prepare("SELECT * FROM event_log WHERE state_id = ? ORDER BY server_ts ASC, seq ASC LIMIT ?")
|
|
134
|
+
.all(stateId, limit + 1);
|
|
135
|
+
}
|
|
136
|
+
const hasMore = events.length > limit;
|
|
137
|
+
if (hasMore)
|
|
138
|
+
events.pop();
|
|
139
|
+
return { events, hasMore };
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=state-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-engine.js","sourceRoot":"","sources":["../src/state-engine.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@better-state/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Better-State server — shared state primitive for developers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"better-state": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx watch src/index.ts",
|
|
11
|
+
"build": "tsc && node scripts/postbuild.js",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"seed": "tsx src/seed.ts",
|
|
14
|
+
"test": "tsx --test src/__tests__/*.test.ts",
|
|
15
|
+
"clean": "rm -rf dist"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"!dist/__tests__",
|
|
20
|
+
"public"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"state",
|
|
24
|
+
"shared-state",
|
|
25
|
+
"realtime",
|
|
26
|
+
"sync",
|
|
27
|
+
"websocket",
|
|
28
|
+
"server"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/better-state/better-state"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"better-sqlite3": "^11.0.0",
|
|
40
|
+
"cors": "^2.8.5",
|
|
41
|
+
"express": "^4.21.0",
|
|
42
|
+
"socket.io": "^4.8.0",
|
|
43
|
+
"uuid": "^11.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
47
|
+
"@types/cors": "^2.8.0",
|
|
48
|
+
"@types/express": "^5.0.0",
|
|
49
|
+
"@types/uuid": "^10.0.0",
|
|
50
|
+
"socket.io-client": "^4.8.3",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.7.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Better-State Playground</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
12
|
+
background: #0a0a0f;
|
|
13
|
+
color: #e2e8f0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
padding: 2rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.header {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 0.75rem;
|
|
22
|
+
margin-bottom: 2rem;
|
|
23
|
+
}
|
|
24
|
+
.header h1 {
|
|
25
|
+
font-size: 1.5rem;
|
|
26
|
+
font-weight: 700;
|
|
27
|
+
letter-spacing: -0.02em;
|
|
28
|
+
}
|
|
29
|
+
.header h1 span { color: #36adf6; }
|
|
30
|
+
.badge {
|
|
31
|
+
font-size: 0.7rem;
|
|
32
|
+
background: rgba(12, 147, 231, 0.15);
|
|
33
|
+
color: #7cc8fb;
|
|
34
|
+
padding: 0.15rem 0.5rem;
|
|
35
|
+
border-radius: 9999px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.status-bar {
|
|
39
|
+
display: flex;
|
|
40
|
+
gap: 1.5rem;
|
|
41
|
+
margin-bottom: 2rem;
|
|
42
|
+
font-size: 0.85rem;
|
|
43
|
+
color: #94a3b8;
|
|
44
|
+
}
|
|
45
|
+
.status-dot {
|
|
46
|
+
display: inline-block;
|
|
47
|
+
width: 8px;
|
|
48
|
+
height: 8px;
|
|
49
|
+
border-radius: 50%;
|
|
50
|
+
margin-right: 0.4rem;
|
|
51
|
+
vertical-align: middle;
|
|
52
|
+
}
|
|
53
|
+
.status-dot.green { background: #34d399; }
|
|
54
|
+
.status-dot.red { background: #f87171; }
|
|
55
|
+
.status-dot.yellow { background: #fbbf24; }
|
|
56
|
+
|
|
57
|
+
.grid {
|
|
58
|
+
display: grid;
|
|
59
|
+
grid-template-columns: 1fr 1fr;
|
|
60
|
+
gap: 1.5rem;
|
|
61
|
+
margin-bottom: 2rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.card {
|
|
65
|
+
background: rgba(15, 23, 42, 0.6);
|
|
66
|
+
border: 1px solid #1e293b;
|
|
67
|
+
border-radius: 12px;
|
|
68
|
+
padding: 1.25rem;
|
|
69
|
+
}
|
|
70
|
+
.card h2 {
|
|
71
|
+
font-size: 0.75rem;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
text-transform: uppercase;
|
|
74
|
+
letter-spacing: 0.05em;
|
|
75
|
+
color: #64748b;
|
|
76
|
+
margin-bottom: 1rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.state-display {
|
|
80
|
+
background: #0f172a;
|
|
81
|
+
border: 1px solid #1e293b;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
padding: 1rem;
|
|
84
|
+
font-family: "JetBrains Mono", "Fira Code", monospace;
|
|
85
|
+
font-size: 0.85rem;
|
|
86
|
+
min-height: 120px;
|
|
87
|
+
white-space: pre-wrap;
|
|
88
|
+
word-break: break-all;
|
|
89
|
+
color: #a5b4fc;
|
|
90
|
+
margin-bottom: 1rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.controls { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
94
|
+
|
|
95
|
+
button {
|
|
96
|
+
background: linear-gradient(135deg, #0c93e7, #015da0);
|
|
97
|
+
color: white;
|
|
98
|
+
border: none;
|
|
99
|
+
padding: 0.5rem 1rem;
|
|
100
|
+
border-radius: 8px;
|
|
101
|
+
font-size: 0.8rem;
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
transition: opacity 0.15s;
|
|
105
|
+
}
|
|
106
|
+
button:hover { opacity: 0.85; }
|
|
107
|
+
button.secondary {
|
|
108
|
+
background: rgba(30, 41, 59, 0.8);
|
|
109
|
+
border: 1px solid #334155;
|
|
110
|
+
}
|
|
111
|
+
button.danger {
|
|
112
|
+
background: linear-gradient(135deg, #dc2626, #991b1b);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
input[type="text"] {
|
|
116
|
+
background: #0f172a;
|
|
117
|
+
border: 1px solid #334155;
|
|
118
|
+
color: #e2e8f0;
|
|
119
|
+
padding: 0.5rem 0.75rem;
|
|
120
|
+
border-radius: 8px;
|
|
121
|
+
font-size: 0.85rem;
|
|
122
|
+
flex: 1;
|
|
123
|
+
min-width: 180px;
|
|
124
|
+
}
|
|
125
|
+
input[type="text"]:focus { outline: none; border-color: #36adf6; }
|
|
126
|
+
|
|
127
|
+
.log {
|
|
128
|
+
background: #0f172a;
|
|
129
|
+
border: 1px solid #1e293b;
|
|
130
|
+
border-radius: 8px;
|
|
131
|
+
padding: 1rem;
|
|
132
|
+
font-family: "JetBrains Mono", "Fira Code", monospace;
|
|
133
|
+
font-size: 0.75rem;
|
|
134
|
+
max-height: 300px;
|
|
135
|
+
overflow-y: auto;
|
|
136
|
+
line-height: 1.6;
|
|
137
|
+
}
|
|
138
|
+
.log .entry { margin-bottom: 0.25rem; }
|
|
139
|
+
.log .ts { color: #475569; }
|
|
140
|
+
.log .event { color: #34d399; }
|
|
141
|
+
.log .data { color: #94a3b8; }
|
|
142
|
+
.log .error { color: #f87171; }
|
|
143
|
+
|
|
144
|
+
.hint {
|
|
145
|
+
font-size: 0.8rem;
|
|
146
|
+
color: #475569;
|
|
147
|
+
margin-top: 0.5rem;
|
|
148
|
+
line-height: 1.5;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@media (max-width: 768px) {
|
|
152
|
+
.grid { grid-template-columns: 1fr; }
|
|
153
|
+
}
|
|
154
|
+
</style>
|
|
155
|
+
</head>
|
|
156
|
+
<body>
|
|
157
|
+
<div class="header">
|
|
158
|
+
<h1><span>Better</span>-State</h1>
|
|
159
|
+
<span class="badge">Playground</span>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="status-bar">
|
|
163
|
+
<div><span id="conn-dot" class="status-dot yellow"></span> <span id="conn-text">Connecting...</span></div>
|
|
164
|
+
<div>Client: <code id="client-id">—</code></div>
|
|
165
|
+
<div>Version: <code id="version">0</code></div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div class="grid">
|
|
169
|
+
<!-- Counter state -->
|
|
170
|
+
<div class="card">
|
|
171
|
+
<h2>State: counter</h2>
|
|
172
|
+
<div id="counter-display" class="state-display">0</div>
|
|
173
|
+
<div class="controls">
|
|
174
|
+
<button onclick="increment()">+ Increment</button>
|
|
175
|
+
<button onclick="decrement()" class="secondary">- Decrement</button>
|
|
176
|
+
<button onclick="resetCounter()" class="danger">Reset</button>
|
|
177
|
+
</div>
|
|
178
|
+
<p class="hint">Click buttons in multiple tabs — they all sync in real-time.</p>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Todos state -->
|
|
182
|
+
<div class="card">
|
|
183
|
+
<h2>State: todos</h2>
|
|
184
|
+
<div id="todos-display" class="state-display">[]</div>
|
|
185
|
+
<div class="controls">
|
|
186
|
+
<input id="todo-input" type="text" placeholder="Add a todo..." onkeydown="if(event.key==='Enter') addTodo()" />
|
|
187
|
+
<button onclick="addTodo()">Add</button>
|
|
188
|
+
<button onclick="clearTodos()" class="danger">Clear</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<!-- Event log -->
|
|
194
|
+
<div class="card">
|
|
195
|
+
<h2>Event Log</h2>
|
|
196
|
+
<div id="log" class="log">
|
|
197
|
+
<div class="entry"><span class="ts">[init]</span> <span class="data">Waiting for connection...</span></div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<script type="module">
|
|
202
|
+
// --- Config ---
|
|
203
|
+
// The API key is injected by the server into the page.
|
|
204
|
+
const API_KEY = "__API_KEY__";
|
|
205
|
+
const SERVER_URL = window.location.origin;
|
|
206
|
+
|
|
207
|
+
// --- Load SDK ---
|
|
208
|
+
const { createClient } = await import("/sdk/browser.mjs");
|
|
209
|
+
|
|
210
|
+
const log = document.getElementById("log");
|
|
211
|
+
const connDot = document.getElementById("conn-dot");
|
|
212
|
+
const connText = document.getElementById("conn-text");
|
|
213
|
+
const clientIdEl = document.getElementById("client-id");
|
|
214
|
+
const versionEl = document.getElementById("version");
|
|
215
|
+
|
|
216
|
+
function addLog(event, data, isError = false) {
|
|
217
|
+
const ts = new Date().toLocaleTimeString();
|
|
218
|
+
const entry = document.createElement("div");
|
|
219
|
+
entry.className = "entry";
|
|
220
|
+
entry.innerHTML = `<span class="ts">[${ts}]</span> <span class="${isError ? "error" : "event"}">${event}</span> <span class="data">${data}</span>`;
|
|
221
|
+
log.appendChild(entry);
|
|
222
|
+
log.scrollTop = log.scrollHeight;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- Create client ---
|
|
226
|
+
const client = createClient(SERVER_URL, {
|
|
227
|
+
apiKey: API_KEY,
|
|
228
|
+
debug: true,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
clientIdEl.textContent = "connecting...";
|
|
232
|
+
|
|
233
|
+
// --- Counter state ---
|
|
234
|
+
const counter = client.state("counter", 0);
|
|
235
|
+
|
|
236
|
+
counter.subscribe((val) => {
|
|
237
|
+
document.getElementById("counter-display").textContent = JSON.stringify(val, null, 2);
|
|
238
|
+
versionEl.textContent = counter.getVersion();
|
|
239
|
+
addLog("counter:update", `value=${val} version=${counter.getVersion()}`);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// --- Todos state ---
|
|
243
|
+
const todos = client.state("todos", []);
|
|
244
|
+
|
|
245
|
+
todos.subscribe((val) => {
|
|
246
|
+
document.getElementById("todos-display").textContent = JSON.stringify(val, null, 2);
|
|
247
|
+
addLog("todos:update", `items=${Array.isArray(val) ? val.length : "?"}`);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// --- Connection status ---
|
|
251
|
+
// Poll connection status (simple approach)
|
|
252
|
+
setInterval(() => {
|
|
253
|
+
const connected = counter.getVersion() >= 0;
|
|
254
|
+
if (connText.textContent === "Connecting..." && counter.getVersion() > 0) {
|
|
255
|
+
connDot.className = "status-dot green";
|
|
256
|
+
connText.textContent = "Connected";
|
|
257
|
+
clientIdEl.textContent = client._clientId || "active";
|
|
258
|
+
}
|
|
259
|
+
}, 500);
|
|
260
|
+
|
|
261
|
+
// Mark connected once we get first state
|
|
262
|
+
setTimeout(() => {
|
|
263
|
+
connDot.className = "status-dot green";
|
|
264
|
+
connText.textContent = "Connected";
|
|
265
|
+
}, 2000);
|
|
266
|
+
|
|
267
|
+
// --- Actions (exposed globally for onclick handlers) ---
|
|
268
|
+
window.increment = () => {
|
|
269
|
+
counter.update((n) => n + 1);
|
|
270
|
+
addLog("action", "increment");
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
window.decrement = () => {
|
|
274
|
+
counter.update((n) => n - 1);
|
|
275
|
+
addLog("action", "decrement");
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
window.resetCounter = () => {
|
|
279
|
+
counter.update(() => 0);
|
|
280
|
+
addLog("action", "reset counter");
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
window.addTodo = () => {
|
|
284
|
+
const input = document.getElementById("todo-input");
|
|
285
|
+
const text = input.value.trim();
|
|
286
|
+
if (!text) return;
|
|
287
|
+
const id = crypto.randomUUID();
|
|
288
|
+
todos.update((list) => [...list, { id, text, done: false }]);
|
|
289
|
+
addLog("action", `add todo: "${text}"`);
|
|
290
|
+
input.value = "";
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
window.clearTodos = () => {
|
|
294
|
+
todos.update(() => []);
|
|
295
|
+
addLog("action", "clear todos");
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
addLog("init", "SDK loaded, connecting to server...");
|
|
299
|
+
</script>
|
|
300
|
+
</body>
|
|
301
|
+
</html>
|