jinaga-server 3.7.4 → 3.8.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 (39) hide show
  1. package/.claude/skills/degrees-of-freedom/.openskills.json +7 -0
  2. package/.claude/skills/degrees-of-freedom/SKILL.md +18 -0
  3. package/.claude/skills/degrees-of-freedom/degrees-of-freedom-constitution.md +142 -0
  4. package/.claude/skills/night-shift-setup/.openskills.json +7 -0
  5. package/.claude/skills/night-shift-setup/SKILL.md +95 -0
  6. package/.claude/skills/night-shift-setup/configuring-a-repository.md +111 -0
  7. package/.claude/skills/night-shift-setup/creating-the-log.md +59 -0
  8. package/.claude/skills/night-shift-setup/creating-the-routines.md +80 -0
  9. package/.claude/skills/night-shift-setup/log-model.factual +971 -0
  10. package/.claude/skills/night-shift-worker/.openskills.json +7 -0
  11. package/.claude/skills/night-shift-worker/SKILL.md +199 -0
  12. package/.claude/skills/night-shift-worker/night-shift-log.md +72 -0
  13. package/.claude/skills/night-shift-worker/scripts/register-stack.sh +173 -0
  14. package/.claude/skills/night-shift-worker/stacking.md +57 -0
  15. package/.claude/skills/night-shift-worker/verifying-a-merged-fix.md +16 -0
  16. package/.claude/skills/refining-issues/.openskills.json +7 -0
  17. package/.claude/skills/refining-issues/SKILL.md +327 -0
  18. package/.claude/skills/refining-issues/references/worked-example.md +234 -0
  19. package/.github/workflows/main.yml +2 -2
  20. package/.night-shift/config.md +84 -0
  21. package/.night-shift/skills.md +17 -0
  22. package/README.md +21 -0
  23. package/dist/http/router.js +207 -80
  24. package/dist/http/router.js.map +1 -1
  25. package/dist/http/stream.d.ts +0 -1
  26. package/dist/http/stream.js +6 -17
  27. package/dist/http/stream.js.map +1 -1
  28. package/dist/jinaga-server.js +74 -7
  29. package/dist/jinaga-server.js.map +1 -1
  30. package/integration-test/jinaga-test/package-lock.json +4 -4
  31. package/integration-test/jinaga-test/package.json +1 -1
  32. package/package.json +2 -2
  33. package/src/http/router.ts +249 -99
  34. package/src/http/stream.ts +21 -36
  35. package/src/jinaga-server.ts +92 -4
  36. package/test/http/errorStatusMappingSpec.ts +351 -0
  37. package/test/http/streamLogVolumeSpec.ts +284 -0
  38. package/test/jinaga-server/poolConfigurationSpec.ts +147 -0
  39. package/test/jinaga-server/tracePoolSpec.ts +368 -0
