@nbtca/prompt 1.5.0 → 1.5.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.
@@ -21,6 +21,21 @@ let session = null;
21
21
  let client = null;
22
22
  let catalog = [];
23
23
  let pendingId = '';
24
+ let lifecycleGeneration = 0;
25
+ const closingSessions = new WeakMap();
26
+ function isLifecycleActive(ctx, generation) {
27
+ return generation === lifecycleGeneration && ctx.signal?.aborted !== true;
28
+ }
29
+ async function closeSession(target) {
30
+ let closing = closingSessions.get(target);
31
+ if (!closing) {
32
+ closing = Promise.resolve()
33
+ .then(() => target.close())
34
+ .catch(() => undefined);
35
+ closingSessions.set(target, closing);
36
+ }
37
+ await closing;
38
+ }
24
39
  async function releaseSession(target = session) {
25
40
  if (!target)
26
41
  return;
@@ -28,10 +43,7 @@ async function releaseSession(target = session) {
28
43
  session = null;
29
44
  client = null;
30
45
  }
31
- try {
32
- await target.close();
33
- }
34
- catch { }
46
+ await closeSession(target);
35
47
  }
36
48
  function isTimetableLike(value) {
37
49
  return (!!value &&
@@ -88,12 +100,16 @@ function buildPublicField() {
88
100
  });
89
101
  }
90
102
  const PUBLIC_UPCOMING_FETCH_CAP = 15;
