@agentuity/core 0.0.60 → 0.0.62

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.
Files changed (60) hide show
  1. package/dist/error.d.ts +90 -0
  2. package/dist/error.d.ts.map +1 -0
  3. package/dist/error.js +258 -0
  4. package/dist/error.js.map +1 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/json.d.ts +1 -1
  10. package/dist/json.d.ts.map +1 -1
  11. package/dist/json.js +2 -2
  12. package/dist/json.js.map +1 -1
  13. package/dist/services/_util.d.ts +2 -6
  14. package/dist/services/_util.d.ts.map +1 -1
  15. package/dist/services/_util.js +56 -8
  16. package/dist/services/_util.js.map +1 -1
  17. package/dist/services/adapter.d.ts +2 -1
  18. package/dist/services/adapter.d.ts.map +1 -1
  19. package/dist/services/exception.d.ts +20 -6
  20. package/dist/services/exception.d.ts.map +1 -1
  21. package/dist/services/exception.js +2 -11
  22. package/dist/services/exception.js.map +1 -1
  23. package/dist/services/index.d.ts +0 -1
  24. package/dist/services/index.d.ts.map +1 -1
  25. package/dist/services/keyvalue.d.ts.map +1 -1
  26. package/dist/services/keyvalue.js +5 -1
  27. package/dist/services/keyvalue.js.map +1 -1
  28. package/dist/services/objectstore.d.ts.map +1 -1
  29. package/dist/services/objectstore.js +65 -26
  30. package/dist/services/objectstore.js.map +1 -1
  31. package/dist/services/session.d.ts +2 -0
  32. package/dist/services/session.d.ts.map +1 -1
  33. package/dist/services/session.js +1 -0
  34. package/dist/services/session.js.map +1 -1
  35. package/dist/services/stream.d.ts.map +1 -1
  36. package/dist/services/stream.js +25 -9
  37. package/dist/services/stream.js.map +1 -1
  38. package/dist/services/vector.d.ts.map +1 -1
  39. package/dist/services/vector.js +48 -30
  40. package/dist/services/vector.js.map +1 -1
  41. package/dist/workbench-config.d.ts +38 -0
  42. package/dist/workbench-config.d.ts.map +1 -1
  43. package/dist/workbench-config.js +7 -5
  44. package/dist/workbench-config.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/__test__/error.test.ts +431 -0
  47. package/src/error.ts +384 -0
  48. package/src/index.ts +1 -0
  49. package/src/json.ts +2 -2
  50. package/src/services/__test__/vector.test.ts +20 -14
  51. package/src/services/_util.ts +56 -18
  52. package/src/services/adapter.ts +3 -1
  53. package/src/services/exception.ts +7 -9
  54. package/src/services/index.ts +0 -1
  55. package/src/services/keyvalue.ts +6 -1
  56. package/src/services/objectstore.ts +79 -44
  57. package/src/services/session.ts +1 -0
  58. package/src/services/stream.ts +45 -11
  59. package/src/services/vector.ts +99 -30
  60. package/src/workbench-config.ts +16 -5
