@xano/cli 1.0.6-beta.6 → 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/README.md +6 -6
- 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/paging.d.ts +16 -12
- package/dist/utils/paging.js +19 -14
- package/dist/utils/sse.d.ts +78 -0
- package/dist/utils/sse.js +188 -0
- package/oclif.manifest.json +2332 -2265
- package/package.json +1 -1
package/dist/utils/paging.js
CHANGED
|
@@ -114,17 +114,22 @@ export async function collectAllPages(fetchPage, options = {}) {
|
|
|
114
114
|
/**
|
|
115
115
|
* Build the `--output json` payload for a list command.
|
|
116
116
|
*
|
|
117
|
+
* Field names mirror the Metadata API envelope exactly — `curPage`, `nextPage`,
|
|
118
|
+
* `prevPage`, `itemsTotal`, `items` — so a script can move between the CLI and
|
|
119
|
+
* the raw API without a translation layer. `perPage` is the one field the CLI
|
|
120
|
+
* injects: the API never echoes it back, and it is otherwise invisible when the
|
|
121
|
+
* user leaves the flag at its default.
|
|
122
|
+
*
|
|
117
123
|
* Emits only fields the response actually proves, mirroring the footer's
|
|
118
|
-
* honesty rule: `
|
|
119
|
-
*
|
|
120
|
-
* items in this payload and is the one figure that is unconditionally true.
|
|
124
|
+
* honesty rule: `itemsTotal` appears only when the server sent it, `nextPage`
|
|
125
|
+
* only when the server computed it.
|
|
121
126
|
*
|
|
122
|
-
* Without this, a script driving `--output json` sees a bare array and
|
|
123
|
-
* available stop condition is `length <
|
|
124
|
-
* inference this module refuses to make in the footer.
|
|
127
|
+
* Without this envelope, a script driving `--output json` sees a bare array and
|
|
128
|
+
* its only available stop condition is `length < perPage` — precisely the
|
|
129
|
+
* page-fullness inference this module refuses to make in the footer.
|
|
125
130
|
*/
|
|
126
131
|
export function buildPagingJson(list, options) {
|
|
127
|
-
const payload = {
|
|
132
|
+
const payload = { items: list.items };
|
|
128
133
|
if (options.tier === 'none') {
|
|
129
134
|
return payload;
|
|
130
135
|
}
|
|
@@ -136,17 +141,17 @@ export function buildPagingJson(list, options) {
|
|
|
136
141
|
if (!hasPositionEvidence) {
|
|
137
142
|
return payload;
|
|
138
143
|
}
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
payload.
|
|
144
|
+
const curPage = list.curPage ?? options.page;
|
|
145
|
+
if (curPage !== undefined)
|
|
146
|
+
payload.curPage = curPage;
|
|
142
147
|
if (options.tier === 'envelope' && options.perPage !== undefined)
|
|
143
|
-
payload.
|
|
148
|
+
payload.perPage = options.perPage;
|
|
144
149
|
if (list.nextPage !== undefined)
|
|
145
|
-
payload.
|
|
150
|
+
payload.nextPage = list.nextPage;
|
|
146
151
|
if (list.prevPage !== undefined)
|
|
147
|
-
payload.
|
|
152
|
+
payload.prevPage = list.prevPage;
|
|
148
153
|
if (list.itemsTotal !== undefined)
|
|
149
|
-
payload.
|
|
154
|
+
payload.itemsTotal = list.itemsTotal;
|
|
150
155
|
return payload;
|
|
151
156
|
}
|
|
152
157
|
const SEPARATOR = ' · ';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal `text/event-stream` reader.
|
|
3
|
+
*
|
|
4
|
+
* Bytes in, `{id, event, data}` out. It knows nothing about oclif, HTTP, or any
|
|
5
|
+
* particular server's payloads: `readSseStream` is a pure transform over a
|
|
6
|
+
* `ReadableStream<Uint8Array>`, and `readSseWithReconnect` drives reconnection
|
|
7
|
+
* through a caller-supplied `open()` rather than doing its own requests. That
|
|
8
|
+
* keeps every test a string fixture.
|
|
9
|
+
*/
|
|
10
|
+
/** One dispatched server-sent event. */
|
|
11
|
+
export interface SseEvent {
|
|
12
|
+
/** The joined `data:` lines, newline-separated. Empty when the event carried none. */
|
|
13
|
+
data: string;
|
|
14
|
+
/** The `event:` field, or undefined for the default (unnamed) event type. */
|
|
15
|
+
event?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The `id:` field, parsed as a non-negative integer. Non-numeric ids are
|
|
18
|
+
* ignored: this reader's only use for an id is resuming from `Last-Event-ID`,
|
|
19
|
+
* and a caller that cannot order ids cannot resume from them either.
|
|
20
|
+
*/
|
|
21
|
+
id?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Consecutive fruitless reconnects tolerated before giving up. */
|
|
24
|
+
export declare const DEFAULT_MAX_ATTEMPTS = 5;
|
|
25
|
+
/** 500ms, doubling, capped at 8s: 500, 1000, 2000, 4000, 8000. */
|
|
26
|
+
export declare function defaultBackoffMs(attempt: number): number;
|
|
27
|
+
export interface ReconnectOptions {
|
|
28
|
+
/** Backoff before reconnect attempt `n` (1-based). Defaults to {@link defaultBackoffMs}. */
|
|
29
|
+
backoffMs?: (attempt: number) => number;
|
|
30
|
+
/**
|
|
31
|
+
* Whether this event ends the stream. Supplied by the caller so this module
|
|
32
|
+
* stays free of any particular protocol's terminal shape. A close that
|
|
33
|
+
* follows a terminal event is normal completion, not a drop.
|
|
34
|
+
*/
|
|
35
|
+
isTerminal: (event: SseEvent) => boolean;
|
|
36
|
+
/** Consecutive fruitless reconnects tolerated. Defaults to {@link DEFAULT_MAX_ATTEMPTS}. */
|
|
37
|
+
maxAttempts?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Called once per reconnect attempt, before the backoff. Required for any UI
|
|
40
|
+
* that would otherwise look hung: a drop is invisible while the connection is
|
|
41
|
+
* down, so this is the only signal the caller can render.
|
|
42
|
+
*/
|
|
43
|
+
onReconnecting?: (attempt: number) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Opens (or reopens) the stream. `lastEventId` is the highest id seen so far
|
|
46
|
+
* and must be sent as the `Last-Event-ID` **header** — servers commonly ignore
|
|
47
|
+
* a query parameter, which silently replays the stream from the beginning.
|
|
48
|
+
*/
|
|
49
|
+
open: (lastEventId?: number) => Promise<ReadableStream<Uint8Array>>;
|
|
50
|
+
/** Injectable so tests do not actually sleep. */
|
|
51
|
+
sleep?: (ms: number) => Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parses a `text/event-stream` body into events.
|
|
55
|
+
*
|
|
56
|
+
* Decodes UTF-8 incrementally, so a multi-byte character split across a chunk
|
|
57
|
+
* boundary survives. Handles `\n` and `\r\n`. A trailing partial event at
|
|
58
|
+
* end-of-stream is discarded — an event is only dispatched on a blank line.
|
|
59
|
+
*
|
|
60
|
+
* @yields each parsed event, in arrival order.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readSseStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<SseEvent>;
|
|
63
|
+
/**
|
|
64
|
+
* Reads a stream to its terminal event, reopening across unexpected closes.
|
|
65
|
+
*
|
|
66
|
+
* Tracks the highest id seen and hands it to `open()` so the caller can resume
|
|
67
|
+
* with `Last-Event-ID`. A close that follows a terminal event completes
|
|
68
|
+
* normally; any other close is a drop, reported through `onReconnecting` and
|
|
69
|
+
* retried after a backoff.
|
|
70
|
+
*
|
|
71
|
+
* The attempt budget counts *consecutive fruitless* reopens: a reopen that
|
|
72
|
+
* delivers at least one event resets it, so a long-running stream survives many
|
|
73
|
+
* drops while a dead endpoint still fails fast. Callers that need an absolute
|
|
74
|
+
* ceiling should impose their own wall-clock cap on top of this.
|
|
75
|
+
*
|
|
76
|
+
* @yields each parsed event, up to and including the terminal one.
|
|
77
|
+
*/
|
|
78
|
+
export declare function readSseWithReconnect(options: ReconnectOptions): AsyncGenerator<SseEvent>;
|
|
@@ -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
|
+
}
|