@openparachute/vault 0.6.5-rc.13 → 0.6.5-rc.15

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/src/routing.ts CHANGED
@@ -73,7 +73,6 @@ import {
73
73
  type TagScopeCtx,
74
74
  type WriteCtx,
75
75
  } from "./routes.ts";
76
- import { handleSubscribe } from "./subscribe.ts";
77
76
  import { handleTriggers } from "./triggers-api.ts";
78
77
  import { expandTokenTagScope } from "./tag-scope.ts";
79
78
  import {
@@ -883,10 +882,23 @@ export async function route(
883
882
  };
884
883
 
885
884
  if (apiPath.startsWith("/notes")) return handleNotes(req, store, apiPath.slice(6), vaultName, tagScope, writeCtx);
886
- // Live-query SSE subscription (design 2026-06-08). Snapshot + scoped live
887
- // upsert/remove events over text/event-stream. Auth + tag-scope already
888
- // resolved above and threaded through, mirroring the /notes branch.
889
- if (apiPath === "/subscribe") return handleSubscribe(req, store, vaultName, tagScope);
885
+ // Live-query SSE transport REMOVED (WS-hibernation migration Phase 5). The
886
+ // WebSocket binding (an `Upgrade: websocket` GET /subscribe, handled in
887
+ // server.ts before this pipeline) is now the SOLE live transport; polling
888
+ // GET /notes is the client floor beneath it. A non-WS GET /subscribe — a
889
+ // straggler on a cached pre-WS notes-ui bundle — gets a clean 410 Gone
890
+ // pointing at the WS binding so its consumer degrades to polling gracefully;
891
+ // NEVER a 500 or an unhandled path.
892
+ if (apiPath === "/subscribe") {
893
+ return Response.json(
894
+ {
895
+ error:
896
+ "The live-query SSE transport has been removed. Reconnect with an `Upgrade: websocket` request to GET /api/subscribe, or poll GET /notes.",
897
+ code: "SSE_TRANSPORT_REMOVED",
898
+ },
899
+ { status: 410 },
900
+ );
901
+ }
890
902
  if (apiPath.startsWith("/tags")) return handleTags(req, store, apiPath.slice(5), tagScope);
891
903
  if (apiPath === "/find-path") return handleFindPath(req, store, tagScope);
892
904
  if (apiPath === "/vault") {
package/src/server.ts CHANGED
@@ -527,8 +527,9 @@ const server = Bun.serve({
527
527
  }
528
528
 
529
529
  // Live-query WebSocket upgrade — detected BEFORE the fetch pipeline because
530
- // WS upgrades don't traverse `route()`. Non-upgrade requests (incl. the SSE
531
- // fallback, which has no Upgrade header) fall straight through unchanged.
530
+ // WS upgrades don't traverse `route()`. Non-upgrade requests (incl. a
531
+ // straggler non-WS GET /subscribe, which now gets a 410 Gone in routing.ts
532
+ // the SSE transport was removed in Phase 5) fall straight through unchanged.
532
533
  if (isWebSocketUpgrade(req)) {
533
534
  const verdict = subscribeWs.tryUpgrade(req, server, path);
534
535
  if (verdict.kind === "upgraded") return undefined; // Bun owns the socket now
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Live-query subscription registry (live-query SSE design
3
- * `design/2026-06-08-live-query-sse.md`).
2
+ * Live-query subscription registry — fans out post-commit note events to live
3
+ * WebSocket subscriptions (originally an SSE fan-out — design
4
+ * `design/2026-06-08-live-query-sse.md`; the SSE transport was removed in
5
+ * Phase 5 of the WS-hibernation migration).
4
6
  *
5
7
  * A second consumer of the post-commit hook dispatcher (`core/src/hooks.ts`),
6
8
  * alongside the durable webhook-trigger sink. Where a trigger survives across
@@ -8,24 +10,23 @@
8
10
  * connection-scoped, driving a live UI. Same event source, same predicate,
9
11
  * different durability.
10
12
  *
11
- * ## Two transports, one contract (WS-hibernation migration, 2026-07-04)
13
+ * ## The live transport: WebSocket (WS-hibernation migration)
12
14
  *
13
15
  * The manager is transport-agnostic: it emits abstract `(event, data)` tuples
14
16
  * and a {@link SubscriptionSink} renders them for its wire.
15
- * - {@link SseSink} renders `event:`/`data:` frames bytes UNCHANGED from the
16
- * original SSE contract (surface-client parses them identically).
17
- * - {@link WsSink} renders a WebSocket message `{ type: event, ...data }` — the
18
- * SSE `event:` name folds into a `type` discriminator; the inner payload
19
- * (`note` / `id` / `notes`) serializes byte-for-byte identically to the SSE
20
- * `data:` body. That parity is pinned by the shared frame-corpus fixture and
21
- * is congruent with the cloud door (`parachute-cloud/workers/vault/src/
22
- * live/subscriptions.ts`). See `parachute-cloud/workers/vault/docs/
23
- * live-query-ws.md`.
17
+ * - {@link WsSink} renders a WebSocket message `{ type: event, ...data }`. The
18
+ * inner payload (`note` / `id` / `notes`) is pinned byte-for-byte against the
19
+ * shared frame-corpus fixture and is congruent with the cloud door
20
+ * (`parachute-cloud/workers/vault/src/live/subscriptions.ts`). See
21
+ * `parachute-cloud/workers/vault/docs/live-query-ws.md`.
24
22
  *
25
- * Both transports share ONE process-wide {@link SubscriptionManager}. Self-host
26
- * has no hibernation (a self-run Bun box has no per-connection duration bill)
27
- * the WS binding adds bidirectionality + contract-congruence with cloud, nothing
28
- * more. The Bun.serve WS integration lives in `ws-server.ts` + `ws-subscribe.ts`.
23
+ * WebSocket is the SOLE live transport as of Phase 5 — the earlier SSE binding
24
+ * was removed once cached notes-ui bundles converged, and polling (client-side)
25
+ * is the floor beneath live. All subscriptions share ONE process-wide
26
+ * {@link SubscriptionManager}. Self-host has no hibernation (a self-run Bun box
27
+ * has no per-connection duration bill) — the WS binding gives bidirectionality +
28
+ * contract-congruence with cloud, nothing more. The Bun.serve WS integration
29
+ * lives in `ws-server.ts` + `ws-subscribe.ts`.
29
30
  *
30
31
  * ## How fan-out works
31
32
  *
@@ -108,40 +109,14 @@ interface Subscription {
108
109
  closed: boolean;
109
110
  }
110
111
 
111
- /** SSE wire frame: `event: <name>\ndata: <json>\n\n`. The ONE place SSE bytes
112
- * are formatted (route + tests + SseSink all route through it). */
113
- export function sseFrame(event: string, data: unknown): string {
114
- return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
115
- }
116
-
117
- /** WebSocket message: `{ type: <event>, ...data }`. The SSE `event:` name folds
118
- * into `type`; the inner payload serializes identically to the SSE `data:`
119
- * body. The ONE place WS bytes are formatted. */
112
+ /** WebSocket message: `{ type: <event>, ...data }` the event name becomes the
113
+ * `type` discriminator and the inner payload (`note` / `id` / `notes`)
114
+ * serializes as-is. The ONE place WS bytes are formatted. */
120
115
  export function wsFrame(event: string, data: unknown): string {
121
116
  const body = data && typeof data === "object" && !Array.isArray(data) ? (data as Record<string, unknown>) : {};
122
117
  return JSON.stringify({ type: event, ...body });
123
118
  }
124
119
 
125
- /** SSE sink: renders `(event, data)` to an SSE frame and hands it to `push`
126
- * (the route's stream-queue writer). `push` returns false when the stream is
127
- * gone. `comment()` emits a raw `:` keepalive (SSE-only; not part of the
128
- * abstract contract). Bytes are byte-identical to the pre-migration SSE path. */
129
- export class SseSink implements SubscriptionSink {
130
- constructor(
131
- private readonly push: (frame: string) => boolean,
132
- private readonly onClose: () => void,
133
- ) {}
134
- send(event: string, data: unknown): boolean {
135
- return this.push(sseFrame(event, data));
136
- }
137
- comment(text = ""): boolean {
138
- return this.push(`:${text}\n\n`);
139
- }
140
- close(): void {
141
- this.onClose();
142
- }
143
- }
144
-
145
120
  /** Minimal structural shape of a Bun `ServerWebSocket` (or a test double) that
146
121
  * {@link WsSink} needs — keeps this module free of a hard Bun-server import. */
147
122
  export interface WsLike {
@@ -374,10 +349,5 @@ export interface SubscriptionHandle {
374
349
  close: () => void;
375
350
  }
376
351
 
377
- /** Serialize a snapshot SSE frame (exported for the SSE route + tests). */
378
- export function snapshotFrame(notes: Note[]): string {
379
- return sseFrame("snapshot", { notes });
380
- }
381
-
382
352
  /** Process-wide manager — shared like `defaultHookRegistry`. */
383
353
  export const subscriptionManager = new SubscriptionManager();
@@ -358,11 +358,11 @@ describe("vault create — services.json registration (#208)", () => {
358
358
  * Default-pack seeding on create (originally demo-prep Workstream A — A1/A3;
359
359
  * reshaped for named seed packs).
360
360
  *
361
- * A freshly-created vault must contain the `welcome` pack (three-note welcome
362
- * web + the one capture tag Notes requires) and the `getting-started` guide — and
363
- * NOT the `surface-starter` pack, which is opt-in via `add-pack` (ratified
364
- * 2026-07-02). Idempotent + best-effort: the seed never fails a create and
365
- * never clobbers an edited note.
361
+ * A freshly-created vault must contain the `welcome` pack (the five-guide
362
+ * welcome ring + the capture/guide/pinned tags) and the `getting-started`
363
+ * guide — and NOT the `surface-starter` pack, which is opt-in via `add-pack`
364
+ * (ratified 2026-07-02). Idempotent + best-effort: the seed never fails a
365
+ * create and never clobbers an edited note.
366
366
  */
367
367
  describe("vault create — default pack seeding (welcome + getting-started)", () => {
368
368
  /** Read all (path, content) rows from a created vault's SQLite DB. */
@@ -400,23 +400,28 @@ describe("vault create — default pack seeding (welcome + getting-started)", ()
400
400
  const notes = readNotes("guided");
401
401
  const gs = notes.find((n) => n.path === "Getting Started");
402
402
  const welcome = notes.find((n) => n.path === "Welcome to your vault 🪂");
403
- const tryLinking = notes.find((n) => n.path === "Try linking notes");
403
+ const captureAnything = notes.find((n) => n.path === "Capture anything");
404
+ const tagsGraph = notes.find((n) => n.path === "Tags and the graph");
404
405
  const connectAi = notes.find((n) => n.path === "Connect your AI");
406
+ const yoursToKeep = notes.find((n) => n.path === "Yours to keep");
405
407
  expect(gs).toBeDefined();
406
408
  expect(welcome).toBeDefined();
407
- expect(tryLinking).toBeDefined();
409
+ expect(captureAnything).toBeDefined();
410
+ expect(tagsGraph).toBeDefined();
408
411
  expect(connectAi).toBeDefined();
412
+ expect(yoursToKeep).toBeDefined();
409
413
  expect(gs!.content).toContain("# Getting Started");
410
414
  // Surface Starter is out of the default seed — no note, no dangling link.
411
415
  expect(notes.find((n) => n.path === "Surface Starter")).toBeUndefined();
412
416
  expect(gs!.content).not.toContain("[[Surface Starter]]");
413
417
  expect(gs!.content).toContain("add-pack surface-starter");
414
418
 
415
- // The ONE capture tag Notes requires arrives with the welcome pack
416
- // and nothing else (fresh-vault seed = 4 notes + 1 tag; the retired
419
+ // The capture tag Notes requires arrives with the welcome pack, alongside
420
+ // the guide (skill-file) + pinned tags and nothing else (fresh-vault
421
+ // seed = 6 notes: the 5-guide ring + Getting Started. The retired
417
422
  // capture/text + capture/voice subtypes are no longer seeded).
418
- expect(readTagNames("guided")).toEqual(["capture"]);
419
- expect(notes).toHaveLength(4);
423
+ expect(readTagNames("guided").sort()).toEqual(["capture", "guide", "pinned"]);
424
+ expect(notes).toHaveLength(6);
420
425
  });
421
426
 
422
427
  test("seeding doesn't break --json stdout (notes seeded silently)", () => {
package/src/ws-server.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * - `ws-subscribe.ts` — the PURE half (close codes, query validation, chunked
15
15
  * snapshot, message parsing, verb-rank) shared byte-shaped with cloud.
16
16
  * - `subscriptions.ts` — the transport-agnostic manager + `WsSink`
17
- * (`{ type, ...data }`) / `SseSink` (`event:`/`data:`).
17
+ * (`{ type, ...data }`), the sole live-transport sink since Phase 5.
18
18
  * - this module — the Bun.serve upgrade decision + `open`/`message`/`close`
19
19
  * handlers + the per-vault live-socket registry (the WS cap).
20
20
  *