@openstax/ts-utils 1.41.0 → 1.41.3
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/dist/cjs/services/authProvider/subrequest.js +1 -2
- package/dist/cjs/services/authProvider/utils/userSubrequest.d.ts +1 -1
- package/dist/cjs/services/authProvider/utils/userSubrequest.js +3 -1
- package/dist/cjs/services/documentStore/dynamoEncoding.js +2 -2
- package/dist/cjs/services/documentStore/unversioned/file-system.d.ts +0 -1
- package/dist/cjs/services/documentStore/unversioned/file-system.js +0 -19
- package/dist/cjs/services/documentStore/versioned/file-system.d.ts +0 -1
- package/dist/cjs/services/documentStore/versioned/file-system.js +1 -12
- package/dist/cjs/services/lrsGateway/batching.d.ts +46 -0
- package/dist/cjs/services/lrsGateway/batching.js +106 -0
- package/dist/cjs/services/lrsGateway/file-system.js +52 -2
- package/dist/cjs/services/lrsGateway/index.d.ts +13 -0
- package/dist/cjs/services/lrsGateway/index.js +151 -55
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/services/authProvider/subrequest.js +1 -2
- package/dist/esm/services/authProvider/utils/userSubrequest.d.ts +1 -1
- package/dist/esm/services/authProvider/utils/userSubrequest.js +3 -1
- package/dist/esm/services/documentStore/dynamoEncoding.js +2 -2
- package/dist/esm/services/documentStore/unversioned/file-system.d.ts +0 -1
- package/dist/esm/services/documentStore/unversioned/file-system.js +0 -19
- package/dist/esm/services/documentStore/versioned/file-system.d.ts +0 -1
- package/dist/esm/services/documentStore/versioned/file-system.js +1 -12
- package/dist/esm/services/lrsGateway/batching.d.ts +46 -0
- package/dist/esm/services/lrsGateway/batching.js +102 -0
- package/dist/esm/services/lrsGateway/file-system.js +52 -2
- package/dist/esm/services/lrsGateway/index.d.ts +13 -0
- package/dist/esm/services/lrsGateway/index.js +151 -22
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/script/bin/.init-params-script.bash.swp +0 -0
- package/dist/cjs/services/documentStore/fileSystemAssert.d.ts +0 -1
- package/dist/cjs/services/documentStore/fileSystemAssert.js +0 -14
- package/dist/esm/services/documentStore/fileSystemAssert.d.ts +0 -1
- package/dist/esm/services/documentStore/fileSystemAssert.js +0 -10
|
@@ -27,8 +27,7 @@ const subrequestAuthProvider = (initializer) => (configProvider) => {
|
|
|
27
27
|
return undefined;
|
|
28
28
|
}
|
|
29
29
|
const user = await (0, userSubrequest_1.loadUserData)(initializer.fetch, await accountsBase(), resolvedCookieName, token);
|
|
30
|
-
|
|
31
|
-
if (user.uuid) {
|
|
30
|
+
if (user) {
|
|
32
31
|
logger.setContext({ user: user.uuid });
|
|
33
32
|
return user;
|
|
34
33
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ApiUser } from '..';
|
|
2
2
|
import { GenericFetch } from '../../../fetch';
|
|
3
|
-
export declare const loadUserData: (fetch: GenericFetch, accountsBase: string, cookieName: string, token: string) => Promise<ApiUser>;
|
|
3
|
+
export declare const loadUserData: (fetch: GenericFetch, accountsBase: string, cookieName: string, token: string) => Promise<ApiUser | undefined>;
|
|
@@ -7,7 +7,9 @@ exports.loadUserData = void 0;
|
|
|
7
7
|
const cookie_1 = __importDefault(require("cookie"));
|
|
8
8
|
const loadUserData = (fetch, accountsBase, cookieName, token) => {
|
|
9
9
|
const headers = { cookie: cookie_1.default.serialize(cookieName, token) };
|
|
10
|
+
// this returns `{"error_id":null}` when the token is invalid
|
|
10
11
|
return fetch(accountsBase.replace(/\/+$/, '') + '/api/user', { headers })
|
|
11
|
-
.then(response => response.json())
|
|
12
|
+
.then(response => response.json())
|
|
13
|
+
.then(data => ('error_id' in data ? undefined : data));
|
|
12
14
|
};
|
|
13
15
|
exports.loadUserData = loadUserData;
|
|
@@ -15,7 +15,7 @@ const encodeDynamoAttribute = (value) => {
|
|
|
15
15
|
if (value === null) {
|
|
16
16
|
return { NULL: true };
|
|
17
17
|
}
|
|
18
|
-
if (value
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
19
|
return { L: value.map(exports.encodeDynamoAttribute) };
|
|
20
20
|
}
|
|
21
21
|
if ((0, guards_1.isPlainObject)(value)) {
|
|
@@ -24,7 +24,7 @@ const encodeDynamoAttribute = (value) => {
|
|
|
24
24
|
throw new Error(`unknown attribute type ${typeof value} with value ${value}.`);
|
|
25
25
|
};
|
|
26
26
|
exports.encodeDynamoAttribute = encodeDynamoAttribute;
|
|
27
|
-
const encodeDynamoDocument = (base) => Object.fromEntries(Object.entries(base).map(([key, value]) => ([key, (0, exports.encodeDynamoAttribute)(value)])));
|
|
27
|
+
const encodeDynamoDocument = (base) => Object.fromEntries(Object.entries(base).filter(([, value]) => value !== undefined).map(([key, value]) => ([key, (0, exports.encodeDynamoAttribute)(value)])));
|
|
28
28
|
exports.encodeDynamoDocument = encodeDynamoDocument;
|
|
29
29
|
const decodeDynamoAttribute = (value) => {
|
|
30
30
|
if (value.S !== undefined) {
|
|
@@ -8,7 +8,6 @@ interface Initializer<C> {
|
|
|
8
8
|
export declare const fileSystemUnversionedDocumentStore: <C extends string = "fileSystem">(initializer: Initializer<C>) => <T extends TDocument<T>>() => (configProvider: { [_key in C]: ConfigProviderForConfig<Config>; }) => <K extends keyof T>(_: {}, hashKey: K, options?: {
|
|
9
9
|
afterWrite?: (item: T) => void | Promise<void>;
|
|
10
10
|
batchAfterWrite?: (items: T[]) => void | Promise<void>;
|
|
11
|
-
skipAssert?: boolean;
|
|
12
11
|
}) => {
|
|
13
12
|
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
14
13
|
getItemsByField: (key: keyof T, value: T[K], pageKey?: string) => Promise<{
|
|
@@ -43,13 +43,10 @@ const __1 = require("../../..");
|
|
|
43
43
|
const config_1 = require("../../../config");
|
|
44
44
|
const errors_1 = require("../../../errors");
|
|
45
45
|
const guards_1 = require("../../../guards");
|
|
46
|
-
const fileSystemAssert_1 = require("../fileSystemAssert");
|
|
47
46
|
const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
|
|
48
|
-
var _a;
|
|
49
47
|
const tableName = (0, config_1.resolveConfigValue)(configProvider[initializer.configSpace || 'fileSystem'].tableName);
|
|
50
48
|
const tablePath = tableName.then((table) => path_1.default.join(initializer.dataDir, table));
|
|
51
49
|
const { mkdir, readdir, readFile, writeFile } = (0, guards_1.ifDefined)(initializer.fs, fsModule);
|
|
52
|
-
const skipAssert = (_a = options === null || options === void 0 ? void 0 : options.skipAssert) !== null && _a !== void 0 ? _a : false;
|
|
53
50
|
const mkTableDir = new Promise((resolve, reject) => tablePath.then((path) => mkdir(path, { recursive: true }, (err) => err && err.code !== 'EEXIST' ? reject(err) : resolve())));
|
|
54
51
|
const hashFilename = (value) => `${(0, __1.hashValue)(value)}.json`;
|
|
55
52
|
const filePath = async (filename) => path_1.default.join(await tablePath, filename);
|
|
@@ -84,8 +81,6 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
|
|
|
84
81
|
return {
|
|
85
82
|
loadAllDocumentsTheBadWay,
|
|
86
83
|
getItemsByField: async (key, value, pageKey) => {
|
|
87
|
-
if (!skipAssert)
|
|
88
|
-
(0, fileSystemAssert_1.assertNoUndefined)(value, [key]);
|
|
89
84
|
const pageSize = 10;
|
|
90
85
|
const items = await loadAllDocumentsTheBadWay();
|
|
91
86
|
const filteredItems = items.filter((item) => item[key] === value);
|
|
@@ -98,20 +93,14 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
|
|
|
98
93
|
},
|
|
99
94
|
batchGetItem: async (ids) => {
|
|
100
95
|
const items = await Promise.all(ids.map((id) => {
|
|
101
|
-
if (!skipAssert)
|
|
102
|
-
(0, fileSystemAssert_1.assertNoUndefined)(id, ['id']);
|
|
103
96
|
return load(hashFilename(id));
|
|
104
97
|
}));
|
|
105
98
|
return items.filter(guards_1.isDefined);
|
|
106
99
|
},
|
|
107
100
|
getItem: (id) => {
|
|
108
|
-
if (!skipAssert)
|
|
109
|
-
(0, fileSystemAssert_1.assertNoUndefined)(id, ['id']);
|
|
110
101
|
return load(hashFilename(id));
|
|
111
102
|
},
|
|
112
103
|
incrementItemAttribute: async (id, attribute) => {
|
|
113
|
-
if (!skipAssert)
|
|
114
|
-
(0, fileSystemAssert_1.assertNoUndefined)(id, ['id']);
|
|
115
104
|
const filename = hashFilename(id);
|
|
116
105
|
const path = await filePath(filename);
|
|
117
106
|
await mkTableDir;
|
|
@@ -142,15 +131,11 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
|
|
|
142
131
|
throw new errors_1.NotFoundError(`Item with ${hashKey.toString()} "${id}" does not exist`);
|
|
143
132
|
}
|
|
144
133
|
const newItem = { ...data, ...item };
|
|
145
|
-
if (!skipAssert)
|
|
146
|
-
(0, fileSystemAssert_1.assertNoUndefined)(newItem);
|
|
147
134
|
return new Promise((resolve, reject) => {
|
|
148
135
|
writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve(newItem));
|
|
149
136
|
});
|
|
150
137
|
},
|
|
151
138
|
putItem: async (item) => {
|
|
152
|
-
if (!skipAssert)
|
|
153
|
-
(0, fileSystemAssert_1.assertNoUndefined)(item);
|
|
154
139
|
const path = await filePath(hashFilename(item[hashKey]));
|
|
155
140
|
await mkTableDir;
|
|
156
141
|
return new Promise((resolve, reject) => {
|
|
@@ -158,8 +143,6 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
|
|
|
158
143
|
});
|
|
159
144
|
},
|
|
160
145
|
createItem: async (item) => {
|
|
161
|
-
if (!skipAssert)
|
|
162
|
-
(0, fileSystemAssert_1.assertNoUndefined)(item);
|
|
163
146
|
const hashed = hashFilename(item[hashKey]);
|
|
164
147
|
const existingItem = await load(hashed);
|
|
165
148
|
if (existingItem) {
|
|
@@ -180,8 +163,6 @@ const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvide
|
|
|
180
163
|
// Process items sequentially to ensure consistent conflict detection
|
|
181
164
|
// Note: concurrency parameter is ignored for filesystem to avoid race conditions
|
|
182
165
|
for (const item of items) {
|
|
183
|
-
if (!skipAssert)
|
|
184
|
-
(0, fileSystemAssert_1.assertNoUndefined)(item);
|
|
185
166
|
try {
|
|
186
167
|
const hashed = hashFilename(item[hashKey]);
|
|
187
168
|
const existingItem = await load(hashed);
|
|
@@ -8,7 +8,6 @@ interface Initializer<C> {
|
|
|
8
8
|
}
|
|
9
9
|
export declare const fileSystemVersionedDocumentStore: <C extends string = "fileSystem">(initializer: Initializer<C>) => <T extends VersionedTDocument<T>>() => (configProvider: { [_key in C]: ConfigProviderForConfig<Config>; }) => <K extends keyof T, A extends undefined | ((...a: any[]) => Promise<VersionedDocumentAuthor>)>(_: {}, hashKey: K, options?: {
|
|
10
10
|
getAuthor?: A;
|
|
11
|
-
skipAssert?: boolean;
|
|
12
11
|
}) => {
|
|
13
12
|
loadAllDocumentsTheBadWay: () => Promise<T[]>;
|
|
14
13
|
getVersions: (id: T[K], startVersion?: number) => Promise<{
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fileSystemVersionedDocumentStore = void 0;
|
|
4
|
-
const fileSystemAssert_1 = require("../fileSystemAssert");
|
|
5
4
|
const file_system_1 = require("../unversioned/file-system");
|
|
6
5
|
const PAGE_LIMIT = 5;
|
|
7
6
|
const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
|
|
8
|
-
|
|
9
|
-
const skipAssert = (_a = options === null || options === void 0 ? void 0 : options.skipAssert) !== null && _a !== void 0 ? _a : false;
|
|
10
|
-
const unversionedDocuments = (0, file_system_1.fileSystemUnversionedDocumentStore)(initializer)()(configProvider)({}, 'id', { skipAssert });
|
|
7
|
+
const unversionedDocuments = (0, file_system_1.fileSystemUnversionedDocumentStore)(initializer)()(configProvider)({}, 'id');
|
|
11
8
|
return {
|
|
12
9
|
loadAllDocumentsTheBadWay: () => {
|
|
13
10
|
return unversionedDocuments.loadAllDocumentsTheBadWay().then(documents => documents.map(document => {
|
|
@@ -15,8 +12,6 @@ const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider)
|
|
|
15
12
|
}));
|
|
16
13
|
},
|
|
17
14
|
getVersions: async (id, startVersion) => {
|
|
18
|
-
if (!skipAssert)
|
|
19
|
-
(0, fileSystemAssert_1.assertNoUndefined)(id, ['id']);
|
|
20
15
|
const item = await unversionedDocuments.getItem(id);
|
|
21
16
|
const versions = item === null || item === void 0 ? void 0 : item.items.reverse();
|
|
22
17
|
if (!versions) {
|
|
@@ -31,8 +26,6 @@ const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider)
|
|
|
31
26
|
};
|
|
32
27
|
},
|
|
33
28
|
getItem: async (id, timestamp) => {
|
|
34
|
-
if (!skipAssert)
|
|
35
|
-
(0, fileSystemAssert_1.assertNoUndefined)(id, ['id']);
|
|
36
29
|
const item = await unversionedDocuments.getItem(id);
|
|
37
30
|
if (timestamp) {
|
|
38
31
|
return item === null || item === void 0 ? void 0 : item.items.find(version => version.timestamp === timestamp);
|
|
@@ -48,8 +41,6 @@ const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider)
|
|
|
48
41
|
save: async (changes) => {
|
|
49
42
|
var _a;
|
|
50
43
|
const document = { ...item, ...changes, timestamp, author };
|
|
51
|
-
if (!skipAssert)
|
|
52
|
-
(0, fileSystemAssert_1.assertNoUndefined)(document);
|
|
53
44
|
const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
|
|
54
45
|
const updated = { ...container, items: [...container.items, document] };
|
|
55
46
|
await unversionedDocuments.putItem(updated);
|
|
@@ -61,8 +52,6 @@ const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider)
|
|
|
61
52
|
var _a;
|
|
62
53
|
const author = (options === null || options === void 0 ? void 0 : options.getAuthor) ? await options.getAuthor(...authorArgs) : authorArgs[0];
|
|
63
54
|
const document = { ...item, timestamp: new Date().getTime(), author };
|
|
64
|
-
if (!skipAssert)
|
|
65
|
-
(0, fileSystemAssert_1.assertNoUndefined)(document);
|
|
66
55
|
const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
|
|
67
56
|
const updated = { ...container, items: [...container.items, document] };
|
|
68
57
|
await unversionedDocuments.putItem(updated);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { GenericFetch, Response } from '../../fetch';
|
|
2
|
+
import { METHOD } from '../../routing';
|
|
3
|
+
export interface BatchRequest {
|
|
4
|
+
id: string;
|
|
5
|
+
path: string;
|
|
6
|
+
method: METHOD;
|
|
7
|
+
queryParams?: Record<string, string>;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
body?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BatchResponse {
|
|
12
|
+
id: string;
|
|
13
|
+
status: number;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
body: string;
|
|
16
|
+
}
|
|
17
|
+
interface BatcherConfig {
|
|
18
|
+
batchEndpoint: string;
|
|
19
|
+
getAuthHeader: () => Promise<string>;
|
|
20
|
+
getHost: () => Promise<string>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Handles automatic request batching using microtask queue scheduling.
|
|
24
|
+
* Batches concurrent requests within the same event loop tick.
|
|
25
|
+
*/
|
|
26
|
+
export declare class RequestBatcher {
|
|
27
|
+
private fetcher;
|
|
28
|
+
private config;
|
|
29
|
+
private pendingRequests;
|
|
30
|
+
private flushScheduled;
|
|
31
|
+
private requestCounter;
|
|
32
|
+
constructor(fetcher: GenericFetch, config: BatcherConfig);
|
|
33
|
+
/**
|
|
34
|
+
* Queue a request for batching. Returns a promise that resolves with the response.
|
|
35
|
+
* Automatically flushes the batch on the next microtask.
|
|
36
|
+
*/
|
|
37
|
+
queueRequest(options: Omit<BatchRequest, 'id'>): Promise<Response>;
|
|
38
|
+
singleRequest(options: Omit<BatchRequest, 'id'>): Promise<Response>;
|
|
39
|
+
/**
|
|
40
|
+
* Flush all pending requests. If only one request is pending, makes a direct HTTP call.
|
|
41
|
+
* Otherwise, sends a batch request to the batch API endpoint.
|
|
42
|
+
*/
|
|
43
|
+
private flush;
|
|
44
|
+
private flushHandler;
|
|
45
|
+
}
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RequestBatcher = void 0;
|
|
4
|
+
const routing_1 = require("../../routing");
|
|
5
|
+
/**
|
|
6
|
+
* Handles automatic request batching using microtask queue scheduling.
|
|
7
|
+
* Batches concurrent requests within the same event loop tick.
|
|
8
|
+
*/
|
|
9
|
+
class RequestBatcher {
|
|
10
|
+
constructor(fetcher, config) {
|
|
11
|
+
this.fetcher = fetcher;
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.pendingRequests = [];
|
|
14
|
+
this.flushScheduled = false;
|
|
15
|
+
this.requestCounter = 0;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Queue a request for batching. Returns a promise that resolves with the response.
|
|
19
|
+
* Automatically flushes the batch on the next microtask.
|
|
20
|
+
*/
|
|
21
|
+
queueRequest(options) {
|
|
22
|
+
const id = `req_${++this.requestCounter}`;
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
this.pendingRequests.push({
|
|
25
|
+
...options,
|
|
26
|
+
id, resolve, reject,
|
|
27
|
+
});
|
|
28
|
+
// Schedule microtask flush if not already scheduled
|
|
29
|
+
if (!this.flushScheduled) {
|
|
30
|
+
this.flushScheduled = true;
|
|
31
|
+
queueMicrotask(() => this.flush());
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async singleRequest(options) {
|
|
36
|
+
const { path, method, queryParams, headers, body } = options;
|
|
37
|
+
const host = await this.config.getHost();
|
|
38
|
+
const query = new URLSearchParams(queryParams || {}).toString();
|
|
39
|
+
const url = host.replace(/\/+$/, '') + path + (query ? `?${query}` : '');
|
|
40
|
+
return await this.fetcher(url, { method, headers, body });
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Flush all pending requests. If only one request is pending, makes a direct HTTP call.
|
|
44
|
+
* Otherwise, sends a batch request to the batch API endpoint.
|
|
45
|
+
*/
|
|
46
|
+
flush() {
|
|
47
|
+
this.flushScheduled = false;
|
|
48
|
+
const requests = this.pendingRequests;
|
|
49
|
+
this.pendingRequests = [];
|
|
50
|
+
this.flushHandler(requests).catch(error => {
|
|
51
|
+
// If batch request fails entirely, reject all pending requests
|
|
52
|
+
requests.forEach(req => req.reject(error));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async flushHandler(requests) {
|
|
56
|
+
if (requests.length === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Single request optimization: bypass batching
|
|
60
|
+
if (requests.length === 1) {
|
|
61
|
+
return this.singleRequest(requests[0]).then(requests[0].resolve);
|
|
62
|
+
}
|
|
63
|
+
// Multiple requests: use batch API
|
|
64
|
+
const host = await this.config.getHost();
|
|
65
|
+
const authHeader = await this.config.getAuthHeader();
|
|
66
|
+
// Build batch request payload
|
|
67
|
+
const batchRequests = requests.map(({ id, path, method, queryParams, headers, body }) => {
|
|
68
|
+
return { id, path, method, queryParams, headers, body };
|
|
69
|
+
});
|
|
70
|
+
// Send batch request
|
|
71
|
+
const batchUrl = host.replace(/\/+$/, '') + this.config.batchEndpoint;
|
|
72
|
+
const batchResponse = await this.fetcher(batchUrl, {
|
|
73
|
+
method: routing_1.METHOD.POST,
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: authHeader,
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'X-Experience-API-Version': '1.0.0',
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({ requests: batchRequests }),
|
|
80
|
+
});
|
|
81
|
+
if (batchResponse.status !== 200) {
|
|
82
|
+
const errorText = await batchResponse.text();
|
|
83
|
+
// all requests will be rejected in the catch
|
|
84
|
+
throw new Error(`Batch request failed with status ${batchResponse.status}: ${errorText}`);
|
|
85
|
+
}
|
|
86
|
+
const batchResult = await batchResponse.json();
|
|
87
|
+
// Map responses back to original requests
|
|
88
|
+
const responseMap = new Map(batchResult.responses.map(r => [r.id, r]));
|
|
89
|
+
requests.forEach(req => {
|
|
90
|
+
const batchResp = responseMap.get(req.id);
|
|
91
|
+
if (!batchResp) {
|
|
92
|
+
req.reject(new Error(`No response for request ${req.id} in batch`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
req.resolve({
|
|
96
|
+
status: batchResp.status,
|
|
97
|
+
headers: {
|
|
98
|
+
get: (name) => { var _a; return ((_a = Object.entries(batchResp.headers).find(([key]) => key.toLowerCase() === name.toLowerCase())) === null || _a === void 0 ? void 0 : _a[1]) || null; },
|
|
99
|
+
},
|
|
100
|
+
json: async () => JSON.parse(batchResp.body),
|
|
101
|
+
text: async () => batchResp.body,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.RequestBatcher = RequestBatcher;
|
|
@@ -45,14 +45,24 @@ const assertions_1 = require("../../assertions");
|
|
|
45
45
|
const config_1 = require("../../config");
|
|
46
46
|
const errors_1 = require("../../errors");
|
|
47
47
|
const guards_1 = require("../../guards");
|
|
48
|
+
const hashValue_1 = require("../../misc/hashValue");
|
|
48
49
|
const pageSize = 5;
|
|
49
50
|
const fileSystemLrsGateway = (initializer) => (configProvider) => ({ authProvider }) => {
|
|
50
51
|
const name = (0, config_1.resolveConfigValue)(configProvider[initializer.configSpace || 'fileSystem'].name);
|
|
51
52
|
const filePath = name.then((fileName) => path_1.default.join(initializer.dataDir, fileName));
|
|
52
53
|
const { readFile, writeFile } = (0, guards_1.ifDefined)(initializer.fs, fsModule);
|
|
53
54
|
let data;
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
let stateData;
|
|
56
|
+
/**
|
|
57
|
+
* Creates a composite key for state storage.
|
|
58
|
+
*/
|
|
59
|
+
const makeStateKey = (activityId, agent, stateId, registration) => {
|
|
60
|
+
const agentName = typeof agent === 'string' ? agent : agent.account.name;
|
|
61
|
+
return (0, hashValue_1.hashValue)({ activityId, agentName, stateId, registration });
|
|
62
|
+
};
|
|
63
|
+
const stateFilePath = name.then((fileName) => path_1.default.join(initializer.dataDir, `${fileName}-state`));
|
|
64
|
+
const load = filePath.then(filePath => new Promise(resolve => {
|
|
65
|
+
readFile(filePath, (err, readData) => {
|
|
56
66
|
if (err) {
|
|
57
67
|
console.error(err);
|
|
58
68
|
}
|
|
@@ -70,7 +80,27 @@ const fileSystemLrsGateway = (initializer) => (configProvider) => ({ authProvide
|
|
|
70
80
|
resolve();
|
|
71
81
|
});
|
|
72
82
|
}));
|
|
83
|
+
const loadState = stateFilePath.then(stateFilePath => new Promise(resolve => {
|
|
84
|
+
readFile(stateFilePath, (err, readData) => {
|
|
85
|
+
if (err) {
|
|
86
|
+
// File not existing is expected on first run
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(readData.toString());
|
|
91
|
+
if (typeof parsed === 'object' && parsed !== null && !(parsed instanceof Array)) {
|
|
92
|
+
stateData = parsed;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
console.error(e);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
}));
|
|
73
102
|
let previousSave;
|
|
103
|
+
let previousStateSave;
|
|
74
104
|
const putXapiStatements = async (statements) => {
|
|
75
105
|
const user = (0, assertions_1.assertDefined)(await authProvider.getUser(), new errors_1.UnauthorizedError);
|
|
76
106
|
const statementsWithDefaults = statements.map(statement => ({
|
|
@@ -140,11 +170,31 @@ const fileSystemLrsGateway = (initializer) => (configProvider) => ({ authProvide
|
|
|
140
170
|
statements: allResults.slice(0, pageSize)
|
|
141
171
|
};
|
|
142
172
|
};
|
|
173
|
+
const getState = async (activityId, agent, stateId, registration) => {
|
|
174
|
+
var _a;
|
|
175
|
+
await loadState;
|
|
176
|
+
const key = makeStateKey(activityId, agent, stateId, registration);
|
|
177
|
+
return (_a = stateData === null || stateData === void 0 ? void 0 : stateData[key]) !== null && _a !== void 0 ? _a : null;
|
|
178
|
+
};
|
|
179
|
+
const setState = async (activityId, agent, stateId, body, registration) => {
|
|
180
|
+
await loadState;
|
|
181
|
+
await previousStateSave;
|
|
182
|
+
const key = makeStateKey(activityId, agent, stateId, registration);
|
|
183
|
+
const path = await stateFilePath;
|
|
184
|
+
const save = previousStateSave = new Promise(resolve => {
|
|
185
|
+
stateData = stateData || {};
|
|
186
|
+
stateData[key] = body;
|
|
187
|
+
writeFile(path, JSON.stringify(stateData, null, 2), () => resolve());
|
|
188
|
+
});
|
|
189
|
+
await save;
|
|
190
|
+
};
|
|
143
191
|
return {
|
|
144
192
|
putXapiStatements,
|
|
145
193
|
getAllXapiStatements,
|
|
146
194
|
getXapiStatements,
|
|
147
195
|
getMoreXapiStatements,
|
|
196
|
+
getState,
|
|
197
|
+
setState,
|
|
148
198
|
};
|
|
149
199
|
};
|
|
150
200
|
exports.fileSystemLrsGateway = fileSystemLrsGateway;
|
|
@@ -10,6 +10,17 @@ type Config = {
|
|
|
10
10
|
interface Initializer<C> {
|
|
11
11
|
configSpace?: C;
|
|
12
12
|
fetch: GenericFetch;
|
|
13
|
+
enableBatching?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface XapiAgent {
|
|
16
|
+
objectType: 'Agent';
|
|
17
|
+
account: {
|
|
18
|
+
homePage: string;
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface StateDocument {
|
|
23
|
+
[key: string]: any;
|
|
13
24
|
}
|
|
14
25
|
export interface XapiStatement {
|
|
15
26
|
actor: {
|
|
@@ -118,5 +129,7 @@ export declare const lrsGateway: <C extends string = "lrs">(initializer: Initial
|
|
|
118
129
|
}) & {
|
|
119
130
|
fetchUntil?: (statements: XapiStatement[]) => boolean;
|
|
120
131
|
}) => Promise<XapiStatement[]>;
|
|
132
|
+
getState: (activityId: string, agent: string | XapiAgent, stateId: string, registration?: string) => Promise<StateDocument | null>;
|
|
133
|
+
setState: (activityId: string, agent: string | XapiAgent, stateId: string, body: StateDocument, registration?: string) => Promise<void>;
|
|
121
134
|
};
|
|
122
135
|
export {};
|