char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somperf: use generator for snapshot frames (lazy encode)efe2e1f

main
5.9 KiB168 linesraw
1import type { JsonValue } from "@earendil-works/chord";
2import { diffRevisions, type Op } from "@earendil-works/chord/delta";
3
4/** One json value: its current revision, then the ops of each next one. Durable watches are feeds. */
5export type Feed = {
6  readonly value: unknown;
7  /** Never invoked inline; ops apply to `value` as read just before `start`. */
8  start(listener: (value: unknown, ops: readonly Op[]) => Promise<void>): void;
9  stop(): Promise<unknown>;
10};
11
12export type ClientMessage =
13  | { t: "auth"; token: string }
14  | { t: "sub"; topic: string }
15  | { t: "unsub"; topic: string };
16
17export type ServerMessage =
18  | { t: "ready" }
19  /** `older` counts the entries before `value.entries`, which `older` messages bring, newest first, before any ops. */
20  | { t: "snapshot"; topic: string; value: unknown; older?: number }
21  | { t: "older"; topic: string; entries: unknown[] }
22  | { t: "ops"; topic: string; ops: readonly Op[] }
23  | { t: "error"; topic?: string; message: string };
24
25export type PubsubOptions = {
26  authorize(token: string): boolean;
27  /** `undefined` for unknown topics. */
28  resolve(topic: string): Promise<Feed | undefined>;
29};
30
31const AUTH_TIMEOUT_MS = 10_000;
32
33/** How much of a history its snapshot carries, of the newest; the rest follows in chunks of about `CHUNK_BYTES`. */
34const TAIL_BYTES = 32_768;
35const CHUNK_BYTES = 262_144;
36
37/**
38 * A snapshot's frames. A value with `entries`, as a conversation is, sends its newest entries first and then the rest,
39 * so that a reader far away sees the latest soonest; each frame is encoded only as it is sent.
40 */
41function* snapshot(topic: string, value: unknown): Generator<string> {
42  const entries = (value as { entries?: unknown } | null)?.entries;
43  if (!Array.isArray(entries)) return yield JSON.stringify({ t: "snapshot", topic, value });
44  // the encoded entries from `end` back that fit in `budget`; at least one fits
45  const take = (end: number, budget: number) => {
46    const taken: string[] = [];
47    for (let at = end, bytes = 0; at > 0;) {
48      const encoded = JSON.stringify(entries[at - 1]);
49      if (taken.length > 0 && bytes + encoded.length > budget) break;
50      taken.push(encoded);
51      bytes += encoded.length;
52      at--;
53    }
54    return taken.reverse();
55  };
56  const tail = take(entries.length, TAIL_BYTES);
57  const from = entries.length - tail.length;
58  const { entries: _, ...others } = value as { entries: unknown[] };
59  const rest = JSON.stringify(others).slice(1, -1);
60  yield `{"t":"snapshot","topic":${JSON.stringify(topic)},${
61    from ? `"older":${from},` : ""
62  }"value":{${rest}${rest ? "," : ""}"entries":[${tail.join(",")}]}}`;
63  for (let end = from; end > 0;) {
64    const chunk = take(end, CHUNK_BYTES);
65    yield `{"t":"older","topic":${JSON.stringify(topic)},"entries":[${chunk.join(",")}]}`;
66    end -= chunk.length;
67  }
68}
69
70export function servePubsub(socket: WebSocket, { authorize, resolve }: PubsubOptions): void {
71  let authorized = false;
72  const subscriptions = new Map<string, Promise<Feed | undefined>>();
73  const sendFrame = (frame: string) => {
74    if (socket.readyState === WebSocket.OPEN) socket.send(frame);
75  };
76  const send = (message: ServerMessage) => sendFrame(JSON.stringify(message));
77  const authTimeout = setTimeout(() => socket.close(4001, "unauthorized"), AUTH_TIMEOUT_MS);
78
79  async function subscribe(topic: string) {
80    if (subscriptions.has(topic)) return;
81    const pending = resolve(topic);
82    const entry = pending.catch(() => undefined);
83    subscriptions.set(topic, entry);
84    let feed: Feed | undefined;
85    try {
86      feed = await pending;
87    } catch (error) {
88      feed = undefined;
89      console.error(`[pubsub] ${topic}:`, error);
90    }
91    // unsubscribed meanwhile: unsubscribe() stops the feed
92    if (subscriptions.get(topic) !== entry) return;
93    if (feed === undefined) {
94      subscriptions.delete(topic);
95      send({ t: "error", topic, message: `no topic ${topic}` });
96      return;
97    }
98    for (const frame of snapshot(topic, feed.value)) sendFrame(frame);
99    feed.start((_, ops) => Promise.resolve(send({ t: "ops", topic, ops })));
100  }
101
102  async function unsubscribe(topic: string) {
103    const pending = subscriptions.get(topic);
104    subscriptions.delete(topic);
105    await (await pending)?.stop();
106  }
107
108  socket.addEventListener("message", (event) => {
109    let message: ClientMessage;
110    try {
111      message = JSON.parse(String(event.data));
112    } catch {
113      socket.close(4000, "malformed message");
114      return;
115    }
116    if (!authorized) {
117      clearTimeout(authTimeout);
118      if (message.t !== "auth" || !authorize(message.token)) {
119        socket.close(4001, "unauthorized");
120        return;
121      }
122      authorized = true;
123      send({ t: "ready" });
124      return;
125    }
126    if (message.t === "sub") void subscribe(message.topic);
127    else if (message.t === "unsub") void unsubscribe(message.topic);
128  });
129
130  socket.addEventListener("close", () => {
131    clearTimeout(authTimeout);
132    for (const topic of [...subscriptions.keys()]) void unsubscribe(topic);
133  });
134}
135
136/** A json value the server changes, which any number of feeds follow as a snapshot and then diffs. */
137export function published<T>(initial: T) {
138  let value = initial;
139  const listeners = new Set<(value: unknown, ops: readonly Op[]) => Promise<void>>();
140  return {
141    get value() {
142      return value;
143    },
144    set(next: T) {
145      if (next === value) return;
146      const ops = diffRevisions(value as JsonValue, next as JsonValue);
147      value = next;
148      if (ops.length === 0) return;
149      for (const listener of listeners) void listener(value, ops);
150    },
151    feed(): Feed {
152      let listener: ((value: unknown, ops: readonly Op[]) => Promise<void>) | undefined;
153      return {
154        get value() {
155          return value;
156        },
157        start(next) {
158          listener = next;
159          listeners.add(next);
160        },
161        stop() {
162          if (listener) listeners.delete(listener);
163          return Promise.resolve();
164        },
165      };
166    },
167  };
168}