91
- async function goToPublic(ctx) {
103
+ async function goToPublic(ctx, generation = lifecycleGeneration) {
104
+ if (!isLifecycleActive(ctx, generation))
105
+ return;
92
106
  setVimKeysActive(true);
93
107
  state = { mode: 'public', publicField: buildPublicField() };
94
108
  ctx.rerender();
95
109
  try {
96
- const cal = await loadCalendarOrThrow();
110
+ const cal = await loadCalendarOrThrow(ctx.signal);
111
+ if (!isLifecycleActive(ctx, generation))
112
+ return;
97
113
  const now = new Date();
98
114
  const windowEvents = cal.inRange(addLocalDays(now, -400), addLocalDays(now, 400));
99
115
  const publicWindow = currentAcademicWindow(windowEvents, now);
@@ -105,13 +121,18 @@ async function goToPublic(ctx) {
105
121
  state = { ...state, publicWindow, publicUpcoming };
106
122
  }
107
123
  catch {
124
+ if (!isLifecycleActive(ctx, generation))
125
+ return;
108
126
  state = { ...state, publicWindow: null };
109
127
  }
110
- ctx.rerender();
128
+ if (isLifecycleActive(ctx, generation))
129
+ ctx.rerender();
111
130
  }
112
- async function tryInferWeekOne() {
131
+ async function tryInferWeekOne(ctx, generation) {
113
132
  try {
114
- const cal = await loadCalendarOrThrow();
133
+ const cal = await loadCalendarOrThrow(ctx.signal);
134
+ if (!isLifecycleActive(ctx, generation))
135
+ return null;
115
136
  const now = new Date();
116
137
  const events = cal.inRange(addLocalDays(now, -400), addLocalDays(now, 400));
117
138
  return inferWeekOneMonday(events, now);
@@ -120,19 +141,32 @@ async function tryInferWeekOne() {
120
141
  return null;
121
142
  }
122
143
  }
123
- async function afterAuthenticated(ctx, s) {
144
+ async function afterAuthenticated(ctx, s, generation) {
145
+ if (!isLifecycleActive(ctx, generation)) {
146
+ await closeSession(s);
147
+ return;
148
+ }
124
149
  const hadCache = state.mode === 'hub';
125
150
  if (session && session !== s)
126
151
  await releaseSession(session);
152
+ if (!isLifecycleActive(ctx, generation)) {
153
+ await closeSession(s);
154
+ return;
155
+ }
127
156
  session = s;
128
157
  client = createNbtTimetableClient(s.timetableTransport, { baseUrl: JWXT_ORIGIN });
129
158
  try {
130
- catalog = (await client.listTerms()).map(sanitizeAcademicTerm);
159
+ const nextCatalog = (await client.listTerms(ctx.signal === undefined ? {} : { signal: ctx.signal })).map(sanitizeAcademicTerm);
160
+ if (!isLifecycleActive(ctx, generation))
161
+ return;
162
+ catalog = nextCatalog;
131
163
  const term = resolveTerm(catalog);
132
164
  const key = termKey(term);
133
165
  let weekOne = loadWeekOne(key);
134
166
  if (!weekOne) {
135
- weekOne = await tryInferWeekOne();
167
+ weekOne = await tryInferWeekOne(ctx, generation);
168
+ if (!isLifecycleActive(ctx, generation))
169
+ return;
136
170
  if (weekOne)
137
171
  saveWeekOne(key, weekOne);
138
172
  }
@@ -151,9 +185,11 @@ async function afterAuthenticated(ctx, s) {
151
185
  ctx.rerender();
152
186
  return;
153
187
  }
154
- await fetchAndShowHub(ctx, term, key, weekOne);
188
+ await fetchAndShowHub(ctx, term, key, weekOne, generation);
155
189
  }
156
190
  catch (err) {
191
+ if (!isLifecycleActive(ctx, generation))
192
+ return;
157
193
  if (isSessionExpired(err)) {
158
194
  createSessionStore().clear();
159
195
  await releaseSession(s);
@@ -168,13 +204,15 @@ async function afterAuthenticated(ctx, s) {
168
204
  ctx.rerender();
169
205
  }
170
206
  }
171
- async function fetchAndShowHub(ctx, term, key, weekOne) {
172
- if (!client)
207
+ async function fetchAndShowHub(ctx, term, key, weekOne, generation = lifecycleGeneration) {
208
+ if (!client || !isLifecycleActive(ctx, generation))
173
209
  return;
174
210
  state = { mode: 'loading', statusMessage: t().calendar.loading };
175
211
  ctx.rerender();
176
212
  try {
177
- const timetable = sanitizeTimetable(await client.fetchTerm(term));
213
+ const timetable = sanitizeTimetable(await client.fetchTerm(term, ctx.signal === undefined ? {} : { signal: ctx.signal }));
214
+ if (!isLifecycleActive(ctx, generation))
215
+ return;
178
216
  const schedule = createTimetableSchedule(timetable, { weekOneMonday: weekOne });
179
217
  saveTimetableCache(key, timetable);
180
218
  saveCurrentPointer(key, weekOne);
@@ -188,6 +226,8 @@ async function fetchAndShowHub(ctx, term, key, weekOne) {
188
226
  };
189
227
  }
190
228
  catch (err) {
229
+ if (!isLifecycleActive(ctx, generation))
230
+ return;
191
231
  if (isSessionExpired(err)) {
192
232
  createSessionStore().clear();
193
233
  await releaseSession();
@@ -197,27 +237,36 @@ async function fetchAndShowHub(ctx, term, key, weekOne) {
197
237
  state = { mode: 'error', errorMessage: safeMessage(err) };
198
238
  }
199
239
  }
200
- ctx.rerender();
240
+ if (isLifecycleActive(ctx, generation))
241
+ ctx.rerender();
201
242
  }
202
- async function refreshFromNetwork(ctx) {
243
+ async function refreshFromNetwork(ctx, generation) {
244
+ if (!isLifecycleActive(ctx, generation))
245
+ return;
203
246
  const hadCache = state.mode === 'hub';
204
247
  try {
205
248
  const store = createSessionStore();
206
249
  const persisted = store.load();
207
250
  if (!persisted) {
208
251
  if (!hadCache)
209
- await goToPublic(ctx);
252
+ await goToPublic(ctx, generation);
210
253
  return;
211
254
  }
212
255
  const restored = await restoreNbtSession(persisted);
213
- await afterAuthenticated(ctx, restored);
256
+ if (!isLifecycleActive(ctx, generation)) {
257
+ await closeSession(restored);
258
+ return;
259
+ }
260
+ await afterAuthenticated(ctx, restored, generation);
214
261
  }
215
262
  catch (err) {
263
+ if (!isLifecycleActive(ctx, generation))
264
+ return;
216
265
  if (!hadCache) {
217
266
  if (err instanceof AuthError && isSessionExpired(err)) {
218
267
  createSessionStore().clear();
219
268
  }
220
- await goToPublic(ctx);
269
+ await goToPublic(ctx, generation);
221
270
  }
222
271
  }
223
272
  }
@@ -225,6 +274,9 @@ export const scheduleView = {
225
274
  id: 'schedule',
226
275
  title: t().timetable.menuEntry,
227
276
  async load(ctx) {
277
+ const generation = ++lifecycleGeneration;
278
+ if (!isLifecycleActive(ctx, generation))
279
+ return;
228
280
  const ptr = loadCurrentPointer();
229
281
  const cached = readCachedTimetable(ptr ? loadTimetableCache(ptr.termKey) : null);
230
282
  if (ptr && cached) {
@@ -241,15 +293,20 @@ export const scheduleView = {
241
293
  state = { mode: 'loading' };
242
294
  }
243
295
  ctx.rerender();
244
- await refreshFromNetwork(ctx);
296
+ await refreshFromNetwork(ctx, generation);
245
297
  },
246
298
  async dispose() {
299
+ lifecycleGeneration += 1;
300
+ setVimKeysActive(true);
247
301
  await releaseSession();
248
302
  },
249
303
  render(ctx) {
250
304
  state.termField?.setMaxVisible(computeMaxVisible(ctx.bodyRows));
251
305
  return renderSchedule(state, new Date(), ctx.bodyRows, ctx.size.cols);
252
306
  },
307
+ isBusy() {
308
+ return state.mode === 'loading';
309
+ },
253
310
  capturesInput() {
254
311
  return (state.mode === 'needsLoginId' ||
255
312
  state.mode === 'needsLoginPassword' ||
@@ -331,23 +388,33 @@ export const scheduleView = {
331
388
  }
332
389
  if (result?.submitted !== undefined) {
333
390
  const password = result.submitted;
391
+ const generation = lifecycleGeneration;
334
392
  setVimKeysActive(true);
335
393
  state = { mode: 'authenticating', statusMessage: t().timetable.loginWillSave };
336
394
  ctx.rerender();
337
- void loginWithStudentPassword(pendingId, password)
395
+ void loginWithStudentPassword(pendingId, password, ctx.signal === undefined ? {} : { signal: ctx.signal })
338
396
  .then(async (s) => {
339
- let handedOff = false;
340
397
  try {
341
- createSessionStore().save(await s.snapshot());
342
- handedOff = true;
343
- await afterAuthenticated(ctx, s);
398
+ if (!isLifecycleActive(ctx, generation)) {
399
+ await closeSession(s);
400
+ return;
401
+ }
402
+ const snapshot = await s.snapshot();
403
+ if (!isLifecycleActive(ctx, generation)) {
404
+ await closeSession(s);
405
+ return;
406
+ }
407
+ createSessionStore().save(snapshot);
408
+ await afterAuthenticated(ctx, s, generation);
344
409
  }
345
- finally {
346
- if (!handedOff)
347
- await releaseSession(s);
410
+ catch (error) {
411
+ await closeSession(s);
412
+ throw error;
348
413
  }
349
414
  })
350
415
  .catch((err) => {
416
+ if (!isLifecycleActive(ctx, generation))
417
+ return;
351
418
  goToLoginId(safeMessage(err));
352
419
  ctx.rerender();
353
420
  });
@@ -377,7 +444,7 @@ export const scheduleView = {
377
444
  }
378
445
  saveWeekOne(targetKey, trimmed);
379
446
  setVimKeysActive(true);
380
- void fetchAndShowHub(ctx, targetTerm, targetKey, trimmed);
447
+ void fetchAndShowHub(ctx, targetTerm, targetKey, trimmed, lifecycleGeneration);
381
448
  }
382
449
  return;
383
450
  }
@@ -456,10 +523,11 @@ export const scheduleView = {
456
523
  return;
457
524
  }
458
525
  if (shortcut.key === 'x') {
526
+ const generation = ++lifecycleGeneration;
459
527
  createSessionStore().clear();
460
528
  clearScheduleCache();
461
529
  void releaseSession();
462
- void goToPublic(ctx);
530
+ void goToPublic(ctx, generation);
463
531
  }
464
532
  return;
465
533
  }
@@ -528,7 +596,7 @@ export const scheduleView = {
528
596
  };
529
597
  return;
530
598
  }
531
- void fetchAndShowHub(ctx, term, newTermKey, weekOne);
599
+ void fetchAndShowHub(ctx, term, newTermKey, weekOne, lifecycleGeneration);
532
600
  return;
533
601
  }
534
602
  case 'loading':