@@ -0,0 +1,368 @@
1
+ import { EventEmitter } from "events";
2
+ import { Trace, Tracer } from "jinaga";
3
+ import { Pool } from "pg";
4
+
5
+ import { tracePool } from "../../src/jinaga-server";
6
+
7
+ const windowMilliseconds = 10000;
8
+
9
+ // Jest's own fake timers cannot be installed on this version of Node, because
10
+ // they try to replace the read-only global `performance`. These stand in for
11
+ // the interval timer alone, which is all tracePool schedules, and they record
12
+ // whether it was unreferenced and whether it was cleared.
13
+ class FakeInterval {
14
+ public dueAt: number;
15
+ public cleared = false;
16
+ private referenced = true;
17
+
18
+ constructor(
19
+ private readonly callback: () => void,
20
+ public readonly delay: number,
21
+ now: number
22
+ ) {
23
+ this.dueAt = now + delay;
24
+ }
25
+
26
+ ref(): this {
27
+ this.referenced = true;
28
+ return this;
29
+ }
30
+
31
+ unref(): this {
32
+ this.referenced = false;
33
+ return this;
34
+ }
35
+
36
+ hasRef(): boolean {
37
+ return this.referenced;
38
+ }
39
+
40
+ fire() {
41
+ this.callback();
42
+ }
43
+ }
44
+
45
+ class FakeClock {
46
+ private now = 0;
47
+ private readonly created: FakeInterval[] = [];
48
+
49
+ install() {
50
+ jest.spyOn(global, 'setInterval').mockImplementation(((callback: () => void, delay?: number) => {
51
+ const interval = new FakeInterval(callback, delay ?? 0, this.now);
52
+ this.created.push(interval);
53
+ return interval;
54
+ }) as any);
55
+ jest.spyOn(global, 'clearInterval').mockImplementation(((handle: FakeInterval) => {
56
+ handle.cleared = true;
57
+ }) as any);
58
+ }
59
+
60
+ advance(milliseconds: number) {
61
+ const end = this.now + milliseconds;
62
+ for (;;) {
63
+ const due = this.live()
64
+ .filter(interval => interval.dueAt <= end)
65
+ .sort((a, b) => a.dueAt - b.dueAt)[0];
66
+ if (!due) {
67
+ break;
68
+ }
69
+ this.now = due.dueAt;
70
+ due.dueAt = this.now + due.delay;
71
+ due.fire();
72
+ }
73
+ this.now = end;
74
+ }
75
+
76
+ live(): FakeInterval[] {
77
+ return this.created.filter(interval => !interval.cleared);
78
+ }
79
+
80
+ only(): FakeInterval {
81
+ expect(this.created).toHaveLength(1);
82
+ return this.created[0];
83
+ }
84
+ }
85
+
86
+ interface Counts {
87
+ total: number;
88
+ idle: number;
89
+ waiting: number;
90
+ }
91
+
92
+ interface MetricLine {
93
+ message: string;
94
+ measurements: { [key: string]: number };
95
+ }
96
+
97
+ class RecordingTracer implements Tracer {
98
+ public readonly info_: string[] = [];
99
+ public readonly warnings: string[] = [];
100
+ public readonly errors: any[] = [];
101
+ public readonly metrics: MetricLine[] = [];
102
+
103
+ info(message: string): void {
104
+ this.info_.push(message);
105
+ }
106
+
107
+ warn(message: string): void {
108
+ this.warnings.push(message);
109
+ }
110
+
111
+ error(error: any): void {
112
+ this.errors.push(error);
113
+ }
114
+
115
+ dependency<T>(name: string, data: string, operation: () => Promise<T>): Promise<T> {
116
+ return operation();
117
+ }
118
+
119
+ metric(message: string, measurements: { [key: string]: number }): void {
120
+ this.metrics.push({ message, measurements });
121
+ }
122
+
123
+ counter(name: string, value: number): void {
124
+ }
125
+
126
+ linesNamed(message: string): MetricLine[] {
127
+ return this.metrics.filter(m => m.message === message);
128
+ }
129
+ }
130
+
131
+ // Stands in for a pg Pool. It reports the counts a test chooses and emits the
132
+ // events a real pool emits, so tracePool can be driven through a storm without
133
+ // a database.
134
+ class PoolDouble extends EventEmitter {
135
+ public totalCount = 0;
136
+ public idleCount = 0;
137
+ public waitingCount = 0;
138
+ public ended = false;
139
+
140
+ asPool(): Pool {
141
+ return this as unknown as Pool;
142
+ }
143
+
144
+ acquire(counts: Counts) {
145
+ this.report(counts);
146
+ this.emit('acquire', {});
147
+ }
148
+
149
+ release(counts: Counts) {
150
+ this.report(counts);
151
+ this.emit('release', undefined, {});
152
+ }
153
+
154
+ connect(counts: Counts) {
155
+ this.report(counts);
156
+ this.emit('connect', {});
157
+ }
158
+
159
+ remove(counts: Counts) {
160
+ this.report(counts);
161
+ this.emit('remove', {});
162
+ }
163
+
164
+ fail(error: Error) {
165
+ this.emit('error', error, {});
166
+ }
167
+
168
+ private report(counts: Counts) {
169
+ this.totalCount = counts.total;
170
+ this.idleCount = counts.idle;
171
+ this.waitingCount = counts.waiting;
172
+ }
173
+ }
174
+
175
+ describe('tracePool', () => {
176
+ let tracer: RecordingTracer;
177
+ let pool: PoolDouble;
178
+ let clock: FakeClock;
179
+
180
+ beforeEach(() => {
181
+ clock = new FakeClock();
182
+ clock.install();
183
+ tracer = new RecordingTracer();
184
+ Trace.configure(tracer);
185
+ pool = new PoolDouble();
186
+ });
187
+
188
+ afterEach(() => {
189
+ jest.restoreAllMocks();
190
+ Trace.off();
191
+ });
192
+
193
+ it('should write one line for a window of a thousand acquires', () => {
194
+ tracePool(pool.asPool());
195
+
196
+ for (let i = 0; i < 1000; i++) {
197
+ pool.acquire({ total: 10, idle: 0, waiting: 0 });
198
+ pool.release({ total: 10, idle: 1, waiting: 0 });
199
+ }
200
+
201
+ expect(tracer.metrics).toHaveLength(0);
202
+
203
+ clock.advance(windowMilliseconds);
204
+
205
+ expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
206
+ expect(tracer.metrics).toHaveLength(1);
207
+ });
208
+
209
+ it('should write no line per acquire', () => {
210
+ tracePool(pool.asPool());
211
+
212
+ pool.acquire({ total: 10, idle: 0, waiting: 0 });
213
+ pool.acquire({ total: 10, idle: 0, waiting: 1 });
214
+
215
+ expect(tracer.metrics).toHaveLength(0);
216
+ expect(tracer.linesNamed('Postgres acquired')).toHaveLength(0);
217
+
218
+ clock.advance(windowMilliseconds);
219
+
220
+ expect(tracer.linesNamed('Postgres acquired')).toHaveLength(0);
221
+ });
222
+
223
+ it('should carry the peak waiting count of a spike that came and went', () => {
224
+ tracePool(pool.asPool());
225
+
226
+ pool.acquire({ total: 10, idle: 0, waiting: 0 });
227
+ pool.acquire({ total: 10, idle: 0, waiting: 349 });
228
+ pool.release({ total: 10, idle: 0, waiting: 120 });
229
+ pool.release({ total: 10, idle: 3, waiting: 0 });
230
+
231
+ clock.advance(windowMilliseconds);
232
+
233
+ const lines = tracer.linesNamed('Postgres pool');
234
+ expect(lines).toHaveLength(1);
235
+ expect(lines[0].measurements).toEqual({
236
+ total: 10,
237
+ idle: 3,
238
+ waiting: 0,
239
+ peakWaiting: 349
240
+ });
241
+ });
242
+
243
+ it('should warn about a window whose peak waiting count was above zero', () => {
244
+ tracePool(pool.asPool());
245
+
246
+ pool.acquire({ total: 10, idle: 0, waiting: 349 });
247
+ pool.release({ total: 10, idle: 3, waiting: 0 });
248
+
249
+ clock.advance(windowMilliseconds);
250
+
251
+ expect(tracer.warnings).toHaveLength(1);
252
+ expect(tracer.warnings[0]).toContain('349');
253
+ });
254
+
255
+ it('should not warn about a window that never had a request waiting', () => {
256
+ tracePool(pool.asPool());
257
+
258
+ pool.acquire({ total: 10, idle: 2, waiting: 0 });
259
+
260
+ clock.advance(windowMilliseconds);
261
+
262
+ expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
263
+ expect(tracer.warnings).toHaveLength(0);
264
+ });
265
+
266
+ it('should write nothing for a window with no acquires', () => {
267
+ tracePool(pool.asPool());
268
+
269
+ clock.advance(windowMilliseconds * 3);
270
+
271
+ expect(tracer.metrics).toHaveLength(0);
272
+ expect(tracer.warnings).toHaveLength(0);
273
+ });
274
+
275
+ it('should write nothing for a window in which only a release happened', () => {
276
+ tracePool(pool.asPool());
277
+
278
+ pool.release({ total: 10, idle: 10, waiting: 0 });
279
+
280
+ clock.advance(windowMilliseconds);
281
+
282
+ expect(tracer.metrics).toHaveLength(0);
283
+ });
284
+
285
+ it('should report a storm that stopped in the middle of a window', () => {
286
+ tracePool(pool.asPool());
287
+
288
+ for (let i = 0; i < 200; i++) {
289
+ pool.acquire({ total: 10, idle: 0, waiting: 349 });
290
+ }
291
+ pool.release({ total: 10, idle: 0, waiting: 0 });
292
+
293
+ // The storm is over, and nothing touches the pool again. Only the
294
+ // window's own close can report it.
295
+ clock.advance(windowMilliseconds);
296
+
297
+ const lines = tracer.linesNamed('Postgres pool');
298
+ expect(lines).toHaveLength(1);
299
+ expect(lines[0].measurements.peakWaiting).toBe(349);
300
+
301
+ clock.advance(windowMilliseconds * 2);
302
+
303
+ expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
304
+ });
305
+
306
+ it('should start each window with a fresh peak', () => {
307
+ tracePool(pool.asPool());
308
+
309
+ pool.acquire({ total: 10, idle: 0, waiting: 349 });
310
+ clock.advance(windowMilliseconds);
311
+
312
+ pool.acquire({ total: 10, idle: 4, waiting: 0 });
313
+ clock.advance(windowMilliseconds);
314
+
315
+ const lines = tracer.linesNamed('Postgres pool');
316
+ expect(lines).toHaveLength(2);
317
+ expect(lines[0].measurements.peakWaiting).toBe(349);
318
+ expect(lines[1].measurements.peakWaiting).toBe(0);
319
+ expect(tracer.warnings).toHaveLength(1);
320
+ });
321
+
322
+ it('should not hold the process open with the timer it schedules', () => {
323
+ tracePool(pool.asPool());
324
+
325
+ expect(clock.only().hasRef()).toBe(false);
326
+ });
327
+
328
+ it('should stop reporting once the pool has ended', () => {
329
+ tracePool(pool.asPool());
330
+
331
+ pool.acquire({ total: 10, idle: 0, waiting: 0 });
332
+ clock.advance(windowMilliseconds);
333
+ expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
334
+
335
+ pool.acquire({ total: 10, idle: 0, waiting: 0 });
336
+ pool.ended = true;
337
+
338
+ clock.advance(windowMilliseconds * 5);
339
+
340
+ expect(tracer.linesNamed('Postgres pool')).toHaveLength(1);
341
+ expect(clock.live()).toHaveLength(0);
342
+ });
343
+
344
+ it('should still report each physical connection as it is made and removed', () => {
345
+ tracePool(pool.asPool());
346
+
347
+ pool.connect({ total: 1, idle: 0, waiting: 0 });
348
+ pool.remove({ total: 0, idle: 0, waiting: 0 });
349
+
350
+ expect(tracer.linesNamed('Postgres connected')).toEqual([{
351
+ message: 'Postgres connected',
352
+ measurements: { total: 1, idle: 0, waiting: 0 }
353
+ }]);
354
+ expect(tracer.linesNamed('Postgres disconnected')).toEqual([{
355
+ message: 'Postgres disconnected',
356
+ measurements: { total: 0, idle: 0, waiting: 0 }
357
+ }]);
358
+ });
359
+
360
+ it('should still report a pool error', () => {
361
+ tracePool(pool.asPool());
362
+
363
+ const error = new Error('connection terminated unexpectedly');
364
+ pool.fail(error);
365
+
366
+ expect(tracer.errors).toEqual([error]);
367
+ });
368
+ });