@fdcn/sourcefed 0.3.5 → 0.3.7

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 CHANGED
@@ -29,16 +29,11 @@ import { createSourcefedMcp } from "@fdcn/sourcefed/mcp"
29
29
 
30
30
  ## Host plugins
31
31
 
32
- OpenCode loads this package as both a server and a TUI plugin:
32
+ OpenCode loads this package as both a server and a TUI plugin from one entry:
33
33
 
34
34
  ```json
35
35
  // ~/.config/opencode/opencode.jsonc
36
- { "plugins": ["@fdcn/sourcefed@0.3.5"] }
37
- ```
38
-
39
- ```json
40
- // ~/.config/opencode/cli.json
41
- { "plugins": ["@fdcn/sourcefed@0.3.5"] }
36
+ { "plugins": ["@fdcn/sourcefed@0.3.7"] }
42
37
  ```
43
38
 
44
39
  The package exposes `./server`, `./tui`, `./opencode`, `./opencode/tui`, and `./pi` entrypoints for hosts that load them directly.
@@ -11387,29 +11387,6 @@ async function writeResponse(outgoing, response) {
11387
11387
  stream.pipe(outgoing);
11388
11388
  });
11389
11389
  }
11390
- var RequestRouter = class {
11391
- pending = /* @__PURE__ */ new Map();
11392
- cursor = 1;
11393
- nextID() {
11394
- return this.cursor++;
11395
- }
11396
- register(id) {
11397
- return new Promise((resolve, reject) => {
11398
- this.pending.set(id, { resolve, reject });
11399
- });
11400
- }
11401
- settle(id, frame) {
11402
- const pending = this.pending.get(id);
11403
- if (!pending) return;
11404
- this.pending.delete(id);
11405
- if (frame.error) pending.reject(new Error(frame.error));
11406
- else pending.resolve(frame.result);
11407
- }
11408
- rejectAll(error) {
11409
- for (const { reject } of this.pending.values()) reject(error);
11410
- this.pending.clear();
11411
- }
11412
- };
11413
11390
  function defaultDaemonUrl() {
11414
11391
  const port = process.env.SOURCEFED_DAEMON_PORT ?? 18787;
11415
11392
  return `http://127.0.0.1:${port}`;
@@ -11802,164 +11779,225 @@ function parseDaemonFrame(line) {
11802
11779
  return void 0;
11803
11780
  }
11804
11781
  }
