@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.
Files changed (171) hide show
  1. package/LICENSE +18 -0
  2. package/NOTICE.md +51 -0
  3. package/README.md +211 -2
  4. package/bin/herdr-drift.js +14 -0
  5. package/bin/herdr-skill.js +38 -0
  6. package/dist/admin.d.ts +2 -0
  7. package/dist/admin.js +3 -0
  8. package/dist/constants.d.ts +236 -0
  9. package/dist/constants.js +294 -0
  10. package/dist/core/backoff.d.ts +20 -0
  11. package/dist/core/backoff.js +53 -0
  12. package/dist/core/constants.d.ts +143 -0
  13. package/dist/core/constants.js +98 -0
  14. package/dist/core/errors.d.ts +35 -0
  15. package/dist/core/errors.js +16 -0
  16. package/dist/core/events.d.ts +42 -0
  17. package/dist/core/events.js +128 -0
  18. package/dist/core/kernel.d.ts +65 -0
  19. package/dist/core/kernel.js +138 -0
  20. package/dist/core/link.d.ts +47 -0
  21. package/dist/core/link.js +187 -0
  22. package/dist/core/test-kit/constants.d.ts +51 -0
  23. package/dist/core/test-kit/constants.js +43 -0
  24. package/dist/core/test-kit/fake-core.d.ts +61 -0
  25. package/dist/core/test-kit/fake-core.js +228 -0
  26. package/dist/core/test-kit/frames.d.ts +1 -0
  27. package/dist/core/test-kit/frames.js +480 -0
  28. package/dist/core/test-kit/setup-test.d.ts +18 -0
  29. package/dist/core/test-kit/setup-test.js +35 -0
  30. package/dist/core/wire.d.ts +85 -0
  31. package/dist/core/wire.js +199 -0
  32. package/dist/core-test-kit.d.ts +5 -0
  33. package/dist/core-test-kit.js +5 -0
  34. package/dist/core.d.ts +15 -0
  35. package/dist/core.js +12 -0
  36. package/dist/drift/cli.d.ts +22 -0
  37. package/dist/drift/cli.js +146 -0
  38. package/dist/drift/constants.d.ts +83 -0
  39. package/dist/drift/constants.js +73 -0
  40. package/dist/drift/diff.d.ts +7 -0
  41. package/dist/drift/diff.js +91 -0
  42. package/dist/drift/schema.d.ts +19 -0
  43. package/dist/drift/schema.js +190 -0
  44. package/dist/drift/types.d.ts +76 -0
  45. package/dist/drift/types.js +1 -0
  46. package/dist/drift.d.ts +5 -0
  47. package/dist/drift.js +6 -0
  48. package/dist/effect.d.ts +3 -0
  49. package/dist/effect.js +4 -0
  50. package/dist/herdr/client/connect.d.ts +5 -0
  51. package/dist/herdr/client/connect.js +81 -0
  52. package/dist/herdr/client/held.d.ts +54 -0
  53. package/dist/herdr/client/held.js +100 -0
  54. package/dist/herdr/client/long-poll.d.ts +33 -0
  55. package/dist/herdr/client/long-poll.js +66 -0
  56. package/dist/herdr/client/scoped.d.ts +45 -0
  57. package/dist/herdr/client/scoped.js +241 -0
  58. package/dist/herdr/client/streams.d.ts +9 -0
  59. package/dist/herdr/client/streams.js +40 -0
  60. package/dist/herdr/client/subscribe.d.ts +75 -0
  61. package/dist/herdr/client/subscribe.js +208 -0
  62. package/dist/herdr/client/types.d.ts +150 -0
  63. package/dist/herdr/client/types.js +1 -0
  64. package/dist/herdr/client/watch-pane-set.d.ts +34 -0
  65. package/dist/herdr/client/watch-pane-set.js +271 -0
  66. package/dist/herdr/client/watch.d.ts +117 -0
  67. package/dist/herdr/client/watch.js +138 -0
  68. package/dist/herdr/effect/to-tagged-error.d.ts +2 -0
  69. package/dist/herdr/effect/to-tagged-error.js +54 -0
  70. package/dist/herdr/effect/types.d.ts +51 -0
  71. package/dist/herdr/effect/types.js +26 -0
  72. package/dist/herdr/errors.d.ts +53 -0
  73. package/dist/herdr/errors.js +87 -0
  74. package/dist/herdr/generated/entries/admin.d.ts +2 -0
  75. package/dist/herdr/generated/entries/admin.js +5 -0
  76. package/dist/herdr/generated/entries/client.d.ts +2 -0
  77. package/dist/herdr/generated/entries/client.js +5 -0
  78. package/dist/herdr/generated/entries/held.d.ts +2 -0
  79. package/dist/herdr/generated/entries/held.js +5 -0
  80. package/dist/herdr/generated/entries/prompt.d.ts +2 -0
  81. package/dist/herdr/generated/entries/prompt.js +5 -0
  82. package/dist/herdr/generated/entries/read-only-content.d.ts +2 -0
  83. package/dist/herdr/generated/entries/read-only-content.js +5 -0
  84. package/dist/herdr/generated/entries/read-only.d.ts +2 -0
  85. package/dist/herdr/generated/entries/read-only.js +5 -0
  86. package/dist/herdr/generated/entry-methods.d.ts +9 -0
  87. package/dist/herdr/generated/entry-methods.js +17 -0
  88. package/dist/herdr/generated/errors.d.ts +4 -0
  89. package/dist/herdr/generated/errors.js +3 -0
  90. package/dist/herdr/generated/events.d.ts +274 -0
  91. package/dist/herdr/generated/events.js +3 -0
  92. package/dist/herdr/generated/index.d.ts +20 -0
  93. package/dist/herdr/generated/index.js +19 -0
  94. package/dist/herdr/generated/method-table.d.ts +615 -0
  95. package/dist/herdr/generated/method-table.js +106 -0
  96. package/dist/herdr/generated/names/agent-status.d.ts +10 -0
  97. package/dist/herdr/generated/names/agent-status.js +10 -0
  98. package/dist/herdr/generated/names/capability-class.d.ts +14 -0
  99. package/dist/herdr/generated/names/capability-class.js +15 -0
  100. package/dist/herdr/generated/names/event-kind.d.ts +31 -0
  101. package/dist/herdr/generated/names/event-kind.js +31 -0
  102. package/dist/herdr/generated/names/event-match-kind.d.ts +23 -0
  103. package/dist/herdr/generated/names/event-match-kind.js +24 -0
  104. package/dist/herdr/generated/names/index.d.ts +10 -0
  105. package/dist/herdr/generated/names/index.js +12 -0
  106. package/dist/herdr/generated/names/method.d.ts +209 -0
  107. package/dist/herdr/generated/names/method.js +209 -0
  108. package/dist/herdr/generated/names/result-tag.d.ts +68 -0
  109. package/dist/herdr/generated/names/result-tag.js +68 -0
  110. package/dist/herdr/generated/names/server-capability.d.ts +10 -0
  111. package/dist/herdr/generated/names/server-capability.js +11 -0
  112. package/dist/herdr/generated/names/subscription-event-kind.d.ts +8 -0
  113. package/dist/herdr/generated/names/subscription-event-kind.js +8 -0
  114. package/dist/herdr/generated/names/subscription-type.d.ts +31 -0
  115. package/dist/herdr/generated/names/subscription-type.js +32 -0
  116. package/dist/herdr/generated/names/token-name-pattern.d.ts +2 -0
  117. package/dist/herdr/generated/names/token-name-pattern.js +4 -0
  118. package/dist/herdr/generated/namespaces.d.ts +785 -0
  119. package/dist/herdr/generated/namespaces.js +3 -0
  120. package/dist/herdr/generated/overrides.d.ts +33 -0
  121. package/dist/herdr/generated/overrides.js +20 -0
  122. package/dist/herdr/generated/params.d.ts +799 -0
  123. package/dist/herdr/generated/params.js +3 -0
  124. package/dist/herdr/generated/preset-methods.d.ts +4 -0
  125. package/dist/herdr/generated/preset-methods.js +7 -0
  126. package/dist/herdr/generated/presets/sessions-reader.d.ts +2 -0
  127. package/dist/herdr/generated/presets/sessions-reader.js +5 -0
  128. package/dist/herdr/generated/results.d.ts +978 -0
  129. package/dist/herdr/generated/results.js +52 -0
  130. package/dist/herdr/generated/test-data/defaults.d.ts +739 -0
  131. package/dist/herdr/generated/test-data/defaults.js +741 -0
  132. package/dist/herdr/generated/test-data/shapes.d.ts +1325 -0
  133. package/dist/herdr/generated/test-data/shapes.js +2245 -0
  134. package/dist/herdr/test-kit/fake-herdr.d.ts +13 -0
  135. package/dist/herdr/test-kit/fake-herdr.js +216 -0
  136. package/dist/herdr/test-kit/fake-protocol.d.ts +107 -0
  137. package/dist/herdr/test-kit/fake-protocol.js +436 -0
  138. package/dist/herdr/test-kit/types.d.ts +88 -0
  139. package/dist/herdr/test-kit/types.js +1 -0
  140. package/dist/herdr/wire/codec.d.ts +36 -0
  141. package/dist/herdr/wire/codec.js +62 -0
  142. package/dist/herdr/wire/connection.d.ts +13 -0
  143. package/dist/herdr/wire/connection.js +65 -0
  144. package/dist/herdr/wire/dedicated.d.ts +2 -0
  145. package/dist/herdr/wire/dedicated.js +238 -0
  146. package/dist/herdr/wire/push-decoder.d.ts +2 -0
  147. package/dist/herdr/wire/push-decoder.js +40 -0
  148. package/dist/herdr/wire/rpc.d.ts +3 -0
  149. package/dist/herdr/wire/rpc.js +141 -0
  150. package/dist/herdr/wire/socket-path.d.ts +8 -0
  151. package/dist/herdr/wire/socket-path.js +23 -0
  152. package/dist/herdr/wire/types.d.ts +78 -0
  153. package/dist/herdr/wire/types.js +1 -0
  154. package/dist/index.d.ts +11 -0
  155. package/dist/index.js +7 -0
  156. package/dist/read-only.d.ts +5 -0
  157. package/dist/read-only.js +6 -0
  158. package/dist/schema.d.ts +5 -0
  159. package/dist/schema.js +6 -0
  160. package/dist/test-kit.d.ts +2 -0
  161. package/dist/test-kit.js +3 -0
  162. package/docs/api/events.md +43 -0
  163. package/docs/api/methods.md +232 -0
  164. package/package.json +112 -4
  165. package/schema/herdr-api.schema.json +10759 -0
  166. package/schema/herdr-version.json +5 -0
  167. package/skills/herdr-sdk/SKILL.md +75 -0
  168. package/skills/herdr-sdk/references/api-map.md +28 -0
  169. package/skills/herdr-sdk/references/errors.md +19 -0
  170. package/skills/herdr-sdk/references/recipes.md +171 -0
  171. package/skills/herdr-sdk/references/versions.md +7 -0
