@arenahito/piggychick 0.2.1 → 0.2.2

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.
@@ -146167,7 +146167,7 @@ var fetchJson = async (url, init) => {
146167
146167
  return data;
146168
146168
  };
146169
146169
  var fetchRoots = () => fetchJson("/api/roots?prdSort=desc");
146170
- var rootEventsUrl = (rootId) => `/api/roots/${encodeURIComponent(rootId)}/events`;
146170
+ var allEventsUrl = () => "/api/events";
146171
146171
  var fetchPlan = (rootId, prd) => fetchJson(`/api/roots/${encodeURIComponent(rootId)}/prds/${encodeURIComponent(prd)}/plan`);
146172
146172
  var fetchMarkdown = (rootId, prd, docId) => fetchJson(`/api/roots/${encodeURIComponent(rootId)}/prds/${encodeURIComponent(prd)}/${encodeURIComponent(docId)}`);
146173
146173
  var fetchConfig = () => fetchJson("/api/config");
@@ -154078,7 +154078,10 @@ var normalizeRootChangedEvent = (input, fallbackRootId) => {
154078
154078
  const candidate = input;
154079
154079
  if (candidate.kind !== "changed")
154080
154080
  return null;
154081
- const rootId = typeof candidate.rootId === "string" && candidate.rootId.trim().length > 0 ? candidate.rootId : fallbackRootId;
154081
+ const normalizedFallbackRootId = typeof fallbackRootId === "string" && fallbackRootId.trim().length > 0 ? fallbackRootId : null;
154082
+ const rootId = typeof candidate.rootId === "string" && candidate.rootId.trim().length > 0 ? candidate.rootId : normalizedFallbackRootId;
154083
+ if (!rootId)
154084
+ return null;
154082
154085
  const prdId = typeof candidate.prdId === "string" && candidate.prdId.trim().length > 0 ? candidate.prdId : null;
154083
154086
  const at2 = typeof candidate.at === "string" ? candidate.at : new Date().toISOString();
154084
154087
  return { kind: "changed", rootId, prdId, at: at2 };
@@ -154091,16 +154094,13 @@ var parseRootChangedEvent = (raw, fallbackRootId) => {
154091
154094
  return null;
154092
154095
  }
154093
154096
  };
