@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.
Files changed (34) hide show
  1. package/dist/cjs/services/authProvider/subrequest.js +1 -2
  2. package/dist/cjs/services/authProvider/utils/userSubrequest.d.ts +1 -1
  3. package/dist/cjs/services/authProvider/utils/userSubrequest.js +3 -1
  4. package/dist/cjs/services/documentStore/dynamoEncoding.js +2 -2
  5. package/dist/cjs/services/documentStore/unversioned/file-system.d.ts +0 -1
  6. package/dist/cjs/services/documentStore/unversioned/file-system.js +0 -19
  7. package/dist/cjs/services/documentStore/versioned/file-system.d.ts +0 -1
  8. package/dist/cjs/services/documentStore/versioned/file-system.js +1 -12
  9. package/dist/cjs/services/lrsGateway/batching.d.ts +46 -0
  10. package/dist/cjs/services/lrsGateway/batching.js +106 -0
  11. package/dist/cjs/services/lrsGateway/file-system.js +52 -2
  12. package/dist/cjs/services/lrsGateway/index.d.ts +13 -0
  13. package/dist/cjs/services/lrsGateway/index.js +151 -55
  14. package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
  15. package/dist/esm/services/authProvider/subrequest.js +1 -2
  16. package/dist/esm/services/authProvider/utils/userSubrequest.d.ts +1 -1
  17. package/dist/esm/services/authProvider/utils/userSubrequest.js +3 -1
  18. package/dist/esm/services/documentStore/dynamoEncoding.js +2 -2
  19. package/dist/esm/services/documentStore/unversioned/file-system.d.ts +0 -1
  20. package/dist/esm/services/documentStore/unversioned/file-system.js +0 -19
  21. package/dist/esm/services/documentStore/versioned/file-system.d.ts +0 -1
  22. package/dist/esm/services/documentStore/versioned/file-system.js +1 -12
  23. package/dist/esm/services/lrsGateway/batching.d.ts +46 -0
  24. package/dist/esm/services/lrsGateway/batching.js +102 -0
  25. package/dist/esm/services/lrsGateway/file-system.js +52 -2
  26. package/dist/esm/services/lrsGateway/index.d.ts +13 -0
  27. package/dist/esm/services/lrsGateway/index.js +151 -22
  28. package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/script/bin/.init-params-script.bash.swp +0 -0
  31. package/dist/cjs/services/documentStore/fileSystemAssert.d.ts +0 -1
  32. package/dist/cjs/services/documentStore/fileSystemAssert.js +0 -14
  33. package/dist/esm/services/documentStore/fileSystemAssert.d.ts +0 -1
  34. 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
- // this returns `{"error_id":null}` when the token is invalid
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 instanceof Array) {
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
- var _a;
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
- const load = filePath.then(path => new Promise(resolve => {
55
- readFile(path, (err, readData) => {
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 {};