add forms

This commit is contained in:
metacryst
2025-12-18 20:32:10 -06:00
parent 4d969864a5
commit c94d60a288
8 changed files with 245 additions and 3 deletions

View File

@@ -1,3 +0,0 @@
{
}

43
forms/bin/forms.js Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env node
import { spawn } from "child_process"
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const KERNEL = path.join(__dirname, "../kernel/kernel.js")
const PID_FILE = "/tmp/forms.pid"
const cmd = process.argv[2]
if (cmd === "start") {
if (fs.existsSync(PID_FILE)) {
console.log("forms kernel already running")
process.exit(0)
}
const proc = spawn("node", [KERNEL], {
detached: true,
stdio: "ignore"
})
proc.unref()
fs.writeFileSync(PID_FILE, String(proc.pid))
console.log("forms kernel started")
}
if (cmd === "stop") {
if (!fs.existsSync(PID_FILE)) {
console.log("forms kernel not running")
process.exit(0)
}
const pid = Number(fs.readFileSync(PID_FILE))
process.kill(pid)
fs.unlinkSync(PID_FILE)
console.log("forms kernel stopped")
}
if (!cmd) {
console.log("usage: forms start | stop")
}

60
forms/client/index.js Normal file
View File

@@ -0,0 +1,60 @@
import WebSocket from "ws"
export default class FormsClient {
constructor(url = "ws://localhost:4001") {
this.url = url
this.ws = null
this.seq = 0
this.handlers = new Map()
}
connect() {
this.ws = new WebSocket(this.url)
this.ws.on("message", msg => this._onMessage(msg))
this.ws.on("open", () => console.log("[forms] connected"))
this.ws.on("close", () => console.log("[forms] disconnected"))
}
append(form, data) {
this.ws.send(JSON.stringify({
op: "append",
form,
data
}))
}
watch(form, handler) {
if (!this.handlers.has(form)) {
this.handlers.set(form, new Set())
}
this.handlers.get(form).add(handler)
this.ws.send(JSON.stringify({
op: "replay",
form,
fromSeq: this.seq
}))
}
_onMessage(raw) {
const msg = JSON.parse(raw.toString())
if (msg.type === "hello") {
this.seq = msg.seq
return
}
if (msg.type === "form:update") {
this.seq = msg.seq
const set = this.handlers.get(msg.form)
if (set) {
for (const fn of set) {
fn(msg.data, msg)
}
}
}
}
}

120
forms/kernel/kernel.js Normal file
View File

@@ -0,0 +1,120 @@
import fs from "fs"
import path from "path"
import { WebSocketServer } from "ws"
const DATA_DIR = path.resolve("./")
const DATA_FILE = path.join(DATA_DIR, "master.forms")
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true })
}
if (!fs.existsSync(DATA_FILE)) {
fs.writeFileSync(DATA_FILE, "")
}
class Kernel {
port
seq = 0
clients = new Set()
constructor(port = 10000) {
this.port = port
this._loadSeq()
this.wss = new WebSocketServer({ port: this.port })
this.wss.on("connection", ws => this._onConnection(ws))
console.log(`[kernel] running on ws://localhost:${port}`)
}
_loadSeq() {
const data = fs.readFileSync(DATA_FILE, "utf8")
if (!data) return
const lines = data.trim().split("\n")
const last = lines[lines.length - 1]
if (last) {
const entry = JSON.parse(last)
this.seq = entry.seq
}
}
_onConnection(ws) {
this.clients.add(ws)
ws.on("message", msg => this._onMessage(ws, msg))
ws.on("close", () => this.clients.delete(ws))
ws.send(JSON.stringify({
type: "hello",
seq: this.seq
}))
}
_onMessage(ws, raw) {
let msg
try {
msg = JSON.parse(raw.toString())
} catch {
return
}
if (msg.op === "append") {
this._append(msg.form, msg.data)
}
if (msg.op === "replay") {
this._replay(ws, msg.form, msg.fromSeq)
}
}
_append(form, data) {
const entry = {
seq: ++this.seq,
time: Date.now(),
form,
data
}
fs.appendFileSync(DATA_FILE, JSON.stringify(entry) + "\n")
const payload = JSON.stringify({
type: "form:update",
...entry
})
for (const ws of this.clients) {
if (ws.readyState === ws.OPEN) {
ws.send(payload)
}
}
}
_replay(ws, form, fromSeq = 0) {
const stream = fs.createReadStream(DATA_FILE, { encoding: "utf8" })
let buffer = ""
stream.on("data", chunk => {
buffer += chunk
let idx
while ((idx = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, idx)
buffer = buffer.slice(idx + 1)
if (!line) continue
const entry = JSON.parse(line)
if (entry.seq > fromSeq && entry.form === form) {
ws.send(JSON.stringify({
type: "form:update",
...entry
}))
}
}
})
}
}
new Kernel()

14
forms/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "forms",
"version": "0.1.0",
"type": "module",
"bin": {
"forms": "./bin/forms.js"
},
"exports": {
".": "./client/index.js"
},
"dependencies": {
"ws": "^8.16.0"
}
}

8
forms/readme.md Normal file
View File

@@ -0,0 +1,8 @@
cd forms
npm link
In app:
npm link forms
Start:
forms start

0
master.forms Normal file
View File