@stateforge-labs/herdr-sdk 0.0.0-stage → 0.3.0
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/LICENSE +18 -0
- package/NOTICE.md +51 -0
- package/README.md +211 -2
- package/bin/herdr-drift.js +14 -0
- package/bin/herdr-skill.js +38 -0
- package/dist/admin.d.ts +2 -0
- package/dist/admin.js +3 -0
- package/dist/constants.d.ts +236 -0
- package/dist/constants.js +294 -0
- package/dist/core/backoff.d.ts +20 -0
- package/dist/core/backoff.js +53 -0
- package/dist/core/constants.d.ts +143 -0
- package/dist/core/constants.js +98 -0
- package/dist/core/errors.d.ts +35 -0
- package/dist/core/errors.js +16 -0
- package/dist/core/events.d.ts +42 -0
- package/dist/core/events.js +128 -0
- package/dist/core/kernel.d.ts +65 -0
- package/dist/core/kernel.js +138 -0
- package/dist/core/link.d.ts +47 -0
- package/dist/core/link.js +187 -0
- package/dist/core/test-kit/constants.d.ts +51 -0
- package/dist/core/test-kit/constants.js +43 -0
- package/dist/core/test-kit/fake-core.d.ts +61 -0
- package/dist/core/test-kit/fake-core.js +228 -0
- package/dist/core/test-kit/frames.d.ts +1 -0
- package/dist/core/test-kit/frames.js +480 -0
- package/dist/core/test-kit/setup-test.d.ts +18 -0
- package/dist/core/test-kit/setup-test.js +35 -0
- package/dist/core/wire.d.ts +85 -0
- package/dist/core/wire.js +199 -0
- package/dist/core-test-kit.d.ts +5 -0
- package/dist/core-test-kit.js +5 -0
- package/dist/core.d.ts +15 -0
- package/dist/core.js +12 -0
- package/dist/drift/cli.d.ts +22 -0
- package/dist/drift/cli.js +146 -0
- package/dist/drift/constants.d.ts +83 -0
- package/dist/drift/constants.js +73 -0
- package/dist/drift/diff.d.ts +7 -0
- package/dist/drift/diff.js +91 -0
- package/dist/drift/schema.d.ts +19 -0
- package/dist/drift/schema.js +190 -0
- package/dist/drift/types.d.ts +76 -0
- package/dist/drift/types.js +1 -0
- package/dist/drift.d.ts +5 -0
- package/dist/drift.js +6 -0
- package/dist/effect.d.ts +3 -0
- package/dist/effect.js +4 -0
- package/dist/herdr/client/connect.d.ts +5 -0
- package/dist/herdr/client/connect.js +81 -0
- package/dist/herdr/client/held.d.ts +54 -0
- package/dist/herdr/client/held.js +100 -0
- package/dist/herdr/client/long-poll.d.ts +33 -0
- package/dist/herdr/client/long-poll.js +66 -0
- package/dist/herdr/client/scoped.d.ts +45 -0
- package/dist/herdr/client/scoped.js +241 -0
- package/dist/herdr/client/streams.d.ts +9 -0
- package/dist/herdr/client/streams.js +40 -0
- package/dist/herdr/client/subscribe.d.ts +75 -0
- package/dist/herdr/client/subscribe.js +208 -0
- package/dist/herdr/client/types.d.ts +150 -0
- package/dist/herdr/client/types.js +1 -0
- package/dist/herdr/client/watch-pane-set.d.ts +34 -0
- package/dist/herdr/client/watch-pane-set.js +271 -0
- package/dist/herdr/client/watch.d.ts +117 -0
- package/dist/herdr/client/watch.js +138 -0
- package/dist/herdr/effect/to-tagged-error.d.ts +2 -0
- package/dist/herdr/effect/to-tagged-error.js +54 -0
- package/dist/herdr/effect/types.d.ts +51 -0
- package/dist/herdr/effect/types.js +26 -0
- package/dist/herdr/errors.d.ts +53 -0
- package/dist/herdr/errors.js +87 -0
- package/dist/herdr/generated/entries/admin.d.ts +2 -0
- package/dist/herdr/generated/entries/admin.js +5 -0
- package/dist/herdr/generated/entries/client.d.ts +2 -0
- package/dist/herdr/generated/entries/client.js +5 -0
- package/dist/herdr/generated/entries/held.d.ts +2 -0
- package/dist/herdr/generated/entries/held.js +5 -0
- package/dist/herdr/generated/entries/prompt.d.ts +2 -0
- package/dist/herdr/generated/entries/prompt.js +5 -0
- package/dist/herdr/generated/entries/read-only-content.d.ts +2 -0
- package/dist/herdr/generated/entries/read-only-content.js +5 -0
- package/dist/herdr/generated/entries/read-only.d.ts +2 -0
- package/dist/herdr/generated/entries/read-only.js +5 -0
- package/dist/herdr/generated/entry-methods.d.ts +9 -0
- package/dist/herdr/generated/entry-methods.js +17 -0
- package/dist/herdr/generated/errors.d.ts +4 -0
- package/dist/herdr/generated/errors.js +3 -0
- package/dist/herdr/generated/events.d.ts +274 -0
- package/dist/herdr/generated/events.js +3 -0
- package/dist/herdr/generated/index.d.ts +20 -0
- package/dist/herdr/generated/index.js +19 -0
- package/dist/herdr/generated/method-table.d.ts +615 -0
- package/dist/herdr/generated/method-table.js +106 -0
- package/dist/herdr/generated/names/agent-status.d.ts +10 -0
- package/dist/herdr/generated/names/agent-status.js +10 -0
- package/dist/herdr/generated/names/capability-class.d.ts +14 -0
- package/dist/herdr/generated/names/capability-class.js +15 -0
- package/dist/herdr/generated/names/event-kind.d.ts +31 -0
- package/dist/herdr/generated/names/event-kind.js +31 -0
- package/dist/herdr/generated/names/event-match-kind.d.ts +23 -0
- package/dist/herdr/generated/names/event-match-kind.js +24 -0
- package/dist/herdr/generated/names/index.d.ts +10 -0
- package/dist/herdr/generated/names/index.js +12 -0
- package/dist/herdr/generated/names/method.d.ts +209 -0
- package/dist/herdr/generated/names/method.js +209 -0
- package/dist/herdr/generated/names/result-tag.d.ts +68 -0
- package/dist/herdr/generated/names/result-tag.js +68 -0
- package/dist/herdr/generated/names/server-capability.d.ts +10 -0
- package/dist/herdr/generated/names/server-capability.js +11 -0
- package/dist/herdr/generated/names/subscription-event-kind.d.ts +8 -0
- package/dist/herdr/generated/names/subscription-event-kind.js +8 -0
- package/dist/herdr/generated/names/subscription-type.d.ts +31 -0
- package/dist/herdr/generated/names/subscription-type.js +32 -0
- package/dist/herdr/generated/names/token-name-pattern.d.ts +2 -0
- package/dist/herdr/generated/names/token-name-pattern.js +4 -0
- package/dist/herdr/generated/namespaces.d.ts +785 -0
- package/dist/herdr/generated/namespaces.js +3 -0
- package/dist/herdr/generated/overrides.d.ts +33 -0
- package/dist/herdr/generated/overrides.js +20 -0
- package/dist/herdr/generated/params.d.ts +799 -0
- package/dist/herdr/generated/params.js +3 -0
- package/dist/herdr/generated/preset-methods.d.ts +4 -0
- package/dist/herdr/generated/preset-methods.js +7 -0
- package/dist/herdr/generated/presets/sessions-reader.d.ts +2 -0
- package/dist/herdr/generated/presets/sessions-reader.js +5 -0
- package/dist/herdr/generated/results.d.ts +978 -0
- package/dist/herdr/generated/results.js +52 -0
- package/dist/herdr/generated/test-data/defaults.d.ts +739 -0
- package/dist/herdr/generated/test-data/defaults.js +741 -0
- package/dist/herdr/generated/test-data/shapes.d.ts +1325 -0
- package/dist/herdr/generated/test-data/shapes.js +2245 -0
- package/dist/herdr/test-kit/fake-herdr.d.ts +13 -0
- package/dist/herdr/test-kit/fake-herdr.js +216 -0
- package/dist/herdr/test-kit/fake-protocol.d.ts +107 -0
- package/dist/herdr/test-kit/fake-protocol.js +436 -0
- package/dist/herdr/test-kit/types.d.ts +88 -0
- package/dist/herdr/test-kit/types.js +1 -0
- package/dist/herdr/wire/codec.d.ts +36 -0
- package/dist/herdr/wire/codec.js +62 -0
- package/dist/herdr/wire/connection.d.ts +13 -0
- package/dist/herdr/wire/connection.js +65 -0
- package/dist/herdr/wire/dedicated.d.ts +2 -0
- package/dist/herdr/wire/dedicated.js +238 -0
- package/dist/herdr/wire/push-decoder.d.ts +2 -0
- package/dist/herdr/wire/push-decoder.js +40 -0
- package/dist/herdr/wire/rpc.d.ts +3 -0
- package/dist/herdr/wire/rpc.js +141 -0
- package/dist/herdr/wire/socket-path.d.ts +8 -0
- package/dist/herdr/wire/socket-path.js +23 -0
- package/dist/herdr/wire/types.d.ts +78 -0
- package/dist/herdr/wire/types.js +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +7 -0
- package/dist/read-only.d.ts +5 -0
- package/dist/read-only.js +6 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +6 -0
- package/dist/test-kit.d.ts +2 -0
- package/dist/test-kit.js +3 -0
- package/docs/api/events.md +43 -0
- package/docs/api/methods.md +232 -0
- package/package.json +112 -4
- package/schema/herdr-api.schema.json +10759 -0
- package/schema/herdr-version.json +5 -0
- package/skills/herdr-sdk/SKILL.md +75 -0
- package/skills/herdr-sdk/references/api-map.md +28 -0
- package/skills/herdr-sdk/references/errors.md +19 -0
- package/skills/herdr-sdk/references/recipes.md +171 -0
- package/skills/herdr-sdk/references/versions.md +7 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AsyncQueue } from './kernel.js';
|
|
2
|
+
import { type Duration } from './kernel.js';
|
|
3
|
+
import { HcKind } from './constants.js';
|
|
4
|
+
import type { HcEvent, HcHeartbeat, HcMissed, HcParams, HcSnapshot, SocketPath } from './wire.js';
|
|
5
|
+
export interface HcSnapshotFeed {
|
|
6
|
+
readonly kind: typeof HcKind.Snapshot;
|
|
7
|
+
readonly snapshot: HcSnapshot;
|
|
8
|
+
}
|
|
9
|
+
export type HcCoreFeed = HcSnapshotFeed | HcMissed | HcHeartbeat | HcEvent;
|
|
10
|
+
export interface CoreEventsTiming {
|
|
11
|
+
readonly backoffBase: Duration;
|
|
12
|
+
readonly backoffCap: Duration;
|
|
13
|
+
readonly silence: Duration;
|
|
14
|
+
}
|
|
15
|
+
export interface CoreEventsParts {
|
|
16
|
+
readonly path: SocketPath;
|
|
17
|
+
readonly filter?: HcParams;
|
|
18
|
+
readonly timing?: CoreEventsTiming;
|
|
19
|
+
}
|
|
20
|
+
export declare class CoreEvents {
|
|
21
|
+
readonly feed: AsyncQueue<HcCoreFeed>;
|
|
22
|
+
private readonly path;
|
|
23
|
+
private readonly filter;
|
|
24
|
+
private readonly timing;
|
|
25
|
+
private socket;
|
|
26
|
+
private watchdog;
|
|
27
|
+
private readonly backoff;
|
|
28
|
+
private phase;
|
|
29
|
+
private life;
|
|
30
|
+
constructor(parts: CoreEventsParts);
|
|
31
|
+
start(): void;
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
private connect;
|
|
34
|
+
private arm;
|
|
35
|
+
private receive;
|
|
36
|
+
private readonly acceptors;
|
|
37
|
+
private accept;
|
|
38
|
+
private forward;
|
|
39
|
+
private snapshotOf;
|
|
40
|
+
private lost;
|
|
41
|
+
private clearTimers;
|
|
42
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Role: CoreEvents: events.subscribe as a feed (snapshot, events, heartbeat watchdog, missed means a fresh snapshot).
|
|
2
|
+
// Informs HERDR-ADR-0020 (the herdr-core client); ported from the desktop work in herdr-plugins.
|
|
3
|
+
import { createConnection } from 'node:net';
|
|
4
|
+
import { byKind, isErr, isOk, must } from './kernel.js';
|
|
5
|
+
import { AsyncQueue } from './kernel.js';
|
|
6
|
+
import { duration } from './kernel.js';
|
|
7
|
+
import { Backoff } from './backoff.js';
|
|
8
|
+
import { FIRST_CALL_ID, HC_BACKOFF_BASE, HC_BACKOFF_CAP, HC_HEARTBEAT_EVERY, HC_HEARTBEAT_MISSES, HC_SUBSCRIBE_METHOD, EventsPhase, HcKind, Life, RetryPhase, SocketEvent, TimerHold } from './constants.js';
|
|
9
|
+
import { HcCoreLines, encodeHcCoreRequest, parseHcCallId, parseHcCoreMessage, parseHcParams, parseHcSnapshot, parseWireChunk } from './wire.js';
|
|
10
|
+
const SUBSCRIBE_ID = must(parseHcCallId(FIRST_CALL_ID));
|
|
11
|
+
const NO_FILTER = must(parseHcParams({}));
|
|
12
|
+
const DEFAULT_TIMING = {
|
|
13
|
+
backoffBase: HC_BACKOFF_BASE,
|
|
14
|
+
backoffCap: HC_BACKOFF_CAP,
|
|
15
|
+
silence: duration(HC_HEARTBEAT_EVERY * HC_HEARTBEAT_MISSES),
|
|
16
|
+
};
|
|
17
|
+
export class CoreEvents {
|
|
18
|
+
feed = new AsyncQueue();
|
|
19
|
+
path;
|
|
20
|
+
filter;
|
|
21
|
+
timing;
|
|
22
|
+
socket = null;
|
|
23
|
+
watchdog = null;
|
|
24
|
+
backoff;
|
|
25
|
+
phase = EventsPhase.Waiting;
|
|
26
|
+
life = Life.Open;
|
|
27
|
+
constructor(parts) {
|
|
28
|
+
this.path = parts.path;
|
|
29
|
+
this.filter = parts.filter ?? NO_FILTER;
|
|
30
|
+
this.timing = parts.timing ?? DEFAULT_TIMING;
|
|
31
|
+
this.backoff = new Backoff(this.timing, TimerHold.HoldsProcess);
|
|
32
|
+
}
|
|
33
|
+
start() {
|
|
34
|
+
if (this.socket === null && this.backoff.phase === RetryPhase.Idle && this.life === Life.Open) {
|
|
35
|
+
this.connect();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async close() {
|
|
39
|
+
this.life = Life.Closed;
|
|
40
|
+
this.clearTimers();
|
|
41
|
+
this.feed.end();
|
|
42
|
+
const socket = this.socket;
|
|
43
|
+
if (socket === null) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.socket = null;
|
|
47
|
+
await new Promise((resolve) => { socket.once(SocketEvent.Close, () => { resolve(); }); socket.destroy(); });
|
|
48
|
+
}
|
|
49
|
+
connect() {
|
|
50
|
+
const socket = createConnection(this.path);
|
|
51
|
+
const lines = new HcCoreLines(() => { socket.destroy(); });
|
|
52
|
+
this.socket = socket;
|
|
53
|
+
this.phase = EventsPhase.Waiting;
|
|
54
|
+
socket.on(SocketEvent.Data, (chunk) => {
|
|
55
|
+
const bytes = parseWireChunk(chunk);
|
|
56
|
+
this.arm(socket);
|
|
57
|
+
if (isOk(bytes)) {
|
|
58
|
+
this.receive(lines.feed(bytes.value), socket);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
socket.on(SocketEvent.Error, () => undefined);
|
|
62
|
+
socket.on(SocketEvent.Close, () => { this.lost(socket); });
|
|
63
|
+
socket.once(SocketEvent.Connect, () => {
|
|
64
|
+
this.arm(socket);
|
|
65
|
+
socket.write(encodeHcCoreRequest(SUBSCRIBE_ID, HC_SUBSCRIBE_METHOD, this.filter));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
arm(socket) {
|
|
69
|
+
if (this.watchdog !== null) {
|
|
70
|
+
clearTimeout(this.watchdog);
|
|
71
|
+
}
|
|
72
|
+
this.watchdog = setTimeout(() => { socket.destroy(); }, this.timing.silence);
|
|
73
|
+
this.watchdog.unref();
|
|
74
|
+
}
|
|
75
|
+
receive(values, socket) {
|
|
76
|
+
for (const value of values) {
|
|
77
|
+
const parsed = parseHcCoreMessage(value);
|
|
78
|
+
if (isErr(parsed)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
this.accept(parsed.value, socket);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
acceptors = {
|
|
85
|
+
[HcKind.Fault]: (_message, socket) => { socket.destroy(); },
|
|
86
|
+
[HcKind.Odd]: (_message, socket) => { socket.destroy(); },
|
|
87
|
+
[HcKind.Reply]: (message, socket) => { this.snapshotOf(message.result, socket); },
|
|
88
|
+
[HcKind.Event]: (message) => { this.forward(message); },
|
|
89
|
+
[HcKind.Heartbeat]: (message) => { this.forward(message); },
|
|
90
|
+
[HcKind.Missed]: (message) => { this.forward(message); },
|
|
91
|
+
};
|
|
92
|
+
accept(message, socket) {
|
|
93
|
+
byKind(this.acceptors, message, socket);
|
|
94
|
+
}
|
|
95
|
+
forward(feed) {
|
|
96
|
+
if (this.phase === EventsPhase.Subscribed) {
|
|
97
|
+
this.feed.push(feed);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
snapshotOf(result, socket) {
|
|
101
|
+
const snapshot = parseHcSnapshot(result);
|
|
102
|
+
if (isErr(snapshot) || this.phase === EventsPhase.Subscribed) {
|
|
103
|
+
socket.destroy();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.phase = EventsPhase.Subscribed;
|
|
107
|
+
this.backoff.survive();
|
|
108
|
+
this.feed.push({ kind: HcKind.Snapshot, snapshot: snapshot.value });
|
|
109
|
+
}
|
|
110
|
+
lost(socket) {
|
|
111
|
+
if (socket !== this.socket) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.socket = null;
|
|
115
|
+
this.clearTimers();
|
|
116
|
+
if (this.life === Life.Closed) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.backoff.schedule(() => { this.connect(); });
|
|
120
|
+
}
|
|
121
|
+
clearTimers() {
|
|
122
|
+
if (this.watchdog !== null) {
|
|
123
|
+
clearTimeout(this.watchdog);
|
|
124
|
+
}
|
|
125
|
+
this.watchdog = null;
|
|
126
|
+
this.backoff.stop();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ResultKind } from './constants.js';
|
|
2
|
+
import type { ParseFailure } from './errors.js';
|
|
3
|
+
declare const marker: unique symbol;
|
|
4
|
+
export type Brand<Carrier, Name> = Carrier & {
|
|
5
|
+
readonly [marker]: Name;
|
|
6
|
+
};
|
|
7
|
+
export declare function brand<Carrier, Name>(carrier: Carrier): Brand<Carrier, Name>;
|
|
8
|
+
export type Fields = Readonly<Record<string, unknown>>;
|
|
9
|
+
export declare function isNumber(input: unknown): input is number;
|
|
10
|
+
export declare function isString(input: unknown): input is string;
|
|
11
|
+
export declare function isFields(input: unknown): input is Fields;
|
|
12
|
+
export type Ok<T> = {
|
|
13
|
+
readonly kind: typeof ResultKind.Ok;
|
|
14
|
+
readonly value: T;
|
|
15
|
+
};
|
|
16
|
+
export type Err<E> = {
|
|
17
|
+
readonly kind: typeof ResultKind.Err;
|
|
18
|
+
readonly error: E;
|
|
19
|
+
};
|
|
20
|
+
export type Result<T, E> = Ok<T> | Err<E>;
|
|
21
|
+
export declare const ok: <T>(value: T) => Result<T, never>;
|
|
22
|
+
export declare const err: <E>(error: E) => Result<never, E>;
|
|
23
|
+
export declare function isOk<T, E>(result: Result<T, E>): result is Ok<T>;
|
|
24
|
+
export declare function isErr<T, E>(result: Result<T, E>): result is Err<E>;
|
|
25
|
+
export type KindOf<U> = U extends {
|
|
26
|
+
readonly kind: infer K extends string;
|
|
27
|
+
} ? K : never;
|
|
28
|
+
export type KindMap<U extends {
|
|
29
|
+
readonly kind: string;
|
|
30
|
+
}, R, C = undefined> = {
|
|
31
|
+
readonly [K in KindOf<U>]: (value: Extract<U, {
|
|
32
|
+
readonly kind: K;
|
|
33
|
+
}>, context: C) => R;
|
|
34
|
+
};
|
|
35
|
+
export declare function byKind<U extends {
|
|
36
|
+
readonly kind: string;
|
|
37
|
+
}, R, C = undefined>(map: KindMap<U, R, C>, value: U, context: C): R;
|
|
38
|
+
export declare function must<T, E>(result: Result<T, E>): T;
|
|
39
|
+
export type Count = Brand<number, 'Count'>;
|
|
40
|
+
export type PositiveCount = Brand<number, 'PositiveCount'>;
|
|
41
|
+
export declare function parseCount(input: unknown): Result<Count, ParseFailure>;
|
|
42
|
+
export declare function parsePositiveCount(input: unknown): Result<PositiveCount, ParseFailure>;
|
|
43
|
+
export type Text = Brand<string, 'Text'>;
|
|
44
|
+
export type NonEmptyText = Brand<string, 'NonEmptyText'>;
|
|
45
|
+
export declare function parseText(input: unknown): Result<Text, ParseFailure>;
|
|
46
|
+
export declare function parseNonEmptyText(input: unknown): Result<NonEmptyText, ParseFailure>;
|
|
47
|
+
export type Instant = Brand<number, 'Instant'>;
|
|
48
|
+
export type Duration = Brand<number, 'Duration'>;
|
|
49
|
+
export declare function instant(ms: number): Instant;
|
|
50
|
+
export declare function duration(ms: number): Duration;
|
|
51
|
+
export declare function elapsed(from: Instant, to: Instant): Duration;
|
|
52
|
+
export type Milliseconds = Brand<number, 'Milliseconds'>;
|
|
53
|
+
export declare function parseMilliseconds(input: unknown): Result<Milliseconds, ParseFailure>;
|
|
54
|
+
export declare function parseInstant(input: unknown): Result<Instant, ParseFailure>;
|
|
55
|
+
export declare function parseDuration(input: unknown): Result<Duration, ParseFailure>;
|
|
56
|
+
export declare class AsyncQueue<T> implements AsyncIterable<T> {
|
|
57
|
+
private readonly items;
|
|
58
|
+
private readonly waiters;
|
|
59
|
+
private ended;
|
|
60
|
+
push(item: T): void;
|
|
61
|
+
end(): void;
|
|
62
|
+
private next;
|
|
63
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Role: the small value types of the core layer: brand, fields, Result (with isOk/isErr), byKind dispatch, Count, Text, time and an async queue.
|
|
2
|
+
// Informs HERDR-ADR-0020; merged by HERDR-ADR-0021 from brand.ts, fields.ts, result.ts, count.ts, text.ts, time.ts, async-queue.ts.
|
|
3
|
+
import { JsType, ParseFailureKind, ResultKind } from './constants.js';
|
|
4
|
+
import { CoreInvariantError } from './errors.js';
|
|
5
|
+
export function brand(carrier) {
|
|
6
|
+
return carrier;
|
|
7
|
+
}
|
|
8
|
+
export function isNumber(input) {
|
|
9
|
+
return typeof input === JsType.Number;
|
|
10
|
+
}
|
|
11
|
+
export function isString(input) {
|
|
12
|
+
return typeof input === JsType.String;
|
|
13
|
+
}
|
|
14
|
+
export function isFields(input) {
|
|
15
|
+
return typeof input === JsType.Object && input !== null;
|
|
16
|
+
}
|
|
17
|
+
export const ok = (value) => ({ kind: ResultKind.Ok, value });
|
|
18
|
+
export const err = (error) => ({ kind: ResultKind.Err, error });
|
|
19
|
+
export function isOk(result) {
|
|
20
|
+
return result.kind === ResultKind.Ok;
|
|
21
|
+
}
|
|
22
|
+
export function isErr(result) {
|
|
23
|
+
return result.kind === ResultKind.Err;
|
|
24
|
+
}
|
|
25
|
+
export function byKind(map, value, context) {
|
|
26
|
+
const handle = map[value.kind];
|
|
27
|
+
return handle(value, context);
|
|
28
|
+
}
|
|
29
|
+
export function must(result) {
|
|
30
|
+
if (isErr(result)) {
|
|
31
|
+
throw new CoreInvariantError(JSON.stringify(result.error));
|
|
32
|
+
}
|
|
33
|
+
return result.value;
|
|
34
|
+
}
|
|
35
|
+
export function parseCount(input) {
|
|
36
|
+
if (!isNumber(input)) {
|
|
37
|
+
return err({ kind: ParseFailureKind.NotANumber });
|
|
38
|
+
}
|
|
39
|
+
if (!Number.isInteger(input)) {
|
|
40
|
+
return err({ kind: ParseFailureKind.NotAnInteger });
|
|
41
|
+
}
|
|
42
|
+
if (input < 0 || !Number.isSafeInteger(input)) {
|
|
43
|
+
return err({ kind: ParseFailureKind.OutOfRange });
|
|
44
|
+
}
|
|
45
|
+
return ok(brand(input));
|
|
46
|
+
}
|
|
47
|
+
export function parsePositiveCount(input) {
|
|
48
|
+
const count = parseCount(input);
|
|
49
|
+
if (isErr(count)) {
|
|
50
|
+
return count;
|
|
51
|
+
}
|
|
52
|
+
if (count.value === 0) {
|
|
53
|
+
return err({ kind: ParseFailureKind.OutOfRange });
|
|
54
|
+
}
|
|
55
|
+
return ok(brand(count.value));
|
|
56
|
+
}
|
|
57
|
+
export function parseText(input) {
|
|
58
|
+
if (!isString(input)) {
|
|
59
|
+
return err({ kind: ParseFailureKind.NotAString });
|
|
60
|
+
}
|
|
61
|
+
return ok(brand(input));
|
|
62
|
+
}
|
|
63
|
+
export function parseNonEmptyText(input) {
|
|
64
|
+
const text = parseText(input);
|
|
65
|
+
if (isErr(text)) {
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
if (text.value.trim().length === 0) {
|
|
69
|
+
return err({ kind: ParseFailureKind.Empty });
|
|
70
|
+
}
|
|
71
|
+
return ok(brand(text.value));
|
|
72
|
+
}
|
|
73
|
+
export function instant(ms) {
|
|
74
|
+
return ms;
|
|
75
|
+
}
|
|
76
|
+
export function duration(ms) {
|
|
77
|
+
return ms;
|
|
78
|
+
}
|
|
79
|
+
export function elapsed(from, to) {
|
|
80
|
+
return duration(Math.max(0, to - from));
|
|
81
|
+
}
|
|
82
|
+
export function parseMilliseconds(input) {
|
|
83
|
+
if (!isNumber(input) || !Number.isFinite(input)) {
|
|
84
|
+
return err({ kind: ParseFailureKind.NotANumber });
|
|
85
|
+
}
|
|
86
|
+
if (input < 0) {
|
|
87
|
+
return err({ kind: ParseFailureKind.OutOfRange });
|
|
88
|
+
}
|
|
89
|
+
return ok(brand(input));
|
|
90
|
+
}
|
|
91
|
+
export function parseInstant(input) {
|
|
92
|
+
const ms = parseMilliseconds(input);
|
|
93
|
+
if (isErr(ms)) {
|
|
94
|
+
return ms;
|
|
95
|
+
}
|
|
96
|
+
return ok(brand(ms.value));
|
|
97
|
+
}
|
|
98
|
+
export function parseDuration(input) {
|
|
99
|
+
const ms = parseMilliseconds(input);
|
|
100
|
+
if (isErr(ms)) {
|
|
101
|
+
return ms;
|
|
102
|
+
}
|
|
103
|
+
return ok(brand(ms.value));
|
|
104
|
+
}
|
|
105
|
+
export class AsyncQueue {
|
|
106
|
+
items = [];
|
|
107
|
+
waiters = [];
|
|
108
|
+
ended = false;
|
|
109
|
+
push(item) {
|
|
110
|
+
if (this.ended) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const wake = this.waiters.shift();
|
|
114
|
+
if (wake === undefined) {
|
|
115
|
+
this.items.push(item);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
wake({ value: item, done: false });
|
|
119
|
+
}
|
|
120
|
+
end() {
|
|
121
|
+
this.ended = true;
|
|
122
|
+
for (const wake of this.waiters.splice(0)) {
|
|
123
|
+
wake({ value: undefined, done: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
next() {
|
|
127
|
+
if (this.items.length > 0) {
|
|
128
|
+
return Promise.resolve({ value: this.items.shift(), done: false });
|
|
129
|
+
}
|
|
130
|
+
if (this.ended) {
|
|
131
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
132
|
+
}
|
|
133
|
+
return new Promise((resolve) => { this.waiters.push(resolve); });
|
|
134
|
+
}
|
|
135
|
+
[Symbol.asyncIterator]() {
|
|
136
|
+
return { next: () => this.next() };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Duration } from './kernel.js';
|
|
2
|
+
import { type NonEmptyText } from './kernel.js';
|
|
3
|
+
import { type Unknown } from './errors.js';
|
|
4
|
+
import { OutcomeKind } from './constants.js';
|
|
5
|
+
import type { HcCoreHello, HcParams, HcPayload, SocketPath } from './wire.js';
|
|
6
|
+
export interface HcAnswered {
|
|
7
|
+
readonly kind: typeof OutcomeKind.Answer;
|
|
8
|
+
readonly value: HcPayload;
|
|
9
|
+
}
|
|
10
|
+
export type HcCallResult = HcAnswered | Unknown;
|
|
11
|
+
export interface CoreLinkTiming {
|
|
12
|
+
readonly helloTimeout: Duration;
|
|
13
|
+
readonly backoffBase: Duration;
|
|
14
|
+
readonly backoffCap: Duration;
|
|
15
|
+
}
|
|
16
|
+
export interface CoreLinkParts {
|
|
17
|
+
readonly path: SocketPath;
|
|
18
|
+
readonly timing?: CoreLinkTiming;
|
|
19
|
+
}
|
|
20
|
+
export declare class CoreLink {
|
|
21
|
+
private readonly path;
|
|
22
|
+
private readonly timing;
|
|
23
|
+
private readonly pending;
|
|
24
|
+
private socket;
|
|
25
|
+
private state;
|
|
26
|
+
private connecting;
|
|
27
|
+
private readonly backoff;
|
|
28
|
+
private issued;
|
|
29
|
+
private info;
|
|
30
|
+
constructor(parts: CoreLinkParts);
|
|
31
|
+
get hello(): HcCoreHello | null;
|
|
32
|
+
open(): Promise<Unknown | null>;
|
|
33
|
+
call(method: NonEmptyText, params: HcParams, timeout: Duration): Promise<HcCallResult>;
|
|
34
|
+
private attempt;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
private readonly readiness;
|
|
37
|
+
private ready;
|
|
38
|
+
private connect;
|
|
39
|
+
private dial;
|
|
40
|
+
private handshake;
|
|
41
|
+
private takeHello;
|
|
42
|
+
private request;
|
|
43
|
+
private receive;
|
|
44
|
+
private lost;
|
|
45
|
+
private reconnect;
|
|
46
|
+
private settleAll;
|
|
47
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Role: CoreLink: one persistent connection to the core socket with hello, per-call timeouts, reconnect with capped backoff.
|
|
2
|
+
// Informs HERDR-ADR-0020 (the herdr-core client); ported from the desktop work in herdr-plugins.
|
|
3
|
+
import { createConnection } from 'node:net';
|
|
4
|
+
import { parseCount } from './kernel.js';
|
|
5
|
+
import { byKind, isErr, isOk, must } from './kernel.js';
|
|
6
|
+
import { parseText } from './kernel.js';
|
|
7
|
+
import { isUnknown, unknown } from './errors.js';
|
|
8
|
+
import { CoreText, HC_BACKOFF_BASE, HC_BACKOFF_CAP, HC_GENERATION, HC_HELLO_METHOD, HC_HELLO_TIMEOUT, HcKind, LinkState, OutcomeKind, SocketEvent, TimerHold, UnknownWhy, } from './constants.js';
|
|
9
|
+
import { Backoff } from './backoff.js';
|
|
10
|
+
import { HcCoreLines, encodeHcCoreRequest, parseHcCallId, parseHcCoreHello, parseHcCoreMessage, parseHcParams, parseWireChunk } from './wire.js';
|
|
11
|
+
const ZERO = must(parseCount(0));
|
|
12
|
+
const ONE = must(parseCount(1));
|
|
13
|
+
const HELLO_PARAMS = must(parseHcParams({ generation: HC_GENERATION }));
|
|
14
|
+
const DEFAULT_TIMING = { helloTimeout: HC_HELLO_TIMEOUT, backoffBase: HC_BACKOFF_BASE, backoffCap: HC_BACKOFF_CAP };
|
|
15
|
+
const unreachable = (detail) => unknown({ why: UnknownWhy.Unreachable, detail });
|
|
16
|
+
const timedOut = (after) => unknown({ why: UnknownWhy.Timeout, after });
|
|
17
|
+
const none = () => null;
|
|
18
|
+
const ANSWER_OF = {
|
|
19
|
+
[HcKind.Reply]: (message) => ({ kind: OutcomeKind.Answer, value: message.result }),
|
|
20
|
+
[HcKind.Odd]: () => unknown({ why: UnknownWhy.Unreadable, detail: CoreText.OddResult }),
|
|
21
|
+
[HcKind.Fault]: (message) => unknown({ why: UnknownWhy.Failed, code: message.code, detail: CoreText.Fault(message.reason, message.message) }),
|
|
22
|
+
[HcKind.Event]: none,
|
|
23
|
+
[HcKind.Heartbeat]: none,
|
|
24
|
+
[HcKind.Missed]: none,
|
|
25
|
+
};
|
|
26
|
+
const ID_OF = {
|
|
27
|
+
[HcKind.Reply]: (message) => message.id,
|
|
28
|
+
[HcKind.Odd]: (message) => message.id,
|
|
29
|
+
[HcKind.Fault]: (message) => message.id,
|
|
30
|
+
[HcKind.Event]: none,
|
|
31
|
+
[HcKind.Heartbeat]: none,
|
|
32
|
+
[HcKind.Missed]: none,
|
|
33
|
+
};
|
|
34
|
+
export class CoreLink {
|
|
35
|
+
path;
|
|
36
|
+
timing;
|
|
37
|
+
pending = new Map();
|
|
38
|
+
socket = null;
|
|
39
|
+
state = LinkState.Idle;
|
|
40
|
+
connecting = null;
|
|
41
|
+
backoff;
|
|
42
|
+
issued = ZERO;
|
|
43
|
+
info = null;
|
|
44
|
+
constructor(parts) {
|
|
45
|
+
this.path = parts.path;
|
|
46
|
+
this.timing = parts.timing ?? DEFAULT_TIMING;
|
|
47
|
+
this.backoff = new Backoff(this.timing, TimerHold.LetsProcessEnd);
|
|
48
|
+
}
|
|
49
|
+
get hello() {
|
|
50
|
+
return this.info;
|
|
51
|
+
}
|
|
52
|
+
open() {
|
|
53
|
+
return this.ready();
|
|
54
|
+
}
|
|
55
|
+
call(method, params, timeout) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const timer = setTimeout(() => { resolve(timedOut(timeout)); }, timeout);
|
|
58
|
+
timer.unref();
|
|
59
|
+
void this.attempt(method, params, timeout).then((result) => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
resolve(result);
|
|
62
|
+
return result;
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async attempt(method, params, timeout) {
|
|
67
|
+
const down = await this.ready();
|
|
68
|
+
return down === null ? this.request(method, params, timeout) : down;
|
|
69
|
+
}
|
|
70
|
+
async close() {
|
|
71
|
+
this.state = LinkState.Closed;
|
|
72
|
+
this.backoff.stop();
|
|
73
|
+
this.settleAll(unreachable(CoreText.Closed));
|
|
74
|
+
const socket = this.socket;
|
|
75
|
+
if (socket === null) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.socket = null;
|
|
79
|
+
await new Promise((resolve) => { socket.once(SocketEvent.Close, () => { resolve(); }); socket.destroy(); });
|
|
80
|
+
}
|
|
81
|
+
readiness = {
|
|
82
|
+
[LinkState.Ready]: () => Promise.resolve(null),
|
|
83
|
+
[LinkState.Connecting]: () => this.connecting ?? Promise.resolve(unreachable(CoreText.Connecting)),
|
|
84
|
+
[LinkState.Backoff]: () => Promise.resolve(unreachable(CoreText.Down)),
|
|
85
|
+
[LinkState.Closed]: () => Promise.resolve(unreachable(CoreText.Closed)),
|
|
86
|
+
[LinkState.Idle]: () => this.connect(),
|
|
87
|
+
};
|
|
88
|
+
ready() {
|
|
89
|
+
return this.readiness[this.state]();
|
|
90
|
+
}
|
|
91
|
+
connect() {
|
|
92
|
+
this.state = LinkState.Connecting;
|
|
93
|
+
const attempt = this.dial().finally(() => { this.connecting = null; });
|
|
94
|
+
this.connecting = attempt;
|
|
95
|
+
return attempt;
|
|
96
|
+
}
|
|
97
|
+
dial() {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const socket = createConnection(this.path);
|
|
100
|
+
const lines = new HcCoreLines(() => { socket.destroy(); });
|
|
101
|
+
let reason = CoreText.Dropped;
|
|
102
|
+
this.socket = socket;
|
|
103
|
+
socket.on(SocketEvent.Data, (chunk) => {
|
|
104
|
+
const bytes = parseWireChunk(chunk);
|
|
105
|
+
if (isOk(bytes)) {
|
|
106
|
+
this.receive(lines.feed(bytes.value));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
socket.on(SocketEvent.Error, (cause) => { reason = must(parseText(cause.message)); });
|
|
110
|
+
socket.on(SocketEvent.Close, () => { this.lost(socket); resolve(unreachable(reason)); });
|
|
111
|
+
socket.once(SocketEvent.Connect, () => { void this.handshake(socket).then(resolve); });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async handshake(socket) {
|
|
115
|
+
const reply = await this.request(HC_HELLO_METHOD, HELLO_PARAMS, this.timing.helloTimeout);
|
|
116
|
+
const refusal = this.takeHello(reply);
|
|
117
|
+
if (refusal !== null) {
|
|
118
|
+
socket.destroy();
|
|
119
|
+
return refusal;
|
|
120
|
+
}
|
|
121
|
+
this.state = LinkState.Ready;
|
|
122
|
+
this.backoff.survive();
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
takeHello(reply) {
|
|
126
|
+
if (isUnknown(reply)) {
|
|
127
|
+
return reply;
|
|
128
|
+
}
|
|
129
|
+
const hello = parseHcCoreHello(reply.value);
|
|
130
|
+
if (isErr(hello)) {
|
|
131
|
+
return unknown({ why: UnknownWhy.Unreadable, detail: CoreText.HelloUnreadable });
|
|
132
|
+
}
|
|
133
|
+
if (hello.value.generation !== HC_GENERATION) {
|
|
134
|
+
return unknown({ why: UnknownWhy.Unreadable, detail: CoreText.Generation(hello.value.generation) });
|
|
135
|
+
}
|
|
136
|
+
this.info = hello.value;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
request(method, params, timeout) {
|
|
140
|
+
const socket = this.socket;
|
|
141
|
+
if (socket === null || socket.destroyed) {
|
|
142
|
+
return Promise.resolve(unreachable(CoreText.NoConnection));
|
|
143
|
+
}
|
|
144
|
+
this.issued = must(parseCount(this.issued + ONE));
|
|
145
|
+
const id = must(parseHcCallId(this.issued));
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const timer = setTimeout(() => { this.pending.delete(id); resolve(timedOut(timeout)); }, timeout);
|
|
148
|
+
timer.unref();
|
|
149
|
+
this.pending.set(id, (result) => { clearTimeout(timer); resolve(result); });
|
|
150
|
+
socket.write(encodeHcCoreRequest(id, method, params));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
receive(values) {
|
|
154
|
+
for (const value of values) {
|
|
155
|
+
const parsed = parseHcCoreMessage(value);
|
|
156
|
+
const id = isOk(parsed) ? byKind(ID_OF, parsed.value, undefined) : null;
|
|
157
|
+
const answer = isOk(parsed) ? byKind(ANSWER_OF, parsed.value, undefined) : null;
|
|
158
|
+
const settle = id === null ? undefined : this.pending.get(id);
|
|
159
|
+
if (id !== null && settle !== undefined && answer !== null) {
|
|
160
|
+
this.pending.delete(id);
|
|
161
|
+
settle(answer);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
lost(socket) {
|
|
166
|
+
if (socket !== this.socket) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.socket = null;
|
|
170
|
+
this.settleAll(unreachable(CoreText.Dropped));
|
|
171
|
+
if (this.state === LinkState.Closed) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.state = LinkState.Backoff;
|
|
175
|
+
this.backoff.schedule(() => { void this.reconnect(); });
|
|
176
|
+
}
|
|
177
|
+
reconnect() {
|
|
178
|
+
return this.state === LinkState.Backoff ? this.connect() : Promise.resolve(null);
|
|
179
|
+
}
|
|
180
|
+
settleAll(result) {
|
|
181
|
+
const settles = [...this.pending.values()];
|
|
182
|
+
this.pending.clear();
|
|
183
|
+
for (const settle of settles) {
|
|
184
|
+
settle(result);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const FakeName: {
|
|
2
|
+
readonly DirPrefix: 'herdr-core-fake-';
|
|
3
|
+
readonly Socket: 'default.sock';
|
|
4
|
+
};
|
|
5
|
+
export declare const FakeFaultKind: {
|
|
6
|
+
readonly MethodNotFound: 'method_not_found';
|
|
7
|
+
readonly NotFound: 'not_found';
|
|
8
|
+
};
|
|
9
|
+
export declare const FakeRpcCode: {
|
|
10
|
+
readonly MethodNotFound: -32601;
|
|
11
|
+
readonly NotFound: -32000;
|
|
12
|
+
};
|
|
13
|
+
export declare const FrameName: {
|
|
14
|
+
readonly Hello: 'hello';
|
|
15
|
+
readonly Subscribed: 'subscribed';
|
|
16
|
+
readonly Created: 'event:created';
|
|
17
|
+
readonly eventOf: (type: string) => string;
|
|
18
|
+
};
|
|
19
|
+
export declare const FakeField: {
|
|
20
|
+
readonly Address: 'address';
|
|
21
|
+
readonly HiddenFinished: 'hidden_finished';
|
|
22
|
+
readonly Channel: 'channel';
|
|
23
|
+
};
|
|
24
|
+
export declare const FakeText: {
|
|
25
|
+
readonly NoFrame: (name: string) => string;
|
|
26
|
+
readonly NoMethod: (method: string) => string;
|
|
27
|
+
readonly NoTerminal: (address: unknown) => string;
|
|
28
|
+
readonly Screen: (terminal: unknown) => string;
|
|
29
|
+
};
|
|
30
|
+
export declare const FakeScreen: {
|
|
31
|
+
readonly How: 'screen';
|
|
32
|
+
};
|
|
33
|
+
export declare const SPLIT_AT = 7;
|
|
34
|
+
export declare const SPLIT_DELAY = 15;
|
|
35
|
+
export declare const DemoTerminal: {
|
|
36
|
+
readonly First: 't1';
|
|
37
|
+
readonly Second: 't2';
|
|
38
|
+
readonly Working: 'working';
|
|
39
|
+
readonly Idle: 'idle';
|
|
40
|
+
readonly Live: 'live';
|
|
41
|
+
readonly App: 'claude-code';
|
|
42
|
+
readonly Argv: readonly ['claude'];
|
|
43
|
+
readonly Cwd: '~/work/probe';
|
|
44
|
+
readonly Name: 'claude';
|
|
45
|
+
readonly Pid: 4242;
|
|
46
|
+
readonly Labels: {
|
|
47
|
+
readonly 'herdr.tab': 'tb1';
|
|
48
|
+
readonly 'herdr.space': 'sp1';
|
|
49
|
+
readonly where: 'S / 1';
|
|
50
|
+
};
|
|
51
|
+
};
|