@openstax/ts-utils 1.40.2 → 1.41.2
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/accountsGateway/index.d.ts +3 -0
- package/dist/cjs/services/accountsGateway/index.js +1 -0
- 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/accountsGateway/index.d.ts +3 -0
- package/dist/esm/services/accountsGateway/index.js +1 -0
- 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/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
|
@@ -13,6 +13,7 @@ interface Initializer<C> {
|
|
|
13
13
|
}
|
|
14
14
|
export type FindUserPayload = ({
|
|
15
15
|
external_id: string;
|
|
16
|
+
role?: string;
|
|
16
17
|
} | {
|
|
17
18
|
uuid: string;
|
|
18
19
|
}) & {
|
|
@@ -45,10 +46,12 @@ export type FindUserResponse = (FindOrCreateUserResponse & {
|
|
|
45
46
|
export type LinkUserPayload = {
|
|
46
47
|
userId: number;
|
|
47
48
|
externalId: string;
|
|
49
|
+
role?: string;
|
|
48
50
|
};
|
|
49
51
|
export type LinkUserResponse = {
|
|
50
52
|
user_id: number;
|
|
51
53
|
external_id: string;
|
|
54
|
+
role?: string;
|
|
52
55
|
};
|
|
53
56
|
export type SearchUsersPayload = {
|
|
54
57
|
q: string;
|
|
@@ -53,6 +53,7 @@ export const accountsGateway = (initializer) => (configProvider) => {
|
|
|
53
53
|
body: {
|
|
54
54
|
external_id: body.externalId,
|
|
55
55
|
user_id: body.userId,
|
|
56
|
+
...(body.role && { role: body.role }),
|
|
56
57
|
}
|
|
57
58
|
});
|
|
58
59
|
const searchUsers = async (payload) => request(METHOD.GET, `users?${queryString.stringify(payload)}`, {});
|
|
@@ -12,7 +12,7 @@ export const encodeDynamoAttribute = (value) => {
|
|
|
12
12
|
if (value === null) {
|
|
13
13
|
return { NULL: true };
|
|
14
14
|
}
|
|
15
|
-
if (value
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
16
|
return { L: value.map(encodeDynamoAttribute) };
|
|
17
17
|
}
|
|
18
18
|
if (isPlainObject(value)) {
|
|
@@ -20,7 +20,7 @@ export const encodeDynamoAttribute = (value) => {
|
|
|
20
20
|
}
|
|
21
21
|
throw new Error(`unknown attribute type ${typeof value} with value ${value}.`);
|
|
22
22
|
};
|
|
23
|
-
export const encodeDynamoDocument = (base) => Object.fromEntries(Object.entries(base).map(([key, value]) => ([key, encodeDynamoAttribute(value)])));
|
|
23
|
+
export const encodeDynamoDocument = (base) => Object.fromEntries(Object.entries(base).filter(([, value]) => value !== undefined).map(([key, value]) => ([key, encodeDynamoAttribute(value)])));
|
|
24
24
|
export const decodeDynamoAttribute = (value) => {
|
|
25
25
|
if (value.S !== undefined) {
|
|
26
26
|
return value.S;
|
|
@@ -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<{
|
|
@@ -4,13 +4,10 @@ import { hashValue } from '../../..';
|
|
|
4
4
|
import { resolveConfigValue } from '../../../config';
|
|
5
5
|
import { ConflictError, NotFoundError } from '../../../errors';
|
|
6
6
|
import { ifDefined, isDefined } from '../../../guards';
|
|
7
|
-
import { assertNoUndefined } from '../fileSystemAssert';
|
|
8
7
|
export const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
|
|
9
|
-
var _a;
|
|
10
8
|
const tableName = resolveConfigValue(configProvider[initializer.configSpace || 'fileSystem'].tableName);
|
|
11
9
|
const tablePath = tableName.then((table) => path.join(initializer.dataDir, table));
|
|
12
10
|
const { mkdir, readdir, readFile, writeFile } = ifDefined(initializer.fs, fsModule);
|
|
13
|
-
const skipAssert = (_a = options === null || options === void 0 ? void 0 : options.skipAssert) !== null && _a !== void 0 ? _a : false;
|
|
14
11
|
const mkTableDir = new Promise((resolve, reject) => tablePath.then((path) => mkdir(path, { recursive: true }, (err) => err && err.code !== 'EEXIST' ? reject(err) : resolve())));
|
|
15
12
|
const hashFilename = (value) => `${hashValue(value)}.json`;
|
|
16
13
|
const filePath = async (filename) => path.join(await tablePath, filename);
|
|
@@ -45,8 +42,6 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
45
42
|
return {
|
|
46
43
|
loadAllDocumentsTheBadWay,
|
|
47
44
|
getItemsByField: async (key, value, pageKey) => {
|
|
48
|
-
if (!skipAssert)
|
|
49
|
-
assertNoUndefined(value, [key]);
|
|
50
45
|
const pageSize = 10;
|
|
51
46
|
const items = await loadAllDocumentsTheBadWay();
|
|
52
47
|
const filteredItems = items.filter((item) => item[key] === value);
|
|
@@ -59,20 +54,14 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
59
54
|
},
|
|
60
55
|
batchGetItem: async (ids) => {
|
|
61
56
|
const items = await Promise.all(ids.map((id) => {
|
|
62
|
-
if (!skipAssert)
|
|
63
|
-
assertNoUndefined(id, ['id']);
|
|
64
57
|
return load(hashFilename(id));
|
|
65
58
|
}));
|
|
66
59
|
return items.filter(isDefined);
|
|
67
60
|
},
|
|
68
61
|
getItem: (id) => {
|
|
69
|
-
if (!skipAssert)
|
|
70
|
-
assertNoUndefined(id, ['id']);
|
|
71
62
|
return load(hashFilename(id));
|
|
72
63
|
},
|
|
73
64
|
incrementItemAttribute: async (id, attribute) => {
|
|
74
|
-
if (!skipAssert)
|
|
75
|
-
assertNoUndefined(id, ['id']);
|
|
76
65
|
const filename = hashFilename(id);
|
|
77
66
|
const path = await filePath(filename);
|
|
78
67
|
await mkTableDir;
|
|
@@ -103,15 +92,11 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
103
92
|
throw new NotFoundError(`Item with ${hashKey.toString()} "${id}" does not exist`);
|
|
104
93
|
}
|
|
105
94
|
const newItem = { ...data, ...item };
|
|
106
|
-
if (!skipAssert)
|
|
107
|
-
assertNoUndefined(newItem);
|
|
108
95
|
return new Promise((resolve, reject) => {
|
|
109
96
|
writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve(newItem));
|
|
110
97
|
});
|
|
111
98
|
},
|
|
112
99
|
putItem: async (item) => {
|
|
113
|
-
if (!skipAssert)
|
|
114
|
-
assertNoUndefined(item);
|
|
115
100
|
const path = await filePath(hashFilename(item[hashKey]));
|
|
116
101
|
await mkTableDir;
|
|
117
102
|
return new Promise((resolve, reject) => {
|
|
@@ -119,8 +104,6 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
119
104
|
});
|
|
120
105
|
},
|
|
121
106
|
createItem: async (item) => {
|
|
122
|
-
if (!skipAssert)
|
|
123
|
-
assertNoUndefined(item);
|
|
124
107
|
const hashed = hashFilename(item[hashKey]);
|
|
125
108
|
const existingItem = await load(hashed);
|
|
126
109
|
if (existingItem) {
|
|
@@ -141,8 +124,6 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
141
124
|
// Process items sequentially to ensure consistent conflict detection
|
|
142
125
|
// Note: concurrency parameter is ignored for filesystem to avoid race conditions
|
|
143
126
|
for (const item of items) {
|
|
144
|
-
if (!skipAssert)
|
|
145
|
-
assertNoUndefined(item);
|
|
146
127
|
try {
|
|
147
128
|
const hashed = hashFilename(item[hashKey]);
|
|
148
129
|
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,10 +1,7 @@
|
|
|
1
|
-
import { assertNoUndefined } from '../fileSystemAssert';
|
|
2
1
|
import { fileSystemUnversionedDocumentStore } from '../unversioned/file-system';
|
|
3
2
|
const PAGE_LIMIT = 5;
|
|
4
3
|
export const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
|
|
5
|
-
|
|
6
|
-
const skipAssert = (_a = options === null || options === void 0 ? void 0 : options.skipAssert) !== null && _a !== void 0 ? _a : false;
|
|
7
|
-
const unversionedDocuments = fileSystemUnversionedDocumentStore(initializer)()(configProvider)({}, 'id', { skipAssert });
|
|
4
|
+
const unversionedDocuments = fileSystemUnversionedDocumentStore(initializer)()(configProvider)({}, 'id');
|
|
8
5
|
return {
|
|
9
6
|
loadAllDocumentsTheBadWay: () => {
|
|
10
7
|
return unversionedDocuments.loadAllDocumentsTheBadWay().then(documents => documents.map(document => {
|
|
@@ -12,8 +9,6 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
|
|
|
12
9
|
}));
|
|
13
10
|
},
|
|
14
11
|
getVersions: async (id, startVersion) => {
|
|
15
|
-
if (!skipAssert)
|
|
16
|
-
assertNoUndefined(id, ['id']);
|
|
17
12
|
const item = await unversionedDocuments.getItem(id);
|
|
18
13
|
const versions = item === null || item === void 0 ? void 0 : item.items.reverse();
|
|
19
14
|
if (!versions) {
|
|
@@ -28,8 +23,6 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
|
|
|
28
23
|
};
|
|
29
24
|
},
|
|
30
25
|
getItem: async (id, timestamp) => {
|
|
31
|
-
if (!skipAssert)
|
|
32
|
-
assertNoUndefined(id, ['id']);
|
|
33
26
|
const item = await unversionedDocuments.getItem(id);
|
|
34
27
|
if (timestamp) {
|
|
35
28
|
return item === null || item === void 0 ? void 0 : item.items.find(version => version.timestamp === timestamp);
|
|
@@ -45,8 +38,6 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
|
|
|
45
38
|
save: async (changes) => {
|
|
46
39
|
var _a;
|
|
47
40
|
const document = { ...item, ...changes, timestamp, author };
|
|
48
|
-
if (!skipAssert)
|
|
49
|
-
assertNoUndefined(document);
|
|
50
41
|
const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
|
|
51
42
|
const updated = { ...container, items: [...container.items, document] };
|
|
52
43
|
await unversionedDocuments.putItem(updated);
|
|
@@ -58,8 +49,6 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
|
|
|
58
49
|
var _a;
|
|
59
50
|
const author = (options === null || options === void 0 ? void 0 : options.getAuthor) ? await options.getAuthor(...authorArgs) : authorArgs[0];
|
|
60
51
|
const document = { ...item, timestamp: new Date().getTime(), author };
|
|
61
|
-
if (!skipAssert)
|
|
62
|
-
assertNoUndefined(document);
|
|
63
52
|
const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
|
|
64
53
|
const updated = { ...container, items: [...container.items, document] };
|
|
65
54
|
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,102 @@
|
|
|
1
|
+
import { METHOD } from '../../routing';
|
|
2
|
+
/**
|
|
3
|
+
* Handles automatic request batching using microtask queue scheduling.
|
|
4
|
+
* Batches concurrent requests within the same event loop tick.
|
|
5
|
+
*/
|
|
6
|
+
export class RequestBatcher {
|
|
7
|
+
constructor(fetcher, config) {
|
|
8
|
+
this.fetcher = fetcher;
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.pendingRequests = [];
|
|
11
|
+
this.flushScheduled = false;
|
|
12
|
+
this.requestCounter = 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Queue a request for batching. Returns a promise that resolves with the response.
|
|
16
|
+
* Automatically flushes the batch on the next microtask.
|
|
17
|
+
*/
|
|
18
|
+
queueRequest(options) {
|
|
19
|
+
const id = `req_${++this.requestCounter}`;
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
this.pendingRequests.push({
|
|
22
|
+
...options,
|
|
23
|
+
id, resolve, reject,
|
|
24
|
+
});
|
|
25
|
+
// Schedule microtask flush if not already scheduled
|
|
26
|
+
if (!this.flushScheduled) {
|
|
27
|
+
this.flushScheduled = true;
|
|
28
|
+
queueMicrotask(() => this.flush());
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async singleRequest(options) {
|
|
33
|
+
const { path, method, queryParams, headers, body } = options;
|
|
34
|
+
const host = await this.config.getHost();
|
|
35
|
+
const query = new URLSearchParams(queryParams || {}).toString();
|
|
36
|
+
const url = host.replace(/\/+$/, '') + path + (query ? `?${query}` : '');
|
|
37
|
+
return await this.fetcher(url, { method, headers, body });
|
|
38
|
+
}
|
|
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
|
+
flush() {
|
|
44
|
+
this.flushScheduled = false;
|
|
45
|
+
const requests = this.pendingRequests;
|
|
46
|
+
this.pendingRequests = [];
|
|
47
|
+
this.flushHandler(requests).catch(error => {
|
|
48
|
+
// If batch request fails entirely, reject all pending requests
|
|
49
|
+
requests.forEach(req => req.reject(error));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async flushHandler(requests) {
|
|
53
|
+
if (requests.length === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Single request optimization: bypass batching
|
|
57
|
+
if (requests.length === 1) {
|
|
58
|
+
return this.singleRequest(requests[0]).then(requests[0].resolve);
|
|
59
|
+
}
|
|
60
|
+
// Multiple requests: use batch API
|
|
61
|
+
const host = await this.config.getHost();
|
|
62
|
+
const authHeader = await this.config.getAuthHeader();
|
|
63
|
+
// Build batch request payload
|
|
64
|
+
const batchRequests = requests.map(({ id, path, method, queryParams, headers, body }) => {
|
|
65
|
+
return { id, path, method, queryParams, headers, body };
|
|
66
|
+
});
|
|
67
|
+
// Send batch request
|
|
68
|
+
const batchUrl = host.replace(/\/+$/, '') + this.config.batchEndpoint;
|
|
69
|
+
const batchResponse = await this.fetcher(batchUrl, {
|
|
70
|
+
method: METHOD.POST,
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: authHeader,
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
'X-Experience-API-Version': '1.0.0',
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({ requests: batchRequests }),
|
|
77
|
+
});
|
|
78
|
+
if (batchResponse.status !== 200) {
|
|
79
|
+
const errorText = await batchResponse.text();
|
|
80
|
+
// all requests will be rejected in the catch
|
|
81
|
+
throw new Error(`Batch request failed with status ${batchResponse.status}: ${errorText}`);
|
|
82
|
+
}
|
|
83
|
+
const batchResult = await batchResponse.json();
|
|
84
|
+
// Map responses back to original requests
|
|
85
|
+
const responseMap = new Map(batchResult.responses.map(r => [r.id, r]));
|
|
86
|
+
requests.forEach(req => {
|
|
87
|
+
const batchResp = responseMap.get(req.id);
|
|
88
|
+
if (!batchResp) {
|
|
89
|
+
req.reject(new Error(`No response for request ${req.id} in batch`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
req.resolve({
|
|
93
|
+
status: batchResp.status,
|
|
94
|
+
headers: {
|
|
95
|
+
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; },
|
|
96
|
+
},
|
|
97
|
+
json: async () => JSON.parse(batchResp.body),
|
|
98
|
+
text: async () => batchResp.body,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -6,14 +6,24 @@ import { assertDefined } from '../../assertions';
|
|
|
6
6
|
import { resolveConfigValue } from '../../config';
|
|
7
7
|
import { UnauthorizedError } from '../../errors';
|
|
8
8
|
import { ifDefined } from '../../guards';
|
|
9
|
+
import { hashValue } from '../../misc/hashValue';
|
|
9
10
|
const pageSize = 5;
|
|
10
11
|
export const fileSystemLrsGateway = (initializer) => (configProvider) => ({ authProvider }) => {
|
|
11
12
|
const name = resolveConfigValue(configProvider[initializer.configSpace || 'fileSystem'].name);
|
|
12
13
|
const filePath = name.then((fileName) => path.join(initializer.dataDir, fileName));
|
|
13
14
|
const { readFile, writeFile } = ifDefined(initializer.fs, fsModule);
|
|
14
15
|
let data;
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
let stateData;
|
|
17
|
+
/**
|
|
18
|
+
* Creates a composite key for state storage.
|
|
19
|
+
*/
|
|
20
|
+
const makeStateKey = (activityId, agent, stateId, registration) => {
|
|
21
|
+
const agentName = typeof agent === 'string' ? agent : agent.account.name;
|
|
22
|
+
return hashValue({ activityId, agentName, stateId, registration });
|
|
23
|
+
};
|
|
24
|
+
const stateFilePath = name.then((fileName) => path.join(initializer.dataDir, `${fileName}-state`));
|
|
25
|
+
const load = filePath.then(filePath => new Promise(resolve => {
|
|
26
|
+
readFile(filePath, (err, readData) => {
|
|
17
27
|
if (err) {
|
|
18
28
|
console.error(err);
|
|
19
29
|
}
|
|
@@ -31,7 +41,27 @@ export const fileSystemLrsGateway = (initializer) => (configProvider) => ({ auth
|
|
|
31
41
|
resolve();
|
|
32
42
|
});
|
|
33
43
|
}));
|
|
44
|
+
const loadState = stateFilePath.then(stateFilePath => new Promise(resolve => {
|
|
45
|
+
readFile(stateFilePath, (err, readData) => {
|
|
46
|
+
if (err) {
|
|
47
|
+
// File not existing is expected on first run
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(readData.toString());
|
|
52
|
+
if (typeof parsed === 'object' && parsed !== null && !(parsed instanceof Array)) {
|
|
53
|
+
stateData = parsed;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.error(e);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
resolve();
|
|
61
|
+
});
|
|
62
|
+
}));
|
|
34
63
|
let previousSave;
|
|
64
|
+
let previousStateSave;
|
|
35
65
|
const putXapiStatements = async (statements) => {
|
|
36
66
|
const user = assertDefined(await authProvider.getUser(), new UnauthorizedError);
|
|
37
67
|
const statementsWithDefaults = statements.map(statement => ({
|
|
@@ -101,10 +131,30 @@ export const fileSystemLrsGateway = (initializer) => (configProvider) => ({ auth
|
|
|
101
131
|
statements: allResults.slice(0, pageSize)
|
|
102
132
|
};
|
|
103
133
|
};
|
|
134
|
+
const getState = async (activityId, agent, stateId, registration) => {
|
|
135
|
+
var _a;
|
|
136
|
+
await loadState;
|
|
137
|
+
const key = makeStateKey(activityId, agent, stateId, registration);
|
|
138
|
+
return (_a = stateData === null || stateData === void 0 ? void 0 : stateData[key]) !== null && _a !== void 0 ? _a : null;
|
|
139
|
+
};
|
|
140
|
+
const setState = async (activityId, agent, stateId, body, registration) => {
|
|
141
|
+
await loadState;
|
|
142
|
+
await previousStateSave;
|
|
143
|
+
const key = makeStateKey(activityId, agent, stateId, registration);
|
|
144
|
+
const path = await stateFilePath;
|
|
145
|
+
const save = previousStateSave = new Promise(resolve => {
|
|
146
|
+
stateData = stateData || {};
|
|
147
|
+
stateData[key] = body;
|
|
148
|
+
writeFile(path, JSON.stringify(stateData, null, 2), () => resolve());
|
|
149
|
+
});
|
|
150
|
+
await save;
|
|
151
|
+
};
|
|
104
152
|
return {
|
|
105
153
|
putXapiStatements,
|
|
106
154
|
getAllXapiStatements,
|
|
107
155
|
getXapiStatements,
|
|
108
156
|
getMoreXapiStatements,
|
|
157
|
+
getState,
|
|
158
|
+
setState,
|
|
109
159
|
};
|
|
110
160
|
};
|
|
@@ -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 {};
|