@milkio/stargate 1.0.0-alpha.1 → 1.0.0-alpha.100

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.
File without changes
package/README.md CHANGED
File without changes
package/index.ts CHANGED
@@ -40,6 +40,64 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
40
40
  const $fetch = stargateOptions.fetch ?? fetch;
41
41
  const $abort = stargateOptions.abort ?? AbortController;
42
42
 
43
+ type StargateEvents = {
44
+ "milkio:executeBefore": { path: string; options: Mixin<ExecuteOptions, { headers: Record<string, string>; baseUrl: string }> };
45
+ "milkio:fetchBefore": { path: string; options: Mixin<ExecuteOptions, { headers: Record<string, string>; baseUrl: string }>; body: string };
46
+ "milkio:executeError": {
47
+ path: string;
48
+ options: Mixin<ExecuteOptions, { headers: Record<string, string>; baseUrl: string }>;
49
+ error: Partial<Generated["rejectCode"]>;
50
+ handleError: <K extends keyof Partial<Generated["rejectCode"]>>(error: any, key: K, handler: (error: Partial<Generated["rejectCode"][K]>) => boolean | Promise<boolean>) => Promise<void>;
51
+ };
52
+ };
53
+
54
+ const handleError: any = async (error: any, key: string, handler: (error: any) => boolean | Promise<boolean>) => {
55
+ if (key in error) {
56
+ const handled = await handler(error[key]);
57
+ if (handled) delete error[key];
58
+ }
59
+ };
60
+
61
+ const __initEventManager = () => {
62
+ const handlers = new Map<(event: any) => void, string>();
63
+ const indexed = new Map<string, Set<(event: any) => void>>();
64
+
65
+ const eventManager = {
66
+ on: <Key extends keyof StargateEvents, Handler extends (event: StargateEvents[Key]) => void>(key: Key, handler: Handler) => {
67
+ handlers.set(handler, key as string);
68
+ if (indexed.has(key as string) === false) {
69
+ indexed.set(key as string, new Set());
70
+ }
71
+ const set = indexed.get(key as string)!;
72
+ set.add(handler);
73
+ handlers.set(handler, key as string);
74
+
75
+ return () => {
76
+ handlers.delete(handler);
77
+ set.delete(handler);
78
+ };
79
+ },
80
+ off: <Key extends keyof StargateEvents, Handler extends (event: StargateEvents[Key]) => void>(key: Key, handler: Handler) => {
81
+ const set = indexed.get(key as string);
82
+ if (!set) return;
83
+ handlers.delete(handler);
84
+ set.delete(handler);
85
+ },
86
+ emit: async <Key extends keyof StargateEvents, Value extends StargateEvents[Key]>(key: Key, value: Value): Promise<void> => {
87
+ const h = indexed.get(key as string);
88
+ if (h) {
89
+ for (const handler of h) {
90
+ await handler(value);
91
+ }
92
+ }
93
+ },
94
+ };
95
+
96
+ return eventManager;
97
+ };
98
+
99
+ const eventManager = __initEventManager();
100
+
43
101
  const bootstrap = async () => {
44
102
  let baseUrl = stargateOptions.baseUrl;
45
103
  if (typeof baseUrl === "function") baseUrl = await baseUrl();
@@ -51,25 +109,27 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
51
109
  const baseUrl: Promise<string> = bootstrap();
52
110
 
53
111
  const stargate = {
112
+ ...eventManager,
54
113
  $types: {
55
114
  generated: void 0 as unknown as Generated,
56
115
  },
57
116
  options: stargateOptions,
58
- async execute<Path extends keyof Generated["routeSchema"]["$types"]>(
117
+ async execute<Path extends keyof Generated["routeSchema"]>(
59
118
  path: Path,
60
119
  options?: Mixin<
61
120
  ExecuteOptions,
62
121
  {
63
- params?: Generated["routeSchema"]["$types"][Path]["params"];
122
+ params?: Generated["routeSchema"][Path]["types"]["params"];
64
123
  }
65
124
  >,
66
125
  ): Promise<
67
- Generated["routeSchema"]["$types"][Path]["🐣"] extends boolean
126
+ Generated["routeSchema"][Path]["types"]["🐣"] extends boolean
68
127
  ? // action
69
- [Partial<Generated["rejectCode"]>, null, ExecuteResultsOption] | [null, Generated["routeSchema"]["$types"][Path]["result"], ExecuteResultsOption]
128
+ [Partial<Generated["rejectCode"]>, null, ExecuteResultsOption] | [null, Generated["routeSchema"][Path]["types"]["result"], ExecuteResultsOption]
70
129
  : // stream
71
- [Partial<Generated["rejectCode"]>, null, ExecuteResultsOption] | [null, AsyncGenerator<[Partial<Generated["rejectCode"]>, null] | [null, GeneratorGeneric<Generated["routeSchema"]["$types"][Path]["result"]>], ExecuteResultsOption>]
130
+ [Partial<Generated["rejectCode"]>, null, ExecuteResultsOption] | [null, AsyncGenerator<[Partial<Generated["rejectCode"]>, null] | [null, GeneratorGeneric<Generated["routeSchema"][Path]["types"]["result"]>], ExecuteResultsOption>]
72
131
  > {
132
+ // biome-ignore lint/style/noParameterAssign: <explanation>
73
133
  if (!options) options = {};
74
134
  if (options.headers === undefined) options.headers = {};
75
135
 
@@ -79,17 +139,23 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
79
139
  if (typeof baseUrl === "function") baseUrl = await baseUrl();
80
140
  if (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
81
141
  url = baseUrl + (path as string);
82
- } else url = (await baseUrl) + (path as string);
142
+ } else {
143
+ url = (await baseUrl) + (path as string);
144
+ }
83
145
 
84
146
  if (options.type !== "stream") {
85
147
  // action
86
- if (options.headers["Accept"] === undefined) options.headers["Accept"] = "application/json";
148
+ if (options.headers.Accept === undefined) options.headers.Accept = "application/json";
87
149
  if (options.headers["Content-Type"] === undefined) options.headers["Content-Type"] = "application/json";
88
-
89
- const body = TSON.stringify(options.params) ?? "";
90
-
91
150
  let result: { value: Record<any, any> };
151
+
92
152
  try {
153
+ await eventManager.emit("milkio:executeBefore", { path: path as string, options: options as any });
154
+
155
+ const body = TSON.stringify(options.params) ?? "";
156
+ await eventManager.emit("milkio:fetchBefore", { path: path as string, options: options as any, body });
157
+
158
+ // biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
93
159
  const response = await new Promise<string>(async (resolve, reject) => {
94
160
  const timeout = options?.timeout ?? options?.timeout ?? 6000;
95
161
  const timer = setTimeout(() => {
@@ -106,22 +172,27 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
106
172
  });
107
173
  result = { value: TSON.parse(response) };
108
174
  } catch (error: any) {
109
- if (error?.[0]?.REQUEST_TIMEOUT) return error;
110
- return [{ REQUEST_FAIL: error }, null, { executeId: "unknown" }];
175
+ if (error?.[0]?.REQUEST_TIMEOUT) {
176
+ await eventManager.emit("milkio:executeError", { handleError, path: path as string, options: options as any, error });
177
+ return error;
178
+ }
179
+ const errorPined = { REQUEST_FAIL: error };
180
+ await eventManager.emit("milkio:executeError", { handleError, path: path as string, options: options as any, error: errorPined });
181
+ return [errorPined, null, { executeId: "unknown" }];
111
182
  }
112
183
  if (result.value.success !== true) {
113
184
  const error: any = {};
114
- error[result.value.code] = result.value.reject;
185
+ error[result.value.code] = result.value.reject ?? null;
186
+ await eventManager.emit("milkio:executeError", { handleError, path: path as string, options: options as any, error });
115
187
  return [error, null, { executeId: "unknown" }];
116
188
  }
117
189
 
118
190
  return [null, result.value.data, { executeId: result.value.executeId }] as any;
119
191
  } else {
120
192
  // stream
121
- if (options.headers["Accept"] === undefined) options.headers["Accept"] = "text/event-stream";
193
+ if (options.headers.Accept === undefined) options.headers.Accept = "text/event-stream";
122
194
  if (options.headers["Content-Type"] === undefined) options.headers["Content-Type"] = "application/json";
123
195
 
124
- const body = TSON.stringify(options.params) ?? "";
125
196
  const stacks: Map<
126
197
  number,
127
198
  {
@@ -131,10 +202,10 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
131
202
  reject: (reason: any) => void;
132
203
  }
133
204
  > = new Map();
134
- let stacksIndex: number = 0;
135
- let iteratorIndex: number = 0;
136
- let streamResult: any = undefined;
137
- let streamResultFetched = withResolvers<undefined>();
205
+ let stacksIndex = 0;
206
+ let iteratorIndex = 0;
207
+ let streamResult: any;
208
+ const streamResultFetched = withResolvers<undefined>();
138
209
 
139
210
  const timeout = stargateOptions?.timeout ?? options?.timeout ?? 6000;
140
211
  const timer = setTimeout(() => {
@@ -151,7 +222,6 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
151
222
  streamResultFetched.reject([{ REQUEST_FAIL: error }, null, { executeId: "unknown" }]);
152
223
  clearTimeout(timer);
153
224
  }
154
- return;
155
225
  } else {
156
226
  const index = ++stacksIndex;
157
227
  if (stacks.has(index)) {
@@ -174,10 +244,15 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
174
244
  iterator.return();
175
245
  });
176
246
  try {
247
+ await eventManager.emit("milkio:executeBefore", { path: path as string, options: options as any });
248
+
249
+ const body = TSON.stringify(options!.params) ?? "";
250
+ await eventManager.emit("milkio:fetchBefore", { path: path as string, options: options as any, body });
251
+
177
252
  const response = await $fetch(url, {
178
253
  method: "POST",
179
254
  headers: options!.headers,
180
- body: body,
255
+ body,
181
256
  signal: curRequestController.signal,
182
257
  });
183
258
 
@@ -188,15 +263,13 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
188
263
 
189
264
  await getBytes(response.body!, getLines(getMessages(onmessage)));
190
265
 
191
- // for (const m of _afterExecuteMiddlewares) {
192
- // await m.middleware({ path: path as string, storage: options.storage as ClientStorage, result: { value: undefined } });
193
- // }
194
-
195
266
  await iterator.return();
196
267
  } catch (err) {
197
268
  if (!curRequestController.signal.aborted) curRequestController.abort();
269
+ const error = { REQUEST_FAIL: err };
270
+ await eventManager.emit("milkio:executeError", { handleError, path: path as string, options: options as any, error });
198
271
  await iterator.throw(err);
199
- streamResultFetched.reject([{ REQUEST_FAIL: err }, null, { executeId: "unknown" }]);
272
+ streamResultFetched.reject([error, null, { executeId: "unknown" }]);
200
273
  }
201
274
  }
202
275
 
@@ -258,7 +331,7 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
258
331
  }
259
332
  }
260
333
  },
261
- cookbook: {
334
+ __cookbook: {
262
335
  subscribe: async (baseUrl: string) => {
263
336
  const headers = {
264
337
  "Content-Type": "application/json",
@@ -276,8 +349,8 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
276
349
  reject: (reason: any) => void;
277
350
  }
278
351
  > = new Map();
279
- let stacksIndex: number = 0;
280
- let iteratorIndex: number = 0;
352
+ let stacksIndex = 0;
353
+ let iteratorIndex = 0;
281
354
 
282
355
  const onmessage = (event: EventSourceMessage) => {
283
356
  const index = ++stacksIndex;
@@ -301,8 +374,8 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
301
374
  try {
302
375
  const response = await $fetch(`${baseUrl}/$subscribe`, {
303
376
  method: "POST",
304
- headers: headers,
305
- body: body,
377
+ headers,
378
+ body,
306
379
  signal: curRequestController.signal,
307
380
  });
308
381
 
@@ -363,10 +436,11 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
363
436
  },
364
437
  },
365
438
  async ping(options?: { timeout?: number }): Promise<Ping> {
439
+ // biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
366
440
  return await new Promise<Ping>(async (resolve) => {
367
- const url = (await baseUrl) + "/generate_204";
441
+ const url = `${await baseUrl}/generate_204`;
368
442
  const timeout = stargateOptions?.timeout ?? options?.timeout ?? 6000;
369
- let startsTime = Date.now();
443
+ const startsTime = Date.now();
370
444
  const timer = setTimeout(() => {
371
445
  const endsTime = Date.now();
372
446
  resolve([{ connect: false, delay: endsTime - startsTime, error: { REQUEST_TIMEOUT: { timeout, message: `Execute timeout after ${timeout}ms.` } } }, null]);
@@ -377,13 +451,13 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
377
451
  const endsTime = Date.now();
378
452
  clearTimeout(timer);
379
453
  if (response.status !== 204) {
380
- resolve([{ connect: false, delay: endsTime - startsTime, error: { REQUEST_FAIL: { response, status: response.status, message: `Status code not 204` } } }, null]);
454
+ resolve([{ connect: false, delay: endsTime - startsTime, error: { REQUEST_FAIL: { response, status: response.status, message: "Status code not 204" } } }, null]);
381
455
  }
382
456
 
383
457
  resolve([null, { connect: true, delay: endsTime - startsTime, serverTimestamp: Number(response.headers.get("Content-Type")!.substring(17)) }]);
384
458
  } catch (error: any) {
385
459
  const endsTime = Date.now();
386
- return [{ connect: false, delay: endsTime - startsTime, error: error }, null];
460
+ return [{ connect: false, delay: endsTime - startsTime, error }, null];
387
461
  }
388
462
  });
389
463
  },
@@ -392,12 +466,12 @@ export async function createStargate<Generated extends { routeSchema: any; rejec
392
466
  return stargate;
393
467
  }
394
468
 
395
- export type ExecuteStreamOptions = {
469
+ export interface ExecuteStreamOptions {
396
470
  headers?: Record<string, string>;
397
471
  timeout?: number;
398
- };
472
+ }
399
473
 
400
- export type ApiSchemaExtend = {
474
+ export interface ApiSchemaExtend {
401
475
  apiValidator: {
402
476
  generatedAt: number;
403
477
  validate: Record<any, any>;
@@ -405,7 +479,7 @@ export type ApiSchemaExtend = {
405
479
  apiMethodsSchema: Record<any, any>;
406
480
  apiMethodsTypeSchema: Record<any, any>;
407
481
  apiTestsSchema: Record<any, any>;
408
- };
482
+ }
409
483
 
410
484
  export type FailCodeExtend = Record<any, (...args: Array<any>) => any>;
411
485
 
@@ -413,27 +487,27 @@ export type BootstrapMiddleware = (data: { storage: ClientStorage }) => Promise<
413
487
  export type BeforeExecuteMiddleware = (data: { path: string; params: any; headers: Record<string, string>; storage: ClientStorage }) => Promise<void> | void;
414
488
  export type AfterExecuteMiddleware = (data: { path: string; result: { value: any }; storage: ClientStorage }) => Promise<void> | void;
415
489
 
416
- export type MiddlewareOptions = {
490
+ export interface MiddlewareOptions {
417
491
  bootstrap?: BootstrapMiddleware;
418
492
  beforeExecute?: BeforeExecuteMiddleware;
419
493
  afterExecute?: AfterExecuteMiddleware;
420
- };
494
+ }
421
495
 
422
- export type ClientStorage = {
496
+ export interface ClientStorage {
423
497
  getItem: (key: string) => Promise<string | null>;
424
498
  setItem: (key: string, value: string) => Promise<void>;
425
499
  removeItem: (key: string) => Promise<void>;
426
- };
500
+ }
427
501
 
428
- export type ExecuteResultSuccess<Result> = {
502
+ export interface ExecuteResultSuccess<Result> {
429
503
  executeId: string;
430
504
  success: true;
431
505
  data: Result;
432
- };
506
+ }
433
507
 
434
508
  export type GeneratorGeneric<T> = T extends AsyncGenerator<infer I> ? I : never;
435
509
 
436
- export type FlattenKeys<T extends any, Prefix extends string = ""> = {
510
+ export type FlattenKeys<T, Prefix extends string = ""> = {
437
511
  [K in keyof T]: T[K] extends object ? FlattenKeys<T[K], `${Prefix}${Exclude<K, symbol>}.`> : `$input.${Prefix}${Exclude<K, symbol>}`;
438
512
  }[keyof T];
439
513
 
@@ -457,18 +531,12 @@ export interface EventSourceMessage {
457
531
  export async function getBytes(stream: ReadableStream<Uint8Array>, onChunk: (arr: Uint8Array) => void) {
458
532
  const reader = stream.getReader();
459
533
  let result: ReadableStreamReadResult<Uint8Array>;
534
+ // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
460
535
  while (!(result = await reader.read()).done) {
461
536
  onChunk(result.value);
462
537
  }
463
538
  }
464
539
 
465
- const enum ControlChars {
466
- NewLine = 10,
467
- CarriageReturn = 13,
468
- Space = 32,
469
- Colon = 58,
470
- }
471
-
472
540
  /**
473
541
  * Parses arbitary byte chunks into EventSource line buffers.
474
542
  * Each line should be of the format "field: value" and ends with \r, \n, or \r\n.
@@ -496,7 +564,7 @@ export function getLines(onLine: (line: Uint8Array, fieldLength: number) => void
496
564
  let lineStart = 0; // index where the current line starts
497
565
  while (position < bufLength) {
498
566
  if (discardTrailingNewline) {
499
- if (buffer[position] === ControlChars.NewLine) {
567
+ if (buffer[position] === 10) {
500
568
  lineStart = ++position; // skip to next char
501
569
  }
502
570
 
@@ -507,16 +575,16 @@ export function getLines(onLine: (line: Uint8Array, fieldLength: number) => void
507
575
  let lineEnd = -1; // index of the \r or \n char
508
576
  for (; position < bufLength && lineEnd === -1; ++position) {
509
577
  switch (buffer[position]) {
510
- case ControlChars.Colon:
578
+ case 58:
511
579
  if (fieldLength === -1) {
512
580
  // first colon in line
513
581
  fieldLength = position - lineStart;
514
582
  }
515
583
  break;
516
- // @ts-ignore:7029 \r case below should fallthrough to \n:
517
- case ControlChars.CarriageReturn:
584
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: <explanation>
585
+ case 13:
518
586
  discardTrailingNewline = true;
519
- case ControlChars.NewLine:
587
+ case 10:
520
588
  lineEnd = position;
521
589
  break;
522
590
  }
@@ -567,14 +635,14 @@ export function getMessages(onMessage?: (msg: EventSourceMessage) => void) {
567
635
  // line is of format "<field>:<value>" or "<field>: <value>"
568
636
  // https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
569
637
  const field = decoder.decode(line.subarray(0, fieldLength));
570
- const valueOffset = fieldLength + (line[fieldLength + 1] === ControlChars.Space ? 2 : 1);
638
+ const valueOffset = fieldLength + (line[fieldLength + 1] === 32 ? 2 : 1);
571
639
  const value = decoder.decode(line.subarray(valueOffset));
572
640
 
573
641
  switch (field) {
574
642
  case "data":
575
643
  // if this message already has data, append the new value to the old.
576
644
  // otherwise, just set to the new value:
577
- message.data = message.data ? message.data + "\n" + value : value; // otherwise,
645
+ message.data = message.data ? `${message.data}\n${value}` : value; // otherwise,
578
646
  break;
579
647
  }
580
648
  }
package/package.json CHANGED
@@ -1,15 +1,12 @@
1
1
  {
2
2
  "name": "@milkio/stargate",
3
- "module": "index.ts",
4
- "version": "1.0.0-alpha.1",
5
3
  "type": "module",
4
+ "version": "1.0.0-alpha.100",
5
+ "module": "index.ts",
6
6
  "dependencies": {
7
7
  "@southern-aurora/tson": "^2.0.2"
8
8
  },
9
9
  "devDependencies": {
10
10
  "@types/bun": "latest"
11
- },
12
- "peerDependencies": {
13
- "typescript": "5.6.0"
14
11
  }
15
12
  }
package/tsconfig.json CHANGED
@@ -1,27 +1,28 @@
1
1
  {
2
2
  "compilerOptions": {
3
+ "target": "ESNext",
4
+ "jsx": "react-jsx",
5
+ "erasableSyntaxOnly":true,
3
6
  // Enable latest features
4
7
  "lib": ["ESNext", "DOM"],
5
- "target": "ESNext",
6
- "module": "ESNext",
7
8
  "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
9
+ "module": "ESNext",
10
10
 
11
11
  // Bundler mode
12
12
  "moduleResolution": "bundler",
13
13
  "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "noEmit": true,
14
+ "allowJs": true,
16
15
 
17
16
  // Best practices
18
17
  "strict": true,
19
- "skipLibCheck": true,
20
18
  "noFallthroughCasesInSwitch": true,
21
19
 
20
+ "noPropertyAccessFromIndexSignature": false,
22
21
  // Some stricter flags (disabled by default)
23
22
  "noUnusedLocals": false,
24
23
  "noUnusedParameters": false,
25
- "noPropertyAccessFromIndexSignature": false
24
+ "noEmit": true,
25
+ "verbatimModuleSyntax": true,
26
+ "skipLibCheck": true
26
27
  }
27
28
  }