@sentio/runtime 4.3.5-rc.2 → 4.3.5-rc.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentio/runtime",
3
- "version": "4.3.5-rc.2",
3
+ "version": "4.3.5-rc.3",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
package/src/db-context.ts CHANGED
@@ -36,6 +36,19 @@ type ProcessStreamResponseV3Init = MessageInitShape<typeof ProcessStreamResponse
36
36
  type Request = NonNullable<MessageInitShape<typeof DBRequestSchema>['op']>
37
37
  type RequestType = NonNullable<Request['case']>
38
38
 
39
+ // Both ends of a stack carry the useful information, so a deep one is trimmed in the
40
+ // middle rather than at the tail: the top frames are where the late call was actually
41
+ // made (runtime frames are already trimmed off by captureStackTrace), and the bottom
42
+ // ones are the entry point that led there — which is the closest thing to naming the
43
+ // handler that the stack can offer, on the occasions it has not been lost.
44
+ const MAX_HEAD_FRAMES = 8
45
+ const MAX_TAIL_FRAMES = 4
46
+
47
+ // V8 keeps only the innermost `Error.stackTraceLimit` frames — 10 by default — so on a
48
+ // deep stack the entry point is discarded before we ever see it. Raised just for this
49
+ // capture, since keeping both ends is the entire point, and restored immediately after.
50
+ const STACK_CAPTURE_DEPTH = 50
51
+
39
52
  export const timeoutError = new Error('timeout')
40
53
 
41
54
  // Name the entity and id, not just the op kind: "get BinanceAlphaPriceEntity 56-0x…"
@@ -58,6 +71,34 @@ function describeRequest(request: Request): string {
58
71
  }
59
72
  }
60
73
 
74
+ /**
75
+ * Keep both ends of a stack, dropping the middle when it is too long to inline.
76
+ *
77
+ * A diagnostic must not grow with the depth of what it describes, but truncating from one
78
+ * end throws away half of what makes it useful: the top frames are where the late call was
79
+ * made, the bottom ones the entry point that led there.
80
+ *
81
+ * Node's own internals are dropped first so the budget goes to frames the reader can act
82
+ * on — otherwise a deep call inside the test runner or the processor host fills the tail
83
+ * with `node:internal/...` and pushes the user's entry point into the omitted middle.
84
+ * V8-synthesised markers like `async Promise.all (index 3)` are not `node:` frames and are
85
+ * kept; they are often the single most informative line.
86
+ */
87
+ function boundedFrames(frames: string[]): string[] {
88
+ const ownCode = frames.filter((frame) => !frame.includes('node:'))
89
+ // If everything was internal there is nothing better to show.
90
+ const usable = ownCode.length ? ownCode : frames
91
+ if (usable.length <= MAX_HEAD_FRAMES + MAX_TAIL_FRAMES) {
92
+ return usable
93
+ }
94
+ const omitted = usable.length - MAX_HEAD_FRAMES - MAX_TAIL_FRAMES
95
+ return [
96
+ ...usable.slice(0, MAX_HEAD_FRAMES),
97
+ ` ... ${omitted} frame(s) omitted ...`,
98
+ ...usable.slice(-MAX_TAIL_FRAMES)
99
+ ]
100
+ }
101
+
61
102
  export interface IStoreContext {
62
103
  sendRequest(request: Request, timeoutSecs?: number): Promise<DBResponse>
63
104
 
@@ -325,15 +366,29 @@ export abstract class AbstractStoreContext implements IStoreContext {
325
366
  * reads is their own code rather than two frames of ours.
326
367
  */
327
368
  protected reportLateMessage(what: string, boundary?: (...args: never[]) => unknown): Error {
328
- const err = new Error(
329
- `[sentio] ${what} was issued after process ${this.processId} had already finished, so it was dropped. ` +
330
- `Something in the handler was still running after the handler returned — every store write, ` +
331
- `store read and metric must be awaited before the handler completes. A common cause is one ` +
332
- `Promise.all() branch rejecting while its siblings keep running: Promise.all rejects immediately ` +
333
- `but does NOT cancel the others, so give each branch its own .catch() (or use Promise.allSettled) ` +
334
- `to keep them inside the handler. The stack below points at the call that arrived too late.`
335
- )
369
+ const summary =
370
+ `[sentio] ${what} was issued after process ${this.processId} had already finished, so it was ` +
371
+ `dropped. Something in the handler was still running after the handler returned — every store ` +
372
+ `write, store read and metric must be awaited before the handler completes. A common cause is ` +
373
+ `one Promise.all() branch rejecting while its siblings keep running: Promise.all rejects ` +
374
+ `immediately but does NOT cancel the others, so give each branch its own .catch() (or use ` +
375
+ `Promise.allSettled) to keep them inside the handler.`
376
+
377
+ const previousLimit = Error.stackTraceLimit
378
+ Error.stackTraceLimit = Math.max(previousLimit, STACK_CAPTURE_DEPTH)
379
+ const err = new Error(summary)
336
380
  Error.captureStackTrace?.(err, boundary ?? this.reportLateMessage)
381
+ // Read `stack` before restoring: the frames are captured at construction, but forcing
382
+ // the (lazy) formatting here keeps this independent of when anyone reads it.
383
+ const captured = err.stack ?? ''
384
+ Error.stackTraceLimit = previousLimit
385
+
386
+ // Inline the frames into `message`. The datasource log view renders only the message,
387
+ // so a stack left in `err.stack` alone is invisible to the user who has to act on it.
388
+ const frames = boundedFrames(captured.split('\n').slice(1))
389
+ if (frames.length) {
390
+ err.message = `${summary}\nIssued at:\n${frames.join('\n')}`
391
+ }
337
392
  console.error(err)
338
393
  return err
339
394
  }