@milkio/stargate 1.0.0-alpha.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.
- package/.publish/publish.json +7 -0
- package/README.md +15 -0
- package/index.ts +652 -0
- package/package.json +15 -0
- package/tsconfig.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# milkio-world
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.1.29. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
package/index.ts
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import { TSON } from "@southern-aurora/tson";
|
|
2
|
+
|
|
3
|
+
export type MilkioStargateOptions = {
|
|
4
|
+
baseUrl: string | (() => string) | (() => Promise<string>);
|
|
5
|
+
timeout?: number;
|
|
6
|
+
// middlewares?: () => Array<MiddlewareOptions & { isMiddleware: true }>;
|
|
7
|
+
fetch?: typeof fetch;
|
|
8
|
+
abort?: typeof AbortController;
|
|
9
|
+
// storage?: {
|
|
10
|
+
// getItem: (key: string) => string | null | Promise<string | null>;
|
|
11
|
+
// setItem: (key: string, value: string) => void | Promise<void>;
|
|
12
|
+
// removeItem: (key: string) => void | Promise<void>;
|
|
13
|
+
// };
|
|
14
|
+
// memoryStorage?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type Mixin<T, U> = U & Omit<T, keyof U>;
|
|
18
|
+
|
|
19
|
+
export type ExecuteOptions = {
|
|
20
|
+
params?: Record<any, any>;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
timeout?: number;
|
|
23
|
+
type?: "action" | "stream";
|
|
24
|
+
baseUrl?: string | (() => string) | (() => Promise<string>);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ExecuteResultsOption = { executeId: string };
|
|
28
|
+
|
|
29
|
+
export type Ping =
|
|
30
|
+
| [
|
|
31
|
+
{
|
|
32
|
+
connect: false;
|
|
33
|
+
delay: number;
|
|
34
|
+
error: any;
|
|
35
|
+
},
|
|
36
|
+
null,
|
|
37
|
+
]
|
|
38
|
+
| [
|
|
39
|
+
null,
|
|
40
|
+
{
|
|
41
|
+
connect: true;
|
|
42
|
+
delay: number;
|
|
43
|
+
serverTimestamp: number;
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
export async function createStargate<Generated extends { routeSchema: any; rejectCode: any }>(stargateOptions: MilkioStargateOptions) {
|
|
47
|
+
const $fetch = stargateOptions.fetch ?? fetch;
|
|
48
|
+
const $abort = stargateOptions.abort ?? AbortController;
|
|
49
|
+
|
|
50
|
+
const bootstrap = async () => {
|
|
51
|
+
let baseUrl = stargateOptions.baseUrl;
|
|
52
|
+
if (typeof baseUrl === "function") baseUrl = await baseUrl();
|
|
53
|
+
if (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
|
|
54
|
+
|
|
55
|
+
// if (options.middlewares) {
|
|
56
|
+
// const middlewares = [...builtinMiddlewares, ...options.middlewares()];
|
|
57
|
+
|
|
58
|
+
// const push = (index: number, middlewares: Array<any>, middleware: any) => {
|
|
59
|
+
// const id = ++guid;
|
|
60
|
+
// middlewares.push({ id, index, middleware });
|
|
61
|
+
// return () =>
|
|
62
|
+
// middlewares.splice(
|
|
63
|
+
// middlewares.findIndex((v) => v.id === id),
|
|
64
|
+
// 1,
|
|
65
|
+
// );
|
|
66
|
+
// };
|
|
67
|
+
|
|
68
|
+
// const _middlewareHandler = (index: number, options: MiddlewareOptions) => {
|
|
69
|
+
// if (options.bootstrap) push(index, _bootstrapMiddlewares, options.bootstrap);
|
|
70
|
+
// if (options.beforeExecute) push(index, _beforeExecuteMiddlewares, options.beforeExecute);
|
|
71
|
+
// if (options.afterExecute) push(index, _afterExecuteMiddlewares, options.afterExecute);
|
|
72
|
+
// };
|
|
73
|
+
|
|
74
|
+
// for (let index = 0; index < middlewares.length; index++) {
|
|
75
|
+
// const middlewareOptions = middlewares[index];
|
|
76
|
+
// _middlewareHandler(index, middlewareOptions);
|
|
77
|
+
// }
|
|
78
|
+
|
|
79
|
+
// _bootstrapMiddlewares.sort((a, b) => a.index - b.index);
|
|
80
|
+
// _beforeExecuteMiddlewares.sort((a, b) => a.index - b.index);
|
|
81
|
+
// _afterExecuteMiddlewares.sort((a, b) => b.index - a.index);
|
|
82
|
+
|
|
83
|
+
// for (const m of _bootstrapMiddlewares) {
|
|
84
|
+
// await m.middleware({ storage: options.storage as ClientStorage });
|
|
85
|
+
// }
|
|
86
|
+
// }
|
|
87
|
+
|
|
88
|
+
return baseUrl;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const baseUrl: Promise<string> = bootstrap();
|
|
92
|
+
|
|
93
|
+
const stargate = {
|
|
94
|
+
$types: {
|
|
95
|
+
generated: void 0 as unknown as Generated,
|
|
96
|
+
},
|
|
97
|
+
options: stargateOptions,
|
|
98
|
+
async execute<Path extends keyof Generated["routeSchema"]["$types"]>(
|
|
99
|
+
path: Path,
|
|
100
|
+
options?: Mixin<
|
|
101
|
+
ExecuteOptions,
|
|
102
|
+
{
|
|
103
|
+
params?: Generated["routeSchema"]["$types"][Path]["params"];
|
|
104
|
+
}
|
|
105
|
+
>,
|
|
106
|
+
): Promise<
|
|
107
|
+
Generated["routeSchema"]["$types"][Path]["🐣"] extends boolean
|
|
108
|
+
? // action
|
|
109
|
+
[Partial<Generated["rejectCode"]>, null, ExecuteResultsOption] | [null, Generated["routeSchema"]["$types"][Path]["result"], ExecuteResultsOption]
|
|
110
|
+
: // stream
|
|
111
|
+
[Partial<Generated["rejectCode"]>, null, ExecuteResultsOption] | [null, AsyncGenerator<[Partial<Generated["rejectCode"]>, null] | [null, GeneratorGeneric<Generated["routeSchema"]["$types"][Path]["result"]>], ExecuteResultsOption>]
|
|
112
|
+
> {
|
|
113
|
+
if (!options) options = {};
|
|
114
|
+
if (options.headers === undefined) options.headers = {};
|
|
115
|
+
|
|
116
|
+
let url: string;
|
|
117
|
+
if (options.baseUrl) {
|
|
118
|
+
let baseUrl = options.baseUrl;
|
|
119
|
+
if (typeof baseUrl === "function") baseUrl = await baseUrl();
|
|
120
|
+
if (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
|
|
121
|
+
url = baseUrl + (path as string);
|
|
122
|
+
} else url = (await baseUrl) + (path as string);
|
|
123
|
+
|
|
124
|
+
if (options.type !== "stream") {
|
|
125
|
+
// action
|
|
126
|
+
if (options.headers["Accept"] === undefined) options.headers["Accept"] = "application/json";
|
|
127
|
+
if (options.headers["Content-Type"] === undefined) options.headers["Content-Type"] = "application/json";
|
|
128
|
+
|
|
129
|
+
// for (const m of _beforeExecuteMiddlewares) {
|
|
130
|
+
// await m.middleware({ path: path as string, params: options.params, headers: options.headers, storage: options.storage as ClientStorage });
|
|
131
|
+
// }
|
|
132
|
+
|
|
133
|
+
const body = TSON.stringify(options.params) ?? "";
|
|
134
|
+
|
|
135
|
+
let result: { value: Record<any, any> };
|
|
136
|
+
try {
|
|
137
|
+
const response = await new Promise<string>(async (resolve, reject) => {
|
|
138
|
+
const timeout = options?.timeout ?? options?.timeout ?? 6000;
|
|
139
|
+
const timer = setTimeout(() => {
|
|
140
|
+
reject([{ REQUEST_TIMEOUT: { timeout, message: `Execute timeout after ${timeout}ms.` } }, null]);
|
|
141
|
+
}, timeout);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const value = await (await $fetch(url, { method: "POST", body, headers: options.headers })).text();
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
resolve(value);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
reject(error);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
result = { value: TSON.parse(response) };
|
|
152
|
+
} catch (error: any) {
|
|
153
|
+
if (error?.[0]?.REQUEST_TIMEOUT) return error;
|
|
154
|
+
return [{ REQUEST_FAIL: error }, null, { executeId: "unknown" }];
|
|
155
|
+
}
|
|
156
|
+
if (result.value.success !== true) {
|
|
157
|
+
const error: any = {};
|
|
158
|
+
error[result.value.code] = result.value.reject;
|
|
159
|
+
return [error, null, { executeId: "unknown" }];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// for (const m of _afterExecuteMiddlewares) {
|
|
163
|
+
// await m.middleware({ path: path as string, storage: options.storage as ClientStorage, result });
|
|
164
|
+
// }
|
|
165
|
+
|
|
166
|
+
return [null, result.value.data, { executeId: result.value.executeId }] as any;
|
|
167
|
+
} else {
|
|
168
|
+
// stream
|
|
169
|
+
if (options.headers["Accept"] === undefined) options.headers["Accept"] = "text/event-stream";
|
|
170
|
+
if (options.headers["Content-Type"] === undefined) options.headers["Content-Type"] = "application/json";
|
|
171
|
+
|
|
172
|
+
const body = TSON.stringify(options.params) ?? "";
|
|
173
|
+
const stacks: Map<
|
|
174
|
+
number,
|
|
175
|
+
{
|
|
176
|
+
promise: Promise<IteratorResult<any>>;
|
|
177
|
+
resolve: (value: IteratorResult<any>) => void;
|
|
178
|
+
reject: (reason: any) => void;
|
|
179
|
+
}
|
|
180
|
+
> = new Map();
|
|
181
|
+
let stacksIndex: number = 0;
|
|
182
|
+
let iteratorIndex: number = 0;
|
|
183
|
+
let streamResult: any = undefined;
|
|
184
|
+
let streamResultFetched = withResolvers<undefined>();
|
|
185
|
+
|
|
186
|
+
const timeout = stargateOptions?.timeout ?? options?.timeout ?? 6000;
|
|
187
|
+
const timer = setTimeout(() => {
|
|
188
|
+
streamResultFetched.reject([{ REQUEST_TIMEOUT: { timeout, message: `Execute timeout after ${timeout}ms.` } }, null, { executeId: "unknown" }]);
|
|
189
|
+
}, timeout);
|
|
190
|
+
|
|
191
|
+
const onmessage = (event: EventSourceMessage) => {
|
|
192
|
+
if (event.data.startsWith("@")) {
|
|
193
|
+
try {
|
|
194
|
+
streamResult = TSON.parse(event.data.slice(1));
|
|
195
|
+
streamResultFetched.resolve(undefined);
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
streamResultFetched.reject([{ REQUEST_FAIL: error }, null, { executeId: "unknown" }]);
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
} else {
|
|
203
|
+
const index = ++stacksIndex;
|
|
204
|
+
if (stacks.has(index)) stacks.get(index)!.resolve({ done: false, value: TSON.parse(event.data) });
|
|
205
|
+
else {
|
|
206
|
+
const stack = withResolvers<IteratorResult<any>>();
|
|
207
|
+
stack.resolve({ done: false, value: TSON.parse(event.data) });
|
|
208
|
+
stacks.set(index, stack);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
let curRequestController: AbortController;
|
|
214
|
+
|
|
215
|
+
async function create() {
|
|
216
|
+
curRequestController = new $abort();
|
|
217
|
+
curRequestController.signal.addEventListener("abort", () => iterator.return());
|
|
218
|
+
try {
|
|
219
|
+
// for (const m of _beforeExecuteMiddlewares) {
|
|
220
|
+
// await m.middleware({ path: path as string, params: eventOptions.params, headers: eventOptions.headers!, storage: options.storage as ClientStorage });
|
|
221
|
+
// }
|
|
222
|
+
|
|
223
|
+
const response = await $fetch(url, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: options!.headers,
|
|
226
|
+
body: body,
|
|
227
|
+
signal: curRequestController.signal,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const contentType = response.headers.get("Content-Type");
|
|
231
|
+
if (!contentType?.startsWith("text/event-stream")) {
|
|
232
|
+
throw new Error(`Expected content-type to be ${"text/event-stream"}, Actual: ${contentType}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await getBytes(response.body!, getLines(getMessages(onmessage)));
|
|
236
|
+
|
|
237
|
+
// for (const m of _afterExecuteMiddlewares) {
|
|
238
|
+
// await m.middleware({ path: path as string, storage: options.storage as ClientStorage, result: { value: undefined } });
|
|
239
|
+
// }
|
|
240
|
+
|
|
241
|
+
await iterator.return();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (!curRequestController.signal.aborted) curRequestController.abort();
|
|
244
|
+
await iterator.throw(err);
|
|
245
|
+
streamResultFetched.reject([{ REQUEST_FAIL: err }, null, { executeId: "unknown" }]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
void create();
|
|
250
|
+
|
|
251
|
+
const iterator = {
|
|
252
|
+
...({
|
|
253
|
+
next(): Promise<IteratorResult<unknown>> {
|
|
254
|
+
const index = ++iteratorIndex;
|
|
255
|
+
if (stacks.has(index - 2)) stacks.delete(index - 2);
|
|
256
|
+
if (stacks.has(index)) {
|
|
257
|
+
return stacks.get(index)!.promise;
|
|
258
|
+
} else {
|
|
259
|
+
const stack = withResolvers<IteratorResult<any>>();
|
|
260
|
+
stacks.set(index, stack);
|
|
261
|
+
return stack.promise;
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
async return(): Promise<IteratorResult<void>> {
|
|
265
|
+
if (!curRequestController.signal.aborted) curRequestController.abort();
|
|
266
|
+
for (const [_, iterator] of stacks) iterator.resolve({ done: true, value: undefined });
|
|
267
|
+
return { done: true, value: undefined };
|
|
268
|
+
},
|
|
269
|
+
async throw(err: any): Promise<IteratorResult<void>> {
|
|
270
|
+
streamResult = {
|
|
271
|
+
success: false,
|
|
272
|
+
executeId: streamResult?.executeId ?? "",
|
|
273
|
+
fail: {
|
|
274
|
+
code: "NETWORK_ERROR",
|
|
275
|
+
message: "Network Error",
|
|
276
|
+
fromClient: true,
|
|
277
|
+
data: err,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
if (!curRequestController.signal.aborted) curRequestController.abort();
|
|
281
|
+
for (const [_, iterator] of stacks) iterator.resolve({ done: true, value: undefined });
|
|
282
|
+
return { done: true, value: undefined };
|
|
283
|
+
},
|
|
284
|
+
} satisfies AsyncIterator<unknown>),
|
|
285
|
+
[Symbol.asyncIterator]() {
|
|
286
|
+
return this;
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
await streamResultFetched.promise;
|
|
292
|
+
return [null, iterator, { executeId: streamResult.executeId }] as any;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return error as any;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
cookbook: {
|
|
299
|
+
subscribe: async (baseUrl: string) => {
|
|
300
|
+
const headers = {
|
|
301
|
+
"Content-Type": "application/json",
|
|
302
|
+
Accept: "text/event-stream",
|
|
303
|
+
};
|
|
304
|
+
const params = {};
|
|
305
|
+
|
|
306
|
+
const body = TSON.stringify(params) ?? "";
|
|
307
|
+
const stacks: Map<
|
|
308
|
+
number,
|
|
309
|
+
{
|
|
310
|
+
promise: Promise<IteratorResult<any>>;
|
|
311
|
+
resolve: (value: IteratorResult<any>) => void;
|
|
312
|
+
reject: (reason: any) => void;
|
|
313
|
+
}
|
|
314
|
+
> = new Map();
|
|
315
|
+
let stacksIndex: number = 0;
|
|
316
|
+
let iteratorIndex: number = 0;
|
|
317
|
+
let streamResult: any = undefined;
|
|
318
|
+
|
|
319
|
+
const onmessage = (event: EventSourceMessage) => {
|
|
320
|
+
const index = ++stacksIndex;
|
|
321
|
+
if (stacks.has(index)) stacks.get(index)!.resolve({ done: false, value: TSON.parse(event.data) });
|
|
322
|
+
else {
|
|
323
|
+
const stack = withResolvers<IteratorResult<any>>();
|
|
324
|
+
stack.resolve({ done: false, value: TSON.parse(event.data) });
|
|
325
|
+
stacks.set(index, stack);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
let curRequestController: AbortController;
|
|
330
|
+
|
|
331
|
+
async function create() {
|
|
332
|
+
curRequestController = new $abort();
|
|
333
|
+
curRequestController.signal.addEventListener("abort", () => iterator.return());
|
|
334
|
+
try {
|
|
335
|
+
const response = await $fetch(`${baseUrl}/$subscribe`, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: headers,
|
|
338
|
+
body: body,
|
|
339
|
+
signal: curRequestController.signal,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const contentType = response.headers.get("Content-Type");
|
|
343
|
+
if (!contentType?.startsWith("text/event-stream")) {
|
|
344
|
+
throw new Error(`Expected content-type to be ${"text/event-stream"}, Actual: ${contentType}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await getBytes(response.body!, getLines(getMessages(onmessage)));
|
|
348
|
+
|
|
349
|
+
await iterator.return();
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (!curRequestController.signal.aborted) curRequestController.abort();
|
|
352
|
+
await iterator.throw(err);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
void create();
|
|
357
|
+
|
|
358
|
+
const iterator = {
|
|
359
|
+
...({
|
|
360
|
+
next(): Promise<IteratorResult<unknown>> {
|
|
361
|
+
const index = ++iteratorIndex;
|
|
362
|
+
if (stacks.has(index - 2)) stacks.delete(index - 2);
|
|
363
|
+
if (stacks.has(index)) {
|
|
364
|
+
return stacks.get(index)!.promise;
|
|
365
|
+
} else {
|
|
366
|
+
const stack = withResolvers<IteratorResult<any>>();
|
|
367
|
+
stacks.set(index, stack);
|
|
368
|
+
return stack.promise;
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
async return(): Promise<IteratorResult<void>> {
|
|
372
|
+
if (!curRequestController.signal.aborted) curRequestController.abort();
|
|
373
|
+
for (const [_, iterator] of stacks) iterator.resolve({ done: true, value: undefined });
|
|
374
|
+
return { done: true, value: undefined };
|
|
375
|
+
},
|
|
376
|
+
async throw(err: any): Promise<IteratorResult<void>> {
|
|
377
|
+
streamResult = {
|
|
378
|
+
success: false,
|
|
379
|
+
fail: {
|
|
380
|
+
code: "NETWORK_ERROR",
|
|
381
|
+
message: "Network Error",
|
|
382
|
+
fromClient: true,
|
|
383
|
+
data: err,
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
if (!curRequestController.signal.aborted) curRequestController.abort();
|
|
387
|
+
for (const [_, iterator] of stacks) iterator.resolve({ done: true, value: undefined });
|
|
388
|
+
return { done: true, value: undefined };
|
|
389
|
+
},
|
|
390
|
+
} satisfies AsyncIterator<unknown>),
|
|
391
|
+
[Symbol.asyncIterator]() {
|
|
392
|
+
return this;
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
return iterator;
|
|
398
|
+
} catch (error) {
|
|
399
|
+
return error as any;
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
async ping(options?: { timeout?: number }): Promise<Ping> {
|
|
404
|
+
return await new Promise<Ping>(async (resolve) => {
|
|
405
|
+
const url = (await baseUrl) + "/generate_204";
|
|
406
|
+
const timeout = stargateOptions?.timeout ?? options?.timeout ?? 6000;
|
|
407
|
+
let startsTime = Date.now();
|
|
408
|
+
const timer = setTimeout(() => {
|
|
409
|
+
const endsTime = Date.now();
|
|
410
|
+
resolve([{ connect: false, delay: endsTime - startsTime, error: { REQUEST_TIMEOUT: { timeout, message: `Execute timeout after ${timeout}ms.` } } }, null]);
|
|
411
|
+
}, timeout);
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const response = await await $fetch(url, { method: "HEAD" });
|
|
415
|
+
const endsTime = Date.now();
|
|
416
|
+
clearTimeout(timer);
|
|
417
|
+
if (response.status !== 204) {
|
|
418
|
+
resolve([{ connect: false, delay: endsTime - startsTime, error: { REQUEST_FAIL: { response, status: response.status, message: `Status code not 204` } } }, null]);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
resolve([null, { connect: true, delay: endsTime - startsTime, serverTimestamp: Number(response.headers.get("Content-Type")!.substring(17)) }]);
|
|
422
|
+
} catch (error: any) {
|
|
423
|
+
const endsTime = Date.now();
|
|
424
|
+
return [{ connect: false, delay: endsTime - startsTime, error: error }, null];
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return stargate;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// export function defineMiddleware(options: MiddlewareOptions): () => MiddlewareOptions & { isMiddleware: true } {
|
|
434
|
+
// return () => ({
|
|
435
|
+
// ...options,
|
|
436
|
+
// isMiddleware: true,
|
|
437
|
+
// });
|
|
438
|
+
// }
|
|
439
|
+
|
|
440
|
+
let guid = 0;
|
|
441
|
+
|
|
442
|
+
export type ExecuteStreamOptions = {
|
|
443
|
+
headers?: Record<string, string>;
|
|
444
|
+
timeout?: number;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export type ApiSchemaExtend = {
|
|
448
|
+
apiValidator: {
|
|
449
|
+
generatedAt: number;
|
|
450
|
+
validate: Record<any, any>;
|
|
451
|
+
};
|
|
452
|
+
apiMethodsSchema: Record<any, any>;
|
|
453
|
+
apiMethodsTypeSchema: Record<any, any>;
|
|
454
|
+
apiTestsSchema: Record<any, any>;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export type FailCodeExtend = Record<any, (...args: Array<any>) => any>;
|
|
458
|
+
|
|
459
|
+
export type BootstrapMiddleware = (data: { storage: ClientStorage }) => Promise<void> | void;
|
|
460
|
+
export type BeforeExecuteMiddleware = (data: { path: string; params: any; headers: Record<string, string>; storage: ClientStorage }) => Promise<void> | void;
|
|
461
|
+
export type AfterExecuteMiddleware = (data: { path: string; result: { value: any }; storage: ClientStorage }) => Promise<void> | void;
|
|
462
|
+
|
|
463
|
+
export type MiddlewareOptions = {
|
|
464
|
+
bootstrap?: BootstrapMiddleware;
|
|
465
|
+
beforeExecute?: BeforeExecuteMiddleware;
|
|
466
|
+
afterExecute?: AfterExecuteMiddleware;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
export type ClientStorage = {
|
|
470
|
+
getItem: (key: string) => Promise<string | null>;
|
|
471
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
472
|
+
removeItem: (key: string) => Promise<void>;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
export type ExecuteResultSuccess<Result> = {
|
|
476
|
+
executeId: string;
|
|
477
|
+
success: true;
|
|
478
|
+
data: Result;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export type GeneratorGeneric<T> = T extends AsyncGenerator<infer I> ? I : never;
|
|
482
|
+
|
|
483
|
+
export type FlattenKeys<T extends any, Prefix extends string = ""> = {
|
|
484
|
+
[K in keyof T]: T[K] extends object ? FlattenKeys<T[K], `${Prefix}${Exclude<K, symbol>}.`> : `$input.${Prefix}${Exclude<K, symbol>}`;
|
|
485
|
+
}[keyof T];
|
|
486
|
+
|
|
487
|
+
// *** This part of the code is based on `@microsoft/fetch-event-source` rewrite, thanks to the work of Microsoft *** //
|
|
488
|
+
// *** https://github.com/Azure/fetch-event-source/blob/main/src/parse.ts *** //
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Represents a message sent in an event stream
|
|
492
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format
|
|
493
|
+
*/
|
|
494
|
+
export interface EventSourceMessage {
|
|
495
|
+
data: string;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Converts a ReadableStream into a callback pattern.
|
|
500
|
+
* @param stream The input ReadableStream.
|
|
501
|
+
* @param onChunk A function that will be called on each new byte chunk in the stream.
|
|
502
|
+
* @returns {Promise<void>} A promise that will be resolved when the stream closes.
|
|
503
|
+
*/
|
|
504
|
+
export async function getBytes(stream: ReadableStream<Uint8Array>, onChunk: (arr: Uint8Array) => void) {
|
|
505
|
+
const reader = stream.getReader();
|
|
506
|
+
let result: ReadableStreamReadResult<Uint8Array>;
|
|
507
|
+
while (!(result = await reader.read()).done) {
|
|
508
|
+
onChunk(result.value);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const enum ControlChars {
|
|
513
|
+
NewLine = 10,
|
|
514
|
+
CarriageReturn = 13,
|
|
515
|
+
Space = 32,
|
|
516
|
+
Colon = 58,
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Parses arbitary byte chunks into EventSource line buffers.
|
|
521
|
+
* Each line should be of the format "field: value" and ends with \r, \n, or \r\n.
|
|
522
|
+
* @param onLine A function that will be called on each new EventSource line.
|
|
523
|
+
* @returns A function that should be called for each incoming byte chunk.
|
|
524
|
+
*/
|
|
525
|
+
export function getLines(onLine: (line: Uint8Array, fieldLength: number) => void) {
|
|
526
|
+
let buffer: Uint8Array | undefined;
|
|
527
|
+
let position: number; // current read position
|
|
528
|
+
let fieldLength: number; // length of the `field` portion of the line
|
|
529
|
+
let discardTrailingNewline = false;
|
|
530
|
+
|
|
531
|
+
// return a function that can process each incoming byte chunk:
|
|
532
|
+
return function onChunk(arr: Uint8Array) {
|
|
533
|
+
if (buffer === undefined) {
|
|
534
|
+
buffer = arr;
|
|
535
|
+
position = 0;
|
|
536
|
+
fieldLength = -1;
|
|
537
|
+
} else {
|
|
538
|
+
// we're still parsing the old line. Append the new bytes into buffer:
|
|
539
|
+
buffer = concat(buffer, arr);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const bufLength = buffer.length;
|
|
543
|
+
let lineStart = 0; // index where the current line starts
|
|
544
|
+
while (position < bufLength) {
|
|
545
|
+
if (discardTrailingNewline) {
|
|
546
|
+
if (buffer[position] === ControlChars.NewLine) {
|
|
547
|
+
lineStart = ++position; // skip to next char
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
discardTrailingNewline = false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// start looking forward till the end of line:
|
|
554
|
+
let lineEnd = -1; // index of the \r or \n char
|
|
555
|
+
for (; position < bufLength && lineEnd === -1; ++position) {
|
|
556
|
+
switch (buffer[position]) {
|
|
557
|
+
case ControlChars.Colon:
|
|
558
|
+
if (fieldLength === -1) {
|
|
559
|
+
// first colon in line
|
|
560
|
+
fieldLength = position - lineStart;
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
// @ts-ignore:7029 \r case below should fallthrough to \n:
|
|
564
|
+
case ControlChars.CarriageReturn:
|
|
565
|
+
discardTrailingNewline = true;
|
|
566
|
+
case ControlChars.NewLine:
|
|
567
|
+
lineEnd = position;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (lineEnd === -1) {
|
|
573
|
+
// We reached the end of the buffer but the line hasn't ended.
|
|
574
|
+
// Wait for the next arr and then continue parsing:
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// we've reached the line end, send it out:
|
|
579
|
+
onLine(buffer.subarray(lineStart, lineEnd), fieldLength);
|
|
580
|
+
lineStart = position; // we're now on the next line
|
|
581
|
+
fieldLength = -1;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (lineStart === bufLength) {
|
|
585
|
+
buffer = undefined; // we've finished reading it
|
|
586
|
+
} else if (lineStart !== 0) {
|
|
587
|
+
// Create a new view into buffer beginning at lineStart so we don't
|
|
588
|
+
// need to copy over the previous lines when we get the new arr:
|
|
589
|
+
buffer = buffer.subarray(lineStart);
|
|
590
|
+
position -= lineStart;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Parses line buffers into EventSourceMessages.
|
|
597
|
+
* @param onId A function that will be called on each `id` field.
|
|
598
|
+
* @param onRetry A function that will be called on each `retry` field.
|
|
599
|
+
* @param onMessage A function that will be called on each message.
|
|
600
|
+
* @returns A function that should be called for each incoming line buffer.
|
|
601
|
+
*/
|
|
602
|
+
export function getMessages(onMessage?: (msg: EventSourceMessage) => void) {
|
|
603
|
+
let message = newMessage();
|
|
604
|
+
const decoder = new TextDecoder();
|
|
605
|
+
|
|
606
|
+
// return a function that can process each incoming line buffer:
|
|
607
|
+
return function onLine(line: Uint8Array, fieldLength: number) {
|
|
608
|
+
if (line.length === 0) {
|
|
609
|
+
// empty line denotes end of message. Trigger the callback and start a new message:
|
|
610
|
+
onMessage?.(message);
|
|
611
|
+
message = newMessage();
|
|
612
|
+
} else if (fieldLength > 0) {
|
|
613
|
+
// exclude comments and lines with no values
|
|
614
|
+
// line is of format "<field>:<value>" or "<field>: <value>"
|
|
615
|
+
// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
|
616
|
+
const field = decoder.decode(line.subarray(0, fieldLength));
|
|
617
|
+
const valueOffset = fieldLength + (line[fieldLength + 1] === ControlChars.Space ? 2 : 1);
|
|
618
|
+
const value = decoder.decode(line.subarray(valueOffset));
|
|
619
|
+
|
|
620
|
+
switch (field) {
|
|
621
|
+
case "data":
|
|
622
|
+
// if this message already has data, append the new value to the old.
|
|
623
|
+
// otherwise, just set to the new value:
|
|
624
|
+
message.data = message.data ? message.data + "\n" + value : value; // otherwise,
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function concat(a: Uint8Array, b: Uint8Array) {
|
|
632
|
+
const res = new Uint8Array(a.length + b.length);
|
|
633
|
+
res.set(a);
|
|
634
|
+
res.set(b, a.length);
|
|
635
|
+
return res;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function newMessage(): EventSourceMessage {
|
|
639
|
+
return {
|
|
640
|
+
data: "",
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export function withResolvers<T = any>(): PromiseWithResolvers<T> {
|
|
645
|
+
let resolve: PromiseWithResolvers<T>["resolve"];
|
|
646
|
+
let reject: PromiseWithResolvers<T>["reject"];
|
|
647
|
+
const promise = new Promise<T>((res, rej) => {
|
|
648
|
+
resolve = res;
|
|
649
|
+
reject = rej;
|
|
650
|
+
});
|
|
651
|
+
return { promise, resolve: resolve!, reject: reject! };
|
|
652
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@milkio/stargate",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"version": "1.0.0-alpha.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@southern-aurora/tson": "^2.0.2"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/bun": "latest"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"typescript": "5.6.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Enable latest features
|
|
4
|
+
"lib": ["ESNext", "DOM"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
|
|
22
|
+
// Some stricter flags (disabled by default)
|
|
23
|
+
"noUnusedLocals": false,
|
|
24
|
+
"noUnusedParameters": false,
|
|
25
|
+
"noPropertyAccessFromIndexSignature": false
|
|
26
|
+
}
|
|
27
|
+
}
|