facility-core 2.2.0 → 2.3.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/package.json +2 -2
- package/src/facilityCore.d.ts +22 -0
- package/src/facilityCore.js +151 -1
- package/src/facilityCore.js.map +1 -1
- package/src/facilityCore.ts +181 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "facility-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Common code for the Facility API Framework.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsc && eslint src --ext .ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"eslint": "^7.28.0",
|
|
28
28
|
"eslint-config-prettier": "^8.3.0",
|
|
29
29
|
"eslint-plugin-prettier": "^3.4.0",
|
|
30
|
-
"mocha": "^
|
|
30
|
+
"mocha": "^10.0.0",
|
|
31
31
|
"prettier": "^2.3.1",
|
|
32
32
|
"typescript": "~3.9.9"
|
|
33
33
|
}
|
package/src/facilityCore.d.ts
CHANGED
|
@@ -41,9 +41,11 @@ export declare namespace HttpClientUtility {
|
|
|
41
41
|
/** The minimal fetch response. */
|
|
42
42
|
interface IFetchResponse {
|
|
43
43
|
status: number;
|
|
44
|
+
ok?: boolean;
|
|
44
45
|
headers: {
|
|
45
46
|
get(name: string): string | null;
|
|
46
47
|
};
|
|
48
|
+
body?: IWebReadableStream | INodeReadableStream | null;
|
|
47
49
|
json(): Promise<unknown>;
|
|
48
50
|
}
|
|
49
51
|
/** A fetch response with any fetched content. */
|
|
@@ -53,10 +55,30 @@ export declare namespace HttpClientUtility {
|
|
|
53
55
|
/** The fetched JSON, if any. */
|
|
54
56
|
json?: unknown;
|
|
55
57
|
}
|
|
58
|
+
/** Web Streams API ReadableStream (browser). */
|
|
59
|
+
interface IWebReadableStream {
|
|
60
|
+
getReader(): {
|
|
61
|
+
read(): Promise<{
|
|
62
|
+
done: boolean;
|
|
63
|
+
value: Uint8Array;
|
|
64
|
+
}>;
|
|
65
|
+
cancel(): Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Node.js ReadableStream. */
|
|
69
|
+
interface INodeReadableStream {
|
|
70
|
+
on(event: 'data', listener: (chunk: Uint8Array | ArrayBuffer) => void): void;
|
|
71
|
+
on(event: 'end', listener: () => void): void;
|
|
72
|
+
on(event: 'error', listener: (err: Error) => void): void;
|
|
73
|
+
off?(event: string, listener: (...args: unknown[]) => void): void;
|
|
74
|
+
removeAllListeners?(): void;
|
|
75
|
+
}
|
|
56
76
|
/** Fetch JSON using the specified fetch, URI, and request. */
|
|
57
77
|
function fetchResponse(fetch: IFetch, uri: string, request: IFetchRequest, context?: unknown): Promise<IFetchedResponseWithContent>;
|
|
58
78
|
/** Creates an error result for the specified response. */
|
|
59
79
|
function createResponseError(status: number, json?: unknown): IServiceResultBase;
|
|
60
80
|
/** Creates an error result for a required request field. */
|
|
61
81
|
function createRequiredRequestFieldError(name: string): IServiceResultBase;
|
|
82
|
+
/** Creates an async iterable stream from a fetch SSE response. */
|
|
83
|
+
function createFetchEventStream<T>(fetchFunc: IFetch, url: string, fetchRequest: IFetchRequest, context?: unknown): Promise<IServiceResult<AsyncIterable<IServiceResult<T>>>>;
|
|
62
84
|
}
|
package/src/facilityCore.js
CHANGED
|
@@ -63,12 +63,162 @@ var HttpClientUtility;
|
|
|
63
63
|
return {
|
|
64
64
|
error: {
|
|
65
65
|
code: 'InvalidRequest',
|
|
66
|
-
message: `
|
|
66
|
+
message: `'${name}' is required.`,
|
|
67
67
|
},
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
HttpClientUtility.createRequiredRequestFieldError = createRequiredRequestFieldError;
|
|
71
|
+
/** Creates an async iterable stream from a fetch SSE response. */
|
|
72
|
+
function createFetchEventStream(fetchFunc, url, fetchRequest, context) {
|
|
73
|
+
return new Promise((outerResolve, outerReject) => {
|
|
74
|
+
fetchFunc(url, fetchRequest, context)
|
|
75
|
+
.then((response) => {
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
outerReject(new Error(`HTTP error! status: ${response.status}`));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!response.body) {
|
|
81
|
+
outerReject(new Error('Response body is null'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const body = response.body;
|
|
85
|
+
if (!isWebReadableStream(body) && !isNodeReadableStream(body)) {
|
|
86
|
+
outerReject(new Error('Response body is not a readable stream'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const asyncIterable = {
|
|
90
|
+
[Symbol.asyncIterator]() {
|
|
91
|
+
const queue = [];
|
|
92
|
+
let resolveNext = null;
|
|
93
|
+
let isDone = false;
|
|
94
|
+
let buffer = '';
|
|
95
|
+
const decoder = new TextDecoder();
|
|
96
|
+
const processLine = (line) => {
|
|
97
|
+
if (line.startsWith('data: ')) {
|
|
98
|
+
const data = line.slice(6);
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(data);
|
|
101
|
+
const result = { value: parsed };
|
|
102
|
+
if (resolveNext) {
|
|
103
|
+
resolveNext({ value: result, done: false });
|
|
104
|
+
resolveNext = null;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
queue.push(result);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (parseError) {
|
|
111
|
+
const errorResult = {
|
|
112
|
+
error: { code: 'InvalidResponse', message: 'Failed to parse SSE data' },
|
|
113
|
+
};
|
|
114
|
+
if (resolveNext) {
|
|
115
|
+
resolveNext({ value: errorResult, done: false });
|
|
116
|
+
resolveNext = null;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
queue.push(errorResult);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const processChunk = (chunk) => {
|
|
125
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
126
|
+
const lines = buffer.split('\n');
|
|
127
|
+
buffer = lines.pop() || '';
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
processLine(line);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const handleError = () => {
|
|
133
|
+
isDone = true;
|
|
134
|
+
if (resolveNext) {
|
|
135
|
+
resolveNext({
|
|
136
|
+
value: {
|
|
137
|
+
error: { code: 'InternalError', message: 'Stream read error' },
|
|
138
|
+
},
|
|
139
|
+
done: false,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const handleEnd = () => {
|
|
144
|
+
isDone = true;
|
|
145
|
+
if (resolveNext) {
|
|
146
|
+
resolveNext({ value: undefined, done: true });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
if (isWebReadableStream(body)) {
|
|
150
|
+
// Handle Web Streams (browser)
|
|
151
|
+
const reader = body.getReader();
|
|
152
|
+
const readStream = () => {
|
|
153
|
+
reader
|
|
154
|
+
.read()
|
|
155
|
+
.then(({ done, value }) => {
|
|
156
|
+
if (done) {
|
|
157
|
+
handleEnd();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
processChunk(value);
|
|
161
|
+
readStream();
|
|
162
|
+
})
|
|
163
|
+
.catch(handleError);
|
|
164
|
+
};
|
|
165
|
+
readStream();
|
|
166
|
+
}
|
|
167
|
+
else if (isNodeReadableStream(body)) {
|
|
168
|
+
// Handle Node.js Streams
|
|
169
|
+
body.on('data', (chunk) => {
|
|
170
|
+
const uint8Array = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
171
|
+
processChunk(uint8Array);
|
|
172
|
+
});
|
|
173
|
+
body.on('end', handleEnd);
|
|
174
|
+
body.on('error', handleError);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
async next() {
|
|
178
|
+
if (queue.length > 0) {
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
180
|
+
return { value: queue.shift(), done: false };
|
|
181
|
+
}
|
|
182
|
+
if (isDone) {
|
|
183
|
+
return { value: undefined, done: true };
|
|
184
|
+
}
|
|
185
|
+
return new Promise((res) => {
|
|
186
|
+
resolveNext = res;
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
async return() {
|
|
190
|
+
isDone = true;
|
|
191
|
+
if (isWebReadableStream(body)) {
|
|
192
|
+
const reader = body.getReader();
|
|
193
|
+
reader.cancel();
|
|
194
|
+
}
|
|
195
|
+
else if (isNodeReadableStream(body)) {
|
|
196
|
+
if (body.removeAllListeners) {
|
|
197
|
+
body.removeAllListeners();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { value: undefined, done: true };
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
outerResolve({ value: asyncIterable });
|
|
206
|
+
})
|
|
207
|
+
.catch((err) => {
|
|
208
|
+
outerReject(err);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
HttpClientUtility.createFetchEventStream = createFetchEventStream;
|
|
71
213
|
})(HttpClientUtility = exports.HttpClientUtility || (exports.HttpClientUtility = {}));
|
|
214
|
+
/** Type guard to check if a stream is a Web ReadableStream. */
|
|
215
|
+
function isWebReadableStream(stream) {
|
|
216
|
+
return typeof stream.getReader === 'function';
|
|
217
|
+
}
|
|
218
|
+
/** Type guard to check if a stream is a Node.js ReadableStream. */
|
|
219
|
+
function isNodeReadableStream(stream) {
|
|
220
|
+
return typeof stream.on === 'function';
|
|
221
|
+
}
|
|
72
222
|
function isServiceError(json) {
|
|
73
223
|
return typeof json === 'object' && json != null && typeof json.code === 'string';
|
|
74
224
|
}
|
package/src/facilityCore.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facilityCore.js","sourceRoot":"","sources":["facilityCore.ts"],"names":[],"mappings":";;;AAgCA,gCAAgC;AAChC,2DAA2D;AAC3D,IAAiB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"facilityCore.js","sourceRoot":"","sources":["facilityCore.ts"],"names":[],"mappings":"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"}
|
package/src/facilityCore.ts
CHANGED
|
@@ -48,9 +48,11 @@ export namespace HttpClientUtility {
|
|
|
48
48
|
/** The minimal fetch response. */
|
|
49
49
|
export interface IFetchResponse {
|
|
50
50
|
status: number;
|
|
51
|
+
ok?: boolean;
|
|
51
52
|
headers: {
|
|
52
53
|
get(name: string): string | null;
|
|
53
54
|
};
|
|
55
|
+
body?: IWebReadableStream | INodeReadableStream | null;
|
|
54
56
|
json(): Promise<unknown>;
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -62,6 +64,23 @@ export namespace HttpClientUtility {
|
|
|
62
64
|
json?: unknown;
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/** Web Streams API ReadableStream (browser). */
|
|
68
|
+
export interface IWebReadableStream {
|
|
69
|
+
getReader(): {
|
|
70
|
+
read(): Promise<{ done: boolean; value: Uint8Array }>;
|
|
71
|
+
cancel(): Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Node.js ReadableStream. */
|
|
76
|
+
export interface INodeReadableStream {
|
|
77
|
+
on(event: 'data', listener: (chunk: Uint8Array | ArrayBuffer) => void): void;
|
|
78
|
+
on(event: 'end', listener: () => void): void;
|
|
79
|
+
on(event: 'error', listener: (err: Error) => void): void;
|
|
80
|
+
off?(event: string, listener: (...args: unknown[]) => void): void;
|
|
81
|
+
removeAllListeners?(): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
const standardErrorCodes: { [index: number]: string } = {
|
|
66
85
|
'304': 'NotModified',
|
|
67
86
|
'400': 'InvalidRequest',
|
|
@@ -127,10 +146,171 @@ export namespace HttpClientUtility {
|
|
|
127
146
|
return {
|
|
128
147
|
error: {
|
|
129
148
|
code: 'InvalidRequest',
|
|
130
|
-
message: `
|
|
149
|
+
message: `'${name}' is required.`,
|
|
131
150
|
},
|
|
132
151
|
};
|
|
133
152
|
}
|
|
153
|
+
|
|
154
|
+
/** Creates an async iterable stream from a fetch SSE response. */
|
|
155
|
+
export function createFetchEventStream<T>(
|
|
156
|
+
fetchFunc: IFetch,
|
|
157
|
+
url: string,
|
|
158
|
+
fetchRequest: IFetchRequest,
|
|
159
|
+
context?: unknown
|
|
160
|
+
): Promise<IServiceResult<AsyncIterable<IServiceResult<T>>>> {
|
|
161
|
+
return new Promise((outerResolve, outerReject) => {
|
|
162
|
+
fetchFunc(url, fetchRequest, context)
|
|
163
|
+
.then((response) => {
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
outerReject(new Error(`HTTP error! status: ${response.status}`));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!response.body) {
|
|
169
|
+
outerReject(new Error('Response body is null'));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const body = response.body;
|
|
174
|
+
|
|
175
|
+
if (!isWebReadableStream(body) && !isNodeReadableStream(body)) {
|
|
176
|
+
outerReject(new Error('Response body is not a readable stream'));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const asyncIterable: AsyncIterable<IServiceResult<T>> = {
|
|
181
|
+
[Symbol.asyncIterator]() {
|
|
182
|
+
const queue: Array<IServiceResult<T>> = [];
|
|
183
|
+
let resolveNext: ((value: IteratorResult<IServiceResult<T>>) => void) | null = null;
|
|
184
|
+
let isDone = false;
|
|
185
|
+
let buffer = '';
|
|
186
|
+
const decoder = new TextDecoder();
|
|
187
|
+
|
|
188
|
+
const processLine = (line: string) => {
|
|
189
|
+
if (line.startsWith('data: ')) {
|
|
190
|
+
const data = line.slice(6);
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(data) as T;
|
|
193
|
+
const result: IServiceResult<T> = { value: parsed };
|
|
194
|
+
if (resolveNext) {
|
|
195
|
+
resolveNext({ value: result, done: false });
|
|
196
|
+
resolveNext = null;
|
|
197
|
+
} else {
|
|
198
|
+
queue.push(result);
|
|
199
|
+
}
|
|
200
|
+
} catch (parseError) {
|
|
201
|
+
const errorResult: IServiceResult<T> = {
|
|
202
|
+
error: { code: 'InvalidResponse', message: 'Failed to parse SSE data' },
|
|
203
|
+
};
|
|
204
|
+
if (resolveNext) {
|
|
205
|
+
resolveNext({ value: errorResult, done: false });
|
|
206
|
+
resolveNext = null;
|
|
207
|
+
} else {
|
|
208
|
+
queue.push(errorResult);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const processChunk = (chunk: Uint8Array) => {
|
|
215
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
216
|
+
const lines = buffer.split('\n');
|
|
217
|
+
buffer = lines.pop() || '';
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
processLine(line);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const handleError = () => {
|
|
224
|
+
isDone = true;
|
|
225
|
+
if (resolveNext) {
|
|
226
|
+
resolveNext({
|
|
227
|
+
value: {
|
|
228
|
+
error: { code: 'InternalError', message: 'Stream read error' },
|
|
229
|
+
},
|
|
230
|
+
done: false,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const handleEnd = () => {
|
|
236
|
+
isDone = true;
|
|
237
|
+
if (resolveNext) {
|
|
238
|
+
resolveNext({ value: undefined, done: true });
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (isWebReadableStream(body)) {
|
|
243
|
+
// Handle Web Streams (browser)
|
|
244
|
+
const reader = body.getReader();
|
|
245
|
+
const readStream = (): void => {
|
|
246
|
+
reader
|
|
247
|
+
.read()
|
|
248
|
+
.then(({ done, value }: { done: boolean; value: Uint8Array }) => {
|
|
249
|
+
if (done) {
|
|
250
|
+
handleEnd();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
processChunk(value);
|
|
254
|
+
readStream();
|
|
255
|
+
})
|
|
256
|
+
.catch(handleError);
|
|
257
|
+
};
|
|
258
|
+
readStream();
|
|
259
|
+
} else if (isNodeReadableStream(body)) {
|
|
260
|
+
// Handle Node.js Streams
|
|
261
|
+
body.on('data', (chunk: Uint8Array | ArrayBuffer) => {
|
|
262
|
+
const uint8Array = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
263
|
+
processChunk(uint8Array);
|
|
264
|
+
});
|
|
265
|
+
body.on('end', handleEnd);
|
|
266
|
+
body.on('error', handleError);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
async next() {
|
|
271
|
+
if (queue.length > 0) {
|
|
272
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
273
|
+
return { value: queue.shift()!, done: false };
|
|
274
|
+
}
|
|
275
|
+
if (isDone) {
|
|
276
|
+
return { value: undefined, done: true };
|
|
277
|
+
}
|
|
278
|
+
return new Promise((res) => {
|
|
279
|
+
resolveNext = res;
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
async return() {
|
|
283
|
+
isDone = true;
|
|
284
|
+
if (isWebReadableStream(body)) {
|
|
285
|
+
const reader = body.getReader();
|
|
286
|
+
reader.cancel();
|
|
287
|
+
} else if (isNodeReadableStream(body)) {
|
|
288
|
+
if (body.removeAllListeners) {
|
|
289
|
+
body.removeAllListeners();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { value: undefined, done: true };
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
outerResolve({ value: asyncIterable });
|
|
298
|
+
})
|
|
299
|
+
.catch((err) => {
|
|
300
|
+
outerReject(err);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Type guard to check if a stream is a Web ReadableStream. */
|
|
307
|
+
function isWebReadableStream(stream: unknown): stream is HttpClientUtility.IWebReadableStream {
|
|
308
|
+
return typeof (stream as HttpClientUtility.IWebReadableStream).getReader === 'function';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Type guard to check if a stream is a Node.js ReadableStream. */
|
|
312
|
+
function isNodeReadableStream(stream: unknown): stream is HttpClientUtility.INodeReadableStream {
|
|
313
|
+
return typeof (stream as HttpClientUtility.INodeReadableStream).on === 'function';
|
|
134
314
|
}
|
|
135
315
|
|
|
136
316
|
function isServiceError(json: unknown): json is IServiceError {
|