@xano/cli 1.0.6-beta.7 → 1.0.6-beta.8
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/dist/commands/hatch/index.d.ts +130 -0
- package/dist/commands/hatch/index.js +386 -0
- package/dist/utils/hatch-client.d.ts +74 -0
- package/dist/utils/hatch-client.js +163 -0
- package/dist/utils/hatch-contract.d.ts +92 -0
- package/dist/utils/hatch-contract.js +34 -0
- package/dist/utils/hatch-render.d.ts +128 -0
- package/dist/utils/hatch-render.js +183 -0
- package/dist/utils/sse.d.ts +78 -0
- package/dist/utils/sse.js +188 -0
- package/oclif.manifest.json +3372 -3305
- package/package.json +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* eslint-disable n/no-unsupported-features/node-builtins -- Web Streams (ReadableStream, TextDecoder) are what fetch bodies are; available since Node 18 */
|
|
2
|
+
/* eslint-disable no-await-in-loop -- reading a stream and backing off between reconnects is sequential by definition */
|
|
3
|
+
/** Consecutive fruitless reconnects tolerated before giving up. */
|
|
4
|
+
export const DEFAULT_MAX_ATTEMPTS = 5;
|
|
5
|
+
/** 500ms, doubling, capped at 8s: 500, 1000, 2000, 4000, 8000. */
|
|
6
|
+
export function defaultBackoffMs(attempt) {
|
|
7
|
+
return Math.min(500 * 2 ** (attempt - 1), 8000);
|
|
8
|
+
}
|
|
9
|
+
const defaultSleep = (ms) => new Promise((resolve) => {
|
|
10
|
+
setTimeout(resolve, ms);
|
|
11
|
+
});
|
|
12
|
+
function errorMessage(error) {
|
|
13
|
+
return error instanceof Error ? error.message : String(error);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Accumulates the fields of one event until a blank line dispatches it.
|
|
17
|
+
*
|
|
18
|
+
* Kept separate from the byte plumbing so the framing rules — comments, field
|
|
19
|
+
* splitting, multi-line data — read as a unit and are testable through the
|
|
20
|
+
* generator with plain strings.
|
|
21
|
+
*/
|
|
22
|
+
class FrameAccumulator {
|
|
23
|
+
dataLines = [];
|
|
24
|
+
eventId;
|
|
25
|
+
eventName;
|
|
26
|
+
sawField = false;
|
|
27
|
+
/** Feeds one line; returns an event when that line completed one. */
|
|
28
|
+
push(line) {
|
|
29
|
+
// A blank line dispatches. A leading colon marks a comment — servers use
|
|
30
|
+
// those to hold an idle connection open (`: keep-alive`), and treating them
|
|
31
|
+
// as events would emit garbage on a quiet stream.
|
|
32
|
+
if (line === '')
|
|
33
|
+
return this.dispatch();
|
|
34
|
+
if (line.startsWith(':'))
|
|
35
|
+
return undefined;
|
|
36
|
+
const colon = line.indexOf(':');
|
|
37
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
38
|
+
let value = colon === -1 ? '' : line.slice(colon + 1);
|
|
39
|
+
if (value.startsWith(' '))
|
|
40
|
+
value = value.slice(1);
|
|
41
|
+
switch (field) {
|
|
42
|
+
case 'data': {
|
|
43
|
+
this.dataLines.push(value);
|
|
44
|
+
this.sawField = true;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case 'event': {
|
|
48
|
+
this.eventName = value;
|
|
49
|
+
this.sawField = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'id': {
|
|
53
|
+
if (/^\d+$/.test(value))
|
|
54
|
+
this.eventId = Number(value);
|
|
55
|
+
this.sawField = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
// `retry:` and anything unrecognized are not this reader's business.
|
|
59
|
+
default:
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
dispatch() {
|
|
64
|
+
if (!this.sawField)
|
|
65
|
+
return undefined;
|
|
66
|
+
const event = { data: this.dataLines.join('\n'), event: this.eventName, id: this.eventId };
|
|
67
|
+
this.dataLines = [];
|
|
68
|
+
this.eventName = undefined;
|
|
69
|
+
this.eventId = undefined;
|
|
70
|
+
this.sawField = false;
|
|
71
|
+
return event;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parses a `text/event-stream` body into events.
|
|
76
|
+
*
|
|
77
|
+
* Decodes UTF-8 incrementally, so a multi-byte character split across a chunk
|
|
78
|
+
* boundary survives. Handles `\n` and `\r\n`. A trailing partial event at
|
|
79
|
+
* end-of-stream is discarded — an event is only dispatched on a blank line.
|
|
80
|
+
*
|
|
81
|
+
* @yields each parsed event, in arrival order.
|
|
82
|
+
*/
|
|
83
|
+
export async function* readSseStream(stream) {
|
|
84
|
+
const reader = stream.getReader();
|
|
85
|
+
const decoder = new TextDecoder('utf8');
|
|
86
|
+
const frames = new FrameAccumulator();
|
|
87
|
+
let buffer = '';
|
|
88
|
+
try {
|
|
89
|
+
for (;;) {
|
|
90
|
+
let chunk;
|
|
91
|
+
try {
|
|
92
|
+
chunk = await reader.read();
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new Error(`failed to read event stream: ${errorMessage(error)}`);
|
|
96
|
+
}
|
|
97
|
+
if (chunk.done)
|
|
98
|
+
break;
|
|
99
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
100
|
+
let newline = buffer.indexOf('\n');
|
|
101
|
+
while (newline !== -1) {
|
|
102
|
+
const line = buffer.slice(0, newline).replace(/\r$/, '');
|
|
103
|
+
buffer = buffer.slice(newline + 1);
|
|
104
|
+
newline = buffer.indexOf('\n');
|
|
105
|
+
const event = frames.push(line);
|
|
106
|
+
if (event)
|
|
107
|
+
yield event;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
// Cancel rather than release: an early `break` by the consumer (on a
|
|
113
|
+
// terminal event, say) must close the underlying connection, not leak it.
|
|
114
|
+
await reader.cancel().catch(() => { });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** An already-closed stream, used to route a failed reopen through the same retry accounting. */
|
|
118
|
+
function closedStream() {
|
|
119
|
+
return new ReadableStream({
|
|
120
|
+
start(controller) {
|
|
121
|
+
controller.close();
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Reads a stream to its terminal event, reopening across unexpected closes.
|
|
127
|
+
*
|
|
128
|
+
* Tracks the highest id seen and hands it to `open()` so the caller can resume
|
|
129
|
+
* with `Last-Event-ID`. A close that follows a terminal event completes
|
|
130
|
+
* normally; any other close is a drop, reported through `onReconnecting` and
|
|
131
|
+
* retried after a backoff.
|
|
132
|
+
*
|
|
133
|
+
* The attempt budget counts *consecutive fruitless* reopens: a reopen that
|
|
134
|
+
* delivers at least one event resets it, so a long-running stream survives many
|
|
135
|
+
* drops while a dead endpoint still fails fast. Callers that need an absolute
|
|
136
|
+
* ceiling should impose their own wall-clock cap on top of this.
|
|
137
|
+
*
|
|
138
|
+
* @yields each parsed event, up to and including the terminal one.
|
|
139
|
+
*/
|
|
140
|
+
export async function* readSseWithReconnect(options) {
|
|
141
|
+
const { backoffMs = defaultBackoffMs, isTerminal, maxAttempts = DEFAULT_MAX_ATTEMPTS, onReconnecting, open, sleep = defaultSleep, } = options;
|
|
142
|
+
let stream;
|
|
143
|
+
try {
|
|
144
|
+
stream = await open();
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new Error(`failed to open event stream: ${errorMessage(error)}`);
|
|
148
|
+
}
|
|
149
|
+
let lastEventId;
|
|
150
|
+
let attempt = 0;
|
|
151
|
+
let reopenError;
|
|
152
|
+
for (;;) {
|
|
153
|
+
let progressed = false;
|
|
154
|
+
let terminated = false;
|
|
155
|
+
for await (const event of readSseStream(stream)) {
|
|
156
|
+
if (event.id !== undefined && (lastEventId === undefined || event.id > lastEventId)) {
|
|
157
|
+
lastEventId = event.id;
|
|
158
|
+
}
|
|
159
|
+
progressed = true;
|
|
160
|
+
yield event;
|
|
161
|
+
if (isTerminal(event)) {
|
|
162
|
+
terminated = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (terminated)
|
|
167
|
+
return;
|
|
168
|
+
if (progressed)
|
|
169
|
+
attempt = 0;
|
|
170
|
+
attempt += 1;
|
|
171
|
+
if (attempt > maxAttempts) {
|
|
172
|
+
const cause = reopenError ? `: ${errorMessage(reopenError)}` : '';
|
|
173
|
+
throw new Error(`event stream closed before a terminal event and did not recover after ${maxAttempts} reconnect attempts${cause}`);
|
|
174
|
+
}
|
|
175
|
+
onReconnecting?.(attempt);
|
|
176
|
+
await sleep(backoffMs(attempt));
|
|
177
|
+
try {
|
|
178
|
+
stream = await open(lastEventId);
|
|
179
|
+
reopenError = undefined;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
// Reopening failed. Substitute an empty stream so the next pass through
|
|
183
|
+
// the loop charges this against the same attempt budget as a close does.
|
|
184
|
+
reopenError = error;
|
|
185
|
+
stream = closedStream();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|