reslop 0.1.8 → 0.2.0

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 (95) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +27 -38
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +6 -2
  5. package/lib/api.js +2 -2
  6. package/lib/capabilities.js +10 -14
  7. package/lib/cli.js +28 -47
  8. package/lib/clipboard.js +1 -1
  9. package/lib/deps-diff.js +712 -0
  10. package/lib/deps.js +556 -32
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +34 -8
  20. package/lib/{git/branches.js → git-branches.js} +103 -10
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git.js +592 -65
  23. package/lib/github.js +28 -197
  24. package/lib/gitlab.js +24 -195
  25. package/lib/highlight/bash.js +1 -3
  26. package/lib/highlight/css.js +1 -3
  27. package/lib/highlight/js.js +11 -14
  28. package/lib/highlight.js +6 -9
  29. package/lib/keys.js +22 -9
  30. package/lib/manifest.js +330 -0
  31. package/lib/remote.js +374 -0
  32. package/lib/render/branches.js +34 -66
  33. package/lib/render/chrome.js +57 -22
  34. package/lib/render/commits.js +85 -0
  35. package/lib/render/diff.js +16 -16
  36. package/lib/render/files.js +29 -37
  37. package/lib/render/frame.js +29 -13
  38. package/lib/render/notes.js +4 -3
  39. package/lib/render/primitives.js +46 -1
  40. package/lib/{render.js → render/render.js} +6 -7
  41. package/lib/render/unit.js +41 -0
  42. package/lib/review.js +750 -14
  43. package/lib/select.js +5 -10
  44. package/lib/session/accessors.js +9 -1
  45. package/lib/session/actions.js +93 -36
  46. package/lib/session/branches.js +130 -144
  47. package/lib/session/change-actions.js +206 -124
  48. package/lib/session/commit.js +217 -62
  49. package/lib/session/compose.js +158 -13
  50. package/lib/session/dispatch.js +109 -55
  51. package/lib/session/files-pane.js +145 -142
  52. package/lib/session/items.js +131 -34
  53. package/lib/session/lifecycle.js +82 -42
  54. package/lib/session/load.js +19 -33
  55. package/lib/session/navigation.js +15 -3
  56. package/lib/session/ops.js +28 -41
  57. package/lib/session/pointer.js +58 -54
  58. package/lib/session/review.js +23 -37
  59. package/lib/session/terminal.js +6 -12
  60. package/lib/session/todos.js +8 -12
  61. package/lib/session/unit.js +208 -0
  62. package/lib/session/update.js +33 -46
  63. package/lib/session/view.js +82 -11
  64. package/lib/session/watch.js +11 -19
  65. package/lib/session.js +29 -226
  66. package/lib/source.js +3 -3
  67. package/lib/update.js +40 -75
  68. package/lib/{git/proc.js → utilities.js} +75 -11
  69. package/lib/wrap.js +1 -3
  70. package/package.json +2 -2
  71. package/lib/deps/changes.js +0 -331
  72. package/lib/deps/items.js +0 -131
  73. package/lib/deps/manifest.js +0 -145
  74. package/lib/deps/merge.js +0 -120
  75. package/lib/deps/proposals.js +0 -164
  76. package/lib/deps/reports.js +0 -180
  77. package/lib/deps/rows.js +0 -177
  78. package/lib/deps/scan.js +0 -138
  79. package/lib/deps/usage.js +0 -68
  80. package/lib/deps/versions.js +0 -119
  81. package/lib/diff.js +0 -33
  82. package/lib/git/changes.js +0 -164
  83. package/lib/git/read.js +0 -253
  84. package/lib/git/run.js +0 -100
  85. package/lib/npm/commands.js +0 -100
  86. package/lib/remote/changes.js +0 -37
  87. package/lib/remote/headers.js +0 -36
  88. package/lib/remote/http.js +0 -122
  89. package/lib/remote/notes.js +0 -94
  90. package/lib/review/format.js +0 -434
  91. package/lib/review/storage.js +0 -150
  92. package/lib/review/store.js +0 -178
  93. package/lib/review/templates.js +0 -49
  94. package/lib/session/origins.js +0 -7
  95. package/lib/sys.js +0 -84