@@ -0,0 +1,45 @@
1
+ import type { Method } from '../generated/index.js';
2
+ import type { AllowedMethods, BuildNamespaceTree, Invoke, CoreClient, StreamMembers, ConnectAdmin, ConnectPrompt, ConnectReadOnly } from './types.js';
3
+ import type { AssertLongPollBounds, RequestTimeoutMs } from './long-poll.js';
4
+ export declare const allowedMethods: AllowedMethods;
5
+ export declare const buildNamespaceTree: BuildNamespaceTree;
6
+ /** The P4a helpers a long-poll call needs; tests pass stubs. */
7
+ export interface InvokeDeps {
8
+ readonly requestTimeoutMs: RequestTimeoutMs;
9
+ readonly assertLongPollBounds: AssertLongPollBounds;
10
+ }
11
+ export declare function createInvoke(core: CoreClient, allowed: ReadonlySet<Method>, deps?: InvokeDeps): Invoke;
12
+ /** Throws before anything is sent when the method is not in the entry's allow-list. */
13
+ export declare function assertAllowed(method: Method, allowed: ReadonlySet<Method>): void;
14
+ export declare function lazyStreams(core: CoreClient): StreamMembers;
15
+ /** Which members beyond the namespace tree an entry has. */
16
+ export interface Members {
17
+ readonly call: boolean;
18
+ readonly streams: boolean;
19
+ readonly hold: boolean;
20
+ }
21
+ /** connect() and connectAdmin() have everything; the read-only entries stream; prompt has no streams and no call(). */
22
+ export declare const ENTRY_MEMBERS: {
23
+ readonly client: Members;
24
+ readonly admin: Members;
25
+ readonly prompt: Members;
26
+ readonly readOnly: Members;
27
+ readonly readOnlyContent: Members;
28
+ };
29
+ /** The preset form of connectReadOnly: a fixed list with no streams and no call(). */
30
+ export declare const PRESET_MEMBERS: Members;
31
+ /**
32
+ * Builds a public client of type T from the core and the allow-list. The tree is built from exactly `allowed`;
33
+ * T is the generated type of the same entry. The runtime object is assembled from the tree and the members, so its static
34
+ * type is asserted to T by the final cast; the contract type tests (test/types) check the members of each T.
35
+ * `streams` is the lazy loader of the stream members (lazy-streams.ts); tests pass their own.
36
+ */
37
+ export declare function composeClient<T>(core: CoreClient, allowed: ReadonlySet<Method>, members: Members, streams?: StreamMembers): T;
38
+ /**
39
+ * composeClient, but closes the core when composition refuses (a watch on a method list without its methods), so the
40
+ * connection the core opened is not left live. Every connect function composes through this.
41
+ */
42
+ export declare function composeOrClose<T>(core: CoreClient, allowed: ReadonlySet<Method>, members: Members, streams?: StreamMembers): T;
43
+ export declare const connectAdmin: ConnectAdmin;
44
+ export declare const connectPrompt: ConnectPrompt;
45
+ export declare const connectReadOnly: ConnectReadOnly;
@@ -0,0 +1,241 @@
1
+ // Role: the capability-scoped clients (read-only, prompt, admin): allowed methods, the namespace tree, invoke and the stream members.
2
+ // Informs HERDR-ADR-0005, HERDR-ADR-0017, plan 3.4, plan 3.3, HERDR-ADR-0007, plan 3.6, plan 10.1, HERDR-ADR-0015; merged by HERDR-ADR-0021 from allowed-methods.ts, namespace-tree.ts, invoke.ts, lazy-streams.ts, compose.ts, connect-admin.ts, connect-prompt.ts, connect-read-only.ts.
3
+ import { LONG_POLL } from '../generated/index.js';
4
+ import { METHOD_SEPARATOR, TypeofKind, ErrorText, EntryKey, TreeKey, Presets } from '../../constants.js';
5
+ import { MethodEventsSubscribe, MethodSessionSnapshot } from '../generated/names/method.js';
6
+ import { HerdrTransportError } from '../errors.js';
7
+ import { assertLongPollBounds as defaultAssertBounds, requestTimeoutMs as defaultRequestTimeoutMs } from './long-poll.js';
8
+ import { ADMIN_METHODS } from '../generated/entries/admin.js';
9
+ import { connectCore } from './connect.js';
10
+ import { PROMPT_METHODS } from '../generated/entries/prompt.js';
11
+ import { READ_ONLY_CONTENT_METHODS } from '../generated/entries/read-only-content.js';
12
+ import { READ_ONLY_METHODS } from '../generated/entries/read-only.js';
13
+ import { SESSIONS_READER_METHODS } from '../generated/presets/sessions-reader.js';
14
+ export const allowedMethods = function allowedMethods(base, options = {}) {
15
+ return options.methods === undefined ? new Set(base) : narrow(base, options.methods);
16
+ };
17
+ // A method outside the base list is dropped, not added: `methods` can only narrow the entry.
18
+ function narrow(base, methods) {
19
+ const inBase = new Set(base);
20
+ return new Set(methods.filter((method) => inBase.has(method)));
21
+ }
22
+ export const buildNamespaceTree = function buildNamespaceTree(allowed, invoke) {
23
+ const root = {};
24
+ for (const method of allowed) {
25
+ // events.subscribe is never a leaf: the composer adds the real Subscribe in its place.
26
+ if (method !== MethodEventsSubscribe) {
27
+ addLeaf(root, method, invoke);
28
+ }
29
+ }
30
+ return freezeTree(root);
31
+ };
32
+ // `agent.view.set` becomes root.agent.view.set; the names come from the allow-list itself, so no generated table is needed.
33
+ function addLeaf(root, method, invoke) {
34
+ const path = method.split(METHOD_SEPARATOR);
35
+ const leafName = path.pop();
36
+ if (leafName === undefined) {
37
+ return;
38
+ }
39
+ const branch = ensureBranch(root, path);
40
+ branch[leafName] = leafFor(method, invoke);
41
+ }
42
+ // No generated method name is also a branch of another (test/herdr/client/scoped/namespace-tree.test.ts), so a leaf is never replaced by a branch.
43
+ function ensureBranch(root, segments) {
44
+ let node = root;
45
+ for (const segment of segments) {
46
+ const existing = node[segment];
47
+ if (existing === undefined || isLeaf(existing)) {
48
+ const created = {};
49
+ node[segment] = created;
50
+ node = created;
51
+ }
52
+ else {
53
+ node = existing;
54
+ }
55
+ }
56
+ return node;
57
+ }
58
+ function isLeaf(node) {
59
+ return typeof node === TypeofKind.Function;
60
+ }
61
+ // The default params are an empty object, so `herdr.ping()` sends `{}` as every generated namespace does.
62
+ function leafFor(method, invoke) {
63
+ return (params = {}, options) => invoke(method, params, options);
64
+ }
65
+ function freezeTree(node) {
66
+ for (const value of Object.values(node)) {
67
+ if (!isLeaf(value)) {
68
+ freezeTree(value);
69
+ }
70
+ }
71
+ return Object.freeze(node);
72
+ }
73
+ const DEFAULT_DEPS = {
74
+ requestTimeoutMs: defaultRequestTimeoutMs,
75
+ assertLongPollBounds: defaultAssertBounds,
76
+ };
77
+ export function createInvoke(core, allowed, deps = DEFAULT_DEPS) {
78
+ return async function invoke(method, params, options = {}) {
79
+ assertAllowed(method, allowed);
80
+ // The generated tree and the generic call() both pass params typed by the method name; the server checks the shape.
81
+ const wire = params;
82
+ if (!isLongPoll(method)) {
83
+ return core.call(method, wire, options);
84
+ }
85
+ deps.assertLongPollBounds(method, params);
86
+ const timeoutMs = deps.requestTimeoutMs(method, params, core.transport.timeoutMs);
87
+ return core.call(method, wire, withTimeout(options, timeoutMs));
88
+ };
89
+ }
90
+ /** Throws before anything is sent when the method is not in the entry's allow-list. */
91
+ export function assertAllowed(method, allowed) {
92
+ if (!allowed.has(method)) {
93
+ throw new HerdrTransportError(ErrorText.NotAllowed(method));
94
+ }
95
+ }
96
+ function isLongPoll(method) {
97
+ return Object.hasOwn(LONG_POLL, method);
98
+ }
99
+ // The computed long-poll timeout is a default: a timeoutMs the caller set on this call wins (connect-core.callRpcOptions says the same).
100
+ function withTimeout(options, timeoutMs) {
101
+ return timeoutMs === undefined || options.timeoutMs !== undefined ? options : { ...options, timeoutMs };
102
+ }
103
+ // One load per client: the first member used starts the import, every later one awaits the same promise.
104
+ export function lazyStreams(core) {
105
+ let loaded;
106
+ const load = () => {
107
+ loaded ??= import('./streams.js').then((module) => module.createStreamMembers(core));
108
+ return loaded;
109
+ };
110
+ const subscribe = async (specs, options) => {
111
+ const members = await load();
112
+ return members.subscribe(specs, options);
113
+ };
114
+ const hold = async (options) => {
115
+ const members = await load();
116
+ return members.hold(options);
117
+ };
118
+ return { subscribe, watch: lazyWatch(load), hold };
119
+ }
120
+ // watch() is synchronous, so it returns at once; the real watch is created when iteration starts, as the real one does. close() before that ends the iteration without creating it.
121
+ function lazyWatch(load) {
122
+ return function watch(options) {
123
+ let stopped = false;
124
+ let inner;
125
+ async function* changes() {
126
+ const members = await load();
127
+ if (stopped) {
128
+ return;
129
+ }
130
+ inner = members.watch(options);
131
+ yield* inner;
132
+ }
133
+ return {
134
+ close() {
135
+ stopped = true;
136
+ inner?.close();
137
+ },
138
+ [Symbol.asyncIterator]: changes,
139
+ };
140
+ };
141
+ }
142
+ const FULL_MEMBERS = { call: true, streams: true, hold: true };
143
+ const STREAM_MEMBERS = { call: false, streams: true, hold: false };
144
+ const NO_MEMBERS = { call: false, streams: false, hold: false };
145
+ /** connect() and connectAdmin() have everything; the read-only entries stream; prompt has no streams and no call(). */
146
+ export const ENTRY_MEMBERS = {
147
+ [EntryKey.client]: FULL_MEMBERS,
148
+ [EntryKey.admin]: FULL_MEMBERS,
149
+ [EntryKey.prompt]: NO_MEMBERS,
150
+ [EntryKey.readOnly]: STREAM_MEMBERS,
151
+ [EntryKey.readOnlyContent]: STREAM_MEMBERS,
152
+ };
153
+ /** The preset form of connectReadOnly: a fixed list with no streams and no call(). */
154
+ export const PRESET_MEMBERS = NO_MEMBERS;
155
+ /**
156
+ * Builds a public client of type T from the core and the allow-list. The tree is built from exactly `allowed`;
157
+ * T is the generated type of the same entry. The runtime object is assembled from the tree and the members, so its static
158
+ * type is asserted to T by the final cast; the contract type tests (test/types) check the members of each T.
159
+ * `streams` is the lazy loader of the stream members (lazy-streams.ts); tests pass their own.
160
+ */
161
+ export function composeClient(core, allowed, members, streams = lazyStreams(core)) {
162
+ const invoke = createInvoke(core, allowed);
163
+ const tree = buildNamespaceTree(allowed, invoke);
164
+ const client = {
165
+ ...tree,
166
+ capabilities: core.capabilities,
167
+ serverInfo: core.serverInfo,
168
+ close: () => {
169
+ core.close();
170
+ },
171
+ };
172
+ if (members.call) {
173
+ client.call = callWith(invoke);
174
+ }
175
+ if (members.streams) {
176
+ // The refusals are synchronous, before anything is sent or loaded: watch() is built on events.subscribe and session.snapshot.
177
+ if (allowed.has(MethodEventsSubscribe)) {
178
+ client.events = eventsWith(tree, streams.subscribe);
179
+ }
180
+ requireAllowed(MethodEventsSubscribe, allowed);
181
+ requireAllowed(MethodSessionSnapshot, allowed);
182
+ client.watch = streams.watch;
183
+ }
184
+ if (members.hold) {
185
+ client.hold = streams.hold;
186
+ }
187
+ return Object.freeze(client);
188
+ }
189
+ function requireAllowed(method, allowed) {
190
+ if (!allowed.has(method)) {
191
+ throw new HerdrTransportError(ErrorText.NotAllowed(method));
192
+ }
193
+ }
194
+ /**
195
+ * composeClient, but closes the core when composition refuses (a watch on a method list without its methods), so the
196
+ * connection the core opened is not left live. Every connect function composes through this.
197
+ */
198
+ export function composeOrClose(core, allowed, members, streams = lazyStreams(core)) {
199
+ try {
200
+ return composeClient(core, allowed, members, streams);
201
+ }
202
+ catch (error) {
203
+ core.close();
204
+ throw error;
205
+ }
206
+ }
207
+ // call() is the generic escape hatch: the same allow-list and long-poll path as every leaf, with the result typed by the method.
208
+ function callWith(invoke) {
209
+ return function call(method, params, options) {
210
+ return invoke(method, params, options);
211
+ };
212
+ }
213
+ // The generated events branch (every event leaf except subscribe) plus the composed subscribe.
214
+ function eventsWith(tree, subscribe) {
215
+ const branch = tree[TreeKey.Events];
216
+ return Object.freeze({ ...branch, [TreeKey.Subscribe]: subscribe });
217
+ }
218
+ export const connectAdmin = async function connectAdmin(options = {}) {
219
+ const core = await connectCore(options);
220
+ return composeOrClose(core, allowedMethods(ADMIN_METHODS), ENTRY_MEMBERS[EntryKey.admin], lazyStreams(core));
221
+ };
222
+ export const connectPrompt = async function connectPrompt(options = {}) {
223
+ const core = await connectCore(options);
224
+ return composeOrClose(core, allowedMethods(PROMPT_METHODS), ENTRY_MEMBERS[EntryKey.prompt]);
225
+ };
226
+ // The preset lists this entry can name. Only the list of each preset is imported, not the table of every entry.
227
+ const PRESET_LISTS = {
228
+ [Presets.SessionsReader]: SESSIONS_READER_METHODS,
229
+ };
230
+ async function connectReadOnlyForm(options = {}) {
231
+ const core = await connectCore(options);
232
+ if (options.preset !== undefined) {
233
+ return composeOrClose(core, allowedMethods(PRESET_LISTS[options.preset]), PRESET_MEMBERS, lazyStreams(core));
234
+ }
235
+ const entry = options.content === true ? EntryKey.readOnlyContent : EntryKey.readOnly;
236
+ const base = entry === EntryKey.readOnlyContent ? READ_ONLY_CONTENT_METHODS : READ_ONLY_METHODS;
237
+ const allowed = allowedMethods(base, options.methods === undefined ? {} : { methods: options.methods });
238
+ return composeOrClose(core, allowed, ENTRY_MEMBERS[entry], lazyStreams(core));
239
+ }
240
+ // One implementation for the three overloads: the union return is narrowed by the overload the caller's options match.
241
+ export const connectReadOnly = connectReadOnlyForm;
@@ -0,0 +1,9 @@
1
+ import type { CreateHold, Hold } from './held.js';
2
+ import type { CreateSubscribe, Subscribe } from './subscribe.js';
3
+ import type { CreateWatch, WatchFn } from './watch.js';
4
+ import type { ClientTransport, CoreClient, StreamMembers } from './types.js';
5
+ export declare function subscribeFor(transport: ClientTransport, create?: CreateSubscribe): Subscribe;
6
+ export declare function watchFor(subscribe: Subscribe, create?: CreateWatch): WatchFn;
7
+ export declare function holdFor(core: CoreClient, create?: CreateHold): Hold;
8
+ /** The three stream members of one client, built from its core. Called once, by the lazy loader, on first use. */
9
+ export declare function createStreamMembers(core: CoreClient): StreamMembers;
@@ -0,0 +1,40 @@
1
+ // Role: builds the stream members of one client from its transport: events.subscribe, the watch factory and hold, each with the client's resolved settings.
2
+ // This module is the heavy half: it is loaded through a dynamic import by lazyStreams in scoped.ts, so it stays out of a client that never streams (plan 10.1 step 3).
3
+ // Informs plan 3.3 (the transport is resolved once), plan 3.6 and HERDR-ADR-0015; contracts in subscribe.ts, watch.ts and held.ts.
4
+ import { openDedicated } from '../wire/dedicated.js';
5
+ import { createHold as defaultCreateHold } from './held.js';
6
+ import { createSubscribe as defaultCreateSubscribe } from './subscribe.js';
7
+ import { createWatch as defaultCreateWatch } from './watch.js';
8
+ import { allowedMethods, buildNamespaceTree } from './scoped.js';
9
+ // The factories are parameters so a test can spy on what they receive; the defaults are the real ones.
10
+ export function subscribeFor(transport, create = defaultCreateSubscribe) {
11
+ return create({
12
+ socketPath: transport.socketPath,
13
+ timeoutMs: transport.timeoutMs,
14
+ onCall: transport.onCall,
15
+ onUnknown: transport.onUnknown,
16
+ signal: transport.closed,
17
+ open: openDedicated,
18
+ });
19
+ }
20
+ // watch() is built on events.subscribe and session.snapshot. Whether the client may use both is checked by compose.ts before this runs (synchronously, plan 3.4).
21
+ export function watchFor(subscribe, create = defaultCreateWatch) {
22
+ return create(subscribe);
23
+ }
24
+ export function holdFor(core, create = defaultCreateHold) {
25
+ return create({
26
+ socketPath: core.transport.socketPath,
27
+ timeoutMs: core.transport.timeoutMs,
28
+ capabilities: core.capabilities,
29
+ onCall: core.transport.onCall,
30
+ open: openDedicated,
31
+ signal: core.transport.closed,
32
+ buildTree: buildNamespaceTree,
33
+ allowedMethods,
34
+ });
35
+ }
36
+ /** The three stream members of one client, built from its core. Called once, by the lazy loader, on first use. */
37
+ export function createStreamMembers(core) {
38
+ const subscribe = subscribeFor(core.transport);
39
+ return { subscribe, watch: watchFor(subscribe), hold: holdFor(core) };
40
+ }
@@ -0,0 +1,75 @@
1
+ import { Overflow } from '../../constants.js';
2
+ import type { Subscription } from '../generated/index.js';
3
+ import type { DecodedFrame, OnCall, OpenDedicated, UnknownFrame } from '../wire/types.js';
4
+ /** Bounded queue. Without it the queue is unbounded (plan 3.6). */
5
+ export interface QueueOptions {
6
+ readonly max: number;
7
+ /** DropOldest drops the oldest queued frame; Fail ends the iterator with HerdrTransportError. */
8
+ readonly overflow: Overflow;
9
+ }
10
+ export interface SubscribeOptions {
11
+ /** Abort closes the subscription (same as close()). */
12
+ readonly signal?: AbortSignal | undefined;
13
+ /** How long to wait for the `subscription_started` ack; default: the client's timeoutMs. */
14
+ readonly ackTimeoutMs?: number | undefined;
15
+ readonly queue?: QueueOptions | undefined;
16
+ /** Called once per UnknownFrame, before it is yielded, after the client-level onUnknown (P3: counted, never dropped). */
17
+ readonly onUnknown?: ((frame: UnknownFrame) => void) | undefined;
18
+ }
19
+ /**
20
+ * A live subscription on its own connection. Iterating yields frames in arrival order. close() (or abort) ends the
21
+ * iteration normally and destroys the connection. A server close ends it by throwing SubscriptionEnded; a malformed
22
+ * line throws HerdrTransportError. No history is replayed: frames are changes, not the present (W5).
23
+ */
24
+ export interface EventSubscription extends AsyncIterable<DecodedFrame> {
25
+ close(): void;
26
+ }
27
+ /**
28
+ * `herdr.events.subscribe(specs, options)`: opens a dedicated connection, sends `events.subscribe`, waits for the ack
29
+ * (id-matched), then streams. Rejects with HerdrError on an error envelope (e.g. invalid_params for a per-pane type
30
+ * without pane_id, W6), HerdrTransportError on connect/ack timeout.
31
+ */
32
+ export type Subscribe = (specs: readonly Subscription[], options?: SubscribeOptions) => Promise<EventSubscription>;
33
+ /** What the client hands the subscribe factory. */
34
+ export interface SubscribeDeps {
35
+ readonly socketPath: string;
36
+ /** The client's resolved timeout (ClientTransport.timeoutMs); the ack timeout default. */
37
+ readonly timeoutMs: number;
38
+ readonly onCall?: OnCall | undefined;
39
+ /** The client-level hook (ClientTransport.onUnknown); called before SubscribeOptions.onUnknown. */
40
+ readonly onUnknown?: ((frame: UnknownFrame) => void) | undefined;
41
+ /** The client close signal (ClientTransport.closed): when it aborts, every subscription of this client closes. */
42
+ readonly signal: AbortSignal;
43
+ /** Injected so tests can stub the connection (default wire/dedicated.ts openDedicated). */
44
+ readonly open: OpenDedicated;
45
+ }
46
+ /** Builds the subscribe function of one client. Implemented by client/subscribe/subscribe.ts (P4a). */
47
+ export type CreateSubscribe = (deps: SubscribeDeps) => Subscribe;
48
+ export declare const SubscribeText: {
49
+ readonly MalformedPush: 'events.subscribe: malformed push line';
50
+ readonly QueueFull: (max: number) => string;
51
+ };
52
+ export interface CombinedSignal {
53
+ readonly signal: AbortSignal;
54
+ readonly dispose: () => void;
55
+ }
56
+ export declare function anySignal(...sources: (AbortSignal | undefined)[]): CombinedSignal;
57
+ export type UnknownHook = (frame: UnknownFrame) => void;
58
+ export declare function deliverUnknown(frame: UnknownFrame, hooks: readonly (UnknownHook | undefined)[]): void;
59
+ export declare class FrameQueue<T> {
60
+ private readonly items;
61
+ private readonly waiters;
62
+ private readonly max;
63
+ private readonly overflow;
64
+ private readonly onFail;
65
+ private ended;
66
+ private failure;
67
+ constructor(options: QueueOptions | undefined, onFail?: () => void);
68
+ push(value: T): void;
69
+ next(): Promise<IteratorResult<T>>;
70
+ end(): void;
71
+ fail(reason: unknown): void;
72
+ private isClosed;
73
+ }
74
+ export declare function subscriptionOver(queue: FrameQueue<DecodedFrame>, close: () => void): EventSubscription;
75
+ export declare const createSubscribe: CreateSubscribe;
@@ -0,0 +1,208 @@
1
+ // Role: subscriptions: one dedicated connection per subscribe, its frame queue, close signals and unknown-frame delivery.
2
+ // Informs HERDR-ADR-0017, HERDR-ADR-0007, HERDR-ADR-0016, plan 3.6; merged by HERDR-ADR-0021 from types.ts, constants.ts, close-signals.ts, deliver-unknown.ts, frame-queue.ts, subscription.ts, subscribe.ts.
3
+ import { Overflow, UNKNOWN_EVENT } from '../../constants.js';
4
+ import { HerdrTransportError, SubscriptionEnded } from '../errors.js';
5
+ import { MethodEventsSubscribe } from '../generated/names/method.js';
6
+ import { decodePush } from '../wire/push-decoder.js';
7
+ export const SubscribeText = {
8
+ MalformedPush: 'events.subscribe: malformed push line',
9
+ QueueFull: (max) => `subscription queue is full (max ${max})`,
10
+ };
11
+ export function anySignal(...sources) {
12
+ const controller = new AbortController();
13
+ const attached = sources.filter(isAttached);
14
+ const onAbort = () => {
15
+ dispose();
16
+ controller.abort();
17
+ };
18
+ const dispose = () => {
19
+ for (const source of attached) {
20
+ source.removeEventListener('abort', onAbort);
21
+ }
22
+ };
23
+ for (const source of attached) {
24
+ if (source.aborted) {
25
+ onAbort();
26
+ break;
27
+ }
28
+ source.addEventListener('abort', onAbort);
29
+ }
30
+ return { signal: controller.signal, dispose };
31
+ }
32
+ function isAttached(source) {
33
+ return source !== undefined;
34
+ }
35
+ export function deliverUnknown(frame, hooks) {
36
+ for (const hook of hooks) {
37
+ runHook(hook, frame);
38
+ }
39
+ }
40
+ // Best effort, like reportCall: the observer's failure never reaches the consumer.
41
+ function runHook(hook, frame) {
42
+ if (hook === undefined) {
43
+ return;
44
+ }
45
+ try {
46
+ hook(frame);
47
+ }
48
+ catch {
49
+ // The hook is an observer; its exception is not the subscription's error.
50
+ }
51
+ }
52
+ const END_OF_ITERATION = { done: true, value: undefined };
53
+ const NO_FAIL_HOOK = () => undefined;
54
+ export class FrameQueue {
55
+ items = [];
56
+ waiters = [];
57
+ max;
58
+ overflow;
59
+ onFail;
60
+ ended = false;
61
+ failure = undefined;
62
+ // onFail runs once, when the queue fails, so the owner can release what feeds it (the connection).
63
+ constructor(options, onFail = NO_FAIL_HOOK) {
64
+ this.max = options?.max;
65
+ this.overflow = options?.overflow ?? Overflow.DropOldest;
66
+ this.onFail = onFail;
67
+ }
68
+ // Hands the frame to a waiting next(), or queues it; a full queue fails (Fail) or drops its oldest frame (DropOldest).
69
+ push(value) {
70
+ if (this.isClosed()) {
71
+ return;
72
+ }
73
+ const waiter = this.waiters.shift();
74
+ if (waiter !== undefined) {
75
+ waiter.resolve({ value, done: false });
76
+ return;
77
+ }
78
+ const max = this.max;
79
+ if (max !== undefined && this.items.length >= max) {
80
+ if (this.overflow === Overflow.Fail) {
81
+ this.fail(new HerdrTransportError(SubscribeText.QueueFull(max)));
82
+ return;
83
+ }
84
+ this.items.shift();
85
+ }
86
+ this.items.push({ value });
87
+ }
88
+ // The next queued frame; queued frames are read before an end or a failure is seen.
89
+ next() {
90
+ const entry = this.items.shift();
91
+ if (entry !== undefined) {
92
+ return Promise.resolve({ value: entry.value, done: false });
93
+ }
94
+ if (this.failure !== undefined) {
95
+ return Promise.reject(this.failure.reason);
96
+ }
97
+ if (this.ended) {
98
+ return Promise.resolve(END_OF_ITERATION);
99
+ }
100
+ return new Promise((resolve, reject) => {
101
+ this.waiters.push({ resolve, reject });
102
+ });
103
+ }
104
+ // No more frames: the iteration finishes normally once the queued frames are read.
105
+ end() {
106
+ if (this.isClosed()) {
107
+ return;
108
+ }
109
+ this.ended = true;
110
+ for (const waiter of this.waiters.splice(0)) {
111
+ waiter.resolve(END_OF_ITERATION);
112
+ }
113
+ }
114
+ // The iteration throws `reason` once the queued frames are read; the owner is told once so it can release the feed.
115
+ fail(reason) {
116
+ if (this.isClosed()) {
117
+ return;
118
+ }
119
+ this.failure = { reason };
120
+ for (const waiter of this.waiters.splice(0)) {
121
+ waiter.reject(reason);
122
+ }
123
+ this.onFail();
124
+ }
125
+ isClosed() {
126
+ return this.ended || this.failure !== undefined;
127
+ }
128
+ }
129
+ export function subscriptionOver(queue, close) {
130
+ const iterator = {
131
+ next: () => queue.next(),
132
+ return: async () => {
133
+ close();
134
+ return { done: true, value: undefined };
135
+ },
136
+ };
137
+ return {
138
+ close,
139
+ [Symbol.asyncIterator]: () => iterator,
140
+ };
141
+ }
142
+ export const createSubscribe = function createSubscribe(deps) {
143
+ return function subscribe(specs, options = {}) {
144
+ return openSubscription(deps, specs, options);
145
+ };
146
+ };
147
+ async function openSubscription(deps, specs, options) {
148
+ const scope = anySignal(deps.signal, options.signal);
149
+ const connection = await openStream(deps, scope);
150
+ const queue = new FrameQueue(options.queue, () => {
151
+ connection.destroy();
152
+ });
153
+ const stream = { queue, hooks: [deps.onUnknown, options.onUnknown] };
154
+ connection.onPush((line) => {
155
+ routePush(line, stream);
156
+ });
157
+ void connection.ended.then((end) => {
158
+ settleEnd(end, queue, scope);
159
+ });
160
+ try {
161
+ await connection.request(MethodEventsSubscribe, { subscriptions: [...specs] }, { timeoutMs: options.ackTimeoutMs ?? deps.timeoutMs });
162
+ }
163
+ catch (error) {
164
+ connection.destroy();
165
+ throw error;
166
+ }
167
+ return subscriptionOver(queue, () => {
168
+ queue.end();
169
+ connection.destroy();
170
+ });
171
+ }
172
+ async function openStream(deps, scope) {
173
+ try {
174
+ return await deps.open({ socketPath: deps.socketPath, signal: scope.signal, onCall: deps.onCall });
175
+ }
176
+ catch (error) {
177
+ scope.dispose();
178
+ throw error;
179
+ }
180
+ }
181
+ // A frame is decoded, offered to the unknown hooks when it is unknown, then queued. A line that is not a push fails the queue, which releases the connection.
182
+ function routePush(line, stream) {
183
+ const frame = decodePush(line);
184
+ if (frame === undefined) {
185
+ stream.queue.fail(new HerdrTransportError(SubscribeText.MalformedPush));
186
+ return;
187
+ }
188
+ if (isUnknownFrame(frame)) {
189
+ deliverUnknown(frame, stream.hooks);
190
+ }
191
+ stream.queue.push(frame);
192
+ }
193
+ // The connection ended: a server close is an error for the caller, a cause is that error, anything else (close, abort, client close) is a normal end.
194
+ function settleEnd(end, queue, scope) {
195
+ scope.dispose();
196
+ if (end.cause !== undefined) {
197
+ queue.fail(end.cause);
198
+ return;
199
+ }
200
+ if (end.byServer) {
201
+ queue.fail(new SubscriptionEnded(MethodEventsSubscribe));
202
+ return;
203
+ }
204
+ queue.end();
205
+ }
206
+ function isUnknownFrame(frame) {
207
+ return frame.event === UNKNOWN_EVENT;
208
+ }