package/src/error.ts ADDED
@@ -0,0 +1,384 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ // NOTE: these ideas are borrowed from https://github.com/Effect-TS/effect
4
+
5
+ import util from 'node:util';
6
+ import { safeStringify } from './json';
7
+
8
+ type PlainObject = Record<string, any>;
9
+
10
+ const _argsSym = Symbol('@@RichError:plainArgs');
11
+ const _causeSym = Symbol('@@RichError:cause');
12
+ const _metaSym = Symbol('@@RichError:meta'); // reserved for future use (non-enumerable)
13
+ const _structuredSym = Symbol.for('@@StructuredError');
14
+
15
+ const spacer = ' ';
16
+
17
+ export class RichError extends Error {
18
+ // private slots (non-enumerable)
19
+ private [_argsSym]?: PlainObject;
20
+ private [_causeSym]?: unknown;
21
+ private [_metaSym]?: PlainObject;
22
+
23
+ constructor(args?: PlainObject) {
24
+ const message = args?.message ?? undefined;
25
+ const cause = (args?.cause ?? undefined) as unknown;
26
+
27
+ // If platform supports error cause option, pass it to Error constructor
28
+ // (Node 16+ / modern engines)
29
+ if (cause !== undefined) {
30
+ super(message, { cause } as any);
31
+ } else {
32
+ super(message);
33
+ }
34
+
35
+ // correct prototype chain when transpiled to older JS
36
+ Object.setPrototypeOf(this, new.target.prototype);
37
+
38
+ // capture a clean stack (omit this constructor)
39
+ if (typeof (Error as any).captureStackTrace === 'function') {
40
+ (Error as any).captureStackTrace(this, new.target);
41
+ } else {
42
+ // fallback: ensure stack exists
43
+ if (!this.stack) {
44
+ this.stack = new Error(message).stack;
45
+ }
46
+ }
47
+
48
+ if (args && typeof args === 'object') {
49
+ // copy all fields except cause and message (we keep them separate)
50
+ const { cause: _c, message: _m, ...rest } = args;
51
+ if (Object.keys(rest).length > 0) {
52
+ Object.assign(this, rest);
53
+ this[_argsSym] = rest;
54
+ }
55
+ if (cause !== undefined) {
56
+ this[_causeSym] = cause;
57
+ }
58
+ }
59
+ // hide internal symbols and meta (redefine non-enumerable)
60
+ Object.defineProperty(this, _argsSym, {
61
+ value: this[_argsSym],
62
+ enumerable: false,
63
+ writable: true,
64
+ });
65
+ Object.defineProperty(this, _causeSym, {
66
+ value: this[_causeSym],
67
+ enumerable: false,
68
+ writable: true,
69
+ });
70
+ Object.defineProperty(this, _metaSym, {
71
+ value: this[_metaSym] ?? {},
72
+ enumerable: false,
73
+ writable: true,
74
+ });
75
+ }
76
+
77
+ /** Return the stored plain args (if any) */
78
+ get plainArgs(): PlainObject | undefined {
79
+ return this[_argsSym];
80
+ }
81
+
82
+ /** Return the cause (if any) */
83
+ get cause(): unknown | undefined {
84
+ return this[_causeSym];
85
+ }
86
+
87
+ /** Pretty, recursive string representation (follows cause chain). */
88
+ prettyPrint(space = 2): string {
89
+ const lines: string[] = [];
90
+ const visited = new Set<Error>();
91
+
92
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
93
+ let cur: Error | undefined = this;
94
+ let depth = 0;
95
+ while (cur && cur instanceof Error && !visited.has(cur)) {
96
+ const curAny = cur as any;
97
+ visited.add(cur);
98
+ const header = `${cur.name}${curAny._tag && curAny._tag !== cur.name ? ` (${String(curAny._tag)})` : ''}${depth === 0 ? '' : ' [cause]'}`;
99
+ const msg = cur.message !== undefined && cur.message !== '' ? `: ${cur.message}` : '';
100
+ lines.push(header + msg);
101
+
102
+ // include stack if present (limit to first line of stack header for brevity)
103
+ if (cur.stack) {
104
+ lines.push('');
105
+ lines.push(spacer + 'stack trace:');
106
+ const stackLines = String(cur.stack).split('\n').slice(1); // drop first line (it's message)
107
+ if (stackLines.length > 0) {
108
+ // indent stack
109
+ let s = stackLines.map((s) => spacer + spacer + s.trim());
110
+ if (s[s.length - 1].includes('processTicksAndRejections')) {
111
+ s = s.slice(0, s.length - 1);
112
+ }
113
+ lines.push(...s);
114
+ }
115
+ }
116
+
117
+ // include plain args as formatted output (if any)
118
+ if (curAny[_argsSym]) {
119
+ let argsStr = util.formatWithOptions(
120
+ {
121
+ depth: 10,
122
+ sorted: true,
123
+ showHidden: false,
124
+ showProxy: false,
125
+ maxArrayLength: 10,
126
+ maxStringLength: 80 - spacer.length * 2,
127
+ },
128
+ curAny[_argsSym]
129
+ );
130
+ argsStr = argsStr.replace(/^{/, '').replace(/}$/, '');
131
+ const jsonlines = argsStr
132
+ .split('\n')
133
+ .filter(Boolean)
134
+ .map((l: string) => spacer + spacer + l + '\n')
135
+ .join('');
136
+ lines.push('');
137
+ lines.push(spacer + 'context:\n' + jsonlines);
138
+ }
139
+
140
+ // include cause summary if non-Error (could be object)
141
+ const c: unknown = cur.cause ?? curAny[_causeSym];
142
+ if (c && !(c instanceof Error)) {
143
+ lines.push(spacer + 'cause: ' + safeStringify(c, space));
144
+ }
145
+
146
+ cur = c instanceof Error ? c : undefined;
147
+ depth += 1;
148
+ if (cur) lines.push('-- caused by --');
149
+ }
150
+
151
+ return lines.join('\n');
152
+ }
153
+
154
+ toJSON() {
155
+ // Represent as merged: args, public enumerable props, plus well-known fields
156
+ const output: any = {};
157
+
158
+ // include args first (non-enumerable original values)
159
+ if (this[_argsSym]) {
160
+ Object.assign(output, this[_argsSym]);
161
+ }
162
+
163
+ // copy enumerable own props
164
+ for (const k of Object.keys(this)) {
165
+ // skip internal symbols (they are non-enumerable anyway)
166
+ output[k] = (this as any)[k];
167
+ }
168
+
169
+ // canonical fields
170
+ output.name = this.name;
171
+ if (this.message !== undefined) output.message = this.message;
172
+ if (this.stack !== undefined) output.stack = this.stack;
173
+ if (this.cause !== undefined) {
174
+ if (this.cause instanceof Error) {
175
+ output.cause = {
176
+ name: this.cause.name,
177
+ message: this.cause.message,
178
+ stack: (this.cause as any).stack,
179
+ };
180
+ } else {
181
+ output.cause = this.cause;
182
+ }
183
+ }
184
+
185
+ return output;
186
+ }
187
+
188
+ toString(): string {
189
+ // default to pretty print with small indent
190
+ return this.prettyPrint(2);
191
+ }
192
+
193
+ [util.inspect.custom](_depth: number, _options: any) {
194
+ return this.prettyPrint(); // or some concise string/JSON
195
+ }
196
+ }
197
+
198
+ type StructuredErrorConstructor<
199
+ Tag extends string,
200
+ Shape extends PlainObject,
201
+ HasDefault extends boolean,
202
+ > = {
203
+ new (
204
+ args?: Shape extends Record<string, never>
205
+ ? HasDefault extends true
206
+ ? { cause?: unknown }
207
+ : { message?: string; cause?: unknown }
208
+ : HasDefault extends true
209
+ ? Shape & { cause?: unknown }
210
+ : Shape & { message?: string; cause?: unknown }
211
+ ): RichError & { readonly _tag: Tag } & Readonly<Shape>;
212
+ readonly defaultMessage?: string;
213
+ };
214
+
215
+ /**
216
+ * StructuredError factory with automatic tag inference and payload typing.
217
+ *
218
+ * Creates a custom error class with:
219
+ * - A readonly `_tag` property for runtime type discrimination
220
+ * - Automatic stack trace capture
221
+ * - Cause chaining support
222
+ * - Pretty printing with `prettyPrint()` and `toString()`
223
+ * - JSON serialization with `toJSON()`
224
+ *
225
+ * @template Tag - The literal string tag type (automatically inferred from the tag parameter)
226
+ * @param tag - The unique identifier for this error type (used as the error name)
227
+ * @param defaultMessage - Optional default message to use when no message is provided in args
228
+ * @returns A constructor function that can be called directly or with a shape generic
229
+ *
230
+ * @example
231
+ * // Without shape (tag auto-inferred)
232
+ * const NotFound = StructuredError("NotFound")
233
+ * throw new NotFound({ id: 1, message: "nope", cause: someError })
234
+ *
235
+ * @example
236
+ * // With typed shape (tag auto-inferred, shape explicitly typed)
237
+ * const ValidationError = StructuredError("ValidationError")<{ field: string; code: string }>()
238
+ * throw new ValidationError({ field: "email", code: "INVALID", message: "Invalid email" })
239
+ *
240
+ * @example
241
+ * // With default message (message cannot be overridden)
242
+ * const UpgradeRequired = StructuredError("UpgradeRequired", "Upgrade required to access this feature")
243
+ * throw new UpgradeRequired({ feature: "advanced" }) // message is automatically set and cannot be changed
244
+ */
245
+ type StructuredErrorFactory<
246
+ Tag extends string,
247
+ HasDefault extends boolean,
248
+ > = StructuredErrorConstructor<Tag, Record<string, never>, HasDefault> &
249
+ (<Shape extends PlainObject = Record<string, never>>() => StructuredErrorConstructor<
250
+ Tag,
251
+ Shape,
252
+ HasDefault
253
+ >);
254
+
255
+ export function StructuredError<const Tag extends string>(
256
+ tag: Tag,
257
+ defaultMessage: string
258
+ ): StructuredErrorFactory<Tag, true>;
259
+ export function StructuredError<const Tag extends string>(
260
+ tag: Tag
261
+ ): StructuredErrorFactory<Tag, false>;
262
+ export function StructuredError<const Tag extends string>(tag: Tag, defaultMessage?: string) {
263
+ function createErrorClass<
264
+ Shape extends PlainObject = Record<string, never>,
265
+ >(): StructuredErrorConstructor<
266
+ Tag,
267
+ Shape,
268
+ typeof defaultMessage extends string ? true : false
269
+ > {
270
+ // create a unique symbol for this tag's args storage so different factories don't clash
271
+ const tagArgsSym = Symbol.for(`@StructuredError:tag:${tag}`);
272
+
273
+ class Tagged extends RichError {
274
+ // runtime readonly property for tag
275
+ public readonly _tag: Tag = tag as Tag;
276
+ static readonly defaultMessage = defaultMessage;
277
+
278
+ constructor(args?: Shape & { message?: string; cause?: unknown }) {
279
+ // ensure `_tag` isn't copied from args accidentally
280
+ const safeArgs =
281
+ args && typeof args === 'object'
282
+ ? (() => {
283
+ const { _tag: _discard, ...rest } = args as any;
284
+ return rest;
285
+ })()
286
+ : args;
287
+
288
+ // Apply default message if no message provided
289
+ const finalArgs =
290
+ safeArgs && typeof safeArgs === 'object'
291
+ ? { ...safeArgs, message: safeArgs.message ?? defaultMessage }
292
+ : defaultMessage
293
+ ? { message: defaultMessage }
294
+ : safeArgs;
295
+
296
+ super(finalArgs);
297
+ // name the class for nicer stacks
298
+ try {
299
+ Object.defineProperty(this, 'name', { value: tag, configurable: true });
300
+ } catch {
301
+ (this as any).name = tag;
302
+ }
303
+ // mark as StructuredError with brand symbol
304
+ Object.defineProperty(this, _structuredSym, {
305
+ value: true,
306
+ enumerable: false,
307
+ writable: false,
308
+ });
309
+ // store tag args symbol to hide anything specific to this factory (non-enumerable)
310
+ Object.defineProperty(this, tagArgsSym, {
311
+ value: safeArgs,
312
+ enumerable: false,
313
+ writable: true,
314
+ });
315
+ }
316
+ }
317
+
318
+ // set prototype name (works in many engines)
319
+ try {
320
+ Object.defineProperty(Tagged, 'name', { value: String(tag) });
321
+ } catch {
322
+ (Tagged as any).name = tag;
323
+ }
324
+
325
+ return Tagged as unknown as StructuredErrorConstructor<
326
+ Tag,
327
+ Shape,
328
+ typeof defaultMessage extends string ? true : false
329
+ >;
330
+ }
331
+
332
+ // Create a callable constructor: can be used directly or called with generics
333
+ type WithShape = <
334
+ Shape extends Record<string, any> = Record<string, never>,
335
+ >() => StructuredErrorConstructor<
336
+ Tag,
337
+ Shape,
338
+ typeof defaultMessage extends string ? true : false
339
+ >;
340
+ type Result = StructuredErrorConstructor<
341
+ Tag,
342
+ Record<string, never>,
343
+ typeof defaultMessage extends string ? true : false
344
+ > &
345
+ WithShape;
346
+
347
+ const baseClass = createErrorClass<Record<string, never>>();
348
+
349
+ // Use a Proxy to intercept calls and return shaped classes
350
+ const callable = new Proxy(baseClass, {
351
+ apply(_target, _thisArg, _args) {
352
+ // When called as a function (for generic type application), return a new class
353
+ // This happens when TypeScript sees: StructuredError("Tag")<Shape>()
354
+ return createErrorClass();
355
+ },
356
+ });
357
+
358
+ return callable as Result;
359
+ }
360
+
361
+ /**
362
+ * Returns true if the error passed is an instance of a StructuredObject
363
+ *
364
+ * @param err the error object
365
+ * @returns true if err is a StructuredError
366
+ *
367
+ * @example
368
+ * const UpgradeRequired = StructuredError("UpgradeRequired", "Upgrade required to access this feature")
369
+ * try {
370
+ * throw UpgradeRequired();
371
+ * } catch (ex) {
372
+ * if (isStructuredError(ex)) {
373
+ * console.log(ex._tag);
374
+ * }
375
+ * }
376
+ */
377
+ export function isStructuredError(err: unknown): err is RichError & { _tag: string } {
378
+ return (
379
+ typeof err === 'object' &&
380
+ err !== null &&
381
+ (_structuredSym in err || err instanceof RichError) &&
382
+ typeof (err as any)._tag === 'string'
383
+ );
384
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './error';
1
2
  export * from './json';
2
3
  export * from './logger';
3
4
  export * from './services';
package/src/json.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @param obj - The object to stringify
4
4
  * @returns JSON string representation
5
5
  */
6
- export function safeStringify(obj: unknown): string {
6
+ export function safeStringify(obj: unknown, space?: number | string): string {
7
7
  const visited = new WeakSet();
8
8
 
9
9
  function replacer(_key: string, value: unknown): unknown {
@@ -22,5 +22,5 @@ export function safeStringify(obj: unknown): string {
22
22
  return value;
23
23
  }
24
24
 
25
- return JSON.stringify(obj, replacer);
25
+ return JSON.stringify(obj, replacer, space);
26
26
  }
@@ -34,7 +34,7 @@ describe('VectorStorageService', () => {
34
34
  const service = new VectorStorageService(baseUrl, adapter);
35
35
 
36
36
  await expect(service.upsert('my-vectors')).rejects.toThrow(
37
- 'At least one document is required for upsert'
37
+ 'Vector storage requires at least one document for this method'
38
38
  );
39
39
  });
40
40
 
@@ -43,7 +43,7 @@ describe('VectorStorageService', () => {
43
43
  const service = new VectorStorageService(baseUrl, adapter);
44
44
 
45
45
  await expect(service.upsert('my-vectors', { key: '', document: 'text' })).rejects.toThrow(
46
- 'Each document must have a non-empty key'
46
+ 'Vector storage requires each document to have a non-empty key'
47
47
  );
48
48
  });
49
49
 
@@ -53,7 +53,9 @@ describe('VectorStorageService', () => {
53
53
 
54
54
  await expect(
55
55
  service.upsert('my-vectors', { key: 'k1' } as unknown as VectorUpsertParams)
56
- ).rejects.toThrow('Each document must have either embeddings or document text');
56
+ ).rejects.toThrow(
57
+ 'Vector storage requires each document to have either embeddings or document property'
58
+ );
57
59
  });
58
60
 
59
61
  test('rejects empty embeddings array', async () => {
@@ -61,7 +63,7 @@ describe('VectorStorageService', () => {
61
63
  const service = new VectorStorageService(baseUrl, adapter);
62
64
 
63
65
  await expect(service.upsert('my-vectors', { key: 'k1', embeddings: [] })).rejects.toThrow(
64
- 'Embeddings must be a non-empty array of numbers'
66
+ 'Vector storage requires each embeddings property to have a non-empty array of numbers'
65
67
  );
66
68
  });
67
69
 
@@ -70,7 +72,7 @@ describe('VectorStorageService', () => {
70
72
  const service = new VectorStorageService(baseUrl, adapter);
71
73
 
72
74
  await expect(service.upsert('my-vectors', { key: 'k1', document: ' ' })).rejects.toThrow(
73
- 'Document must be a non-empty string'
75
+ 'Vector storage requires each document property to have a non-empty string value'
74
76
  );
75
77
  });
76
78
  });
@@ -90,7 +92,7 @@ describe('VectorStorageService', () => {
90
92
  const service = new VectorStorageService(baseUrl, adapter);
91
93
 
92
94
  await expect(service.get('my-vectors', '')).rejects.toThrow(
93
- 'Key is required and must be a non-empty string'
95
+ 'Vector storage key is required and must be a non-empty string'
94
96
  );
95
97
  });
96
98
  });
