@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/core/src/seed-packs.test.ts +209 -55
- package/core/src/seed-packs.ts +208 -35
- package/package.json +1 -1
- package/src/live-frame-parity.test.ts +16 -41
- package/src/onboarding-seed.test.ts +79 -18
- package/src/onboarding-seed.ts +2 -2
- package/src/routing.ts +17 -5
- package/src/server.ts +3 -2
- package/src/subscriptions.ts +20 -50
- package/src/vault-create.test.ts +16 -11
- package/src/ws-server.ts +1 -1
- package/src/subscribe.test.ts +0 -609
- package/src/subscribe.ts +0 -249
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
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
|
|
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.
|
|
531
|
-
//
|
|
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
|
package/src/subscriptions.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Live-query subscription registry
|
|
3
|
-
*
|
|
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
|
-
* ##
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* the
|
|
28
|
-
*
|
|
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
|
-
/**
|
|
112
|
-
*
|
|
113
|
-
|
|
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();
|
package/src/vault-create.test.ts
CHANGED
|
@@ -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 (
|
|
362
|
-
*
|
|
363
|
-
* NOT the `surface-starter` pack, which is opt-in via `add-pack`
|
|
364
|
-
* 2026-07-02). Idempotent + best-effort: the seed never fails a
|
|
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
|
|
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(
|
|
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
|
|
416
|
-
//
|
|
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(
|
|
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 }`)
|
|
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
|
*
|