@openparachute/vault 0.6.5-rc.9 → 0.6.5
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/.parachute/module.json +1 -0
- package/core/src/core.test.ts +7 -7
- package/core/src/mcp.ts +1 -1
- package/core/src/schema.ts +5 -5
- package/core/src/seed-packs.test.ts +222 -56
- package/core/src/seed-packs.ts +334 -70
- package/core/src/tag-hierarchy.ts +1 -1
- package/core/src/tag-schemas.ts +2 -2
- package/core/src/types.ts +1 -1
- package/package.json +1 -1
- package/src/admin-spa.ts +2 -1
- package/src/auth.ts +1 -1
- package/src/cli.ts +317 -53
- package/src/export-watch.ts +1 -1
- package/src/live-frame-parity.test.ts +201 -0
- package/src/module-manifest.ts +8 -0
- package/src/onboarding-seed.test.ts +82 -20
- package/src/onboarding-seed.ts +2 -2
- package/src/routes.ts +3 -3
- package/src/routing.test.ts +2 -2
- package/src/routing.ts +18 -6
- package/src/self-register.test.ts +19 -0
- package/src/self-register.ts +6 -1
- package/src/server.ts +56 -0
- package/src/services-manifest.ts +8 -0
- package/src/subscriptions.ts +100 -42
- package/src/tag-scope.ts +3 -3
- package/src/test-support/live-frame-corpus.ts +72 -0
- package/src/token-store.ts +2 -2
- package/src/transcription/build.test.ts +86 -5
- package/src/transcription/build.ts +87 -7
- package/src/transcription/capability.test.ts +25 -0
- package/src/transcription/capability.ts +27 -2
- package/src/transcription/download.test.ts +101 -0
- package/src/transcription/download.ts +71 -0
- package/src/transcription/install-python.test.ts +366 -0
- package/src/transcription/install-python.ts +471 -0
- package/src/transcription/providers/onnx-asr.test.ts +229 -0
- package/src/transcription/providers/onnx-asr.ts +239 -0
- package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
- package/src/transcription/providers/parakeet-mlx.ts +242 -0
- package/src/transcription/select.test.ts +166 -1
- package/src/transcription/select.ts +234 -1
- package/src/transcription/tiers.test.ts +197 -0
- package/src/transcription/tiers.ts +184 -0
- package/src/vault-create.test.ts +19 -14
- package/src/vault.test.ts +1 -1
- package/src/ws-server.ts +408 -0
- package/src/ws-subscribe.test.ts +474 -0
- package/src/ws-subscribe.ts +242 -0
- package/src/subscribe.test.ts +0 -609
- package/src/subscribe.ts +0 -248
package/src/subscribe.ts
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live-query SSE subscribe route (live-query SSE — design
|
|
3
|
-
* `design/2026-06-08-live-query-sse.md`).
|
|
4
|
-
*
|
|
5
|
-
* GET /vault/<name>/api/subscribe?<query params>
|
|
6
|
-
*
|
|
7
|
-
* Sends an `event: snapshot` of the currently-matching (scoped) notes, then
|
|
8
|
-
* live `upsert`/`remove` events as notes change. Auth + tag-scope are already
|
|
9
|
-
* resolved by the caller (routing.ts) and threaded in — the same `?key=` /
|
|
10
|
-
* header credential and the same `tagScope` the notes path uses. This route
|
|
11
|
-
* adds NO new auth plumbing.
|
|
12
|
-
*
|
|
13
|
-
* The query string is parsed by the SAME `parseNotesQueryOpts` the structured
|
|
14
|
-
* notes-query branch uses, so the snapshot predicate and the live matcher
|
|
15
|
-
* evaluate an identical `QueryOpts`. `search` (FTS) and `near` (graph BFS) are
|
|
16
|
-
* not evaluable against a single in-memory note, so a subscribe request using
|
|
17
|
-
* them is rejected with 400 BEFORE any stream opens.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type { Store, QueryOpts } from "../core/src/types.ts";
|
|
21
|
-
import { parseNotesQueryOpts, type TagScopeCtx } from "./routes.ts";
|
|
22
|
-
import { filterNotesByTagScope } from "./tag-scope.ts";
|
|
23
|
-
import { buildLiveMatcher, unsupportedSubscriptionReason } from "./live-match.ts";
|
|
24
|
-
import {
|
|
25
|
-
snapshotFrame,
|
|
26
|
-
subscriptionManager,
|
|
27
|
-
type SubscriptionManager,
|
|
28
|
-
type SubscriptionSink,
|
|
29
|
-
} from "./subscriptions.ts";
|
|
30
|
-
|
|
31
|
-
/** Keepalive interval — `:` comment every ~25s to defeat idle-proxy timeouts. */
|
|
32
|
-
const KEEPALIVE_MS = 25_000;
|
|
33
|
-
|
|
34
|
-
function json(data: unknown, status = 200): Response {
|
|
35
|
-
return Response.json(data, { status });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Handle `GET /api/subscribe`. `vaultName` + `tagScope` come from routing.ts
|
|
40
|
-
* (auth already validated; tag-scope already expanded). `manager` is injectable
|
|
41
|
-
* for tests; defaults to the process-wide singleton.
|
|
42
|
-
*/
|
|
43
|
-
export async function handleSubscribe(
|
|
44
|
-
req: Request,
|
|
45
|
-
store: Store,
|
|
46
|
-
vaultName: string,
|
|
47
|
-
tagScope: TagScopeCtx,
|
|
48
|
-
manager: SubscriptionManager = subscriptionManager,
|
|
49
|
-
): Promise<Response> {
|
|
50
|
-
if (req.method !== "GET") {
|
|
51
|
-
return json({ error: "Method not allowed", message: "subscribe is GET-only" }, 405);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const url = new URL(req.url);
|
|
55
|
-
|
|
56
|
-
// Reject the un-live-evaluable query shapes up front (before any stream).
|
|
57
|
-
if (url.searchParams.get("search")) {
|
|
58
|
-
return json(
|
|
59
|
-
{
|
|
60
|
-
error: "search (full-text) is not supported for live subscriptions — FTS can't be evaluated against a single changed note. Drop `search` or poll GET /notes?search=.",
|
|
61
|
-
code: "UNSUPPORTED_SUBSCRIPTION_QUERY",
|
|
62
|
-
},
|
|
63
|
-
400,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
if (url.searchParams.get("near[note_id]")) {
|
|
67
|
-
return json(
|
|
68
|
-
{
|
|
69
|
-
error: "near (graph neighborhood) is not supported for live subscriptions — BFS can't be evaluated against a single changed note. Drop `near` or poll GET /notes?near[note_id]=.",
|
|
70
|
-
code: "UNSUPPORTED_SUBSCRIPTION_QUERY",
|
|
71
|
-
},
|
|
72
|
-
400,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const parsed = parseNotesQueryOpts(url);
|
|
77
|
-
if (parsed.error) return parsed.error;
|
|
78
|
-
const queryOpts = parsed.queryOpts!;
|
|
79
|
-
|
|
80
|
-
// Belt-and-suspenders: reject cursor (paging) too — meaningless for a live set.
|
|
81
|
-
const unsupported = unsupportedSubscriptionReason(queryOpts);
|
|
82
|
-
if (unsupported) {
|
|
83
|
-
return json({ error: unsupported, code: "UNSUPPORTED_SUBSCRIPTION_QUERY" }, 400);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Build the in-process matcher (resolves tag descendants once, same
|
|
87
|
-
// hierarchy the snapshot query uses) — may throw QueryError on a malformed
|
|
88
|
-
// metadata filter shape; surface as 400, same as the notes path.
|
|
89
|
-
let matcher;
|
|
90
|
-
try {
|
|
91
|
-
matcher = await buildLiveMatcher(store, queryOpts);
|
|
92
|
-
} catch (e: any) {
|
|
93
|
-
if (e && e.name === "QueryError") {
|
|
94
|
-
return json({ error: e.message, code: e.code ?? "INVALID_QUERY" }, 400);
|
|
95
|
-
}
|
|
96
|
-
throw e;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Snapshot: the scoped query result. queryNotes throws QueryError on e.g. a
|
|
100
|
-
// non-indexed metadata operator field — surface as 400 (no stream opened).
|
|
101
|
-
//
|
|
102
|
-
// Strip paging (M3): the live matcher ignores limit/offset, so a default
|
|
103
|
-
// limit:50 would truncate the snapshot while live events deliver the full
|
|
104
|
-
// set — snapshot ⊊ live. The snapshot must be the COMPLETE matching set.
|
|
105
|
-
// queryNotes defaults an ABSENT limit to 100 (not unlimited), so pass a
|
|
106
|
-
// large sentinel to fetch every matching row.
|
|
107
|
-
const SNAPSHOT_UNBOUNDED = Number.MAX_SAFE_INTEGER;
|
|
108
|
-
const snapshotOpts: QueryOpts = { ...queryOpts, limit: SNAPSHOT_UNBOUNDED, offset: undefined };
|
|
109
|
-
let snapshotNotes;
|
|
110
|
-
try {
|
|
111
|
-
const raw = await store.queryNotes(snapshotOpts);
|
|
112
|
-
snapshotNotes = filterNotesByTagScope(raw, tagScope.allowed, tagScope.raw);
|
|
113
|
-
} catch (e: any) {
|
|
114
|
-
if (e && e.name === "QueryError") {
|
|
115
|
-
return json({ error: e.message, code: e.code ?? "INVALID_QUERY" }, 400);
|
|
116
|
-
}
|
|
117
|
-
throw e;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Cap check BEFORE opening a stream so we can return a real 503. (The
|
|
121
|
-
// in-`start` re-check below only guards the rare interleave race where two
|
|
122
|
-
// subscribes pass this check before either registers.)
|
|
123
|
-
if (!manager.hasCapacity(vaultName)) {
|
|
124
|
-
return json(
|
|
125
|
-
{
|
|
126
|
-
error: "subscription cap reached for this vault — too many concurrent live subscriptions. Retry shortly or close idle streams.",
|
|
127
|
-
code: "SUBSCRIPTION_CAP_REACHED",
|
|
128
|
-
},
|
|
129
|
-
503,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ---- Build the SSE stream ----
|
|
134
|
-
//
|
|
135
|
-
// A pull ReadableStream with an internal frame queue. The manager's sink
|
|
136
|
-
// writes frames into the queue; `pull` drains them to the controller and
|
|
137
|
-
// notifies the manager (so the backpressure counter decrements). When the
|
|
138
|
-
// client disconnects, `cancel` fires → we unregister the subscription and
|
|
139
|
-
// clear the keepalive timer.
|
|
140
|
-
let handle: { flushed: () => void; close: () => void } | null = null;
|
|
141
|
-
let keepalive: ReturnType<typeof setInterval> | null = null;
|
|
142
|
-
|
|
143
|
-
const queue: string[] = [];
|
|
144
|
-
let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
145
|
-
let cancelled = false;
|
|
146
|
-
const encoder = new TextEncoder();
|
|
147
|
-
|
|
148
|
-
const flushQueue = () => {
|
|
149
|
-
if (!controllerRef || cancelled) return;
|
|
150
|
-
while (queue.length > 0) {
|
|
151
|
-
const frame = queue.shift()!;
|
|
152
|
-
try {
|
|
153
|
-
controllerRef.enqueue(encoder.encode(frame));
|
|
154
|
-
} catch {
|
|
155
|
-
// Controller closed underneath us — stop.
|
|
156
|
-
cancelled = true;
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
handle?.flushed();
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const sink: SubscriptionSink = {
|
|
164
|
-
write(frame: string): boolean {
|
|
165
|
-
if (cancelled) return false;
|
|
166
|
-
queue.push(frame);
|
|
167
|
-
flushQueue();
|
|
168
|
-
return true;
|
|
169
|
-
},
|
|
170
|
-
close(): void {
|
|
171
|
-
if (cancelled) return;
|
|
172
|
-
cancelled = true;
|
|
173
|
-
if (keepalive) {
|
|
174
|
-
clearInterval(keepalive);
|
|
175
|
-
keepalive = null;
|
|
176
|
-
}
|
|
177
|
-
try {
|
|
178
|
-
controllerRef?.close();
|
|
179
|
-
} catch {
|
|
180
|
-
/* already closed */
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const stream = new ReadableStream<Uint8Array>({
|
|
186
|
-
start(controller) {
|
|
187
|
-
controllerRef = controller;
|
|
188
|
-
// 1. Snapshot first — written into the queue and flushed on this tick.
|
|
189
|
-
queue.push(snapshotFrame(snapshotNotes));
|
|
190
|
-
|
|
191
|
-
// 2. Register the live subscription. Hook dispatch is deferred to a
|
|
192
|
-
// microtask, and we register synchronously here within start(), so no
|
|
193
|
-
// live event can slip in front of the snapshot. Over-cap → 503; we
|
|
194
|
-
// can't return a Response from inside the stream, so the cap is also
|
|
195
|
-
// checked below before constructing the Response. (Re-check here for
|
|
196
|
-
// the race where two subscribes interleave.)
|
|
197
|
-
handle = manager.register({
|
|
198
|
-
vaultName,
|
|
199
|
-
matcher,
|
|
200
|
-
tagScopeAllowed: tagScope.allowed,
|
|
201
|
-
tagScopeRaw: tagScope.raw,
|
|
202
|
-
sink,
|
|
203
|
-
});
|
|
204
|
-
if (!handle) {
|
|
205
|
-
// Lost the cap race. Emit nothing further and close; the pre-check
|
|
206
|
-
// below normally catches this, so this path is rare.
|
|
207
|
-
flushQueue();
|
|
208
|
-
try {
|
|
209
|
-
controller.close();
|
|
210
|
-
} catch {
|
|
211
|
-
/* noop */
|
|
212
|
-
}
|
|
213
|
-
cancelled = true;
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
flushQueue();
|
|
218
|
-
|
|
219
|
-
// 3. Keepalive comments.
|
|
220
|
-
keepalive = setInterval(() => {
|
|
221
|
-
if (cancelled) return;
|
|
222
|
-
sink.write(":\n\n");
|
|
223
|
-
}, KEEPALIVE_MS);
|
|
224
|
-
},
|
|
225
|
-
pull() {
|
|
226
|
-
flushQueue();
|
|
227
|
-
},
|
|
228
|
-
cancel() {
|
|
229
|
-
cancelled = true;
|
|
230
|
-
if (keepalive) {
|
|
231
|
-
clearInterval(keepalive);
|
|
232
|
-
keepalive = null;
|
|
233
|
-
}
|
|
234
|
-
handle?.close();
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
return new Response(stream, {
|
|
239
|
-
status: 200,
|
|
240
|
-
headers: {
|
|
241
|
-
"Content-Type": "text/event-stream",
|
|
242
|
-
"Cache-Control": "no-cache, no-transform",
|
|
243
|
-
Connection: "keep-alive",
|
|
244
|
-
// Defeat nginx-style proxy buffering of the event stream.
|
|
245
|
-
"X-Accel-Buffering": "no",
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
}
|