11782
+ var DaemonEventStream = class {
11783
+ constructor(url, headers, onEvents, acknowledge, onClose) {
11784
+ this.url = url;
11785
+ this.headers = headers;
11786
+ this.onEvents = onEvents;
11787
+ this.acknowledge = acknowledge;
11788
+ this.onClose = onClose;
11789
+ }
11790
+ controller = new AbortController();
11791
+ subscribed = Promise.withResolvers();
11792
+ stream;
11793
+ delivering = Promise.resolve();
11794
+ closing;
11795
+ ready = false;
11796
+ failedDelivery = false;
11797
+ async start(signal) {
11798
+ const abort = () => {
11799
+ void this.close();
11800
+ };
11801
+ signal?.addEventListener("abort", abort, { once: true });
11802
+ const setup = withTimeout(this.subscribed.promise, 15e3, `sourcefed event stream did not subscribe at ${this.url}`);
11803
+ if (signal?.aborted) abort();
11804
+ const running = this.run();
11805
+ void running.finally(() => signal?.removeEventListener("abort", abort));
11806
+ try {
11807
+ await setup;
11808
+ } catch (error) {
11809
+ await this.close();
11810
+ throw error;
11811
+ }
11812
+ }
11813
+ close() {
11814
+ if (!this.closing) {
11815
+ this.subscribed.reject(new Error("sourcefed event stream closed"));
11816
+ if (!this.ready) this.controller.abort();
11817
+ this.closing = this.drain();
11818
+ }
11819
+ return this.closing;
11820
+ }
11821
+ async drain() {
11822
+ try {
11823
+ await withTimeout(this.delivering, 15e3, "sourcefed event delivery did not drain");
11824
+ } catch (error) {
11825
+ console.error(`[sourcefed] closing event stream: ${String(error)}`);
11826
+ } finally {
11827
+ this.controller.abort();
11828
+ this.stream?.abort();
11829
+ this.onClose();
11830
+ }
11831
+ }
11832
+ async run() {
11833
+ let consecutiveFailures = 0;
11834
+ while (!this.controller.signal.aborted) {
11835
+ this.failedDelivery = false;
11836
+ try {
11837
+ await this.read();
11838
+ consecutiveFailures = 0;
11839
+ } catch (error) {
11840
+ if (!this.controller.signal.aborted && !this.failedDelivery && ++consecutiveFailures === 1) {
11841
+ console.error(`[sourcefed] event stream ended: ${String(error)}`);
11842
+ }
11843
+ }
11844
+ await this.waitForDelivery();
11845
+ if (!this.controller.signal.aborted) await pause(500, this.controller.signal);
11846
+ }
11847
+ }
11848
+ async read() {
11849
+ const controller = new AbortController();
11850
+ const abort = () => controller.abort();
11851
+ this.stream = controller;
11852
+ this.controller.signal.addEventListener("abort", abort, { once: true });
11853
+ let reader;
11854
+ try {
11855
+ this.controller.signal.throwIfAborted();
11856
+ const response = await fetch(this.url, { signal: controller.signal, headers: this.headers });
11857
+ if (!response.ok || !response.body) {
11858
+ await response.body?.cancel();
11859
+ throw new Error(`event stream failed: ${response.status}`);
11860
+ }
11861
+ reader = response.body.getReader();
11862
+ const decoder = new TextDecoder();
11863
+ let buffer = "";
11864
+ while (!controller.signal.aborted) {
11865
+ const { done, value } = await reader.read();
11866
+ if (done) return;
11867
+ buffer += decoder.decode(value, { stream: true });
11868
+ const frames = buffer.split("\n\n");
11869
+ buffer = frames.pop() ?? "";
11870
+ for (const chunk of frames) await this.frame(chunk);
11871
+ }
11872
+ } finally {
11873
+ await reader?.cancel().catch(() => void 0);
11874
+ reader?.releaseLock();
11875
+ this.controller.signal.removeEventListener("abort", abort);
11876
+ if (this.stream === controller) this.stream = void 0;
11877
+ }
11878
+ }
11879
+ async frame(chunk) {
11880
+ if (this.closing || this.controller.signal.aborted) return;
11881
+ for (const line of chunk.split("\n")) {
11882
+ if (!line.startsWith("data: ")) continue;
11883
+ const frame = parseDaemonFrame(line.slice(6));
11884
+ if (!frame || !("type" in frame)) continue;
11885
+ if (frame.type === "subscribed") {
11886
+ this.ready = true;
11887
+ this.subscribed.resolve();
11888
+ }
11889
+ if (frame.type === "event" && frame.events.length > 0) {
11890
+ this.deliver(frame.events);
11891
+ await this.waitForDelivery();
11892
+ }
11893
+ }
11894
+ }
11895
+ deliver(events) {
11896
+ this.delivering = this.delivering.then(async () => {
11897
+ if (this.failedDelivery || this.controller.signal.aborted) return;
11898
+ try {
11899
+ await this.onEvents(events);
11900
+ await this.acknowledge(events, this.controller.signal);
11901
+ } catch (error) {
11902
+ if (!this.controller.signal.aborted) console.error(`[sourcefed] event delivery failed: ${String(error)}`);
11903
+ this.failedDelivery = true;
11904
+ this.stream?.abort();
11905
+ }
11906
+ });
11907
+ }
11908
+ async waitForDelivery() {
11909
+ if (this.controller.signal.aborted) return;
11910
+ const stopped = Promise.withResolvers();
11911
+ const abort = () => stopped.resolve();
11912
+ this.controller.signal.addEventListener("abort", abort, { once: true });
11913
+ try {
11914
+ await Promise.race([this.delivering, stopped.promise]);
11915
+ } finally {
11916
+ this.controller.signal.removeEventListener("abort", abort);
11917
+ }
11918
+ }
11919
+ };
11920
+ function pause(ms, signal) {
11921
+ return new Promise((resolve) => {
11922
+ const finish = () => {
11923
+ clearTimeout(timer);
11924
+ signal.removeEventListener("abort", finish);
11925
+ resolve();
11926
+ };
11927
+ const timer = setTimeout(finish, ms);
11928
+ signal.addEventListener("abort", finish, { once: true });
11929
+ if (signal.aborted) finish();
11930
+ });
11931
+ }
11805
11932
  async function connectDaemonClient(options) {
11806
11933
  const url = options.url ?? process.env.SOURCEFED_DAEMON_URL ?? defaultDaemonUrl();
11807
11934
  return new HttpDaemonClient(url, options.token ?? process.env.SOURCEFED_DAEMON_TOKEN);
11808
11935
  }