@@ -119,7 +121,7 @@ describe('VectorStorageService', () => {
119
121
  const service = new VectorStorageService(baseUrl, adapter);
120
122
 
121
123
  await expect(service.getMany('my-vectors', 'key1', '', 'key3')).rejects.toThrow(
122
- 'All keys must be non-empty strings'
124
+ 'Vector storage requires all keys to be non-empty strings'
123
125
  );
124
126
  });
125
127
  });
@@ -139,7 +141,7 @@ describe('VectorStorageService', () => {
139
141
  const service = new VectorStorageService(baseUrl, adapter);
140
142
 
141
143
  await expect(service.search('my-vectors', { query: '' })).rejects.toThrow(
142
- 'Query is required and must be a non-empty string'
144
+ 'Vector storage query property is required and must be a non-empty string'
143
145
  );
144
146
  });
145
147
 
@@ -148,7 +150,7 @@ describe('VectorStorageService', () => {
148
150
  const service = new VectorStorageService(baseUrl, adapter);
149
151
 
150
152
  await expect(service.search('my-vectors', { query: 'test', limit: -5 })).rejects.toThrow(
151
- 'Limit must be a positive number'
153
+ 'Vector storage limit property must be positive number'
152
154
  );
153
155
  });
154
156
 
@@ -157,7 +159,7 @@ describe('VectorStorageService', () => {
157
159
  const service = new VectorStorageService(baseUrl, adapter);
158
160
 
159
161
  await expect(service.search('my-vectors', { query: 'test', limit: 0 })).rejects.toThrow(
160
- 'Limit must be a positive number'
162
+ 'Vector storage limit property must be positive number'
161
163
  );
162
164
  });
