@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.
@@ -1,14 +1,16 @@
1
1
  import { glyph, type, space } from '../theme.js';
2
2
  import { visualWidth, padEndV, wrapAnsiToVisualWidth } from '../text.js';
3
- import { ansi, ensureCursorRestored } from '../canvas.js';
4
3
  import { createPainter } from './painter.js';
4
+ import { startRawInput } from './input-session.js';
5
5
  import { t } from '../../i18n/index.js';
6
6
  export function parseKey(data) {
7
7
  const s = data.toString();
8
8
  switch (s) {
9
9
  case '\x1b[A':
10
+ case 'k':
10
11
  return 'up';
11
12
  case '\x1b[B':
13
+ case 'j':
12
14
  return 'down';
13
15
  case '\x1b[5~':
14
16
  return 'pageUp';
@@ -17,16 +19,20 @@ export function parseKey(data) {
17
19
  case '\x1b[H':
18
20
  case '\x1b[1~':
19
21
  case '\x1bOH':
22
+ case 'g':
20
23
  return 'home';
21
24
  case '\x1b[F':
22
25
  case '\x1b[4~':
23
26
  case '\x1bOF':
27
+ case 'G':
24
28
  return 'end';
25
29
  case '\r':
26
30
  case '\n':
31
+ case 'l':
27
32
  return 'enter';
28
33
  case '\x03':
29
34
  case '\x1b':
35
+ case 'q':
30
36
  return 'cancel';
31
37
  default:
32
38
  return 'none';
@@ -97,38 +103,32 @@ export function menuFooter() {
97
103
  const m = t().menu;
98
104
  return `${glyph.updown()} ${m.hintMove} ${glyph.enter()} ${m.hintOpen} q ${m.hintQuit}`;
99
105
  }
100
- // Note: runMenu relies on ambient vim-key translation (j/k/l/g/G/q) being ACTIVE.
101
- // Callers must not invoke it with setVimKeysActive(false) still in effect.
102
106
  export function runMenu(config) {
103
107
  return new Promise((resolve) => {
104
- const stdin = process.stdin;
105
- if (!stdin.isTTY || !process.stdout.isTTY) {
106
- resolve(null);
107
- return;
108
- }
109
108
  let index = config.initialIndex ?? 0;
109
+ let finished = false;
110
110
  const paint = createPainter(() => renderMenu({
111
111
  title: config.title,
112
112
  options: config.options,
113
113
  selectedIndex: index,
114
114
  ...(config.footer === undefined ? {} : { footer: config.footer }),
115
115
  }));
116
- const cleanup = () => {
117
- stdin.removeListener('data', onData);
118
- if (stdin.isTTY)
119
- stdin.setRawMode(false);
120
- process.stdout.write('\n' + ansi.showCursor);
116
+ const finish = (result) => {
117
+ if (finished)
118
+ return;
119
+ finished = true;
120
+ handle?.stop();
121
+ process.stdout.write('\n');
122
+ resolve(result);
121
123
  };
122
124
  const onData = (data) => {
123
125
  const key = parseKey(data);
124
126
  if (key === 'cancel') {
125
- cleanup();
126
- resolve(null);
127
+ finish(null);
127
128
  return;
128
129
  }
129
130
  if (key === 'enter') {
130
- cleanup();
131
- resolve(config.options[index]?.value ?? null);
131
+ finish(config.options[index]?.value ?? null);
132
132
  return;
133
133
  }
134
134
  const next = nextIndex(index, key, config.options.length);
@@ -137,11 +137,12 @@ export function runMenu(config) {
137
137
  paint();
138
138
  }
139
139
  };
140
- ensureCursorRestored();
141
- stdin.setRawMode(true);
142
- stdin.resume();
143
- process.stdout.write(ansi.hideCursor);
140
+ const handle = startRawInput(onData);
141
+ if (!handle) {
142
+ finished = true;
143
+ resolve(null);
144
+ return;
145
+ }
144
146
  paint();
145
- stdin.on('data', onData);
146
147
  });
147
148
  }
@@ -2,12 +2,28 @@ import { getCapabilities } from '../capabilities.js';
2
2
  import { ansi, ensureCursorRestored } from '../canvas.js';
3
3
  import { renderMessage } from './messages.js';
4
4
  import { pickIcon } from '../icons.js';
5
- import { c, space } from '../theme.js';
5
+ import { c, space, type } from '../theme.js';
6
+ import { visualWidth, wrapAnsiWithIndent } from '../text.js';
6
7
  const FRAMES_UNICODE = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
8
  const FRAMES_ASCII = ['|', '/', '-', '\\'];
9
+ export const SPINNER_FRAME_MS = 80;
8
10
  export function renderSpinnerFrame(frame, msg) {
9
11
  return `${space.indent}${c.accent(frame)} ${msg}`;
10
12
  }
13
+ export function spinnerFrame(at = Date.now()) {
14
+ const frames = pickIcon('u', 'a') === 'u' ? FRAMES_UNICODE : FRAMES_ASCII;
15
+ const index = getCapabilities().reducedMotion
16
+ ? 0
17
+ : Math.floor(at / SPINNER_FRAME_MS) % frames.length;
18
+ return frames[index] ?? '|';
19
+ }
20
+ export function loadingLines(label, cols = Number.POSITIVE_INFINITY, at = Date.now()) {
21
+ const message = type.hint(label);
22
+ const spun = `${space.indent}${c.accent(spinnerFrame(at))} ${message}`;
23
+ if (visualWidth(spun) <= cols)
24
+ return [spun];
25
+ return wrapAnsiWithIndent(message, cols, space.indent);
26
+ }
11
27
  export function startSpinner(msg = '', opts = {}) {
12
28
  const write = opts.write ??
13
29
  ((s) => {
package/dist/core/text.js CHANGED
@@ -273,9 +273,13 @@ export function wrapAnsiWithIndent(str, maxWidth, preferredIndent = '') {
273
273
  : Number.POSITIVE_INFINITY;
274
274
  const indentWidth = visualWidth(preferredIndent);
275
275
  const contentWidth = visualWidth(str);
276
- const indent = indentWidth >= width || (contentWidth > width - indentWidth && contentWidth <= width)
276
+ let indent = indentWidth >= width || (contentWidth > width - indentWidth && contentWidth <= width)
277
277
  ? ''
278
278
  : preferredIndent;
279
- const availableWidth = Math.max(1, width - visualWidth(indent));
280
- return wrapAnsiToVisualWidth(str, availableWidth).map((line) => `${indent}${line}`);
279
+ let lines = wrapAnsiToVisualWidth(str, Math.max(1, width - visualWidth(indent)));
280
+ if (indent && lines.some((line) => visualWidth(indent + line) > width)) {
281
+ indent = '';
282
+ lines = wrapAnsiToVisualWidth(str, width);
283
+ }
284
+ return lines.map((line) => `${indent}${line}`);
281
285
  }
@@ -4,24 +4,167 @@ const VIM_TO_SEQ = {
4
4
  l: Buffer.from('\r'),
5
5
  g: Buffer.from('\u001b[H'),
6
6
  G: Buffer.from('\u001b[F'),
7
- q: Buffer.from('\u0003'),
8
7
  };
9
8
  let vimActive = true;
10
9
  export function setVimKeysActive(active) {
11
10
  vimActive = active;
12
11
  }
12
+ function vimKeysAreActive() {
13
+ return vimActive;
14
+ }
15
+ function utf8Length(chunk, start) {
16
+ const first = chunk[start] ?? 0;
17
+ const length = first <= 0x7f
18
+ ? 1
19
+ : first >= 0xc2 && first <= 0xdf
20
+ ? 2
21
+ : first >= 0xe0 && first <= 0xef
22
+ ? 3
23
+ : first >= 0xf0 && first <= 0xf4
24
+ ? 4
25
+ : 1;
26
+ if (start + length > chunk.length)
27
+ return null;
28
+ for (let index = start + 1; index < start + length; index += 1) {
29
+ const byte = chunk[index] ?? 0;
30
+ if (byte < 0x80 || byte > 0xbf)
31
+ return 1;
32
+ }
33
+ return length;
34
+ }
35
+ function escapeLength(chunk, start) {
36
+ const introducer = chunk[start + 1];
37
+ if (introducer === undefined)
38
+ return null;
39
+ if (introducer === 0x1b || introducer <= 0x1f || introducer === 0x7f)
40
+ return 1;
41
+ if (introducer === 0x5b || introducer === 0x4f) {
42
+ for (let index = start + 2; index < chunk.length; index += 1) {
43
+ const byte = chunk[index] ?? 0;
44
+ if (byte >= 0x40 && byte <= 0x7e)
45
+ return index - start + 1;
46
+ if (byte < 0x20 || byte > 0x3f)
47
+ return 1;
48
+ }
49
+ return null;
50
+ }
51
+ if ([0x5d, 0x50, 0x5e, 0x5f].includes(introducer)) {
52
+ for (let index = start + 2; index < chunk.length; index += 1) {
53
+ if (chunk[index] === 0x07)
54
+ return index - start + 1;
55
+ if (chunk[index] === 0x1b && chunk[index + 1] === 0x5c)
56
+ return index - start + 2;
57
+ }
58
+ return null;
59
+ }
60
+ const length = utf8Length(chunk, start + 1);
61
+ return length === null ? null : length + 1;
62
+ }
63
+ function keyLength(chunk, start) {
64
+ return chunk[start] === 0x1b ? escapeLength(chunk, start) : utf8Length(chunk, start);
65
+ }
66
+ function concatBuffers(chunks) {
67
+ const size = chunks.reduce((total, chunk) => total + chunk.length, 0);
68
+ const result = Buffer.allocUnsafe(size);
69
+ let offset = 0;
70
+ for (const chunk of chunks) {
71
+ for (const byte of chunk) {
72
+ result[offset] = byte;
73
+ offset += 1;
74
+ }
75
+ }
76
+ return result;
77
+ }
78
+ class KeyByteFramer {
79
+ pending = Buffer.alloc(0);
80
+ get hasPending() {
81
+ return this.pending.length > 0;
82
+ }
83
+ write(chunk) {
84
+ this.pending = this.pending.length === 0 ? chunk : concatBuffers([this.pending, chunk]);
85
+ return this.drain(false);
86
+ }
87
+ flush() {
88
+ return this.drain(true);
89
+ }
90
+ takePending() {
91
+ const pending = this.pending;
92
+ this.pending = Buffer.alloc(0);
93
+ return pending;
94
+ }
95
+ drain(flush) {
96
+ const keys = [];
97
+ let offset = 0;
98
+ while (offset < this.pending.length) {
99
+ const length = keyLength(this.pending, offset);
100
+ if (length === null) {
101
+ if (flush) {
102
+ keys.push(this.pending.subarray(offset));
103
+ offset = this.pending.length;
104
+ }
105
+ break;
106
+ }
107
+ keys.push(this.pending.subarray(offset, offset + length));
108
+ offset += length;
109
+ }
110
+ this.pending = this.pending.subarray(offset);
111
+ return keys;
112
+ }
113
+ }
13
114
  export function enableVimKeys() {
14
115
  const stdin = process.stdin;
15
116
  if (!stdin.isTTY)
16
117
  return;
17
118
  const originalEmit = stdin.emit.bind(stdin);
119
+ const framer = new KeyByteFramer();
120
+ let flushTimer;
121
+ const clearFlush = () => {
122
+ if (flushTimer === undefined)
123
+ return;
124
+ clearTimeout(flushTimer);
125
+ flushTimer = undefined;
126
+ };
127
+ const emitKeys = (keys) => {
128
+ let emitted = false;
129
+ for (let index = 0; index < keys.length; index += 1) {
130
+ if (!vimKeysAreActive()) {
131
+ emitted = originalEmit('data', concatBuffers(keys.slice(index))) || emitted;
132
+ return emitted;
133
+ }
134
+ const key = keys[index];
135
+ if (key === undefined)
136
+ continue;
137
+ const sequence = key.length === 1 ? VIM_TO_SEQ[String.fromCharCode(key[0] ?? 0)] : undefined;
138
+ emitted = originalEmit('data', sequence ?? key) || emitted;
139
+ }
140
+ return emitted;
141
+ };
142
+ const scheduleFlush = () => {
143
+ if (!framer.hasPending)
144
+ return;
145
+ flushTimer = setTimeout(() => {
146
+ flushTimer = undefined;
147
+ if (!vimKeysAreActive()) {
148
+ originalEmit('data', framer.takePending());
149
+ return;
150
+ }
151
+ emitKeys(framer.flush());
152
+ }, 20);
153
+ };
18
154
  const translatedEmit = (event, ...args) => {
19
- if (event === 'data' && vimActive) {
155
+ if (event === 'data') {
20
156
  const chunk = args[0];
21
- if (Buffer.isBuffer(chunk) && chunk.length === 1) {
22
- const seq = VIM_TO_SEQ[String.fromCharCode(chunk.readUInt8(0))];
23
- if (seq)
24
- return originalEmit('data', seq);
157
+ if (Buffer.isBuffer(chunk)) {
158
+ clearFlush();
159
+ if (!vimKeysAreActive()) {
160
+ const pending = framer.takePending();
161
+ return pending.length === 0
162
+ ? originalEmit(event, ...args)
163
+ : originalEmit('data', concatBuffers([pending, chunk]));
164
+ }
165
+ const emitted = emitKeys(framer.write(chunk));
166
+ scheduleFlush();
167
+ return emitted;
25
168
  }
26
169
  }
27
170
  return originalEmit(event, ...args);
@@ -0,0 +1,27 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getStateDir, getWritableStateDir } from '../config/paths.js';
4
+ const FEED_FILE = 'calendar-feed.ics';
5
+ const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
6
+ export function saveFeedCache(text, dir) {
7
+ try {
8
+ fs.writeFileSync(path.join(dir ?? getWritableStateDir(), FEED_FILE), text, {
9
+ encoding: 'utf8',
10
+ mode: 0o600,
11
+ });
12
+ }
13
+ catch {
14
+ /* best effort */
15
+ }
16
+ }
17
+ export function loadFeedCache(dir, maxAgeMs = MAX_AGE_MS) {
18
+ try {
19
+ const file = path.join(dir ?? getStateDir(), FEED_FILE);
20
+ if (Date.now() - fs.statSync(file).mtimeMs > maxAgeMs)
21
+ return null;
22
+ return fs.readFileSync(file, 'utf8');
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
@@ -1,11 +1,12 @@
1
- import { loadCalendar, FeedFetchError, FeedParseError, eventToICS } from '@nbtca/nbtcal';
1
+ import { fetchFeed, parseCalendar, createCalendar, FeedFetchError, FeedParseError, eventToICS, } from '@nbtca/nbtcal';
2
2
  import chalk from 'chalk';
3
3
  import { c, type, space, glyph } from '../core/theme.js';
4
4
  import { pickIcon } from '../core/icons.js';
5
- import { padEndV, sanitizeTerminalLine, sanitizeTerminalText, truncate, visualWidth, wrapAnsiToVisualWidth, } from '../core/text.js';
5
+ import { padEndV, sanitizeTerminalLine, sanitizeTerminalText, truncate, visualWidth, wrapAnsiWithIndent, wrapAnsiToVisualWidth, } from '../core/text.js';
6
6
  import { t } from '../i18n/index.js';
7
7
  import { addLocalDays } from '../core/calendar-day.js';
8
8
  import { countdownParts, isCountdownUrgent, buildExportFilename } from './calendar-query.js';
9
+ import { loadFeedCache, saveFeedCache } from './calendar-store.js';
9
10
  import { writeFileSync, existsSync } from 'fs';
10
11
  import { join } from 'path';
11
12
  function formatDate(date) {
@@ -22,15 +23,46 @@ function formatTime(date) {
22
23
  const minutes = String(date.getMinutes()).padStart(2, '0');
23
24
  return `${hours}:${minutes}`;
24
25
  }
25
- export async function loadCalendarOrThrow() {
26
+ const MEMO_TTL_MS = 5 * 60 * 1000;
27
+ let memo;
28
+ let inFlight;
29
+ export function peekCalendar() {
30
+ if (memo)
31
+ return memo.calendar;
32
+ const text = loadFeedCache();
33
+ if (text === null)
34
+ return undefined;
26
35
  try {
27
- return await loadCalendar({ timeoutMs: 15000 });
36
+ memo = { calendar: createCalendar(parseCalendar(text)), fetchedAt: 0 };
37
+ return memo.calendar;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ async function refetchCalendar(signal) {
44
+ try {
45
+ const text = await fetchFeed(undefined, {
46
+ timeoutMs: 15000,
47
+ ...(signal === undefined ? {} : { signal }),
48
+ });
49
+ memo = { calendar: createCalendar(parseCalendar(text)), fetchedAt: Date.now() };
50
+ saveFeedCache(text);
51
+ return memo.calendar;
28
52
  }
29
53
  catch (err) {
30
54
  const detail = sanitizeTerminalLine(err instanceof FeedFetchError || err instanceof FeedParseError ? err.message : String(err));
31
55
  throw new Error(`${t().calendar.error}: ${detail}`);
32
56
  }
33
57
  }
58
+ export async function loadCalendarOrThrow(signal) {
59
+ if (memo && Date.now() - memo.fetchedAt < MEMO_TTL_MS)
60
+ return memo.calendar;
61
+ inFlight ??= refetchCalendar(signal).finally(() => {
62
+ inFlight = undefined;
63
+ });
64
+ return inFlight;
65
+ }
34
66
  export function toDisplayEvent(e) {
35
67
  const trans = t();
36
68
  return {
@@ -70,13 +102,30 @@ export function serializeEvents(events) {
70
102
  export function renderEventsTable(events, options) {
71
103
  const trans = t();
72
104
  const useColor = options?.color !== false;
73
- if (events.length === 0)
74
- return `${space.indent}${type.hint(trans.calendar.noEvents)}`;
105
+ const width = options?.width === undefined || !Number.isFinite(options.width)
106
+ ? Number.POSITIVE_INFINITY
107
+ : Math.max(1, Math.floor(options.width));
108
+ if (events.length === 0) {
109
+ return wrapAnsiWithIndent(type.hint(trans.calendar.noEvents), width, space.indent).join('\n');
110
+ }
75
111
  const id = (s) => s;
76
112
  const applyDim = useColor ? chalk.dim : id;
77
113
  const applyCyan = useColor ? chalk.cyan : id;
78
114
  const applyBold = useColor ? chalk.bold : id;
79
115
  const applyGray = useColor ? chalk.gray : id;
116
+ if (width < 68) {
117
+ const lines = [];
118
+ for (const event of events) {
119
+ if (lines.length > 0)
120
+ lines.push('');
121
+ const dateTime = event.time ? `${event.date} ${event.time}` : event.date;
122
+ const marker = event.recurring ? `${pickIcon('↻', '~')} ` : '';
123
+ lines.push(...wrapAnsiWithIndent(applyCyan(dateTime), width, space.indent));
124
+ lines.push(...wrapAnsiWithIndent(applyBold(`${marker}${event.title}`), width, space.indent));
125
+ lines.push(...wrapAnsiWithIndent(applyGray(`${pickIcon('⌖', '@')} ${event.location}`), width, space.indent));
126
+ }
127
+ return lines.join('\n');
128
+ }
80
129
  const dateWidth = 16;
81
130
  const titleWidth = 32;
82
131
  const locWidth = 14;
@@ -0,0 +1,225 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { createDocsClient } from '@nbtca/docs';
3
+ const contextKey = Symbol.for('@nbtca/prompt/docs-fetch-context/v2');
4
+ const globalWithContext = globalThis;
5
+ function abortError(signal) {
6
+ return signal.reason instanceof Error ? signal.reason : new DOMException('Aborted', 'AbortError');
7
+ }
8
+ function raceWithSignal(request, signal) {
9
+ if (!signal)
10
+ return request;
11
+ if (signal.aborted)
12
+ return Promise.reject(abortError(signal));
13
+ return new Promise((resolve, reject) => {
14
+ const finish = (result) => {
15
+ signal.removeEventListener('abort', onAbort);
16
+ result();
17
+ };
18
+ const onAbort = () => {
19
+ finish(() => {
20
+ reject(abortError(signal));
21
+ });
22
+ };
23
+ signal.addEventListener('abort', onAbort, { once: true });
24
+ void request.then((value) => {
25
+ finish(() => {
26
+ resolve(value);
27
+ });
28
+ }, (error) => {
29
+ finish(() => {
30
+ reject(error instanceof Error ? error : new Error(String(error)));
31
+ });
32
+ });
33
+ });
34
+ }
35
+ function createDocsFetchContext() {
36
+ const installed = globalWithContext[contextKey];
37
+ if (installed)
38
+ return installed;
39
+ const nativeFetch = globalThis.fetch;
40
+ const storage = new AsyncLocalStorage();
41
+ let delegate = nativeFetch;
42
+ let fallbackDispatching = false;
43
+ const contextualFetch = (input, init) => {
44
+ const forward = (delegate, options) => options ? delegate(input, options) : delegate(input);
45
+ const store = storage.getStore();
46
+ if (!store) {
47
+ if (fallbackDispatching)
48
+ return forward(nativeFetch, init);
49
+ fallbackDispatching = true;
50
+ try {
51
+ return forward(delegate, init);
52
+ }
53
+ finally {
54
+ fallbackDispatching = false;
55
+ }
56
+ }
57
+ if (store.dispatching)
58
+ return forward(nativeFetch, init);
59
+ if (store.signal?.aborted)
60
+ return Promise.reject(abortError(store.signal));
61
+ const requestSignal = init?.signal;
62
+ const signal = store.signal
63
+ ? requestSignal
64
+ ? AbortSignal.any([store.signal, requestSignal])
65
+ : store.signal
66
+ : requestSignal;
67
+ store.dispatching = true;
68
+ try {
69
+ return raceWithSignal(forward(store.delegate, signal ? { ...init, signal } : init), signal);
70
+ }
71
+ finally {
72
+ store.dispatching = false;
73
+ }
74
+ };
75
+ const getFetch = () => (storage.getStore() ? contextualFetch : delegate);
76
+ const setFetch = (value) => {
77
+ delegate = value;
78
+ };
79
+ const context = {
80
+ active: 0,
81
+ contextualFetch,
82
+ get delegate() {
83
+ return delegate;
84
+ },
85
+ set delegate(value) {
86
+ delegate = value;
87
+ },
88
+ getFetch,
89
+ nativeFetch,
90
+ restoreDescriptor: undefined,
91
+ setFetch,
92
+ storage,
93
+ };
94
+ globalWithContext[contextKey] = context;
95
+ return context;
96
+ }
97
+ function beginDocsFetch() {
98
+ const context = createDocsFetchContext();
99
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'fetch');
100
+ const installedAccessor = descriptor?.get === context.getFetch && descriptor.set === context.setFetch;
101
+ const installedValue = descriptor?.value === context.contextualFetch;
102
+ if (context.active === 0)
103
+ context.restoreDescriptor = descriptor;
104
+ if (!installedAccessor && !installedValue) {
105
+ const observed = globalThis.fetch;
106
+ if (observed !== context.contextualFetch)
107
+ context.delegate = observed;
108
+ if (descriptor?.configurable === false) {
109
+ if (descriptor.writable !== true) {
110
+ return {
111
+ context,
112
+ finish() {
113
+ return undefined;
114
+ },
115
+ };
116
+ }
117
+ globalThis.fetch = context.contextualFetch;
118
+ }
119
+ else {
120
+ Object.defineProperty(globalThis, 'fetch', {
121
+ configurable: true,
122
+ enumerable: descriptor?.enumerable ?? true,
123
+ get: context.getFetch,
124
+ set: context.setFetch,
125
+ });
126
+ }
127
+ }
128
+ context.active += 1;
129
+ let finished = false;
130
+ return {
131
+ context,
132
+ finish() {
133
+ if (finished)
134
+ return;
135
+ finished = true;
136
+ context.active = Math.max(0, context.active - 1);
137
+ if (context.active > 0)
138
+ return;
139
+ const current = Object.getOwnPropertyDescriptor(globalThis, 'fetch');
140
+ if (current?.value === context.contextualFetch && current.writable === true) {
141
+ globalThis.fetch = context.delegate;
142
+ return;
143
+ }
144
+ if (current?.get !== context.getFetch || current.set !== context.setFetch)
145
+ return;
146
+ const restore = context.restoreDescriptor;
147
+ if (restore && ('get' in restore || 'set' in restore)) {
148
+ Object.defineProperty(globalThis, 'fetch', restore);
149
+ }
150
+ else {
151
+ Object.defineProperty(globalThis, 'fetch', {
152
+ configurable: restore?.configurable ?? true,
153
+ enumerable: restore?.enumerable ?? true,
154
+ value: context.delegate,
155
+ writable: restore?.writable ?? true,
156
+ });
157
+ }
158
+ },
159
+ };
160
+ }
161
+ const defaultClient = createDocsClient();
162
+ const clientsBySignal = new WeakMap();
163
+ const clients = new Set([defaultClient]);
164
+ function clientFor(signal) {
165
+ if (!signal)
166
+ return defaultClient;
167
+ const existing = clientsBySignal.get(signal);
168
+ if (existing)
169
+ return existing;
170
+ const client = createDocsClient();
171
+ clientsBySignal.set(signal, client);
172
+ clients.add(client);
173
+ signal.addEventListener('abort', () => {
174
+ client.clear();
175
+ clients.delete(client);
176
+ clientsBySignal.delete(signal);
177
+ }, { once: true });
178
+ return client;
179
+ }
180
+ export function clearDocsClients() {
181
+ for (const client of clients)
182
+ client.clear();
183
+ }
184
+ export function runDocsClientOperation(signal, operation) {
185
+ if (signal?.aborted)
186
+ return Promise.reject(abortError(signal));
187
+ const scope = beginDocsFetch();
188
+ let request;
189
+ try {
190
+ request = scope.context.storage.run({ delegate: scope.context.delegate, dispatching: false, signal }, () => operation(clientFor(signal)));
191
+ }
192
+ catch (error) {
193
+ scope.finish();
194
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
195
+ }
196
+ return new Promise((resolve, reject) => {
197
+ let settled = false;
198
+ const settle = (result) => {
199
+ if (settled)
200
+ return;
201
+ settled = true;
202
+ signal?.removeEventListener('abort', onAbort);
203
+ result();
204
+ };
205
+ const onAbort = () => {
206
+ settle(() => {
207
+ reject(signal ? abortError(signal) : new DOMException('Aborted', 'AbortError'));
208
+ });
209
+ };
210
+ signal?.addEventListener('abort', onAbort, { once: true });
211
+ if (signal?.aborted)
212
+ onAbort();
213
+ void request.then((value) => {
214
+ scope.finish();
215
+ settle(() => {
216
+ resolve(value);
217
+ });
218
+ }, (error) => {
219
+ scope.finish();
220
+ settle(() => {
221
+ reject(error instanceof Error ? error : new Error(String(error)));
222
+ });
223
+ });
224
+ });
225
+ }