pi-weave 0.1.12 → 0.1.13
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 +8 -37
- package/package.json +1 -2
- package/src/core/concurrency.ts +3 -6
- package/src/core/frontmatter.ts +0 -53
- package/src/core/graph/build.ts +6 -7
- package/src/core/graph/current.ts +2 -4
- package/src/core/graph/model.ts +1 -1
- package/src/core/graph/wikilinks.ts +3 -3
- package/src/core/index.ts +26 -27
- package/src/core/paths.ts +0 -7
- package/src/core/vault.ts +16 -681
- package/src/core/view/detail.ts +1 -1
- package/src/core/view/health.ts +1 -1
- package/src/core/view/tree.ts +1 -1
- package/src/pi/index.ts +6 -85
- package/src/pi/summarize.ts +2 -2
- package/src/pi/viewer/tui/bodyStore.ts +4 -7
- package/src/pi/viewer/tui/branding.ts +7 -148
- package/src/pi/viewer/tui/run.ts +3 -17
- package/src/pi/viewer/tui/surface/base.ts +24 -3
- package/src/pi/viewer/tui/surface/explore.ts +41 -6
- package/src/pi/viewer/tui/workspace.ts +23 -351
- package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
- package/src/pi/viewer/web/run.ts +7 -117
- package/src/web/client/api.dom.ts +2 -2
- package/src/web/client/api.ts +14 -223
- package/src/web/client/bootstrap.ts +5 -14
- package/src/web/client/context/context.model.ts +9 -11
- package/src/web/client/dist/app.js +93 -219
- package/src/web/client/graph/dynamics.ts +5 -65
- package/src/web/client/graph/renderer.dom.ts +7 -8
- package/src/web/client/graph/renderer.ts +9 -35
- package/src/web/client/main.tsx +1 -1
- package/src/web/client/note/Note.tsx +21 -63
- package/src/web/client/search/SearchPalette.tsx +45 -36
- package/src/web/client/search/search.model.ts +33 -454
- package/src/web/client/shell/Columns.tsx +13 -83
- package/src/web/client/shell/Header.tsx +2 -10
- package/src/web/client/shell/Shell.tsx +50 -125
- package/src/web/client/shell/StatusBar.tsx +1 -4
- package/src/web/client/shell/icons.model.ts +4 -7
- package/src/web/client/shell/keys.model.ts +5 -42
- package/src/web/client/shell/keys.ts +2 -2
- package/src/web/client/shell/shell.model.ts +10 -133
- package/src/web/client/shell/theme.model.ts +2 -2
- package/src/web/client/shell/theme.ts +33 -157
- package/src/web/client/state.ts +9 -89
- package/src/web/client/tree/Tree.tsx +25 -575
- package/src/web/client/tree/tree.model.ts +8 -162
- package/src/web/client/workspace.ts +72 -242
- package/src/web/server/page.ts +8 -10
- package/src/web/server/routes.ts +30 -563
- package/src/web/server/server.ts +6 -145
- package/src/web/shared/layout.ts +72 -624
- package/src/web/shared/wire.ts +10 -196
- package/src/core/sessions.ts +0 -929
- package/src/pi/sessionScan.ts +0 -104
- package/src/pi/viewer/tui/explorer.ts +0 -586
- package/src/web/client/live.model.ts +0 -275
- package/src/web/client/live.ts +0 -151
- package/src/web/client/note/Editor.tsx +0 -109
- package/src/web/client/note/editor.controller.ts +0 -151
- package/src/web/client/note/editor.model.ts +0 -686
- package/src/web/client/search/search.ts +0 -107
- package/src/web/client/shell/Divider.tsx +0 -44
- package/src/web/client/shell/cssvars.ts +0 -70
- package/src/web/client/shell/drag.model.ts +0 -170
- package/src/web/client/shell/layout.model.ts +0 -500
- package/src/web/client/shell/viewport.ts +0 -29
- package/src/web/server/sse.ts +0 -321
- package/src/web/server/watcher.ts +0 -507
package/src/web/server/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The loopback workspace server — bind, lifecycle, teardown
|
|
3
|
-
* (weave-workspace §5.
|
|
3
|
+
* (weave-workspace §5.3, §5.4, §10).
|
|
4
4
|
*
|
|
5
5
|
* This module owns three things and delegates everything else:
|
|
6
6
|
*
|
|
@@ -8,45 +8,18 @@
|
|
|
8
8
|
* Never a fixed port: a fixed port is a port another process can squat
|
|
9
9
|
* before us, and a port a malicious page can guess without scanning.
|
|
10
10
|
* 2. **Composition.** It builds the {@link RouteDeps} — a
|
|
11
|
-
* {@link WorkspaceCache}
|
|
12
|
-
*
|
|
13
|
-
* 3. **Lifecycle.**
|
|
14
|
-
* disconnects, and a `close()` that actually releases the port.
|
|
15
|
-
*
|
|
16
|
-
* ## Why the hub and the watcher are constructor arguments
|
|
17
|
-
*
|
|
18
|
-
* `SseHub` and `Watcher` are declared in `routes.ts` (the consumer) and
|
|
19
|
-
* arrive here as optional options. That is not ceremony: it means the server
|
|
20
|
-
* can be booted with neither — which is exactly what the route tests do, so
|
|
21
|
-
* they exercise the real socket and the real security policy without a file
|
|
22
|
-
* watcher spinning up on a temp directory, and it means the SSE
|
|
23
|
-
* implementation can be written, replaced, or omitted with no edit to this
|
|
24
|
-
* file. With no hub, `/events` answers `503` and the server has no idle
|
|
25
|
-
* timer to run, because nothing can be idle.
|
|
26
|
-
*
|
|
27
|
-
* ## Idle shutdown, precisely
|
|
28
|
-
*
|
|
29
|
-
* The timer starts when the SSE client count reaches zero and is cancelled
|
|
30
|
-
* when a client attaches. A workspace with a tab open is never idle; a
|
|
31
|
-
* workspace whose last tab closed is reclaimed after
|
|
32
|
-
* {@link DEFAULT_IDLE_MS}. `setTimer` is injectable, so the test asserts the
|
|
33
|
-
* transition rather than waiting thirty minutes for it, and
|
|
34
|
-
* `timer.unref?.()` keeps a pending shutdown from holding the process open.
|
|
11
|
+
* {@link WorkspaceCache} and {@link SecurityPolicy} — and passes them to
|
|
12
|
+
* `handleRequest`.
|
|
13
|
+
* 3. **Lifecycle.** `close()` releases the port.
|
|
35
14
|
*/
|
|
36
15
|
|
|
37
|
-
import { randomBytes } from "node:crypto";
|
|
38
16
|
import { createServer as createHttpServer, type Server } from "node:http";
|
|
39
17
|
import { fileURLToPath } from "node:url";
|
|
40
18
|
import { WorkspaceCache } from "../../core/cache/workspace";
|
|
41
19
|
import { resolveVaultRoot } from "../../core/paths";
|
|
42
|
-
import { handleRequest, type RouteDeps
|
|
20
|
+
import { handleRequest, type RouteDeps } from "./routes";
|
|
43
21
|
import { createSecurityPolicy, type SecurityPolicy } from "./security";
|
|
44
22
|
|
|
45
|
-
export type { SseHub, Watcher } from "./routes";
|
|
46
|
-
|
|
47
|
-
/** §5.4: reclaim the port 30 minutes after the last SSE client disconnects. */
|
|
48
|
-
export const DEFAULT_IDLE_MS = 30 * 60 * 1000;
|
|
49
|
-
|
|
50
23
|
/**
|
|
51
24
|
* The committed bundle, resolved relative to this module rather than to
|
|
52
25
|
* `process.cwd()` — the server runs in whatever directory the user invoked
|
|
@@ -56,24 +29,10 @@ export function defaultBundlePath(): string {
|
|
|
56
29
|
return fileURLToPath(new URL("../client/dist/app.js", import.meta.url));
|
|
57
30
|
}
|
|
58
31
|
|
|
59
|
-
/** A `setTimeout` shaped seam, so the idle path is testable in microseconds. */
|
|
60
|
-
export interface TimerHandle {
|
|
61
|
-
unref?(): unknown;
|
|
62
|
-
}
|
|
63
|
-
export type SetTimer = (fn: () => void, ms: number) => TimerHandle;
|
|
64
|
-
export type ClearTimer = (handle: TimerHandle) => void;
|
|
65
|
-
|
|
66
32
|
export interface StartWorkspaceServerOptions {
|
|
67
33
|
cwd: string;
|
|
68
34
|
/** Defaults to `resolveVaultRoot()`, which honours `PI_WEAVE_VAULT`. */
|
|
69
35
|
vaultRoot?: string;
|
|
70
|
-
/**
|
|
71
|
-
* Live-update hub. Absent → `/events` answers `503` and the idle timer
|
|
72
|
-
* never runs. Supplied by the caller that also constructs the watcher.
|
|
73
|
-
*/
|
|
74
|
-
sse?: SseHub | undefined;
|
|
75
|
-
/** File watcher. Started during boot, closed during teardown. */
|
|
76
|
-
watcher?: Watcher | undefined;
|
|
77
36
|
/** Pre-built cache. Defaults to a fresh one over `cwd` + `vaultRoot`. */
|
|
78
37
|
cache?: WorkspaceCache | undefined;
|
|
79
38
|
/** Fixed token, for tests that need to know it before the boot resolves. */
|
|
@@ -84,33 +43,6 @@ export interface StartWorkspaceServerOptions {
|
|
|
84
43
|
bundlePath?: string | undefined;
|
|
85
44
|
/** Test seam for `POST /api/open`. */
|
|
86
45
|
openNote?: ((slug: string) => Promise<boolean>) | undefined;
|
|
87
|
-
/** Test seam for the note read; see {@link RouteDeps.readNote}. */
|
|
88
|
-
readNote?: RouteDeps["readNote"];
|
|
89
|
-
/**
|
|
90
|
-
* Self-write suppression (§6): tell the watcher to ignore a path it is
|
|
91
|
-
* about to see us change.
|
|
92
|
-
*
|
|
93
|
-
* A separate option rather than a method on {@link Watcher}, because
|
|
94
|
-
* `Watcher` here is the two-method lifecycle contract `routes.ts` declares
|
|
95
|
-
* — start and close — and widening it would force every future watcher
|
|
96
|
-
* (a poller, a remote FS) to implement suppression whether or not the
|
|
97
|
-
* concept applies to it. The caller that constructs the real watcher knows
|
|
98
|
-
* it has a `suppress`, and passes it.
|
|
99
|
-
*
|
|
100
|
-
* Defaults to {@link Watcher.suppress} when the injected watcher has one,
|
|
101
|
-
* so the common wiring is automatic and the option exists for a caller
|
|
102
|
-
* whose suppression lives somewhere else (or for a test that wants to
|
|
103
|
-
* observe it). Absent on both means writes happen unsuppressed, which is
|
|
104
|
-
* exactly right for a server booted with no watcher: there is nothing
|
|
105
|
-
* listening to feed a loop.
|
|
106
|
-
*/
|
|
107
|
-
suppress?: ((absPath: string) => void) | undefined;
|
|
108
|
-
/** Idle shutdown delay. `0` disables it entirely. */
|
|
109
|
-
idleMs?: number | undefined;
|
|
110
|
-
setTimer?: SetTimer | undefined;
|
|
111
|
-
clearTimer?: ClearTimer | undefined;
|
|
112
|
-
/** Invoked when the idle timeout fires, after `close()`. */
|
|
113
|
-
onIdleShutdown?: (() => void) | undefined;
|
|
114
46
|
}
|
|
115
47
|
|
|
116
48
|
export interface WorkspaceServer {
|
|
@@ -122,37 +54,17 @@ export interface WorkspaceServer {
|
|
|
122
54
|
token: string;
|
|
123
55
|
security: SecurityPolicy;
|
|
124
56
|
cache: WorkspaceCache;
|
|
125
|
-
/**
|
|
126
|
-
session: string;
|
|
127
|
-
/**
|
|
128
|
-
* Called by the SSE hub whenever a client attaches or detaches, so the
|
|
129
|
-
* idle timer can be armed or cancelled. Exposed rather than inferred
|
|
130
|
-
* because only the hub knows when a socket actually died.
|
|
131
|
-
*/
|
|
132
|
-
noteActivity(): void;
|
|
133
|
-
/** Release the port, the watcher and every stream. Idempotent. */
|
|
57
|
+
/** Release the port. Idempotent. */
|
|
134
58
|
close(): Promise<void>;
|
|
135
59
|
}
|
|
136
60
|
|
|
137
61
|
export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): Promise<WorkspaceServer> {
|
|
138
62
|
const vaultRoot = opts.vaultRoot ?? resolveVaultRoot();
|
|
139
63
|
const cache = opts.cache ?? new WorkspaceCache({ cwd: opts.cwd, vaultRoot });
|
|
140
|
-
const session = randomBytes(8).toString("hex");
|
|
141
|
-
const setTimer: SetTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
142
|
-
const clearTimer: ClearTimer = opts.clearTimer ?? ((h) => clearTimeout(h as NodeJS.Timeout));
|
|
143
|
-
const idleMs = opts.idleMs ?? DEFAULT_IDLE_MS;
|
|
144
|
-
|
|
145
64
|
// `deps` is assembled before `listen` because the request handler closes
|
|
146
65
|
// over it, but the security policy needs the bound port — so the policy
|
|
147
66
|
// slot is filled after binding and the handler reads it through the
|
|
148
67
|
// mutable holder rather than capturing a stale value.
|
|
149
|
-
// Explicit `suppress` first, else the watcher's own if it has one (§6).
|
|
150
|
-
// Bound as a closure rather than passed as a method reference, so a
|
|
151
|
-
// watcher implementing `suppress` as a class method keeps its `this`.
|
|
152
|
-
const watcherSuppress = opts.watcher?.suppress;
|
|
153
|
-
const suppress: ((absPath: string) => void) | null =
|
|
154
|
-
opts.suppress ?? (watcherSuppress === undefined ? null : (absPath: string) => watcherSuppress.call(opts.watcher, absPath));
|
|
155
|
-
|
|
156
68
|
let deps: RouteDeps | null = null;
|
|
157
69
|
const server: Server = createHttpServer((req, res) => {
|
|
158
70
|
// Unreachable in practice — `listen` resolves before any connection is
|
|
@@ -176,49 +88,11 @@ export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): P
|
|
|
176
88
|
});
|
|
177
89
|
|
|
178
90
|
let closed = false;
|
|
179
|
-
let idleHandle: TimerHandle | null = null;
|
|
180
|
-
|
|
181
|
-
const cancelIdle = (): void => {
|
|
182
|
-
if (idleHandle !== null) {
|
|
183
|
-
clearTimer(idleHandle);
|
|
184
|
-
idleHandle = null;
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const armIdle = (): void => {
|
|
189
|
-
// No hub means `/events` answers 503, so there is no such thing as a
|
|
190
|
-
// client here and nothing for a countdown to count. The server stays up
|
|
191
|
-
// until its owner closes it.
|
|
192
|
-
if (idleMs <= 0 || closed || opts.sse === undefined) return;
|
|
193
|
-
const handle = setTimer(() => {
|
|
194
|
-
idleHandle = null;
|
|
195
|
-
void close().then(() => opts.onIdleShutdown?.());
|
|
196
|
-
}, idleMs);
|
|
197
|
-
// A pending shutdown must not be the reason the process stays alive.
|
|
198
|
-
handle.unref?.();
|
|
199
|
-
idleHandle = handle;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const noteActivity = (): void => {
|
|
203
|
-
cancelIdle();
|
|
204
|
-
// Zero clients right now means the countdown starts now. A hub that
|
|
205
|
-
// reports a live client cancels it and says nothing more until that
|
|
206
|
-
// client leaves. With no hub at all, `armIdle` is already a no-op — the
|
|
207
|
-
// guard lives there rather than being repeated here.
|
|
208
|
-
if (opts.sse === undefined || opts.sse.clientCount() === 0) armIdle();
|
|
209
|
-
};
|
|
210
|
-
|
|
211
91
|
const close = async (): Promise<void> => {
|
|
212
92
|
if (closed) return;
|
|
213
93
|
closed = true;
|
|
214
|
-
cancelIdle();
|
|
215
|
-
opts.sse?.close();
|
|
216
|
-
await opts.watcher?.close();
|
|
217
94
|
await new Promise<void>((resolve) => {
|
|
218
95
|
server.close(() => resolve());
|
|
219
|
-
// Without this, an open SSE stream keeps the server's handle alive and
|
|
220
|
-
// `close()` never resolves — the exact reason a pi session used to
|
|
221
|
-
// hang on shutdown.
|
|
222
96
|
server.closeAllConnections();
|
|
223
97
|
});
|
|
224
98
|
};
|
|
@@ -226,23 +100,12 @@ export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): P
|
|
|
226
100
|
deps = {
|
|
227
101
|
cwd: opts.cwd,
|
|
228
102
|
vaultRoot,
|
|
229
|
-
session,
|
|
230
103
|
cache,
|
|
231
104
|
security,
|
|
232
105
|
bundlePath: opts.bundlePath ?? defaultBundlePath(),
|
|
233
|
-
...(opts.sse !== undefined ? { sse: opts.sse } : {}),
|
|
234
106
|
...(opts.openNote !== undefined ? { openNote: opts.openNote } : {}),
|
|
235
|
-
...(opts.readNote !== undefined ? { readNote: opts.readNote } : {}),
|
|
236
|
-
...(suppress === null ? {} : { suppress }),
|
|
237
|
-
onActivity: noteActivity,
|
|
238
107
|
};
|
|
239
108
|
|
|
240
|
-
await opts.watcher?.start();
|
|
241
|
-
|
|
242
|
-
// A workspace nobody has connected to yet is already idle: if the browser
|
|
243
|
-
// never opens, the port should still be reclaimed.
|
|
244
|
-
if (opts.sse !== undefined) armIdle();
|
|
245
|
-
|
|
246
109
|
return {
|
|
247
110
|
url: security.origin,
|
|
248
111
|
entryUrl: security.entryUrl,
|
|
@@ -250,8 +113,6 @@ export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): P
|
|
|
250
113
|
token: security.token,
|
|
251
114
|
security,
|
|
252
115
|
cache,
|
|
253
|
-
session,
|
|
254
|
-
noteActivity,
|
|
255
116
|
close,
|
|
256
117
|
};
|
|
257
118
|
}
|