163
165
 
@@ -167,7 +169,9 @@ describe('VectorStorageService', () => {
167
169
 
168
170
  await expect(
169
171
  service.search('my-vectors', { query: 'test', similarity: -0.1 })
170
- ).rejects.toThrow('Similarity must be a number between 0.0 and 1.0');
172
+ ).rejects.toThrow(
173
+ 'Vector storage similarity property must be a number between 0.0 and 1.0'
174
+ );
171
175
  });
172
176
 
173
177
  test('rejects similarity > 1', async () => {
@@ -176,7 +180,9 @@ describe('VectorStorageService', () => {
176
180
 
177
181
  await expect(
178
182
  service.search('my-vectors', { query: 'test', similarity: 1.5 })
179
- ).rejects.toThrow('Similarity must be a number between 0.0 and 1.0');
183
+ ).rejects.toThrow(
184
+ 'Vector storage similarity property must be a number between 0.0 and 1.0'
185
+ );
180
186
  });
181
187
 
182
188
  test('rejects invalid metadata', async () => {
@@ -188,7 +194,7 @@ describe('VectorStorageService', () => {
188
194
  query: 'test',
189
195
  metadata: null as unknown as Record<string, unknown>,
190
196
  })
191
- ).rejects.toThrow('Metadata must be a valid object');
197
+ ).rejects.toThrow('Vector storage metadata property must be a valid object');
192
198
  });