154094
- var createRootEventsSubscription = (rootId, onChanged, onError) => {
154095
- const source = new EventSource(rootEventsUrl(rootId));
154096
- const handleChanged = (event4) => {
154097
- const payload = parseRootChangedEvent(event4.data, rootId);
154098
- if (!payload)
154099
- return;
154100
- onChanged(payload);
154101
- };
154102
- const handleMessage = (event4) => {
154103
- const payload = parseRootChangedEvent(event4.data, rootId);
154097
+ var createGlobalEventsSubscription = (onChanged, onError) => {
154098
+ return createEventsSubscription(allEventsUrl(), onChanged, onError);
154099
+ };
154100
+ var createEventsSubscription = (url, onChanged, onError, fallbackRootId) => {
154101
+ const source = new EventSource(url);
154102
+ const handleEvent = (event4) => {
154103
+ const payload = parseRootChangedEvent(event4.data, fallbackRootId);
154104
154104
  if (!payload)
154105
154105
  return;
154106
154106
  onChanged(payload);
@@ -154108,13 +154108,13 @@ var createRootEventsSubscription = (rootId, onChanged, onError) => {
154108
154108
  const handleError2 = (event4) => {
154109
154109
  onError?.(event4);
154110
154110
  };
154111
- source.addEventListener("changed", handleChanged);
154112
- source.addEventListener("message", handleMessage);
154111
+ source.addEventListener("changed", handleEvent);
154112
+ source.addEventListener("message", handleEvent);
154113
154113
  source.addEventListener("error", handleError2);
154114
154114
  return {
154115
154115
  close: () => {
154116
- source.removeEventListener("changed", handleChanged);
154117
- source.removeEventListener("message", handleMessage);
154116
+ source.removeEventListener("changed", handleEvent);
154117
+ source.removeEventListener("message", handleEvent);
154118
154118
  source.removeEventListener("error", handleError2);
154119
154119
  source.close();
154120
154120
  }
@@ -154205,6 +154205,7 @@ var configRequest = 0;
154205
154205
  var configHandle = null;
154206
154206
  var rootEventSubscriptions = new Map;
154207
154207
  var pendingRootChangeEvents = [];
154208
+ var globalEventsSubscriptionKey = "__global__";
154208
154209
  var readBooleanRecord = (key) => {
154209
154210
  try {
154210
154211
  const raw = localStorage.getItem(key);
@@ -154329,6 +154330,10 @@ var findFirstSelection = () => {
154329
154330
  }
154330
154331
  return null;
154331
154332
  };
154333
+ var createGlobalEventsSubscriptionKey = (roots) => {
154334
+ const rootIds = [...new Set(roots.map((rootEntry) => rootEntry.id))].sort((left3, right3) => left3.localeCompare(right3, "en", { sensitivity: "base", numeric: true }));
154335
+ return `${globalEventsSubscriptionKey}:${JSON.stringify(rootIds)}`;
154336
+ };
154332
154337
  var ensureSelection = (route) => {
154333
154338
  let updatedHash = false;
154334
154339
  if (route?.kind === "config") {
@@ -154792,8 +154797,12 @@ var handleRootChangedEvent = (event4) => {
154792
154797
  liveRefreshRunner.trigger();
154793
154798
  };
154794
154799
  var reconcileRootEventStreams = () => {
154795
- const rootIds = state3.roots.map((rootEntry) => rootEntry.id);
154796
- rootEventSubscriptions = reconcileRootSubscriptions(rootEventSubscriptions, rootIds, (rootId) => createRootEventsSubscription(rootId, handleRootChangedEvent));
154800
+ if (state3.roots.length === 0) {
154801
+ closeAllRootEventStreams();
154802
+ return;
154803
+ }
154804
+ const subscriptionKey = createGlobalEventsSubscriptionKey(state3.roots);
154805
+ rootEventSubscriptions = reconcileRootSubscriptions(rootEventSubscriptions, [subscriptionKey], () => createGlobalEventsSubscription(handleRootChangedEvent));
154797
154806
  };
154798
154807
  var syncRoots = async (payload, options2 = {}) => {
154799
154808
  const allowLoadSelection = options2.allowLoadSelection ?? true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arenahito/piggychick",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,7 @@ import { basename, join, sep } from "node:path";
4
4
  import { resolveConfigPath } from "../shared/config";
5
5
  import {
6
6
  inferPrdIdFromWatchPath,
7
+ listRootIds,
7
8
  listRootWatchTargetsByRootId,
8
9
  type RootWatchTarget,
9
10
  } from "./tasks";
@@ -274,6 +275,34 @@ const getOrCreateRootEntry = async (rootId: string, configPath: string) => {
274
275
  return init;
275
276
  };
276
277
 
278
+ const attachSubscriberToRoots = async (
279
+ rootIds: string[],
280
+ configPath: string,
281
+ isClosed: () => boolean,
282
+ subscriber: Subscriber,
283
+ attachedRootIds: Set<string>,
284
+ ) => {
285
+ for (const rootId of rootIds) {
286
+ if (isClosed()) {
287
+ return;
288
+ }
289
+ if (attachedRootIds.has(rootId)) {
290
+ continue;
291
+ }
292
+ let entry: RootEventEntry | null = null;
293
+ try {
294
+ entry = await getOrCreateRootEntry(rootId, configPath);
295
+ } catch {
296
+ entry = null;
297
+ }
298
+ if (!entry || isClosed()) {
299
+ continue;
300
+ }
301
+ entry.subscribers.add(subscriber);
302
+ attachedRootIds.add(rootId);
303
+ }
304
+ };
305
+
277
306
  export const createRootEventsResponse = async (
278
307
  request: Request,
279
308
  rootId: string,
@@ -358,6 +387,102 @@ export const createRootEventsResponse = async (
358
387
  });
359
388
  };
360
389
 
390
+ export const createGlobalEventsResponse = async (
391
+ request: Request,
392
+ configPath = resolveConfigPath(),
393
+ ) => {
394
+ const encoder = new TextEncoder();
395
+ let closed = false;
396
+ let keepalive: ReturnType<typeof setInterval> | null = null;
397
+ let removeAbort: (() => void) | null = null;
398
+ const attachedRootIds = new Set<string>();
399
+ let cleanup = () => {};
400
+
401
+ const stream = new ReadableStream<Uint8Array>({
402
+ async start(controller) {
403
+ const send = (chunk: string) => {
404
+ controller.enqueue(encoder.encode(chunk));
405
+ };
406
+ const subscriber: Subscriber = (event) => {
407
+ if (closed) return;
408
+ try {
409
+ send(formatChangedEvent(event));
410
+ } catch {
411
+ cleanup();
412
+ }
413
+ };
414
+ cleanup = () => {
415
+ if (closed) return;
416
+ closed = true;
417
+ if (keepalive) {
418
+ clearInterval(keepalive);
419
+ keepalive = null;
420
+ }
421
+ for (const rootId of attachedRootIds) {
422
+ const entry = rootEntries.get(rootId);
423
+ if (!entry) continue;
424
+ entry.subscribers.delete(subscriber);
425
+ closeEntryIfUnused(rootId);
426
+ }
427
+ attachedRootIds.clear();
428
+ if (removeAbort) {
429
+ removeAbort();
430
+ removeAbort = null;
431
+ }
432
+ try {
433
+ controller.close();
434
+ } catch {
435
+ return;
436
+ }
437
+ };
438
+
439
+ if (request.signal.aborted) {
440
+ cleanup();
441
+ return;
442
+ }
443
+
444
+ const onAbort = () => cleanup();
445
+ request.signal.addEventListener("abort", onAbort, { once: true });
446
+ removeAbort = () => request.signal.removeEventListener("abort", onAbort);
447
+
448
+ try {
449
+ const rootIds = await listRootIds(configPath).catch(() => []);
450
+ await attachSubscriberToRoots(
451
+ rootIds,
452
+ configPath,
453
+ () => closed,
454
+ subscriber,
455
+ attachedRootIds,
456
+ );
457
+ if (closed) return;
458
+ send(": connected\n\n");
459
+ keepalive = setInterval(() => {
460
+ if (closed) return;
461
+ try {
462
+ send(": keepalive\n\n");
463
+ } catch {
464
+ cleanup();
465
+ }
466
+ }, keepaliveMs);
467
+ } catch {
468
+ cleanup();
469
+ }
470
+ },
471
+ cancel() {
472
+ cleanup();
473
+ },
474
+ });
475
+
476
+ return new Response(stream, {
477
+ headers: {
478
+ "Content-Type": "text/event-stream; charset=utf-8",
479
+ "Cache-Control": "no-cache",
480
+ Connection: "keep-alive",
481
+ "X-Accel-Buffering": "no",
482
+ },
483
+ });
484
+ };
485
+
361
486
  export const getRootEventsDebugSnapshot = () => {
362
487
  let watcherCount = 0;
363
488
  let subscriberCount = 0;
@@ -11,7 +11,7 @@ import {
11
11
  toConfigFile,
12
12
  writeConfigText,
13
13
  } from "../shared/config";
14
- import { createRootEventsResponse } from "./prd-events";
14
+ import { createGlobalEventsResponse, createRootEventsResponse } from "./prd-events";
15
15
  import {
16
16
  listRoots,
17
17
  readMarkdownByRoot,
@@ -70,6 +70,17 @@ export const handleApiRequest = async (request: Request, configPath = resolveCon
70
70
  return jsonError(404, "not_found", "Not found");
71
71
  }
72
72
 
73
+ if (segments[1] === "events" && segments.length === 2) {
74
+ if (request.method !== "GET") {
75
+ return jsonError(405, "method_not_allowed", "Method not allowed");
76
+ }
77
+ try {
78
+ return await createGlobalEventsResponse(request, configPath);
79
+ } catch (error) {
80
+ return handleTasksError(error);
81
+ }
82
+ }
83
+
73
84
  if (segments[1] === "roots" && segments.length === 2) {
74
85
  if (request.method === "GET") {
75
86
  try {
@@ -685,6 +685,12 @@ export const listRoots = async (
685
685
  return { roots };
686
686
  };
687
687
 
688
+ export const listRootIds = async (configPath = resolveConfigPath()) => {
689
+ const config = await loadConfigFile(configPath);
690
+ const normalized = await normalizeConfig(config, { path: configPath });
691
+ return buildRootEntries(normalized.roots).map((entry) => entry.id);
692
+ };
693
+
688
694
  export const resolveRootById = async (rootId: string, configPath = resolveConfigPath()) => {
689
695
  const config = await loadConfigFile(configPath);
690
696
  const normalized = await normalizeConfig(config, { path: configPath });