@re-cinq/floor-client 0.1.4 → 0.1.6
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 +2 -0
- package/dist/filters.d.ts +1 -0
- package/dist/floor-live.d.ts +14 -0
- package/dist/floor-live.js +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/live.d.ts +22 -0
- package/dist/live.js +30 -41
- package/dist/runs.d.ts +3 -0
- package/dist/runs.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -54,6 +54,8 @@ There is no idle timeout here on purpose. A caught-up quiet run legitimately say
|
|
|
54
54
|
|
|
55
55
|
One caveat for a relay: `visit_opened` and `visit_reported` carry the whole visit, `agentSettings` included — the model, the prompt, the image. Filter those before they reach a browser.
|
|
56
56
|
|
|
57
|
+
`runs.watchFloor(options)` follows the whole floor instead of one run: `run_started` and `run_changed`, each with a run's id and nothing else. It has no cursor. The floor opens every connection with `resync`, which means "read your list again", so a watch that drops comes back with nothing to remember. It is refused without the service token and comes back from everything else.
|
|
58
|
+
|
|
57
59
|
## Testing against it
|
|
58
60
|
|
|
59
61
|
Both seams are injectable and neither needs a server: `fetchFn` for HTTP, `socketFn` for the live socket. With `backoffMs: () => 0`, the whole reconnect table is unit-testable in milliseconds — see `live.test.ts`.
|
package/dist/filters.d.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FloorFrame } from "@re-cinq/floor-contracts";
|
|
2
|
+
import { type LiveReachable, type WatchEnd } from "./live.js";
|
|
3
|
+
export interface FloorWatchOptions {
|
|
4
|
+
/** False leaves every close to the caller. */
|
|
5
|
+
reconnect?: boolean;
|
|
6
|
+
/** Attempt to delay. Return 0 in a test. */
|
|
7
|
+
backoffMs?: (attempt: number) => number;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export interface FloorWatch extends AsyncIterable<FloorFrame> {
|
|
11
|
+
readonly ended: Promise<WatchEnd>;
|
|
12
|
+
stop(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function watchFloor(floor: LiveReachable, options?: FloorWatchOptions): FloorWatch;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { follow, openFeed } from "./live.js";
|
|
2
|
+
export function watchFloor(floor, options = {}) {
|
|
3
|
+
const watch = openFeed();
|
|
4
|
+
options.signal?.addEventListener("abort", () => watch.stop());
|
|
5
|
+
void follow({ floor, options, watch, urlFor: () => `${floor.url.replace(/^http/, "ws")}/assembly-runs/live` });
|
|
6
|
+
return watch;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=floor-live.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { serviceToken, visitToken } from "./tokens.js";
|
|
|
4
4
|
export { FloorProblem, isFloorProblem } from "./problem.js";
|
|
5
5
|
export { reach, asked, send } from "./send.js";
|
|
6
6
|
export { CLOSE, watchRun } from "./live.js";
|
|
7
|
+
export { watchFloor } from "./floor-live.js";
|
|
7
8
|
export type { FloorAccess, FloorClient, HealthApi, LinesApi, MigrationsApi, SchedulesApi, Version } from "./client.js";
|
|
8
9
|
export type { AppendRecord, GitCredentialOutcome, VisitAccess, VisitClient } from "./visit.js";
|
|
9
10
|
export type { ServiceToken, VisitToken } from "./tokens.js";
|
|
@@ -14,5 +15,6 @@ export type { EnqueueEvent, EventsApi, EventsPage } from "./events.js";
|
|
|
14
15
|
export type { CostsFilter, CostsGroupBy, EventFilter, OneOf, RunFilter } from "./filters.js";
|
|
15
16
|
export type { CostsRowsApi, RunPage, RunsApi, StartRun, StartedRun } from "./runs.js";
|
|
16
17
|
export type { BriefOutcome, RecordsPage, StationRunsApi } from "./station-runs.js";
|
|
18
|
+
export type { FloorWatch, FloorWatchOptions } from "./floor-live.js";
|
|
17
19
|
export type { LiveSocket, RefusalCode, RunWatch, SocketFn, WatchEnd, WatchOptions } from "./live.js";
|
|
18
20
|
export type * from "@re-cinq/floor-contracts";
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,5 @@ export { serviceToken, visitToken } from "./tokens.js";
|
|
|
5
5
|
export { FloorProblem, isFloorProblem } from "./problem.js";
|
|
6
6
|
export { reach, asked, send } from "./send.js";
|
|
7
7
|
export { CLOSE, watchRun } from "./live.js";
|
|
8
|
+
export { watchFloor } from "./floor-live.js";
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
package/dist/live.d.ts
CHANGED
|
@@ -54,4 +54,26 @@ export interface RunWatch extends AsyncIterable<LiveFrame> {
|
|
|
54
54
|
stop(): void;
|
|
55
55
|
}
|
|
56
56
|
export declare function watchRun(floor: LiveReachable, runId: string, options?: WatchOptions): RunWatch;
|
|
57
|
+
/** What one watch holds while it runs: the frames not yet handed out, the socket, and whether it is over. */
|
|
58
|
+
export interface Feed<Frame> extends AsyncIterable<Frame> {
|
|
59
|
+
readonly waiting: Frame[];
|
|
60
|
+
readonly socket: {
|
|
61
|
+
current: LiveSocket | null;
|
|
62
|
+
};
|
|
63
|
+
readonly ended: Promise<WatchEnd>;
|
|
64
|
+
endedYet(): WatchEnd | null;
|
|
65
|
+
take(frame: Frame): void;
|
|
66
|
+
settle(end: WatchEnd): void;
|
|
67
|
+
stop(): void;
|
|
68
|
+
}
|
|
69
|
+
export declare function openFeed<Frame>(onTake?: (frame: Frame) => void): Feed<Frame>;
|
|
70
|
+
type Reconnecting = Pick<WatchOptions, "reconnect" | "backoffMs">;
|
|
71
|
+
interface Following<Frame> {
|
|
72
|
+
floor: LiveReachable;
|
|
73
|
+
options: Reconnecting;
|
|
74
|
+
watch: Feed<Frame>;
|
|
75
|
+
/** The address for this attempt: a run's moves with its cursor, the floor's does not. */
|
|
76
|
+
urlFor(): string;
|
|
77
|
+
}
|
|
78
|
+
export declare function follow<Frame>(following: Following<Frame>): Promise<void>;
|
|
57
79
|
export {};
|
package/dist/live.js
CHANGED
|
@@ -4,36 +4,26 @@ const REFUSALS = [CLOSE.badCursor, CLOSE.noServiceToken, CLOSE.noSuchRun, CLOSE.
|
|
|
4
4
|
const BACKOFF_START_MS = 250;
|
|
5
5
|
const BACKOFF_CAP_MS = 10_000;
|
|
6
6
|
export function watchRun(floor, runId, options = {}) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const socket = { current: null };
|
|
15
|
-
const reading = journal.reading;
|
|
7
|
+
const cursor = { seq: cursorOf(options.after) };
|
|
8
|
+
const feed = openFeed((frame) => {
|
|
9
|
+
if (frame.type !== "unsupported")
|
|
10
|
+
cursor.seq = frame.seq;
|
|
11
|
+
});
|
|
12
|
+
options.signal?.addEventListener("abort", () => feed.stop());
|
|
13
|
+
void follow({ floor, options, watch: feed, urlFor: () => liveUrl(floor.url, runId, cursor.seq) });
|
|
16
14
|
return {
|
|
17
15
|
get seq() {
|
|
18
|
-
return
|
|
19
|
-
},
|
|
20
|
-
waiting: reading.waiting,
|
|
21
|
-
ended: reading.ended,
|
|
22
|
-
endedYet: reading.endedYet,
|
|
23
|
-
settle: reading.settle,
|
|
24
|
-
take: reading.take,
|
|
25
|
-
socket,
|
|
26
|
-
stop: () => {
|
|
27
|
-
socket.current?.close(CLOSE.settled, "the watcher stopped");
|
|
28
|
-
reading.settle({ reason: "stopped" });
|
|
16
|
+
return cursor.seq;
|
|
29
17
|
},
|
|
30
|
-
|
|
18
|
+
ended: feed.ended,
|
|
19
|
+
stop: feed.stop,
|
|
20
|
+
[Symbol.asyncIterator]: () => feed[Symbol.asyncIterator](),
|
|
31
21
|
};
|
|
32
22
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const held = { seq: from, end: null };
|
|
23
|
+
export function openFeed(onTake = () => { }) {
|
|
24
|
+
const held = { end: null };
|
|
36
25
|
const waiting = [];
|
|
26
|
+
const socket = { current: null };
|
|
37
27
|
const { sleep, wake } = sleeper();
|
|
38
28
|
const { ended, finish } = promised();
|
|
39
29
|
const settle = (end) => {
|
|
@@ -44,22 +34,21 @@ function journalFrom(from) {
|
|
|
44
34
|
wake();
|
|
45
35
|
};
|
|
46
36
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
[Symbol.asyncIterator]: () => handOut(waiting, { endedYet: () => held.end, sleep }),
|
|
37
|
+
waiting,
|
|
38
|
+
ended,
|
|
39
|
+
socket,
|
|
40
|
+
endedYet: () => held.end,
|
|
41
|
+
settle,
|
|
42
|
+
take: (frame) => {
|
|
43
|
+
onTake(frame);
|
|
44
|
+
waiting.push(frame);
|
|
45
|
+
wake();
|
|
46
|
+
},
|
|
47
|
+
stop: () => {
|
|
48
|
+
socket.current?.close(CLOSE.settled, "the watcher stopped");
|
|
49
|
+
settle({ reason: "stopped" });
|
|
62
50
|
},
|
|
51
|
+
[Symbol.asyncIterator]: () => handOut(waiting, { endedYet: () => held.end, sleep }),
|
|
63
52
|
};
|
|
64
53
|
}
|
|
65
54
|
/** A reader waiting on the next frame, woken when one arrives or when the watch is over. */
|
|
@@ -95,7 +84,7 @@ function handOut(waiting, watch) {
|
|
|
95
84
|
},
|
|
96
85
|
};
|
|
97
86
|
}
|
|
98
|
-
async function follow(following) {
|
|
87
|
+
export async function follow(following) {
|
|
99
88
|
const { options, watch } = following;
|
|
100
89
|
let attempt = 0;
|
|
101
90
|
while (!watch.endedYet()) {
|
|
@@ -120,7 +109,7 @@ function endFor(closed, options) {
|
|
|
120
109
|
function connected(following) {
|
|
121
110
|
const { floor, watch } = following;
|
|
122
111
|
const open = floor.socketFn ?? defaultSocket;
|
|
123
|
-
const socket = open(
|
|
112
|
+
const socket = open(following.urlFor(), { headers: { authorization: `Bearer ${floor.token}` } });
|
|
124
113
|
watch.socket.current = socket;
|
|
125
114
|
return new Promise((resolve) => {
|
|
126
115
|
socket.onFrame((said) => {
|
package/dist/runs.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CostsRow, Item, RunView } from "@re-cinq/floor-contracts";
|
|
2
2
|
import type { CostsFilter, CostsGroupBy, RunFilter } from "./filters.js";
|
|
3
3
|
import { type Reachable } from "./send.js";
|
|
4
|
+
import { type FloorWatch, type FloorWatchOptions } from "./floor-live.js";
|
|
4
5
|
import { type RunWatch, type WatchOptions } from "./live.js";
|
|
5
6
|
export interface StartRun {
|
|
6
7
|
repo: string;
|
|
@@ -31,6 +32,8 @@ export interface RunsApi {
|
|
|
31
32
|
cancel(runId: string, reason: string): Promise<RunView>;
|
|
32
33
|
/** Replays the run from `after`, then follows it live. Reconnects from the last seq it handed out. */
|
|
33
34
|
watch(runId: string, options?: WatchOptions): RunWatch;
|
|
35
|
+
/** Follows the whole floor: which runs start and change, by id. `resync` comes first and after any gap. */
|
|
36
|
+
watchFloor(options?: FloorWatchOptions): FloorWatch;
|
|
34
37
|
}
|
|
35
38
|
export declare function runsApi(floor: Reachable): RunsApi;
|
|
36
39
|
export interface CostsRowsApi {
|
package/dist/runs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asked } from "./send.js";
|
|
2
|
+
import { watchFloor } from "./floor-live.js";
|
|
2
3
|
import { watchRun } from "./live.js";
|
|
3
4
|
const HTTP_NOT_FOUND = 404;
|
|
4
5
|
export function runsApi(floor) {
|
|
@@ -7,6 +8,7 @@ export function runsApi(floor) {
|
|
|
7
8
|
get: (runId) => asked(floor, { method: "GET", path: `/assembly-runs/${runId}` }, [HTTP_NOT_FOUND]),
|
|
8
9
|
cancel: async (runId, reason) => (await asked(floor, { method: "POST", path: `/assembly-runs/${runId}/cancel`, body: { reason } })),
|
|
9
10
|
watch: (runId, options) => watchRun(floor, runId, options),
|
|
11
|
+
watchFloor: (options) => watchFloor(floor, options),
|
|
10
12
|
};
|
|
11
13
|
}
|
|
12
14
|
export function costsApi(floor) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@re-cinq/floor-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "The floor over HTTP and its live socket, typed: definitions, runs, visits, blobs, events, schedules and costs, plus a watch that replays a run from a cursor and reconnects without losing a frame.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@re-cinq/floor-contracts": "0.1.
|
|
48
|
+
"@re-cinq/floor-contracts": "0.1.6"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.20.1",
|