package/lib/git/read.js DELETED
@@ -1,253 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
-
6
- const diff = require('../diff.js');
7
- const { parseDiff, synthesizeNewFile, itemsFromFiles } = diff;
8
- const { itemPath, REVIEW_DIR } = require('../files.js');
9
- const git = require('./run.js');
10
- const { requireOk, runGit, runGitAsync } = git;
11
- const { pathArgsOf, cachedDiffArgs, worktreeDiffArgs, commitDiffArgs } = git;
12
-
13
- const BINARY_SIZE = 1000000;
14
-
15
- const isReviewFile = (rel) => {
16
- if (!rel) return false;
17
- const first = rel.split(/[/\\]/)[0];
18
- return first === REVIEW_DIR;
19
- };
20
-
21
- const dropReviewItems = (items) => {
22
- const kept = [];
23
- for (const item of items) {
24
- if (isReviewFile(itemPath(item))) continue;
25
- kept.push(item);
26
- }
27
- return kept;
28
- };
29
-
30
- const isBinaryBuffer = (buf) => {
31
- if (buf.length > BINARY_SIZE) return true;
32
- return buf.includes(0);
33
- };
34
-
35
- const namesFromLs = (stdout) => {
36
- if (!stdout) return [];
37
- return stdout.split('\0').filter(Boolean);
38
- };
39
-
40
- const untrackedLsArgs = (paths) => {
41
- const args = ['ls-files', '-o', '--exclude-standard', '-z'];
42
- if (paths.length) args.push('--', ...paths);
43
- return args;
44
- };
45
-
46
- const listUntracked = (top, paths) => {
47
- const result = requireOk(runGit(untrackedLsArgs(paths), top));
48
- return namesFromLs(result.stdout);
49
- };
50
-
51
- const binaryNewFile = (rel) => ({
52
- oldPath: rel,
53
- newPath: rel,
54
- isNew: true,
55
- isDeleted: false,
56
- isBinary: true,
57
- preamble: [`diff --git a/${rel} b/${rel}`, 'new file mode 100644'],
58
- hunks: [],
59
- });
60
-
61
- const untrackedItemsFrom = (top, names) => {
62
- const files = [];
63
- for (const rel of names) {
64
- if (isReviewFile(rel)) continue;
65
- const full = path.join(top, rel);
66
- let buf;
67
- try {
68
- buf = fs.readFileSync(full);
69
- } catch (error) {
70
- if (error.code === 'ENOENT') continue;
71
- throw error;
72
- }
73
- if (isBinaryBuffer(buf)) {
74
- files.push(binaryNewFile(rel));
75
- continue;
76
- }
77
- files.push(synthesizeNewFile(rel, buf.toString('utf8')));
78
- }
79
- return itemsFromFiles(files, 'untracked');
80
- };
81
-
82
- const mergeItems = (groups) => {
83
- const byPath = new Map();
84
- for (const group of groups) {
85
- for (const item of group) {
86
- const rel = itemPath(item);
87
- const list = byPath.get(rel);
88
- if (list) {
89
- list.push(item);
90
- continue;
91
- }
92
- byPath.set(rel, [item]);
93
- }
94
- }
95
- const items = [];
96
- for (const list of byPath.values()) {
97
- for (const item of list) items.push(item);
98
- }
99
- return items;
100
- };
101
-
102
- const gitText = (top, spec) => {
103
- const result = runGit(['show', spec], top);
104
- if (result.status !== 0) return '';
105
- return result.stdout ?? '';
106
- };
107
-
108
- const worktreeText = (top, rel) => {
109
- try {
110
- return fs.readFileSync(path.join(top, rel), 'utf8');
111
- } catch {
112
- return '';
113
- }
114
- };
115
-
116
- const readDepSides = (top, rev, origin, rel) => {
117
- if (origin === 'untracked') {
118
- const newText = worktreeText(top, rel);
119
- return { oldText: '', newText };
120
- }
121
- if (origin === 'staged') {
122
- const oldText = gitText(top, `HEAD:${rel}`);
123
- const newText = gitText(top, `:${rel}`);
124
- return { oldText, newText };
125
- }
126
- if (origin === 'unstaged') {
127
- const indexed = gitText(top, `:${rel}`);
128
- const oldText = indexed || gitText(top, `HEAD:${rel}`);
129
- const newText = worktreeText(top, rel);
130
- return { oldText, newText };
131
- }
132
- if (origin === 'commit' && rev) {
133
- const oldText = gitText(top, `${rev}^:${rel}`);
134
- const newText = gitText(top, `${rev}:${rel}`);
135
- return { oldText, newText };
136
- }
137
- return { oldText: '', newText: '' };
138
- };
139
-
140
- const toplevel = (cwd) => {
141
- const result = runGit(['rev-parse', '--show-toplevel'], cwd);
142
- if (result.status !== 0) throw new Error('not a git repository');
143
- return result.stdout.trim();
144
- };
145
-
146
- const resolveRev = (cwd, spec) => {
147
- if (!spec) return null;
148
- const top = toplevel(cwd);
149
- const args = ['rev-parse', '--verify', `${spec}^{commit}`];
150
- const result = runGit(args, top);
151
- if (result.status !== 0) return null;
152
- return result.stdout.trim();
153
- };
154
-
155
- const shortRev = (top, commit) => {
156
- const result = runGit(['rev-parse', '--short', commit], top);
157
- if (result.status !== 0) return commit.slice(0, 7);
158
- return result.stdout.trim();
159
- };
160
-
161
- const currentBranch = (top) => {
162
- const args = ['rev-parse', '--abbrev-ref', 'HEAD'];
163
- const result = runGit(args, top);
164
- if (result.status !== 0) return '';
165
- const name = result.stdout.trim();
166
- if (!name || name === 'HEAD') return '';
167
- return name;
168
- };
169
-
170
- const snapshotWorktree = (top, stagedOut, unstagedOut, names) => {
171
- const stagedDiff = parseDiff(stagedOut);
172
- const unstagedDiff = parseDiff(unstagedOut);
173
- const staged = itemsFromFiles(stagedDiff, 'staged');
174
- const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
175
- const untracked = untrackedItemsFrom(top, names);
176
- const combined = mergeItems([staged, unstaged, untracked]);
177
- const parsed = dropReviewItems(combined);
178
- const branch = currentBranch(top);
179
- return { top, parsed, branch };
180
- };
181
-
182
- const snapshotCommit = (top, commit, files) => {
183
- const commitItems = itemsFromFiles(files, 'commit');
184
- const parsed = dropReviewItems(commitItems);
185
- const revShort = shortRev(top, commit);
186
- const branch = currentBranch(top);
187
- return { top, parsed, rev: commit, revShort, branch };
188
- };
189
-
190
- const loadCommit = (top, commit, extra) => {
191
- const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
192
- const args = commitDiffArgs(commit, parent, extra);
193
- const result = requireOk(runGit(args, top));
194
- return parseDiff(result.stdout);
195
- };
196
-
197
- const loadCommitAsync = async (top, commit, extra, signal) => {
198
- const parent = await runGitAsync(
199
- ['rev-parse', '--verify', `${commit}^`],
200
- top,
201
- { signal },
202
- );
203
- const args = commitDiffArgs(commit, parent, extra);
204
- const result = requireOk(await runGitAsync(args, top, { signal }));
205
- return parseDiff(result.stdout);
206
- };
207
-
208
- const readSnapshot = (cwd, paths, options) => {
209
- const top = toplevel(cwd);
210
- const extra = pathArgsOf(paths);
211
- const rev = options.commit;
212
- if (rev) return snapshotCommit(top, rev, loadCommit(top, rev, extra));
213
- const stagedRes = requireOk(runGit(cachedDiffArgs(extra), top));
214
- const unstagedRes = requireOk(runGit(worktreeDiffArgs(extra), top));
215
- const names = listUntracked(top, paths);
216
- return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
217
- };
218
-
219
- const readSnapshotAsync = async (cwd, paths, options) => {
220
- const top = toplevel(cwd);
221
- const extra = pathArgsOf(paths);
222
- const rev = options.commit;
223
- const signal = options.signal;
224
- if (rev) {
225
- const files = await loadCommitAsync(top, rev, extra, signal);
226
- return snapshotCommit(top, rev, files);
227
- }
228
- const stagedArgs = cachedDiffArgs(extra);
229
- const unstagedArgs = worktreeDiffArgs(extra);
230
- const lsArgs = untrackedLsArgs(paths);
231
- const stagedP = runGitAsync(stagedArgs, top, { signal });
232
- const unstagedP = runGitAsync(unstagedArgs, top, { signal });
233
- const lsP = runGitAsync(lsArgs, top, { signal });
234
- const extras = await Promise.all([stagedP, unstagedP, lsP]);
235
- const stagedRes = extras[0];
236
- const unstagedRes = extras[1];
237
- const lsRes = extras[2];
238
- requireOk(stagedRes);
239
- requireOk(unstagedRes);
240
- const names = namesFromLs(requireOk(lsRes).stdout);
241
- return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
242
- };
243
-
244
- module.exports = {
245
- toplevel,
246
- resolveRev,
247
- currentBranch,
248
- gitText,
249
- worktreeText,
250
- readDepSides,
251
- readSnapshot,
252
- readSnapshotAsync,
253
- };
package/lib/git/run.js DELETED
@@ -1,100 +0,0 @@
1
- 'use strict';
2
-
3
- const proc = require('./proc.js');
4
- const { runProc, runProcSync, isAbort } = proc;
5
-
6
- const oneLine = (text, fallback = 'git failed') => proc.oneLine(text, fallback);
7
-
8
- const GIT_CONFIG = [
9
- '-c',
10
- 'core.quotepath=false',
11
- '-c',
12
- 'i18n.logOutputEncoding=utf-8',
13
- ];
14
- const DIFF_OPTS = ['--no-color', '--no-ext-diff', '--no-renames', '-U3'];
15
- const GIT_ENCODING = proc.DEFAULT_ENCODING;
16
- const GIT_MAX_BUFFER = proc.DEFAULT_MAX_BUFFER;
17
-
18
- const gitEnv = () => ({ ...process.env, GIT_OPTIONAL_LOCKS: '0' });
19
-
20
- const gitProcOptions = (cwd, extra = {}) => ({
21
- cwd,
22
- env: gitEnv(),
23
- encoding: GIT_ENCODING,
24
- maxBuffer: GIT_MAX_BUFFER,
25
- input: extra.input,
26
- signal: extra.signal,
27
- });
28
-
29
- const gitResult = (result) => {
30
- if (!result.error) return result;
31
- if (isAbort(result.error)) throw result.error;
32
- if (result.error.code === 'ENOENT') throw new Error('git not found');
33
- throw result.error;
34
- };
35
-
36
- const requireOk = (result, fallback = 'git failed') => {
37
- if (result.status === 0) return result;
38
- const msg = result.stderr || result.stdout || fallback;
39
- throw new Error(oneLine(msg, fallback));
40
- };
41
-
42
- const runGit = (args, cwd, input) => {
43
- const options = gitProcOptions(cwd, { input });
44
- return gitResult(runProcSync('git', args, options));
45
- };
46
-
47
- const runGitAsync = (args, cwd, extra = {}) => {
48
- const options = gitProcOptions(cwd, extra);
49
- return runProc('git', args, options).then(gitResult);
50
- };
51
-
52
- const pathArgsOf = (paths) => (paths.length ? ['--', ...paths] : []);
53
-
54
- const cachedDiffArgs = (pathArgs) => [
55
- ...GIT_CONFIG,
56
- 'diff',
57
- '--cached',
58
- ...DIFF_OPTS,
59
- ...pathArgs,
60
- ];
61
-
62
- const worktreeDiffArgs = (pathArgs) => [
63
- ...GIT_CONFIG,
64
- 'diff',
65
- ...DIFF_OPTS,
66
- ...pathArgs,
67
- ];
68
-
69
- const commitDiffArgs = (commit, parent, pathArgs) => {
70
- if (parent.status === 0) {
71
- const from = parent.stdout.trim();
72
- return [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...pathArgs];
73
- }
74
- return [
75
- ...GIT_CONFIG,
76
- 'diff-tree',
77
- '--no-commit-id',
78
- '--root',
79
- '-p',
80
- ...DIFF_OPTS,
81
- commit,
82
- ...pathArgs,
83
- ];
84
- };
85
-
86
- module.exports = {
87
- GIT_CONFIG,
88
- DIFF_OPTS,
89
- GIT_ENCODING,
90
- GIT_MAX_BUFFER,
91
- oneLine,
92
- gitResult,
93
- requireOk,
94
- runGit,
95
- runGitAsync,
96
- pathArgsOf,
97
- cachedDiffArgs,
98
- worktreeDiffArgs,
99
- commitDiffArgs,
100
- };
@@ -1,100 +0,0 @@
1
- 'use strict';
2
-
3
- const path = require('node:path');
4
-
5
- const { npmBin, npmOpts } = require('../sys.js');
6
- const { depFileMeta, LOCKFILE } = require('../deps.js');
7
- const proc = require('../git/proc.js');
8
- const { runProc, runProcSync } = proc;
9
-
10
- const AUDIT_MS = 15000;
11
- const INSTALL_MS = 120000;
12
-
13
- const npmProcOptions = (cwd, extra = {}) => ({
14
- cwd,
15
- encoding: proc.DEFAULT_ENCODING,
16
- maxBuffer: proc.DEFAULT_MAX_BUFFER,
17
- timeout: extra.timeout,
18
- signal: extra.signal,
19
- shell: npmOpts().shell,
20
- });
21
-
22
- const runNpm = (cwd, args) =>
23
- runProcSync(npmBin(), args, npmProcOptions(cwd, { timeout: INSTALL_MS }));
24
-
25
- const runNpmAsync = (cwd, args, extra = {}) =>
26
- runProc(
27
- npmBin(),
28
- args,
29
- npmProcOptions(cwd, { timeout: INSTALL_MS, signal: extra.signal }),
30
- );
31
-
32
- const runNpmJson = (top, args, timeout) => {
33
- const result = runProcSync(npmBin(), args, npmProcOptions(top, { timeout }));
34
- if (result.error) return '';
35
- return result.stdout || '';
36
- };
37
-
38
- const runNpmJsonAsync = async (top, args, timeout, signal) => {
39
- const result = await runProc(
40
- npmBin(),
41
- args,
42
- npmProcOptions(top, { timeout, signal }),
43
- );
44
- if (result.error) return '';
45
- return result.stdout || '';
46
- };
47
-
48
- const depChange = (item) => item.dep?.change ?? null;
49
-
50
- const depPaths = (item) => item.dep?.files ?? [];
51
-
52
- const firstDepRel = (files, kind) => {
53
- for (const rel of files) {
54
- const meta = depFileMeta(rel);
55
- if (meta && meta.kind === kind) return rel;
56
- }
57
- return '';
58
- };
59
-
60
- const proposedNpmPlan = (top, item) => {
61
- const change = depChange(item);
62
- if (!change) return null;
63
- const files = depPaths(item);
64
- const lockOnly = change.section === 'resolved';
65
- const removing = !!change.unused;
66
- const manifest = firstDepRel(files, 'manifest');
67
- const lockRel = firstDepRel(files, 'lockfile');
68
- const picked = lockOnly ? lockRel : manifest;
69
- const target = picked || files[0] || '';
70
- const meta = depFileMeta(target);
71
- if (!meta) return null;
72
- const cwd = meta.dir === '.' ? top : path.join(top, meta.dir);
73
- let npmArgs = ['i'];
74
- if (removing) npmArgs = ['uninstall', change.name];
75
- else if (lockOnly) npmArgs = ['audit', 'fix'];
76
- const fail = removing ? 'npm uninstall failed' : 'npm i failed';
77
- const dirLock = meta.dir === '.' ? LOCKFILE : `${meta.dir}/${LOCKFILE}`;
78
- const lockPath = lockRel || dirLock;
79
- const prepareManifest = !lockOnly && !removing && !!manifest;
80
- return {
81
- cwd,
82
- npmArgs,
83
- fail,
84
- lockOnly,
85
- lockPath,
86
- manifest,
87
- prepareManifest,
88
- runner: item.dep.install,
89
- };
90
- };
91
-
92
- module.exports = {
93
- AUDIT_MS,
94
- INSTALL_MS,
95
- runNpm,
96
- runNpmAsync,
97
- runNpmJson,
98
- runNpmJsonAsync,
99
- proposedNpmPlan,
100
- };
@@ -1,37 +0,0 @@
1
- 'use strict';
2
-
3
- const { itemPath, isPathInScope } = require('../files.js');
4
-
5
- const changePath = (file) => file.newPath || file.oldPath || '';
6
-
7
- const filterChangeFiles = (files, paths) => {
8
- if (!paths.length) return files;
9
- const kept = [];
10
- for (const file of files) {
11
- if (isPathInScope(changePath(file), paths)) kept.push(file);
12
- }
13
- return kept;
14
- };
15
-
16
- const pushChangeFile = (files, seen, rel) => {
17
- if (!rel || seen.has(rel)) return;
18
- seen.add(rel);
19
- files.push(rel);
20
- };
21
-
22
- const changeFiles = (items) => {
23
- const files = [];
24
- const seen = new Set();
25
- for (const item of items) {
26
- if (item.dep) {
27
- const depFiles = item.dep.files ?? [];
28
- for (const rel of depFiles) pushChangeFile(files, seen, rel);
29
- continue;
30
- }
31
- const rel = itemPath(item);
32
- pushChangeFile(files, seen, rel);
33
- }
34
- return files;
35
- };
36
-
37
- module.exports = { changePath, filterChangeFiles, changeFiles };
@@ -1,36 +0,0 @@
1
- 'use strict';
2
-
3
- const headerValue = (headers, name) => {
4
- if (!headers) return '';
5
- if (typeof headers.get === 'function') {
6
- return headers.get(name) || '';
7
- }
8
- const wanted = name.toLowerCase();
9
- for (const key of Object.keys(headers)) {
10
- const value = headers[key];
11
- if (key.toLowerCase() === wanted) return `${value}`;
12
- }
13
- return '';
14
- };
15
-
16
- const parseLinkNext = (headers) => {
17
- const link = headerValue(headers, 'link');
18
- if (!link) return '';
19
- for (const part of link.split(',')) {
20
- const match = /<([^>]+)>\s*;\s*rel="?next"?/i.exec(part.trim());
21
- if (match) return match[1];
22
- }
23
- return '';
24
- };
25
-
26
- const parseNextPage = (headers, currentUrl) => {
27
- const fromLink = parseLinkNext(headers);
28
- if (fromLink) return fromLink;
29
- const page = headerValue(headers, 'x-next-page');
30
- if (!page) return '';
31
- const url = new URL(currentUrl);
32
- url.searchParams.set('page', page);
33
- return url.toString();
34
- };
35
-
36
- module.exports = { headerValue, parseLinkNext, parseNextPage };
@@ -1,122 +0,0 @@
1
- 'use strict';
2
-
3
- const { delay, isHashObject } = require('metautil');
4
-
5
- const RETRY_ATTEMPTS = 3;
6
- const RETRY_DELAY_MS = 200;
7
- const TRANSIENT_STATUS = [429, 502, 503, 504];
8
-
9
- const isRecord = isHashObject;
10
-
11
- const oneLine = (text) => {
12
- const value = `${text ?? ''}`.trim();
13
- if (!value) return '';
14
- return value.split('\n')[0];
15
- };
16
-
17
- const abortError = (signal) => {
18
- if (signal && signal.reason !== undefined) return signal.reason;
19
- const error = new Error('This operation was aborted');
20
- error.name = 'AbortError';
21
- return error;
22
- };
23
-
24
- const isAbortError = (error, signal) => {
25
- if (signal && signal.aborted) return true;
26
- if (!error || typeof error !== 'object') return false;
27
- if (error.name === 'AbortError') return true;
28
- if (error.code === 'ABORT') return true;
29
- return false;
30
- };
31
-
32
- const throwIfAborted = (signal) => {
33
- if (signal && signal.aborted) throw abortError(signal);
34
- };
35
-
36
- const createTransport = (deps = {}) => {
37
- const fetchImpl = deps.fetch;
38
- const rawDelay = deps.delay ?? delay;
39
- const delayFn = async (ms, abortSignal) => {
40
- try {
41
- await rawDelay(ms, abortSignal);
42
- } catch (error) {
43
- throwIfAborted(abortSignal);
44
- throw error;
45
- }
46
- };
47
- const signal = deps.signal;
48
-
49
- const retry = async (run, options = {}) => {
50
- const attempts = options.attempts ?? RETRY_ATTEMPTS;
51
- const delayMs = options.delayMs ?? RETRY_DELAY_MS;
52
- const isRetryable = options.isRetryable;
53
- for (let i = 0; ; i++) {
54
- try {
55
- return await run();
56
- } catch (error) {
57
- if (isAbortError(error, signal)) throw error;
58
- const isLast = i === attempts - 1;
59
- if (isLast || !isRetryable(error)) throw error;
60
- throwIfAborted(signal);
61
- await delayFn(delayMs * (i + 1), signal);
62
- }
63
- }
64
- };
65
-
66
- const request = async (url, options = {}) => {
67
- throwIfAborted(signal);
68
- if (typeof fetchImpl !== 'function') {
69
- throw options.missingFetch();
70
- }
71
- const headers = options.headers;
72
- const method = options.method || 'GET';
73
- const body = options.body;
74
- const wrapNetwork = options.wrapNetwork;
75
- const toHttpError = options.toHttpError;
76
- const retryOpts = options.retry ?? {};
77
- return retry(
78
- async () => {
79
- let response;
80
- try {
81
- const init = { method, headers };
82
- if (body !== undefined) init.body = body;
83
- if (signal) init.signal = signal;
84
- response = await fetchImpl(url, init);
85
- } catch (error) {
86
- if (isAbortError(error, signal)) throw error;
87
- throw wrapNetwork(error);
88
- }
89
- const text = await response.text();
90
- if (!response.ok) throw toHttpError(response.status, text);
91
- return { body: text, headers: response.headers };
92
- },
93
- { ...retryOpts, isRetryable: options.isRetryable },
94
- );
95
- };
96
-
97
- const listPages = async (url, options = {}) => {
98
- const items = [];
99
- let next = url;
100
- const parsePage = options.parsePage;
101
- const nextPage = options.nextPage;
102
- while (next) {
103
- const page = await request(next, options);
104
- const entries = parsePage(page.body);
105
- for (const entry of entries) items.push(entry);
106
- next = nextPage(page.headers, next);
107
- }
108
- return items;
109
- };
110
-
111
- return { retry, request, listPages };
112
- };
113
-
114
- module.exports = {
115
- RETRY_ATTEMPTS,
116
- RETRY_DELAY_MS,
117
- TRANSIENT_STATUS,
118
- isRecord,
119
- oneLine,
120
- isAbortError,
121
- createTransport,
122
- };