@stateforge-labs/herdr 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,436 @@
1
+ // Role: what the fake herdr says: answers, acks, pongs, param checks, held and subscription bookkeeping and the scripted steps.
2
+ // Informs HERDR-ADR-0016, plan 5, HERDR-ADR-0008; merged by HERDR-ADR-0021 from fake-values.ts, fake-write.ts, fake-socket-path.ts, fake-pong.ts, fake-defaults.ts, fake-ack.ts, fake-request-check.ts, fake-script.ts, fake-answer.ts, fake-held.ts, fake-subscription.ts, fake-subscribers.ts.
3
+ import { TypeofTag, LINE_SEPARATOR, TEXT_ENCODING, Fake, DEFAULT_CAPABILITIES, WireField, EMPTY_SCHEMA_RESULT, FakeMethod, FakeCheckText, FakeErrorCode, TypeofValue, AnswerKind, FakeText, EntryKey, NameSeparator, PER_PANE_SUBSCRIPTION_TYPES, SpecField } from '../../constants.js';
4
+ import { isObject } from '../wire/codec.js';
5
+ import { mkdtempSync, rmSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { ResultTag, Method, ENTRY_METHODS, EventKind, SubscriptionType } from '../generated/index.js';
9
+ import { JsonKind, PARAMS_SHAPE } from '../generated/test-data/shapes.js';
10
+ // The field of a JSON object, or undefined when the value is not an object. JSON has no undefined, so undefined means absent.
11
+ export function fieldOf(value, name) {
12
+ return isObject(value) ? Reflect.get(value, name) : undefined;
13
+ }
14
+ // True for a string value.
15
+ export function isText(value) {
16
+ return typeof value === TypeofTag.String;
17
+ }
18
+ // One NDJSON line. A string is written as it is (a stray line), an object is serialised.
19
+ export function lineOf(value) {
20
+ const text = typeof value === TypeofTag.String ? value : JSON.stringify(value);
21
+ return `${text}${LINE_SEPARATOR}`;
22
+ }
23
+ // A client may have destroyed its socket; a write to it must not crash the fake.
24
+ export function safeWrite(socket, text) {
25
+ try {
26
+ socket.write(text, TEXT_ENCODING);
27
+ }
28
+ catch {
29
+ // The client already went away; there is nobody left to write to.
30
+ }
31
+ }
32
+ // Ends the socket from the server side; a socket that is already gone ignores it.
33
+ export function endSocket(socket) {
34
+ try {
35
+ socket.end();
36
+ }
37
+ catch {
38
+ // The client already went away; there is nothing to end.
39
+ }
40
+ }
41
+ export function fakeSocketLocation(socketPath) {
42
+ if (socketPath !== undefined) {
43
+ return { path: socketPath, ownedDir: undefined };
44
+ }
45
+ const dir = mkdtempSync(join(tmpdir(), Fake.DirPrefix));
46
+ return { path: join(dir, Fake.SocketFile), ownedDir: dir };
47
+ }
48
+ export function removeOwnedDir(location) {
49
+ if (location.ownedDir !== undefined) {
50
+ rmSync(location.ownedDir, { recursive: true, force: true });
51
+ }
52
+ }
53
+ // The capabilities option as the pong uses it: undefined means the default set.
54
+ export function advertisedCapabilities(option) {
55
+ return option === undefined ? DEFAULT_CAPABILITIES : option;
56
+ }
57
+ export function pongBody(settings) {
58
+ const body = {
59
+ [WireField.Type]: ResultTag.Pong,
60
+ [WireField.Protocol]: settings.protocol,
61
+ [WireField.Version]: settings.version,
62
+ };
63
+ if (settings.capabilities !== null) {
64
+ body[WireField.Capabilities] = settings.capabilities;
65
+ }
66
+ return body;
67
+ }
68
+ // The default result of a method, or undefined when the fake has no default for it.
69
+ export function defaultResultOf(method, pong) {
70
+ if (method === Method.Ping) {
71
+ return pongBody(pong);
72
+ }
73
+ if (method === FakeMethod.ApiSchema) {
74
+ return EMPTY_SCHEMA_RESULT;
75
+ }
76
+ return undefined;
77
+ }
78
+ export class AckSwitches {
79
+ splitNext = false;
80
+ strayNext;
81
+ // The next ack is written in two chunks.
82
+ splitNextAck() {
83
+ this.splitNext = true;
84
+ }
85
+ // The next ack is preceded by this line.
86
+ strayBeforeAck(line) {
87
+ this.strayNext = line;
88
+ }
89
+ // Writes the ack of the subscription with this request id, applying each switch once.
90
+ acknowledge(socket, id) {
91
+ const ack = lineOf({
92
+ [WireField.Id]: id,
93
+ [WireField.Result]: { [WireField.Type]: ResultTag.SubscriptionStarted },
94
+ });
95
+ const text = `${this.takeStray()}${ack}`;
96
+ if (this.takeSplit()) {
97
+ writeSplit(socket, text);
98
+ }
99
+ else {
100
+ safeWrite(socket, text);
101
+ }
102
+ }
103
+ takeStray() {
104
+ const line = this.strayNext;
105
+ this.strayNext = undefined;
106
+ return line === undefined ? '' : lineOf(line);
107
+ }
108
+ takeSplit() {
109
+ const split = this.splitNext;
110
+ this.splitNext = false;
111
+ return split;
112
+ }
113
+ }
114
+ // Writes the first half now and the rest after a short delay, so the client reads a partial line first.
115
+ function writeSplit(socket, text) {
116
+ const cut = Math.floor(text.length / 2);
117
+ safeWrite(socket, text.slice(0, cut));
118
+ setTimeout(() => {
119
+ safeWrite(socket, text.slice(cut));
120
+ }, Fake.SplitAckDelayMs);
121
+ }
122
+ // The shape of a method, or undefined when the generated table does not list it (then nothing is checked).
123
+ function shapeOf(method) {
124
+ const shapes = PARAMS_SHAPE;
125
+ return Object.hasOwn(shapes, method) ? shapes[method] : undefined;
126
+ }
127
+ // The first problem with the params, as the error herdr would return; undefined when the params fit the shape.
128
+ // Absent params count as an empty object.
129
+ export function checkParams(method, params) {
130
+ const shape = shapeOf(method);
131
+ if (shape === undefined) {
132
+ return undefined;
133
+ }
134
+ const value = params === undefined ? {} : params;
135
+ if (!isObject(value)) {
136
+ return Object.keys(shape.fields).length === 0 ? undefined : problem(FakeCheckText.NotAnObject(method));
137
+ }
138
+ for (const name of shape.required) {
139
+ if (fieldOf(value, name) === undefined) {
140
+ return problem(FakeCheckText.MissingField(method, name));
141
+ }
142
+ }
143
+ for (const [name, field] of Object.entries(shape.fields)) {
144
+ const present = fieldOf(value, name);
145
+ if (present !== undefined) {
146
+ const fieldProblem = checkField(method, name, field, present);
147
+ if (fieldProblem !== undefined) {
148
+ return fieldProblem;
149
+ }
150
+ }
151
+ }
152
+ return undefined;
153
+ }
154
+ function checkField(method, name, field, value) {
155
+ const kind = jsonKindOf(value);
156
+ if (!field.kinds.some((allowed) => kindFits(kind, allowed))) {
157
+ return problem(FakeCheckText.WrongKind(method, name));
158
+ }
159
+ if (field.enum !== undefined && isText(value) && !field.enum.includes(value)) {
160
+ return problem(FakeCheckText.NotInEnum(method, name));
161
+ }
162
+ if (field.keyPattern !== undefined && isObject(value)) {
163
+ const pattern = new RegExp(field.keyPattern);
164
+ if (Object.keys(value).some((key) => !pattern.test(key))) {
165
+ return problem(FakeCheckText.KeyNotAllowed(method, name));
166
+ }
167
+ }
168
+ return undefined;
169
+ }
170
+ // An integer is also a number (JSON Schema): a field that allows number takes 3.
171
+ function kindFits(actual, allowed) {
172
+ return actual === allowed || (allowed === JsonKind.Number && actual === JsonKind.Integer);
173
+ }
174
+ // The JSON kind of a parsed value.
175
+ export function jsonKindOf(value) {
176
+ if (value === null) {
177
+ return JsonKind.Null;
178
+ }
179
+ if (Array.isArray(value)) {
180
+ return JsonKind.Array;
181
+ }
182
+ if (isText(value)) {
183
+ return JsonKind.String;
184
+ }
185
+ if (typeof value === TypeofValue.Number) {
186
+ return Number.isInteger(value) ? JsonKind.Integer : JsonKind.Number;
187
+ }
188
+ if (typeof value === TypeofValue.Boolean) {
189
+ return JsonKind.Boolean;
190
+ }
191
+ return JsonKind.Object;
192
+ }
193
+ function problem(message) {
194
+ return { code: FakeErrorCode.InvalidParams, message };
195
+ }
196
+ export class FakeScript {
197
+ replies = new Map();
198
+ failures = new Map();
199
+ rawLines = new Map();
200
+ silences = new Set();
201
+ sequences = new Map();
202
+ received = [];
203
+ setReply(method, result) {
204
+ this.replies.set(method, result);
205
+ }
206
+ setFailure(method, error) {
207
+ this.failures.set(method, error);
208
+ }
209
+ setRawLine(method, line) {
210
+ this.rawLines.set(method, line);
211
+ }
212
+ setSilent(method) {
213
+ this.silences.add(method);
214
+ }
215
+ // Stores a copy, so a later change to the caller's array does not change the script.
216
+ setSequence(method, steps) {
217
+ this.sequences.set(method, [...steps]);
218
+ }
219
+ // The next step of the sequence, consumed; undefined when the method has no steps left.
220
+ takeStep(method) {
221
+ return this.sequences.get(method)?.shift();
222
+ }
223
+ record(request) {
224
+ this.received.push(request);
225
+ }
226
+ methods() {
227
+ return this.received.map((request) => request.method);
228
+ }
229
+ requests() {
230
+ return [...this.received];
231
+ }
232
+ // True when a test scripted this method to be silent, raw, refused or with steps left; such a method is not acknowledged as a subscription.
233
+ // A reply alone does not count: events.subscribe is answered by its ack (F2) unless a test scripts one of the outcomes above.
234
+ hasScriptedOutcome(method) {
235
+ return this.silences.has(method) || this.rawLines.has(method) || this.failures.has(method) || (this.sequences.get(method)?.length ?? 0) > 0;
236
+ }
237
+ isSilent(method) {
238
+ return this.silences.has(method);
239
+ }
240
+ rawLineOf(method) {
241
+ return this.rawLines.get(method);
242
+ }
243
+ failureOf(method) {
244
+ return this.failures.get(method);
245
+ }
246
+ hasReply(method) {
247
+ return this.replies.has(method);
248
+ }
249
+ replyOf(method) {
250
+ return this.replies.get(method);
251
+ }
252
+ }
253
+ const SILENT = { kind: AnswerKind.Silent };
254
+ export function answerFor(script, request, pong) {
255
+ const { id, method } = request;
256
+ if (script.isSilent(method)) {
257
+ return SILENT;
258
+ }
259
+ const rawLine = script.rawLineOf(method);
260
+ if (rawLine !== undefined) {
261
+ return { kind: AnswerKind.Raw, line: rawLine };
262
+ }
263
+ const refusal = checkParams(method, request.params);
264
+ if (refusal !== undefined) {
265
+ return errorAnswer(id, refusal);
266
+ }
267
+ const step = script.takeStep(method);
268
+ if (step !== undefined) {
269
+ return stepAnswer(id, step);
270
+ }
271
+ const failure = script.failureOf(method);
272
+ if (failure !== undefined) {
273
+ return errorAnswer(id, failure);
274
+ }
275
+ if (script.hasReply(method)) {
276
+ return resultAnswer(id, script.replyOf(method));
277
+ }
278
+ const fallback = defaultResultOf(method, pong);
279
+ if (fallback !== undefined) {
280
+ return resultAnswer(id, fallback);
281
+ }
282
+ return errorAnswer(id, { code: Fake.UnknownMethodCode, message: FakeText.UnknownMethod(method) });
283
+ }
284
+ // The answer to a line that is not a JSON request: herdr replies with the empty id and an error (W3).
285
+ export function invalidRequestAnswer() {
286
+ return errorAnswer('', { code: Fake.InvalidRequestCode, message: FakeText.InvalidRequest });
287
+ }
288
+ // An envelope with this error.
289
+ export function errorAnswer(id, error) {
290
+ return { kind: AnswerKind.Envelope, body: { [WireField.Id]: id, [WireField.Error]: error } };
291
+ }
292
+ function stepAnswer(id, step) {
293
+ return WireField.Error in step ? errorAnswer(id, step.error) : resultAnswer(id, step.result);
294
+ }
295
+ function resultAnswer(id, result) {
296
+ return { kind: AnswerKind.Envelope, body: { [WireField.Id]: id, [WireField.Result]: result } };
297
+ }
298
+ // True for a class C method: the connection-bound ones herdr keeps open after the answer.
299
+ export function isHeldMethod(method) {
300
+ const heldMethods = ENTRY_METHODS[EntryKey.held];
301
+ return heldMethods.includes(method);
302
+ }
303
+ export class HeldConnections {
304
+ sockets = new Set();
305
+ add(socket) {
306
+ this.sockets.add(socket);
307
+ }
308
+ // A connection that closed leaves the set.
309
+ forget(socket) {
310
+ this.sockets.delete(socket);
311
+ }
312
+ count() {
313
+ return this.sockets.size;
314
+ }
315
+ // Ends every held connection from the server side; the client sees its connection end.
316
+ closeAll() {
317
+ for (const socket of this.sockets) {
318
+ endSocket(socket);
319
+ }
320
+ this.sockets.clear();
321
+ }
322
+ }
323
+ const SUBSCRIPTION_TYPES = new Set(Object.values(SubscriptionType));
324
+ const PER_PANE_TYPES = new Set(PER_PANE_SUBSCRIPTION_TYPES);
325
+ const EVENT_KINDS = new Set(Object.values(EventKind));
326
+ // The frame names that each subscription type answers to. A subscription type names itself (pane.output_matched); an event kind names it
327
+ // with its first separator changed (pane_created is pane.created). Built once, here, from the generated names: no other place maps names.
328
+ const TYPE_BY_FRAME_NAME = frameNameTable();
329
+ // Why herdr would refuse this events.subscribe: params that break the generated shape, a spec that is not a known type, or a per-pane spec without
330
+ // pane_id. Undefined when it is accepted.
331
+ export function subscribeRefusal(params) {
332
+ const shapeProblem = checkParams(Method.EventsSubscribe, params);
333
+ if (shapeProblem !== undefined) {
334
+ return shapeProblem;
335
+ }
336
+ if (entriesOf(params).some((entry) => !isSpecEntry(entry))) {
337
+ return { code: FakeErrorCode.InvalidParams, message: FakeCheckText.WrongKind(Method.EventsSubscribe, SpecField.Subscriptions) };
338
+ }
339
+ const withoutPane = specsOf(params).find((spec) => PER_PANE_TYPES.has(spec.type) && spec.paneId === undefined);
340
+ return withoutPane === undefined ? undefined : { code: FakeErrorCode.InvalidParams, message: FakeCheckText.NeedsPaneId(withoutPane.type) };
341
+ }
342
+ // The specs of an events.subscribe request, in order. Entries that are not a known spec type are skipped (subscribeRefusal refuses them first).
343
+ export function specsOf(params) {
344
+ return entriesOf(params).flatMap((entry) => {
345
+ const type = fieldOf(entry, WireField.Type);
346
+ if (!isSubscriptionType(type)) {
347
+ return [];
348
+ }
349
+ const paneId = fieldOf(entry, SpecField.PaneId);
350
+ return [{ type, paneId: isText(paneId) ? paneId : undefined }];
351
+ });
352
+ }
353
+ // Whether a pushed frame reaches the subscription with these specs. A frame whose name is a subscription type reaches a spec of that type, and a
354
+ // per-pane type reaches only the spec whose pane_id is the frame's. A known event kind with no subscription type (pane_output_changed) reaches nobody:
355
+ // herdr offers it only through events.wait. A kind the fake does not know at all reaches every subscription, on purpose: herdr may push kinds nobody asked for (P3).
356
+ export function reaches(specs, frame) {
357
+ const type = TYPE_BY_FRAME_NAME.get(frame.event);
358
+ if (type === undefined) {
359
+ return !EVENT_KINDS.has(frame.event);
360
+ }
361
+ const paneId = fieldOf(frame.data, SpecField.PaneId);
362
+ return specs.some((spec) => spec.type === type && (!PER_PANE_TYPES.has(type) || spec.paneId === paneId));
363
+ }
364
+ // The frame name table: each subscription type, and its event-kind spelling (first separator changed) when it differs.
365
+ function frameNameTable() {
366
+ const names = new Map();
367
+ for (const type of Object.values(SubscriptionType)) {
368
+ names.set(type, type);
369
+ names.set(type.replace(NameSeparator.Subscription, NameSeparator.Event), type);
370
+ }
371
+ return names;
372
+ }
373
+ function entriesOf(params) {
374
+ const entries = fieldOf(params, SpecField.Subscriptions);
375
+ return Array.isArray(entries) ? entries : [];
376
+ }
377
+ function isSubscriptionType(value) {
378
+ return isText(value) && SUBSCRIPTION_TYPES.has(value);
379
+ }
380
+ function isSpecEntry(entry) {
381
+ return isObject(entry) && isSubscriptionType(fieldOf(entry, WireField.Type));
382
+ }
383
+ export class Subscribers {
384
+ specsBySocket = new Map();
385
+ // Frames queued while pushes are held; undefined when pushes flow.
386
+ queued;
387
+ add(socket, specs) {
388
+ this.specsBySocket.set(socket, specs);
389
+ }
390
+ // A connection that closed leaves the set.
391
+ forget(socket) {
392
+ this.specsBySocket.delete(socket);
393
+ }
394
+ count() {
395
+ return this.specsBySocket.size;
396
+ }
397
+ // Writes the frame to every matching subscription, or queues it while pushes are held.
398
+ push(frame) {
399
+ if (this.queued !== undefined) {
400
+ this.queued.push(frame);
401
+ return;
402
+ }
403
+ this.deliver(frame);
404
+ }
405
+ // Queues pushes until the returned release runs; release writes the queued frames in order. A second release does nothing.
406
+ holdPushes() {
407
+ const queue = [];
408
+ this.queued = queue;
409
+ let released = false;
410
+ return () => {
411
+ if (released) {
412
+ return;
413
+ }
414
+ released = true;
415
+ this.queued = undefined;
416
+ for (const frame of queue) {
417
+ this.deliver(frame);
418
+ }
419
+ };
420
+ }
421
+ // Ends every subscription connection from the server side; the client sees the subscription end.
422
+ closeAll() {
423
+ for (const socket of this.specsBySocket.keys()) {
424
+ endSocket(socket);
425
+ }
426
+ this.specsBySocket.clear();
427
+ }
428
+ deliver(frame) {
429
+ const line = lineOf(frame);
430
+ for (const [socket, specs] of this.specsBySocket) {
431
+ if (reaches(specs, frame)) {
432
+ safeWrite(socket, line);
433
+ }
434
+ }
435
+ }
436
+ }
@@ -0,0 +1,88 @@
1
+ import type { WireFact } from '../../constants.js';
2
+ import type { ErrorBody, Method, PushFrame } from '../generated/index.js';
3
+ import type { ServerCapabilities } from '../generated/results.js';
4
+ import type { ResultOf } from '../wire/types.js';
5
+ export interface FakeHerdrOptions {
6
+ /** Protocol number the pong reports; default the library's PROTOCOL. */
7
+ readonly protocol?: number | undefined;
8
+ /** herdr version the pong reports (client.serverInfo.version); default Fake.Version. */
9
+ readonly version?: string | undefined;
10
+ /** Socket path to listen on; default a fresh temp directory the fake removes on stop. */
11
+ readonly socketPath?: string | undefined;
12
+ /** Capabilities the pong advertises. Undefined: DEFAULT_CAPABILITIES. Null: the pong omits the field. */
13
+ readonly capabilities?: ServerCapabilities | null | undefined;
14
+ }
15
+ /** One request line the fake received, parsed. */
16
+ export interface ReceivedRequest {
17
+ readonly id: string;
18
+ readonly method: string;
19
+ readonly params: unknown;
20
+ }
21
+ /** A scripted step of replySequence: a result, or an error envelope. */
22
+ export type FakeStep<M extends Method> = {
23
+ readonly result: ResultOf<M>;
24
+ } | {
25
+ readonly error: ErrorBody;
26
+ };
27
+ /** A push line with an `event` the library does not know (HERDR-ADR-0016 P3). */
28
+ export interface UnknownPush {
29
+ readonly event: string;
30
+ readonly data: unknown;
31
+ }
32
+ export interface FakeHerdr {
33
+ /** Socket path to pass as connect({ socketPath }). */
34
+ readonly path: string;
35
+ /** Capabilities the pong advertises (after the option's default). */
36
+ readonly capabilities: ServerCapabilities | null;
37
+ /** Measured herdr wire facts this fake reproduces (W1-W6), each covered by a test in test/test-kit. */
38
+ readonly reproduces: readonly WireFact[];
39
+ /**
40
+ * Answers every later call of `method` with a result envelope. For events.subscribe the ack is the answer (F2), so a reply alone is not used there;
41
+ * use fail, silent, rawReply or replySequence to change a subscribe.
42
+ */
43
+ reply<M extends Method>(method: M, result: ResultOf<M>): void;
44
+ /** Answers every later call of `method` with an error envelope. */
45
+ fail(method: Method, error: ErrorBody): void;
46
+ /**
47
+ * Accepts `method` and never answers; the connection stays open until the client closes it.
48
+ * Ack-timeout recipe: `silent(Method.EventsSubscribe)` then subscribe with a short `ackTimeoutMs`.
49
+ */
50
+ silent(method: Method): void;
51
+ /** Writes `line` verbatim for `method`, then ends the connection. */
52
+ rawReply(method: Method, line: string): void;
53
+ /** Every request method received, in order, ping included. */
54
+ receivedMethods(): readonly string[];
55
+ /** Connections open right now. */
56
+ connectionCount(): number;
57
+ /** Closes the server, destroys open connections, removes the temp directory it made. */
58
+ stop(): Promise<void>;
59
+ /** Answers successive calls of `method` with these steps, then falls back to reply/default. */
60
+ replySequence<M extends Method>(method: M, steps: readonly FakeStep<M>[]): void;
61
+ /** Every request received, parsed, in order. */
62
+ requests(): readonly ReceivedRequest[];
63
+ /**
64
+ * Writes one push line to every acked subscription whose specs match it: a frame goes to a subscription that
65
+ * subscribed its type, and a per-pane frame only to the one whose spec has the frame's pane_id (W6). An UnknownPush
66
+ * goes to every acked subscription (herdr may send kinds nobody asked for; P3).
67
+ */
68
+ push(frame: PushFrame | UnknownPush): void;
69
+ /** Closes every subscription connection (the client sees SubscriptionEnded). */
70
+ closeSubscribers(): void;
71
+ /** Subscriptions that are acked and open right now. */
72
+ subscriberCount(): number;
73
+ /** Queues pushes until the returned release function runs (to test frames between ack and snapshot). */
74
+ holdPushes(): () => void;
75
+ /** The next subscribe ack is written in two chunks (framing test, W2). */
76
+ splitAck(): void;
77
+ /** The next subscribe ack is preceded by `line`, a line with another id (stray-line test, W2). */
78
+ strayLine(line: string): void;
79
+ /**
80
+ * Connections that sent a class C (ENTRY_METHODS.held) request and are still open: the fake keeps such a
81
+ * connection open after the reply, as hold() assumes (contracts.md open issue 1).
82
+ */
83
+ heldCount(): number;
84
+ /** Closes every held connection from the server side (the client's HeldConnection.ended resolves). */
85
+ closeHeld(): void;
86
+ }
87
+ /** createFakeHerdr(options). Implemented by test-kit/fake-herdr.ts (P2a minimum, P5a full). */
88
+ export type CreateFakeHerdr = (options?: FakeHerdrOptions) => Promise<FakeHerdr>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import type { METHOD_RESULT, Method, MethodParams, ResponseResult, ResultByTag, ErrorBody } from '../generated/index.js';
2
+ import { ProtocolPolicy, CallOutcome } from '../../constants.js';
3
+ import type { CallEvent, OnCall } from './types.js';
4
+ export type { CallEvent, OnCall } from './types.js';
5
+ export type ParamsOf<M extends Method> = MethodParams[M];
6
+ export type ResultOf<M extends Method> = M extends keyof typeof METHOD_RESULT ? ResultByTag[(typeof METHOD_RESULT)[M]] : ResponseResult;
7
+ export declare function nextId(prefix?: string): string;
8
+ export interface RequestEnvelope<M extends string = string, P = unknown> {
9
+ id: string;
10
+ method: M;
11
+ params: P;
12
+ }
13
+ export interface SuccessEnvelope<R = ResponseResult> {
14
+ id: string;
15
+ result: R;
16
+ }
17
+ export interface ErrorEnvelope {
18
+ id: string;
19
+ error: ErrorBody;
20
+ }
21
+ export type ResponseEnvelope<R = ResponseResult> = SuccessEnvelope<R> | ErrorEnvelope;
22
+ export declare function isErrorEnvelope(envelope: object): envelope is ErrorEnvelope;
23
+ export declare function isObject(value: unknown): value is object;
24
+ export declare function encodeRequest(id: string, method: string, params: unknown): string;
25
+ export declare class LineSplitter {
26
+ private buffer;
27
+ push(chunk: string): string[];
28
+ get pending(): string;
29
+ }
30
+ export declare function isSupportedProtocol(actual: number, policy?: ProtocolPolicy, expected?: number): boolean;
31
+ export interface CallClassification {
32
+ readonly outcome: CallOutcome;
33
+ readonly code?: string;
34
+ }
35
+ export declare function outcomeOf(error: unknown): CallClassification;
36
+ export declare function reportCall(onCall: OnCall | undefined, event: CallEvent): void;
@@ -0,0 +1,62 @@
1
+ // Role: the wire codec: request ids, the JSON-RPC envelopes, the NDJSON line splitter, the protocol policy, the typed params/result helpers and the per-call hook.
2
+ // Informs HERDR-ADR-0017, HERDR-ADR-0002, plan 3.2, HERDR-ADR-0016, plan 3.1, plan 3, HERDR-ADR-0009, HERDR-ADR-0004, plan 3.3; merged by HERDR-ADR-0021 from typed.ts, id.ts, envelope.ts, line-splitter.ts, protocol-policy.ts, call-hook.ts.
3
+ import { PROTOCOL } from '../generated/index.js';
4
+ import { REQUEST_ID_PREFIX, TypeofTag, WireField, LINE_SEPARATOR, ProtocolPolicy, CallOutcome } from '../../constants.js';
5
+ import { HerdrError } from '../errors.js';
6
+ const FIRST_COUNT = 1;
7
+ let counter = 0;
8
+ export function nextId(prefix = REQUEST_ID_PREFIX) {
9
+ counter += FIRST_COUNT;
10
+ return `${prefix}-${counter}-${Date.now().toString(36)}`;
11
+ }
12
+ export function isErrorEnvelope(envelope) {
13
+ return WireField.Error in envelope;
14
+ }
15
+ // A JSON object envelope: an object that is neither null nor an array.
16
+ export function isObject(value) {
17
+ return typeof value === TypeofTag.Object && value !== null && !Array.isArray(value);
18
+ }
19
+ // One request as a JSON line, without the trailing newline (the connection adds it).
20
+ export function encodeRequest(id, method, params) {
21
+ return JSON.stringify({ [WireField.Id]: id, [WireField.Method]: method, [WireField.Params]: params });
22
+ }
23
+ export class LineSplitter {
24
+ buffer = '';
25
+ // Feeds one chunk and returns every complete, non-empty line it finished.
26
+ push(chunk) {
27
+ const parts = (this.buffer + chunk).split(LINE_SEPARATOR);
28
+ this.buffer = parts.pop() ?? '';
29
+ return parts.filter((line) => line.length > 0);
30
+ }
31
+ // Text received after the last newline, not yet a line.
32
+ get pending() {
33
+ return this.buffer;
34
+ }
35
+ }
36
+ export function isSupportedProtocol(actual, policy = ProtocolPolicy.Exact, expected = PROTOCOL) {
37
+ switch (policy) {
38
+ case ProtocolPolicy.Exact:
39
+ return actual === expected;
40
+ case ProtocolPolicy.Minimum:
41
+ return actual >= expected;
42
+ }
43
+ }
44
+ // A server refusal is Refused with its code; everything else (timeout, abort, socket, malformed line) is Transport.
45
+ export function outcomeOf(error) {
46
+ if (error instanceof HerdrError) {
47
+ return { outcome: CallOutcome.Refused, code: error.code };
48
+ }
49
+ return { outcome: CallOutcome.Transport };
50
+ }
51
+ // Runs the hook; a hook that throws changes nothing about the call.
52
+ export function reportCall(onCall, event) {
53
+ if (onCall === undefined) {
54
+ return;
55
+ }
56
+ try {
57
+ onCall(event);
58
+ }
59
+ catch {
60
+ // The observer is best effort: the caller's result or error is never replaced by a hook failure.
61
+ }
62
+ }
@@ -0,0 +1,13 @@
1
+ export interface ConnectionHandlers {
2
+ onLine(line: string): void;
3
+ onClose(): void;
4
+ onError(error: unknown): void;
5
+ }
6
+ export declare class Connection {
7
+ private readonly socket;
8
+ private constructor();
9
+ static open(path: string, handlers: ConnectionHandlers): Promise<Connection>;
10
+ writeLine(line: string): void;
11
+ close(): void;
12
+ destroy(): void;
13
+ }