@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,150 @@
|
|
|
1
|
+
import type { Preset, ProtocolPolicy } from '../../constants.js';
|
|
2
|
+
import type { AdminNamespaces, CallOptions, ClientNamespaces, ENTRY_METHODS, Method, PromptNamespaces, ReadOnlyContentNamespaces, ReadOnlyNamespaces, SessionsReaderNamespaces } from '../generated/index.js';
|
|
3
|
+
import type { ServerCapabilities } from '../generated/results.js';
|
|
4
|
+
import type { EnvMap } from '../wire/socket-path.js';
|
|
5
|
+
import type { OnCall, ParamsOf, ResultOf, UnknownFrame } from '../wire/types.js';
|
|
6
|
+
import type { Hold } from './held.js';
|
|
7
|
+
import type { Subscribe } from './subscribe.js';
|
|
8
|
+
import type { WatchFn } from './watch.js';
|
|
9
|
+
export type { CallOptions } from '../generated/index.js';
|
|
10
|
+
/** Options of every connect function. */
|
|
11
|
+
export interface ConnectOptions {
|
|
12
|
+
/** Explicit path; else HERDR_SOCKET; else ~/.config/herdr/herdr.sock. */
|
|
13
|
+
readonly socketPath?: string | undefined;
|
|
14
|
+
/** Per-request timeout; default Timeout.DefaultMs (10 s). Long-poll calls add their own timeout_ms (plan 3.6). */
|
|
15
|
+
readonly timeoutMs?: number | undefined;
|
|
16
|
+
/** Exact (default) or Minimum. A mismatch rejects with ProtocolMismatchError. */
|
|
17
|
+
readonly protocolPolicy?: ProtocolPolicy | undefined;
|
|
18
|
+
readonly onCall?: OnCall | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Called once per UnknownFrame on every subscription of this client, including the ones watch() and watchPaneSet()
|
|
21
|
+
* open (HERDR-ADR-0016 P3). When a per-call onUnknown is also set, both fire: this one first, then the per-call one.
|
|
22
|
+
* subscribe is the only place that calls them; watch() forwards its own onUnknown to subscribe and never calls either.
|
|
23
|
+
*/
|
|
24
|
+
readonly onUnknown?: ((frame: UnknownFrame) => void) | undefined;
|
|
25
|
+
/** Environment used to find the socket; tests pass their own, so nothing reads process.env. */
|
|
26
|
+
readonly env?: EnvMap | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** What the server advertised in its pong. A missing key is not advertised. */
|
|
29
|
+
export type ClientCapabilities = Readonly<Partial<ServerCapabilities>>;
|
|
30
|
+
/** Server facts fixed at connect time. */
|
|
31
|
+
export interface ServerInfo {
|
|
32
|
+
readonly protocol: number;
|
|
33
|
+
readonly version: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Members every client has. close() is local: later calls reject with HerdrTransportError without contacting herdr,
|
|
37
|
+
* and it aborts ClientTransport.closed, which ends every live subscription, watch loop and held connection of this
|
|
38
|
+
* client (each one closes as if its own close() ran). Idempotent.
|
|
39
|
+
*/
|
|
40
|
+
export interface ClientBase {
|
|
41
|
+
readonly capabilities: ClientCapabilities;
|
|
42
|
+
/** Not `server`: that name is herdr's `server.*` namespace. */
|
|
43
|
+
readonly serverInfo: ServerInfo;
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The connection settings connectCore resolved once (socket path from options/env, timeout default applied). P2b
|
|
48
|
+
* passes them to SubscribeDeps and HoldDeps; nobody resolves them a second time. Not on public clients.
|
|
49
|
+
*/
|
|
50
|
+
export interface ClientTransport {
|
|
51
|
+
readonly socketPath: string;
|
|
52
|
+
readonly timeoutMs: number;
|
|
53
|
+
readonly onCall?: OnCall | undefined;
|
|
54
|
+
readonly onUnknown?: ((frame: UnknownFrame) => void) | undefined;
|
|
55
|
+
/** Aborted by close(). */
|
|
56
|
+
readonly closed: AbortSignal;
|
|
57
|
+
}
|
|
58
|
+
/** The generic escape hatch, limited to the methods `M` of the entry. Not on read-only or prompt clients. */
|
|
59
|
+
export interface Caller<M extends Method> {
|
|
60
|
+
call<K extends M>(method: K, params: ParamsOf<K>, options?: CallOptions): Promise<ResultOf<K>>;
|
|
61
|
+
}
|
|
62
|
+
/** Streaming members (on clients whose class set includes L). */
|
|
63
|
+
export interface Streaming {
|
|
64
|
+
readonly watch: WatchFn;
|
|
65
|
+
}
|
|
66
|
+
/** Held connections (on clients whose class set includes C). */
|
|
67
|
+
export interface Holding {
|
|
68
|
+
readonly hold: Hold;
|
|
69
|
+
}
|
|
70
|
+
/** Replaces the generated `events.subscribe` placeholder with the real Subscribe (array of specs -> EventSubscription). */
|
|
71
|
+
export type WithSubscribe<N extends {
|
|
72
|
+
events: object;
|
|
73
|
+
}> = Omit<N, 'events'> & {
|
|
74
|
+
readonly events: Omit<N['events'], 'subscribe'> & {
|
|
75
|
+
readonly subscribe: Subscribe;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
/** Method names of one entry, from the generated ENTRY_METHODS table. */
|
|
79
|
+
export type EntryName = keyof typeof ENTRY_METHODS;
|
|
80
|
+
export type EntryMethod<E extends EntryName> = (typeof ENTRY_METHODS)[E][number];
|
|
81
|
+
/** P2a's client: ping, protocol policy, capabilities, gate, call(), and the resolved transport. Composed into every public client by P2b. */
|
|
82
|
+
export type CoreClient = ClientBase & Caller<Method> & {
|
|
83
|
+
readonly transport: ClientTransport;
|
|
84
|
+
};
|
|
85
|
+
/** `connect()` (`.`): R-meta, R-content, W, I, H, L, C; no D, S, X. */
|
|
86
|
+
export type Client = WithSubscribe<ClientNamespaces> & ClientBase & Caller<EntryMethod<'client'>> & Streaming & Holding;
|
|
87
|
+
/** `connectAdmin()` (`/admin`): all methods. */
|
|
88
|
+
export type AdminClient = WithSubscribe<AdminNamespaces> & ClientBase & Caller<Method> & Streaming & Holding;
|
|
89
|
+
/** `connectReadOnly()`: R-meta + L. No call(), no writes. */
|
|
90
|
+
export type ReadOnlyClient = WithSubscribe<ReadOnlyNamespaces> & ClientBase & Streaming;
|
|
91
|
+
/** `connectReadOnly({ content: true })`: adds R-content. */
|
|
92
|
+
export type ReadOnlyContentClient = WithSubscribe<ReadOnlyContentNamespaces> & ClientBase & Streaming;
|
|
93
|
+
/** `connectPrompt()`: R-meta + agent.prompt. */
|
|
94
|
+
export type PromptClient = PromptNamespaces & ClientBase;
|
|
95
|
+
/** `connectReadOnly({ preset: Presets.SessionsReader })`: exactly PRESET_METHODS.SessionsReader. */
|
|
96
|
+
export type SessionsReaderClient = SessionsReaderNamespaces & ClientBase;
|
|
97
|
+
/** Options of connectReadOnly without a preset. `methods` narrows at runtime only; the type stays ReadOnlyClient. */
|
|
98
|
+
export interface ReadOnlyOptions extends ConnectOptions {
|
|
99
|
+
readonly content?: boolean | undefined;
|
|
100
|
+
readonly methods?: readonly EntryMethod<'readOnlyContent'>[] | undefined;
|
|
101
|
+
}
|
|
102
|
+
export interface PresetOptions extends ConnectOptions {
|
|
103
|
+
readonly preset: Preset;
|
|
104
|
+
}
|
|
105
|
+
/** P2a: socket, ping, protocol policy, capabilities. Implemented by client/connect-core.ts. */
|
|
106
|
+
export type ConnectCore = (options?: ConnectOptions) => Promise<CoreClient>;
|
|
107
|
+
/** P2b. Implemented by client/connect.ts. */
|
|
108
|
+
export type Connect = (options?: ConnectOptions) => Promise<Client>;
|
|
109
|
+
/** P2b. Implemented by client/scoped/connect-admin.ts. */
|
|
110
|
+
export type ConnectAdmin = (options?: ConnectOptions) => Promise<AdminClient>;
|
|
111
|
+
/** P2b. Implemented by client/scoped/connect-prompt.ts. */
|
|
112
|
+
export type ConnectPrompt = (options?: ConnectOptions) => Promise<PromptClient>;
|
|
113
|
+
/** P2b. Implemented by client/scoped/connect-read-only.ts. Overloads: preset, content, default. */
|
|
114
|
+
export interface ConnectReadOnly {
|
|
115
|
+
(options: PresetOptions): Promise<SessionsReaderClient>;
|
|
116
|
+
(options: ReadOnlyOptions & {
|
|
117
|
+
readonly content: true;
|
|
118
|
+
}): Promise<ReadOnlyContentClient>;
|
|
119
|
+
(options?: ReadOnlyOptions): Promise<ReadOnlyClient>;
|
|
120
|
+
}
|
|
121
|
+
/** Sends one allowed method; the tree calls it for every leaf. Applies the gate, long-poll timeout and allow-list. */
|
|
122
|
+
export type Invoke = (method: Method, params: unknown, options?: CallOptions) => Promise<unknown>;
|
|
123
|
+
/** A namespace tree before it is typed as one of the generated namespace interfaces (plain nested objects). */
|
|
124
|
+
export type NamespaceTree = {
|
|
125
|
+
readonly [segment: string]: NamespaceTree | ((params?: unknown, options?: CallOptions) => Promise<unknown>);
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Builds the nested tree once, eagerly, from exactly `allowed` (no Proxy, no getters; plan 3.3).
|
|
129
|
+
* `events.subscribe` is NOT a leaf of the tree; the composer adds Subscribe. Implemented by client/scoped/namespace-tree.ts (P2b).
|
|
130
|
+
* Also the builder of held connections: hold() calls it with the held list and an Invoke over the dedicated
|
|
131
|
+
* connection. There is no second tree builder.
|
|
132
|
+
*/
|
|
133
|
+
export type BuildNamespaceTree = (allowed: ReadonlySet<Method>, invoke: Invoke) => NamespaceTree;
|
|
134
|
+
/**
|
|
135
|
+
* The allow-list of one scoped client: `base` is the entry's list (ENTRY_METHODS[entry], or a preset's list), narrowed by
|
|
136
|
+
* `methods`. The caller passes the list it needs, so this module never imports the table of every entry (plan 10.1 step 2).
|
|
137
|
+
* Implemented by client/scoped/allowed-methods.ts.
|
|
138
|
+
*/
|
|
139
|
+
export type AllowedMethods = (base: readonly Method[], options?: {
|
|
140
|
+
readonly methods?: readonly Method[];
|
|
141
|
+
}) => ReadonlySet<Method>;
|
|
142
|
+
/**
|
|
143
|
+
* The stream members of one client: events.subscribe, the watch factory and hold. They are loaded on first use
|
|
144
|
+
* (client/scoped/lazy-streams.ts), so a client that never streams does not carry their code.
|
|
145
|
+
*/
|
|
146
|
+
export interface StreamMembers {
|
|
147
|
+
readonly subscribe: Subscribe;
|
|
148
|
+
readonly watch: WatchFn;
|
|
149
|
+
readonly hold: Hold;
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EventKind, SubscriptionType } from '../generated/index.js';
|
|
2
|
+
import type { DecodedFrame } from '../wire/types.js';
|
|
3
|
+
import type { PerPaneSubscription, PerPaneSubscriptionType, WatchPaneSet } from './watch.js';
|
|
4
|
+
import type { QueueOptions } from './subscribe.js';
|
|
5
|
+
export interface PaneLifecycle {
|
|
6
|
+
readonly event: typeof EventKind.PaneCreated | typeof EventKind.PaneClosed;
|
|
7
|
+
readonly paneId: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function lifecycleOf(frame: DecodedFrame): PaneLifecycle | undefined;
|
|
10
|
+
export interface PaneSet {
|
|
11
|
+
/** The pane ids now in the set. */
|
|
12
|
+
ids(): string[];
|
|
13
|
+
/** Adds the pane and returns its signal (aborts on remove or when the parent aborts); undefined when it was already present. */
|
|
14
|
+
add(paneId: string): AbortSignal | undefined;
|
|
15
|
+
/** Removes the pane and aborts its subscriptions; false when it was not in the set. */
|
|
16
|
+
remove(paneId: string): boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function createPaneSet(parent: AbortSignal): PaneSet;
|
|
19
|
+
export type PaneIdOnlyType = Exclude<PerPaneSubscriptionType, typeof SubscriptionType.PaneOutputMatched>;
|
|
20
|
+
export declare const DEFAULT_PANE_TYPES: readonly PaneIdOnlyType[];
|
|
21
|
+
export declare function checkedPaneTypes(types: readonly PerPaneSubscriptionType[]): readonly PaneIdOnlyType[];
|
|
22
|
+
export declare function perPaneSpecs(paneId: string, types: readonly PaneIdOnlyType[]): readonly PerPaneSubscription[];
|
|
23
|
+
export interface ReplayBuffer<T> {
|
|
24
|
+
/** Queues one item. Returns false once the buffer has stopped (overflow under Fail, or end/fail already ran). */
|
|
25
|
+
push(item: T): boolean;
|
|
26
|
+
/** Ends the buffer normally: takers get the queued items, then done. Idempotent. */
|
|
27
|
+
end(): void;
|
|
28
|
+
/** Ends the buffer with an error, after the queued items. Idempotent. */
|
|
29
|
+
fail(error: unknown): void;
|
|
30
|
+
/** The next item in arrival order. Rejects with the failure once the queue is empty; done after end() once empty. */
|
|
31
|
+
take(): Promise<IteratorResult<T>>;
|
|
32
|
+
}
|
|
33
|
+
export declare function createReplayBuffer<T>(queue?: QueueOptions): ReplayBuffer<T>;
|
|
34
|
+
export declare const watchPaneSet: WatchPaneSet;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Role: watchPaneSet(): the set of panes, one per-pane subscription each, and the replay buffer that orders their changes.
|
|
2
|
+
// Informs HERDR-ADR-0016, plan 3.6, HERDR-ADR-0007; merged by HERDR-ADR-0021 from pane-event.ts, pane-set.ts, per-pane-specs.ts, replay-buffer.ts, watch-pane-set.ts.
|
|
3
|
+
import { UNKNOWN_EVENT, PER_PANE_SUBSCRIPTION_TYPES, WatchText, Overflow, PANE_LIFECYCLE_SUBSCRIPTION_TYPES, PaneSetKind, WatchKind } from '../../constants.js';
|
|
4
|
+
import { EventKind, SubscriptionType } from '../generated/index.js';
|
|
5
|
+
import { linkSignal, createBackoffState, resolveBackoff, defaultSleep, forwardFrames, sleepOrAbort, bareSpecs, createWatch } from './watch.js';
|
|
6
|
+
import { UnsupportedWatchError, HerdrTransportError } from '../errors.js';
|
|
7
|
+
export function lifecycleOf(frame) {
|
|
8
|
+
if (frame.event === UNKNOWN_EVENT) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
if (frame.event === EventKind.PaneCreated && frame.data.type === EventKind.PaneCreated) {
|
|
12
|
+
return { event: EventKind.PaneCreated, paneId: frame.data.pane.pane_id };
|
|
13
|
+
}
|
|
14
|
+
if (frame.event === EventKind.PaneClosed && frame.data.type === EventKind.PaneClosed) {
|
|
15
|
+
return { event: EventKind.PaneClosed, paneId: frame.data.pane_id };
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
export function createPaneSet(parent) {
|
|
20
|
+
const controllers = new Map();
|
|
21
|
+
return {
|
|
22
|
+
ids() {
|
|
23
|
+
return [...controllers.keys()];
|
|
24
|
+
},
|
|
25
|
+
add(paneId) {
|
|
26
|
+
if (controllers.has(paneId)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
controllers.set(paneId, controller);
|
|
31
|
+
return linkSignal(controller.signal, parent);
|
|
32
|
+
},
|
|
33
|
+
remove(paneId) {
|
|
34
|
+
const controller = controllers.get(paneId);
|
|
35
|
+
if (controller === undefined) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
controllers.delete(paneId);
|
|
39
|
+
controller.abort();
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isPaneIdOnly(type) {
|
|
45
|
+
return type !== SubscriptionType.PaneOutputMatched;
|
|
46
|
+
}
|
|
47
|
+
// watchPaneSet's default types: PER_PANE_SUBSCRIPTION_TYPES minus pane.output_matched (open issue 8).
|
|
48
|
+
export const DEFAULT_PANE_TYPES = PER_PANE_SUBSCRIPTION_TYPES.filter(isPaneIdOnly);
|
|
49
|
+
// Narrows the caller's types to the ones this lane can subscribe to; a pane.output_matched request is refused, not dropped.
|
|
50
|
+
export function checkedPaneTypes(types) {
|
|
51
|
+
return types.map((type) => {
|
|
52
|
+
if (isPaneIdOnly(type)) {
|
|
53
|
+
return type;
|
|
54
|
+
}
|
|
55
|
+
throw new UnsupportedWatchError(WatchText.OutputMatchUnsupported);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function perPaneSpecs(paneId, types) {
|
|
59
|
+
return types.map((type) => ({ type, pane_id: paneId }));
|
|
60
|
+
}
|
|
61
|
+
export function createReplayBuffer(queue) {
|
|
62
|
+
const items = [];
|
|
63
|
+
let ending;
|
|
64
|
+
let waiter;
|
|
65
|
+
// Serves the single waiting taker as soon as something changed: a queued item first, then the failure, then the end.
|
|
66
|
+
function wake() {
|
|
67
|
+
if (waiter === undefined) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const pending = waiter;
|
|
71
|
+
const next = items.shift();
|
|
72
|
+
if (next !== undefined) {
|
|
73
|
+
waiter = undefined;
|
|
74
|
+
pending.resolve({ value: next, done: false });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (ending?.failed === true) {
|
|
78
|
+
waiter = undefined;
|
|
79
|
+
pending.reject(ending.error);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (ending !== undefined) {
|
|
83
|
+
waiter = undefined;
|
|
84
|
+
pending.resolve({ value: undefined, done: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function stop(next) {
|
|
88
|
+
if (ending !== undefined) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
ending = next;
|
|
92
|
+
wake();
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
push(item) {
|
|
96
|
+
if (ending !== undefined) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (queue !== undefined && items.length >= queue.max) {
|
|
100
|
+
if (queue.overflow === Overflow.Fail) {
|
|
101
|
+
items.length = 0;
|
|
102
|
+
stop({ failed: true, error: new HerdrTransportError(WatchText.ReplayOverflow(queue.max)) });
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
items.shift();
|
|
106
|
+
}
|
|
107
|
+
items.push(item);
|
|
108
|
+
wake();
|
|
109
|
+
return true;
|
|
110
|
+
},
|
|
111
|
+
end() {
|
|
112
|
+
stop({ failed: false });
|
|
113
|
+
},
|
|
114
|
+
fail(error) {
|
|
115
|
+
stop({ failed: true, error });
|
|
116
|
+
},
|
|
117
|
+
take() {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
waiter = { resolve, reject };
|
|
120
|
+
wake();
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export const watchPaneSet = (source, options = {}) => {
|
|
126
|
+
// Checked when watchPaneSet is called, so a pane.output_matched request fails at the call site, not inside the loop.
|
|
127
|
+
const types = checkedPaneTypes(options.types ?? DEFAULT_PANE_TYPES);
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
return {
|
|
130
|
+
close() {
|
|
131
|
+
controller.abort();
|
|
132
|
+
},
|
|
133
|
+
[Symbol.asyncIterator]() {
|
|
134
|
+
return paneChanges(source, options, types, controller);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
async function* paneChanges(source, options, types, controller) {
|
|
139
|
+
const signal = linkSignal(controller.signal, options.signal);
|
|
140
|
+
const output = createReplayBuffer();
|
|
141
|
+
const endOnAbort = () => output.end();
|
|
142
|
+
signal.addEventListener('abort', endOnAbort, { once: true });
|
|
143
|
+
const run = {
|
|
144
|
+
source,
|
|
145
|
+
options,
|
|
146
|
+
types,
|
|
147
|
+
signal,
|
|
148
|
+
output,
|
|
149
|
+
panes: createPaneSet(signal),
|
|
150
|
+
stop: () => controller.abort(),
|
|
151
|
+
};
|
|
152
|
+
followLifecycle(run);
|
|
153
|
+
try {
|
|
154
|
+
for (;;) {
|
|
155
|
+
const next = await output.take();
|
|
156
|
+
if (next.done === true || signal.aborted) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
yield next.value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
signal.removeEventListener('abort', endOnAbort);
|
|
164
|
+
controller.abort();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// The lifecycle is a watch over pane.created and pane.closed; its snapshot is the list of pane ids.
|
|
168
|
+
function followLifecycle(run) {
|
|
169
|
+
const lifecycle = createWatch(run.source.events.subscribe)({
|
|
170
|
+
snapshot: async () => (await run.source.pane.list()).panes.map((pane) => pane.pane_id),
|
|
171
|
+
subscriptions: bareSpecs(PANE_LIFECYCLE_SUBSCRIPTION_TYPES),
|
|
172
|
+
backoff: run.options.backoff,
|
|
173
|
+
sleep: run.options.sleep,
|
|
174
|
+
signal: run.signal,
|
|
175
|
+
onUnknown: run.options.onUnknown,
|
|
176
|
+
});
|
|
177
|
+
void pumpLifecycle(lifecycle, run);
|
|
178
|
+
}
|
|
179
|
+
// The lifecycle ends only when the iteration is closed or the client closed; either way every pane stops with it.
|
|
180
|
+
async function pumpLifecycle(lifecycle, run) {
|
|
181
|
+
try {
|
|
182
|
+
for await (const change of lifecycle) {
|
|
183
|
+
applyChange(change, run);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
run.stop();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function applyChange(change, run) {
|
|
191
|
+
if (change.kind === WatchKind.Live) {
|
|
192
|
+
syncPanes(change.snapshot, run);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const lifecycle = lifecycleOf(change.frame);
|
|
196
|
+
if (lifecycle === undefined) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (lifecycle.event === EventKind.PaneCreated) {
|
|
200
|
+
addPane(lifecycle.paneId, run);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
removePane(lifecycle.paneId, run);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// After a Live the set equals the listed panes: missing ones are added, panes no longer listed are removed.
|
|
207
|
+
function syncPanes(listed, run) {
|
|
208
|
+
const current = run.panes.ids();
|
|
209
|
+
for (const paneId of listed) {
|
|
210
|
+
addPane(paneId, run);
|
|
211
|
+
}
|
|
212
|
+
for (const paneId of current) {
|
|
213
|
+
if (!listed.includes(paneId)) {
|
|
214
|
+
removePane(paneId, run);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function addPane(paneId, run) {
|
|
219
|
+
const paneSignal = run.panes.add(paneId);
|
|
220
|
+
if (paneSignal === undefined) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
run.output.push({ kind: PaneSetKind.Added, paneId });
|
|
224
|
+
void followPane(paneId, paneSignal, run);
|
|
225
|
+
}
|
|
226
|
+
function removePane(paneId, run) {
|
|
227
|
+
if (run.panes.remove(paneId)) {
|
|
228
|
+
run.output.push({ kind: PaneSetKind.Removed, paneId });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// One pane's subscription. A failure resubscribes that pane with backoff while it is still in the set. A normal end that this pane did not ask for means the client closed, so the whole set stops.
|
|
232
|
+
// A Sleep that fails for a reason other than the abort is passed to the consumer's iterator, never left as an unhandled rejection.
|
|
233
|
+
async function followPane(paneId, paneSignal, run) {
|
|
234
|
+
const backoff = createBackoffState(resolveBackoff(run.options.backoff), Date.now);
|
|
235
|
+
const sleep = run.options.sleep ?? defaultSleep;
|
|
236
|
+
try {
|
|
237
|
+
while (!paneSignal.aborted) {
|
|
238
|
+
backoff.startCycle();
|
|
239
|
+
try {
|
|
240
|
+
await runPaneSubscription(paneId, paneSignal, run);
|
|
241
|
+
if (!paneSignal.aborted) {
|
|
242
|
+
run.stop();
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
if (paneSignal.aborted) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
await sleepOrAbort(sleep, backoff.delayAfterFailure(), paneSignal);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
run.output.fail(error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// One subscribe and its frames until the subscription ends. Resolves on a normal end; rejects when the subscribe or the subscription failed.
|
|
259
|
+
async function runPaneSubscription(paneId, paneSignal, run) {
|
|
260
|
+
const subscription = await run.source.events.subscribe(perPaneSpecs(paneId, run.types), {
|
|
261
|
+
signal: paneSignal,
|
|
262
|
+
queue: run.options.queue,
|
|
263
|
+
onUnknown: run.options.onUnknown,
|
|
264
|
+
});
|
|
265
|
+
try {
|
|
266
|
+
await forwardFrames(subscription, (frame) => run.output.push({ kind: PaneSetKind.Frame, paneId, frame }));
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
subscription.close();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { WatchKind } from '../../constants.js';
|
|
2
|
+
import type { PaneSetKind } from '../../constants.js';
|
|
3
|
+
import type { Subscription, SubscriptionType } from '../generated/index.js';
|
|
4
|
+
import type { MethodPaneList } from '../generated/names/method.js';
|
|
5
|
+
import type { DecodedFrame, ParamsOf, ResultOf, UnknownFrame } from '../wire/types.js';
|
|
6
|
+
import type { EventSubscription, QueueOptions, Subscribe } from './subscribe.js';
|
|
7
|
+
/** `Live` carries a fresh snapshot (first, and after every resubscribe); `Frame` a frame received after it. */
|
|
8
|
+
export type WatchChange<S> = {
|
|
9
|
+
readonly kind: typeof WatchKind.Live;
|
|
10
|
+
readonly snapshot: S;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: typeof WatchKind.Frame;
|
|
13
|
+
readonly frame: DecodedFrame;
|
|
14
|
+
};
|
|
15
|
+
/** Resubscribe backoff: InitialMs doubling (Factor) to MaxMs, reset after StableResetMs up (constants Backoff). */
|
|
16
|
+
export interface BackoffPolicy {
|
|
17
|
+
readonly initialMs: number;
|
|
18
|
+
readonly maxMs: number;
|
|
19
|
+
readonly factor: number;
|
|
20
|
+
readonly stableResetMs: number;
|
|
21
|
+
}
|
|
22
|
+
/** Injectable wait, so tests never sleep for real. Resolves after ms, or rejects/ends early when the signal aborts. */
|
|
23
|
+
export type Sleep = (ms: number, signal: AbortSignal) => Promise<void>;
|
|
24
|
+
export interface WatchOptions<S> {
|
|
25
|
+
/** Takes the present. Required: there is no subscribe-only watch (W5). */
|
|
26
|
+
readonly snapshot: (signal: AbortSignal) => Promise<S>;
|
|
27
|
+
/** Default: GLOBAL_SUBSCRIPTION_TYPES (constants.ts), every SubscriptionType that needs no pane_id. */
|
|
28
|
+
readonly subscriptions?: readonly Subscription[] | undefined;
|
|
29
|
+
readonly queue?: QueueOptions | undefined;
|
|
30
|
+
readonly backoff?: Partial<BackoffPolicy> | undefined;
|
|
31
|
+
readonly sleep?: Sleep | undefined;
|
|
32
|
+
readonly signal?: AbortSignal | undefined;
|
|
33
|
+
/** Forwarded to subscribe as SubscribeOptions.onUnknown; watch() itself never calls it (no double count). */
|
|
34
|
+
readonly onUnknown?: ((frame: UnknownFrame) => void) | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The watch loop: subscribe -> ack -> snapshot -> yield Live -> replay the frames that arrived between ack and snapshot
|
|
38
|
+
* reply (the consumer's fold must be idempotent) -> live frames. Any close: resubscribe + resnapshot with backoff.
|
|
39
|
+
* close() (or abort) ends the iteration normally. Never yields a Frame before the first Live.
|
|
40
|
+
*/
|
|
41
|
+
export interface Watch<S> extends AsyncIterable<WatchChange<S>> {
|
|
42
|
+
close(): void;
|
|
43
|
+
}
|
|
44
|
+
/** `herdr.watch(options)`. Synchronous: the loop starts on the first iteration. */
|
|
45
|
+
export type WatchFn = <S>(options: WatchOptions<S>) => Watch<S>;
|
|
46
|
+
/** Builds watch() of one client from its subscribe. Implemented by client/watch/watch.ts (P4b). */
|
|
47
|
+
export type CreateWatch = (subscribe: Subscribe) => WatchFn;
|
|
48
|
+
/** The per-pane subscription specs (W6: the 3 types that need pane_id). */
|
|
49
|
+
export type PerPaneSubscription = Extract<Subscription, {
|
|
50
|
+
pane_id: string;
|
|
51
|
+
}>;
|
|
52
|
+
export type PerPaneSubscriptionType = PerPaneSubscription['type'];
|
|
53
|
+
/** A pane joined or left the set, or a per-pane frame arrived for one pane. */
|
|
54
|
+
export type PaneSetChange = {
|
|
55
|
+
readonly kind: typeof PaneSetKind.Added;
|
|
56
|
+
readonly paneId: string;
|
|
57
|
+
} | {
|
|
58
|
+
readonly kind: typeof PaneSetKind.Removed;
|
|
59
|
+
readonly paneId: string;
|
|
60
|
+
} | {
|
|
61
|
+
readonly kind: typeof PaneSetKind.Frame;
|
|
62
|
+
readonly paneId: string;
|
|
63
|
+
readonly frame: DecodedFrame;
|
|
64
|
+
};
|
|
65
|
+
/** The smallest client watchPaneSet needs; every ReadOnlyClient, Client and AdminClient satisfies it. */
|
|
66
|
+
export interface PaneSetSource {
|
|
67
|
+
readonly pane: {
|
|
68
|
+
list(params?: ParamsOf<typeof MethodPaneList>): Promise<ResultOf<typeof MethodPaneList>>;
|
|
69
|
+
};
|
|
70
|
+
readonly events: {
|
|
71
|
+
subscribe: Subscribe;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface WatchPaneSetOptions {
|
|
75
|
+
/** Which per-pane types to subscribe for each pane (default: PER_PANE_SUBSCRIPTION_TYPES, constants.ts). */
|
|
76
|
+
readonly types?: readonly PerPaneSubscriptionType[] | undefined;
|
|
77
|
+
/** Bounds each per-pane subscription's queue (passed to subscribe). */
|
|
78
|
+
readonly queue?: QueueOptions | undefined;
|
|
79
|
+
/** Forwarded to every per-pane subscribe as SubscribeOptions.onUnknown. */
|
|
80
|
+
readonly onUnknown?: ((frame: UnknownFrame) => void) | undefined;
|
|
81
|
+
readonly backoff?: Partial<BackoffPolicy> | undefined;
|
|
82
|
+
readonly sleep?: Sleep | undefined;
|
|
83
|
+
readonly signal?: AbortSignal | undefined;
|
|
84
|
+
}
|
|
85
|
+
export interface PaneSetWatch extends AsyncIterable<PaneSetChange> {
|
|
86
|
+
close(): void;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Lists panes, subscribes pane by pane to the per-pane types, and keeps the set with pane.created / pane.closed
|
|
90
|
+
* (W6). Implemented by client/watch/watch-pane-set.ts (P4b); the Effect twin wraps it (P6a).
|
|
91
|
+
*/
|
|
92
|
+
export type WatchPaneSet = (source: PaneSetSource, options?: WatchPaneSetOptions) => PaneSetWatch;
|
|
93
|
+
/** Re-exported for implementers that hold a subscription per pane. */
|
|
94
|
+
export type { EventSubscription };
|
|
95
|
+
export declare const defaultSleep: Sleep;
|
|
96
|
+
export declare function sleepOrAbort(sleep: Sleep, ms: number, signal: AbortSignal): Promise<void>;
|
|
97
|
+
export declare function linkSignal(own: AbortSignal, parent: AbortSignal | undefined): AbortSignal;
|
|
98
|
+
export declare function forwardFrames(subscription: EventSubscription, accept: (frame: DecodedFrame) => boolean): Promise<void>;
|
|
99
|
+
export declare function bareSpecs(types: readonly SubscriptionType[]): readonly Subscription[];
|
|
100
|
+
export declare function resolveBackoff(partial?: Partial<BackoffPolicy>): BackoffPolicy;
|
|
101
|
+
export declare function nextDelayMs(policy: BackoffPolicy, attempt: number): number;
|
|
102
|
+
export interface BackoffState {
|
|
103
|
+
/** Marks the start of a cycle, so the next failure can tell whether it stayed up long enough to reset. */
|
|
104
|
+
startCycle(): void;
|
|
105
|
+
/** Records a failed cycle and returns the delay before the next attempt. */
|
|
106
|
+
delayAfterFailure(): number;
|
|
107
|
+
}
|
|
108
|
+
export declare function createBackoffState(policy: BackoffPolicy, now: () => number): BackoffState;
|
|
109
|
+
export interface CycleInput<S> {
|
|
110
|
+
readonly subscribe: Subscribe;
|
|
111
|
+
readonly specs: readonly Subscription[];
|
|
112
|
+
readonly options: WatchOptions<S>;
|
|
113
|
+
readonly signal: AbortSignal;
|
|
114
|
+
}
|
|
115
|
+
export declare function runCycle<S>(input: CycleInput<S>): AsyncGenerator<WatchChange<S>, void, undefined>;
|
|
116
|
+
export declare const createWatch: CreateWatch;
|
|
117
|
+
export declare function createWatchWithClock(subscribe: Subscribe, now: () => number): WatchFn;
|