193
199
 
194
200
  test('accepts boundary similarity values', async () => {
@@ -221,7 +227,7 @@ describe('VectorStorageService', () => {
221
227
  const service = new VectorStorageService(baseUrl, adapter);
222
228
 
223
229
  await expect(service.delete('my-vectors', 'key1', '', 'key3')).rejects.toThrow(
224
- 'All keys must be non-empty strings'
230
+ 'Vector storage requires all keys to be non-empty strings'
225
231
  );
226
232
  });
227
233
 
@@ -1,12 +1,7 @@
1
1
  import { safeStringify } from '../json';
2
- import type { Body } from './adapter';
2
+ import type { Body, HttpMethod } from './adapter';
3
3
  import { ServiceException } from './exception';
4
4
 
5
- /**
6
- * Valid HTTP methods for service calls
7
- */
8
- export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
9
-
10
5
  export const buildUrl = (
11
6
  base: string,
12
7
  path: string,
@@ -29,13 +24,26 @@ export async function toServiceException(
29
24
  method: HttpMethod,
30
25
  url: string,
31
26
  response: Response
32
- ): Promise<ServiceException> {
27
+ ): Promise<InstanceType<typeof ServiceException>> {
28
+ const sessionId = response.headers.get('x-session-id');
33
29
  switch (response.status) {
34
30
  case 401:
35
31
  case 403:
36
- return new ServiceException('Unauthorized', method, url, response.status);
32
+ return new ServiceException({
33
+ message: 'Unauthorized',
34
+ method,
35
+ url,
36
+ statusCode: response.status,
37
+ sessionId,
38
+ });
37
39
  case 404:
38
- return new ServiceException('Not Found', method, url, response.status);
40
+ return new ServiceException({
41
+ message: 'Not Found',
42
+ method,
43
+ url,
44
+ statusCode: response.status,
45
+ sessionId,
46
+ });
39
47
  default:
40
48
  }
41
49
  const ct = response.headers.get('content-type');
@@ -43,24 +51,54 @@ export async function toServiceException(
43
51
  try {
44
52
  const payload = (await response.json()) as { message?: string; error?: string };
45
53
  if (payload.error) {
46
- return new ServiceException(payload.error, method, url, response.status);
54
+ return new ServiceException({
55
+ message: payload.error,
56
+ method,
57
+ url,
58
+ statusCode: response.status,
59
+ sessionId,
60
+ });
47
61
  }
48
62
  if (payload.message) {
49
- return new ServiceException(payload.message, method, url, response.status);
63
+ return new ServiceException({
64
+ message: payload.message,
65
+ method,
66
+ url,
67
+ statusCode: response.status,
68
+ sessionId,
69
+ });
50
70
  }
51
- return new ServiceException(JSON.stringify(payload), method, url, response.status);
71
+ return new ServiceException({
72
+ message: JSON.stringify(payload),
73
+ method,
74
+ url,
75
+ statusCode: response.status,
76
+ sessionId,
77
+ });
52
78
  } catch {
53
79
  /** don't worry */
54
80
  }
55
81
  }
56
82
  try {
57
83
  const body = await response.text();
58
- return new ServiceException(body, method, url, response.status);
84
+ return new ServiceException({
85
+ message: body,
86
+ method,
87
+ url,
88
+ statusCode: response.status,
89
+ sessionId,
90
+ });
59
91
  } catch {
60
92
  /* fall through */
61
93
  }
62
94
 
63
- return new ServiceException(response.statusText, method, url, response.status);
95
+ return new ServiceException({
96
+ message: response.statusText,
97
+ method,
98
+ url,
99
+ statusCode: response.status,
100
+ sessionId,
101
+ });
64
102
  }
65
103
 
66
104
  const binaryContentType = 'application/octet-stream';
@@ -126,10 +164,10 @@ export async function fromResponse<T>(
126
164
  if (contentType?.includes(binaryContentType)) {
127
165
  return (await response.arrayBuffer()) as T;
128
166
  }
129
- throw new ServiceException(
130
- `Unsupported content-type: ${contentType}`,
167
+ throw new ServiceException({
168
+ message: `Unsupported content-type: ${contentType}`,
131
169
  method,
132
170
  url,
133
- response.status
134
- );
171
+ statusCode: response.status,
172
+ });
135
173
  }
@@ -14,8 +14,10 @@ export type FetchResponse<T> = FetchErrorResponse | FetchSuccessResponse<T>;
14
14
 
15
15
  export type Body = string | Buffer | ArrayBuffer | ReadableStream;
16
16
 
17
+ export type HttpMethod = 'GET' | 'PUT' | 'POST' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PATCH';
18
+
17
19
  export interface FetchRequest {
18
- method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'HEAD';
20
+ method: HttpMethod;
19
21
  body?: Body;
20
22
  signal?: AbortSignal;
21
23
  contentType?: string;
@@ -1,11 +1,9 @@
1
- export class ServiceException extends Error {
1
+ import { StructuredError } from '../error';
2
+ import { HttpMethod } from './adapter';
3
+
4
+ export const ServiceException = StructuredError('ServiceException')<{
2
5
  statusCode: number;
3
- method: string;
6
+ method: HttpMethod;
4
7
  url: string;
5
- constructor(message: string, method: string, url: string, statusCode: number) {
6
- super(message);
7
- this.method = method;
8
- this.url = url;
9
- this.statusCode = statusCode;
10
- }
11
- }
8
+ sessionId?: string | null;
9
+ }>();
@@ -7,4 +7,3 @@ export * from './session';
7
7
  export * from './stream';
8
8
  export * from './vector';
9
9
  export { buildUrl, toServiceException, toPayload, fromResponse } from './_util';
10
- export type { HttpMethod } from './_util';