11809
11936
  var HttpDaemonClient = class {
11810
- baseURL;
11811
- token;
11812
- router = new RequestRouter();
11813
- listeners = /* @__PURE__ */ new Map();
11814
- streams = /* @__PURE__ */ new Set();
11815
11937
  constructor(baseURL, token) {
11816
- this.baseURL = baseURL.replace(/\/+$/, "");
11817
11938
  this.token = token;
11939
+ this.baseURL = baseURL.replace(/\/+$/, "");
11818
11940
  }
11941
+ baseURL;
11942
+ streams = /* @__PURE__ */ new Set();
11943
+ lifetime = new AbortController();
11944
+ cursor = 0;
11945
+ closing;
11819
11946
  headers() {
11820
11947
  return this.token ? { authorization: `Bearer ${this.token}` } : {};
11821
11948
  }
11822
- async request(method, params) {
11823
- const id = this.router.nextID();
11824
- const promise = this.router.register(id);
11949
+ async request(method, params, signal = this.lifetime.signal) {
11950
+ if (this.closing && signal === this.lifetime.signal) throw new Error("daemon closed");
11951
+ signal.throwIfAborted();
11952
+ const response = await fetch(`${this.baseURL}/rpc`, {
11953
+ method: "POST",
11954
+ headers: { "content-type": "application/json", ...this.headers() },
11955
+ body: JSON.stringify({ id: ++this.cursor, method, params }),
11956
+ signal: AbortSignal.any([signal, this.lifetime.signal, AbortSignal.timeout(15e3)])
11957
+ });
11958
+ const frame = await response.json();
11959
+ if (!response.ok || frame.error) throw new Error(frame.error ?? `daemon request failed: ${response.status}`);
11960
+ return frame.result;
11961
+ }
11962
+ async subscribe(target, onEvents, options) {
11963
+ if (this.closing) throw new Error("daemon closed");
11964
+ this.lifetime.signal.throwIfAborted();
11965
+ const stream = new DaemonEventStream(
11966
+ `${this.baseURL}/events?target=${encodeTarget(target)}`,
11967
+ this.headers(),
11968
+ onEvents,
11969
+ (events, signal) => this.acknowledge(target, events, signal),
11970
+ () => this.streams.delete(stream)
11971
+ );
11972
+ this.streams.add(stream);
11825
11973
  try {
11826
- const response = await fetch(`${this.baseURL}/rpc`, {
11827
- method: "POST",
11828
- headers: { "content-type": "application/json", ...this.headers() },
11829
- body: JSON.stringify({ id, method, params })
11830
- });
11831
- const frame = await response.json();
11832
- if (!response.ok || frame.error) throw new Error(frame.error ?? `daemon request failed: ${response.status}`);
11833
- this.router.settle(id, { result: frame.result });
11974
+ await stream.start(options?.signal);
11975
+ return { close: () => stream.close() };
11834
11976
  } catch (error) {
11835
- this.router.settle(id, { error: error instanceof Error ? error.message : String(error) });
11977
+ await stream.close();
11978
+ throw error;
11836
11979
  }
11837
- return promise;
11838
11980
  }
11839
- async subscribe(target, onEvents) {
11840
- const key = targetKey(target);
11841
- let callbacks = this.listeners.get(key);
11842
- if (!callbacks) {
11843
- callbacks = /* @__PURE__ */ new Set();
11844
- this.listeners.set(key, callbacks);
11845
- }
11846
- callbacks.add(onEvents);
11847
- const controller = new AbortController();
11848
- this.streams.add(controller);
11849
- const url = `${this.baseURL}/events?target=${encodeTarget(target)}`;
11850
- let markSubscribed;
11851
- const subscribed = new Promise((resolve) => {
11852
- markSubscribed = () => resolve();
11853
- });
11854
- let delivering = Promise.resolve();
11855
- let activeStream;
11856
- let retryStream = false;
11857
- const deliver = (events) => {
11858
- if (!callbacks?.has(onEvents) || events.length === 0) return Promise.resolve();
11859
- delivering = delivering.then(async () => {
11860
- try {
11861
- await onEvents(events);
11862
- } catch (error) {
11863
- console.error(`[sourcefed] event delivery failed: ${error instanceof Error ? error.message : String(error)}`);
11864
- retryStream = true;
11865
- activeStream?.abort();
11866
- return;
11867
- }
11868
- await acknowledge(events);
11869
- }).catch((error) => {
11870
- console.error(`[sourcefed] event delivery failed: ${error instanceof Error ? error.message : String(error)}`);
11871
- });
11872
- return delivering;
11873
- };
11874
- const acknowledge = async (events) => {
11875
- let attempt = 0;
11876
- while (!controller.signal.aborted && callbacks?.has(onEvents)) {
11877
- try {
11878
- await this.request("monitor.ack", { target, eventIDs: events.map((event) => event.id) });
11879
- return;
11880
- } catch (error) {
11881
- attempt += 1;
11882
- if (attempt === 3) {
11883
- console.error(`[sourcefed] event acknowledgement still failing; retrying without repeating delivery: ${error instanceof Error ? error.message : String(error)}`);
11884
- }
11885
- await sleep(Math.min(250 * 2 ** (attempt - 1), 4e3));
11886
- }
11887
- }
11888
- };
11889
- const runStream = async () => {
11890
- const streamController = new AbortController();
11891
- const closeStream = () => streamController.abort();
11892
- activeStream = streamController;
11893
- controller.signal.addEventListener("abort", closeStream, { once: true });
11981
+ async acknowledge(target, events, signal) {
11982
+ let attempt = 0;
11983
+ while (!signal.aborted) {
11894
11984
  try {
11895
- const response = await fetch(url, { signal: streamController.signal, headers: this.headers() });
11896
- if (!response.ok || !response.body) throw new Error(`event stream failed: ${response.status}`);
11897
- const reader = response.body.getReader();
11898
- const decoder = new TextDecoder();
11899
- let buffer = "";
11900
- while (true) {
11901
- const { done, value } = await reader.read();
11902
- if (done) break;
11903
- buffer += decoder.decode(value, { stream: true });
11904
- const frames = buffer.split("\n\n");
11905
- buffer = frames.pop() ?? "";
11906
- for (const chunk of frames) {
11907
- for (const line of chunk.split("\n")) {
11908
- if (!line.startsWith("data: ")) continue;
11909
- const frame = parseDaemonFrame(line.slice(6));
11910
- if (!frame || !("type" in frame)) continue;
11911
- if (frame.type === "subscribed") markSubscribed();
11912
- if (frame.type === "event") void deliver(frame.events);
11913
- }
11914
- }
11915
- }
11916
- consecutiveFailures = 0;
11985
+ await this.request("monitor.ack", { target, eventIDs: events.map((event) => event.id) }, signal);
11986
+ return;
11917
11987
  } catch (error) {
11918
- if (!controller.signal.aborted && !retryStream) {
11919
- consecutiveFailures += 1;
11920
- if (consecutiveFailures === 1) {
11921
- console.error(`[sourcefed] event stream ended: ${error instanceof Error ? error.message : String(error)}`);
11922
- }
11988
+ if (++attempt === 3) {
11989
+ console.error(`[sourcefed] event acknowledgement still failing; retrying without repeating delivery: ${String(error)}`);
11923
11990
  }
11924
- } finally {
11925
- controller.signal.removeEventListener("abort", closeStream);
11926
- if (activeStream === streamController) activeStream = void 0;
11927
- retryStream = false;
11991
+ await pause(Math.min(250 * 2 ** Math.min(attempt - 1, 4), 4e3), signal);
11928
11992
  }
11929
- };
11930
- let consecutiveFailures = 0;
11931
- void (async () => {
11932
- while (!controller.signal.aborted && callbacks?.has(onEvents)) {
11933
- await runStream();
11934
- if (controller.signal.aborted) break;
11935
- await sleep(500);
11936
- }
11937
- })();
11938
- try {
11939
- await withTimeout(subscribed, 15e3, `sourcefed event stream did not subscribe at ${url}`);
11940
- } catch (error) {
11941
- const callbacks2 = this.listeners.get(key);
11942
- callbacks2?.delete(onEvents);
11943
- if (callbacks2 && callbacks2.size === 0) this.listeners.delete(key);
11944
- controller.abort();
11945
- this.streams.delete(controller);
11946
- throw error;
11947
11993
  }
11948
- return {
11949
- close: async () => {
11950
- const callbacks2 = this.listeners.get(key);
11951
- callbacks2?.delete(onEvents);
11952
- if (callbacks2 && callbacks2.size === 0) this.listeners.delete(key);
11953
- controller.abort();
11954
- this.streams.delete(controller);
11955
- }
11956
- };
11957
11994
  }
11958
- async close() {
11959
- for (const controller of this.streams) controller.abort();
11960
- this.streams.clear();
11961
- this.router.rejectAll(new Error("daemon closed"));
11962
- this.listeners.clear();
11995
+ close() {
11996
+ this.closing ??= Promise.all([...this.streams].map((stream) => stream.close())).then(() => {
11997
+ this.lifetime.abort();
11998
+ this.streams.clear();
11999
+ });
12000
+ return this.closing;
11963
12001
  }
11964
12002
  };
11965
12003
  async function dispatchDaemonRequest(daemon, method, params) {