@cplieger/ui 0.0.0-stage → 1.0.0-dev.1
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/LICENSE +202 -0
- package/NOTICE +3 -0
- package/README.md +157 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/css/ui-primitives.css +344 -0
- package/jsr.json +44 -0
- package/package.json +105 -4
- package/src/actions/api.ts +268 -0
- package/src/actions/async-feedback.ts +217 -0
- package/src/actions/cleanup.ts +66 -0
- package/src/actions/debounce.ts +150 -0
- package/src/actions/define-helpers.ts +87 -0
- package/src/actions/define.ts +851 -0
- package/src/actions/error.ts +161 -0
- package/src/actions/index.ts +60 -0
- package/src/actions/loading.ts +131 -0
- package/src/actions/notifier.ts +61 -0
- package/src/actions/poll-until.ts +133 -0
- package/src/actions/poll.ts +156 -0
- package/src/actions/registry.ts +259 -0
- package/src/actions/retry.ts +79 -0
- package/src/actions/testing.ts +29 -0
- package/src/actions/transport.ts +104 -0
- package/src/actions/types.ts +249 -0
- package/src/dom/bind-list.ts +103 -0
- package/src/dom/el.ts +67 -0
- package/src/dom/index.ts +9 -0
- package/src/dom/reconcile-tree.ts +268 -0
- package/src/dom/reconcile.ts +76 -0
- package/src/fetch/index.ts +18 -0
- package/src/fetch/instance.ts +37 -0
- package/src/fetch/request.ts +437 -0
- package/src/fetch/timeout.ts +67 -0
- package/src/fetch/types.ts +206 -0
- package/src/fetch/verbs.ts +103 -0
- package/src/index.ts +4 -0
- package/src/primitives/announce.ts +134 -0
- package/src/primitives/ask.ts +406 -0
- package/src/primitives/dialog-engine.ts +500 -0
- package/src/primitives/dialog.ts +263 -0
- package/src/primitives/disclosure.ts +287 -0
- package/src/primitives/focus-trap.ts +157 -0
- package/src/primitives/fx.ts +204 -0
- package/src/primitives/live-regions.ts +83 -0
- package/src/primitives/modal-host.ts +214 -0
- package/src/primitives/modal.ts +247 -0
- package/src/primitives/owner.ts +322 -0
- package/src/primitives/popover.ts +500 -0
- package/src/primitives/popup-core.ts +603 -0
- package/src/primitives/popup.ts +23 -0
- package/src/primitives/roving-focus.ts +198 -0
- package/src/primitives/scroll-lock.ts +119 -0
- package/src/primitives/skeleton.ts +142 -0
- package/src/primitives/theme.ts +202 -0
- package/src/primitives/toast/engine.ts +327 -0
- package/src/primitives/toast/index.ts +139 -0
- package/src/primitives/toast/view.ts +244 -0
- package/src/primitives/tooltip.ts +377 -0
- package/src/primitives/transition.ts +119 -0
- package/src/primitives/view-transition.ts +57 -0
- package/src/signal/bus.ts +126 -0
- package/src/signal/collection.ts +182 -0
- package/src/signal/index.ts +34 -0
- package/src/signal/resource.ts +102 -0
- package/src/signal/signal-map.ts +46 -0
- package/src/signal/signal.ts +1002 -0
- package/src/signal/store.ts +84 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Structural tree-diff: reconcile a parent's children against new nodes.
|
|
2
|
+
//
|
|
3
|
+
// A child the parent HAS corresponds to a child the caller ASKED for by, in
|
|
4
|
+
// order: the same node; the same key (a `*-id` attribute over `data-col`,
|
|
5
|
+
// same-key siblings pairing in document order); the next unclaimed unkeyed
|
|
6
|
+
// child. Each child is claimed at most once, so the tiers cannot disagree.
|
|
7
|
+
|
|
8
|
+
const handlerKeysMap = new WeakMap<HTMLElement, Set<string>>();
|
|
9
|
+
|
|
10
|
+
/** Register an on* handler property key for reconciliation tracking. */
|
|
11
|
+
export function trackHandler(el: HTMLElement, key: string): void {
|
|
12
|
+
let keys = handlerKeysMap.get(el);
|
|
13
|
+
if (!keys) {
|
|
14
|
+
keys = new Set();
|
|
15
|
+
handlerKeysMap.set(el, keys);
|
|
16
|
+
}
|
|
17
|
+
keys.add(key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Replace a parent's children with the given nodes or strings, reusing each
|
|
21
|
+
* existing child that corresponds to a requested one, so focus, selection,
|
|
22
|
+
* scroll and live input state survive. Strings become text nodes, a
|
|
23
|
+
* `DocumentFragment` contributes its children, `null`/`undefined` are skipped.
|
|
24
|
+
* A node already in `parent` is moved there unmodified; any other node is a
|
|
25
|
+
* template whose content may be copied into a reused child, and is never
|
|
26
|
+
* inserted itself. */
|
|
27
|
+
export function patch(
|
|
28
|
+
parent: Node,
|
|
29
|
+
...children: (string | Node | DocumentFragment | null | undefined)[]
|
|
30
|
+
): void {
|
|
31
|
+
const newChildren: Node[] = [];
|
|
32
|
+
for (const child of children) {
|
|
33
|
+
if (child == null) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (typeof child === "string") {
|
|
37
|
+
newChildren.push(document.createTextNode(child));
|
|
38
|
+
} else if (child.nodeType === 11) {
|
|
39
|
+
newChildren.push(...Array.from(child.childNodes));
|
|
40
|
+
} else {
|
|
41
|
+
newChildren.push(child);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
reconcileChildren(parent, newChildren);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Reconcile a parent node's children against a new set of child nodes, patching in place.
|
|
48
|
+
*
|
|
49
|
+
* @internal Not on the package's `exports` map. `patch(parent, ...nodes)` is
|
|
50
|
+
* the same call for a `Node[]`, with string, fragment and nullish handling on
|
|
51
|
+
* top.
|
|
52
|
+
*/
|
|
53
|
+
export function reconcileChildren(parent: Node, newChildren: Node[]): void {
|
|
54
|
+
// A node the caller NAMED is reserved for itself. This is the one fact that
|
|
55
|
+
// keeps the three tiers disjoint: a node already in this parent is seated as
|
|
56
|
+
// itself (tier 1), so tiers 2 and 3 must never hand it to a different child.
|
|
57
|
+
// Without the reservation a node can be claimed twice, and the second claim
|
|
58
|
+
// either removes it as a tag mismatch or patches another child's content into
|
|
59
|
+
// it — which also means reading out of a live sibling this loop has already
|
|
60
|
+
// mutated.
|
|
61
|
+
const named = new Set<Node>(newChildren);
|
|
62
|
+
|
|
63
|
+
// Partition the current children ONCE, before any mutation, by the tier
|
|
64
|
+
// allowed to claim them: a keyed child only ever through its key's FIFO queue,
|
|
65
|
+
// an unkeyed one only ever through the positional cursor. Nothing is in both,
|
|
66
|
+
// so tiers 2 and 3 cannot collide, and neither can yield a node twice — a
|
|
67
|
+
// queue consumes by shift, the cursor only moves forward.
|
|
68
|
+
const byKey = new Map<string, Node[]>();
|
|
69
|
+
const positional: Node[] = [];
|
|
70
|
+
for (let node = parent.firstChild; node !== null; node = node.nextSibling) {
|
|
71
|
+
const key = nodeKey(node);
|
|
72
|
+
if (key) {
|
|
73
|
+
const queue = byKey.get(key);
|
|
74
|
+
if (queue === undefined) {
|
|
75
|
+
byKey.set(key, [node]);
|
|
76
|
+
} else {
|
|
77
|
+
queue.push(node);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
positional.push(node);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The node currently at the index being resolved. Every branch below seats the
|
|
85
|
+
// resolved child at exactly that index, so the children before it are final
|
|
86
|
+
// and the next reference node is the seated child's next sibling — O(1), where
|
|
87
|
+
// a childNodes.item(i) lookup per step is not. That invariant is also what
|
|
88
|
+
// makes the sweep at the end of the loop exact.
|
|
89
|
+
let ref: Node | null = parent.firstChild;
|
|
90
|
+
let cursor = 0;
|
|
91
|
+
|
|
92
|
+
for (const newChild of newChildren) {
|
|
93
|
+
// Tier 1 — the same NODE. A child the caller handed back needs no lookup and
|
|
94
|
+
// no reconciliation: it already IS the requested child, so it falls straight
|
|
95
|
+
// through to being seated as itself below. Short-circuiting the other two
|
|
96
|
+
// tiers is what stops a sibling being claimed as its host — with duplicate
|
|
97
|
+
// keys, the key queue would otherwise skip the reserved node, reach the next
|
|
98
|
+
// one, find it patchable on the shared tag, and let the sweep delete the node
|
|
99
|
+
// the caller actually named.
|
|
100
|
+
let matched: Node | null = null;
|
|
101
|
+
if (newChild.parentNode !== parent) {
|
|
102
|
+
const key = nodeKey(newChild);
|
|
103
|
+
if (key) {
|
|
104
|
+
// Tier 2 — the same KEY.
|
|
105
|
+
const queue = byKey.get(key);
|
|
106
|
+
while (queue !== undefined && queue.length > 0) {
|
|
107
|
+
const candidate = queue.shift();
|
|
108
|
+
if (candidate !== undefined && !named.has(candidate)) {
|
|
109
|
+
matched = candidate;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
// Tier 3 — the same POSITION.
|
|
115
|
+
while (cursor < positional.length) {
|
|
116
|
+
const candidate = positional[cursor++];
|
|
117
|
+
if (candidate !== undefined && !named.has(candidate)) {
|
|
118
|
+
matched = candidate;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (matched !== null && canPatch(matched, newChild)) {
|
|
126
|
+
// An existing child hosts this one: seat it, then patch it to match. The
|
|
127
|
+
// requested node is a template and is not inserted.
|
|
128
|
+
//
|
|
129
|
+
// Re-inserting a node that is already in place still detaches and
|
|
130
|
+
// reattaches it, which blurs a focused input, so move only when the
|
|
131
|
+
// position actually differs.
|
|
132
|
+
if (ref !== matched) {
|
|
133
|
+
parent.insertBefore(matched, ref);
|
|
134
|
+
}
|
|
135
|
+
if (matched.nodeType === 3) {
|
|
136
|
+
if (matched.textContent !== newChild.textContent) {
|
|
137
|
+
matched.textContent = newChild.textContent;
|
|
138
|
+
}
|
|
139
|
+
} else if (matched.nodeType === 1) {
|
|
140
|
+
patchAttrs(matched as HTMLElement, newChild as HTMLElement);
|
|
141
|
+
reconcileChildren(matched, Array.from(newChild.childNodes));
|
|
142
|
+
}
|
|
143
|
+
ref = matched.nextSibling;
|
|
144
|
+
} else {
|
|
145
|
+
// Nothing to reconcile: either the caller handed back a node this parent
|
|
146
|
+
// already has, or nothing corresponds to it, or the tags disagree. Either
|
|
147
|
+
// way the requested node itself is seated here. A child that was matched
|
|
148
|
+
// but not patchable is NOT removed at this point — it now sits at or after
|
|
149
|
+
// `ref`, was consumed so no later index can claim it, and the sweep below
|
|
150
|
+
// takes it. One removal path, not two.
|
|
151
|
+
if (ref !== newChild) {
|
|
152
|
+
parent.insertBefore(newChild, ref);
|
|
153
|
+
}
|
|
154
|
+
ref = newChild.nextSibling;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Everything from `ref` on is a child the caller did not ask for. The
|
|
159
|
+
// invariant above makes that exact where comparing counts cannot be: pass one
|
|
160
|
+
// node twice and the requested count exceeds what a tree can hold, leaving a
|
|
161
|
+
// stale sibling behind.
|
|
162
|
+
while (ref !== null) {
|
|
163
|
+
const next = ref.nextSibling;
|
|
164
|
+
parent.removeChild(ref);
|
|
165
|
+
ref = next;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function canPatch(oldNode: Node, newNode: Node): boolean {
|
|
170
|
+
if (oldNode.nodeType !== newNode.nodeType) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (oldNode.nodeType === 3) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (oldNode.nodeType !== 1) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
return oldNode.nodeName === newNode.nodeName;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function nodeKey(node: Node): string {
|
|
183
|
+
if (node.nodeType !== 1) {
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
// A specific entity id (`*-id`) wins over the generic `data-col` key. The
|
|
187
|
+
// `data-col` scan is last-wins: `setAttributeNS` with a foreign namespace can
|
|
188
|
+
// add a second attribute named "data-col", and first-wins would key the node
|
|
189
|
+
// off it, recreating the node instead of reusing it.
|
|
190
|
+
let colKey = "";
|
|
191
|
+
for (const attr of (node as Element).attributes) {
|
|
192
|
+
if (attr.name.endsWith("-id")) {
|
|
193
|
+
return `${attr.name}=${attr.value}`;
|
|
194
|
+
}
|
|
195
|
+
if (attr.name === "data-col") {
|
|
196
|
+
colKey = `${attr.name}=${attr.value}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return colKey;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function patchAttrs(oldEl: HTMLElement, newEl: HTMLElement): void {
|
|
203
|
+
for (const attr of newEl.attributes) {
|
|
204
|
+
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
205
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const attr of Array.from(oldEl.attributes)) {
|
|
209
|
+
if (!newEl.hasAttribute(attr.name)) {
|
|
210
|
+
oldEl.removeAttribute(attr.name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// `checked`, `selected` and an input's or textarea's `value` are dirty-flag
|
|
215
|
+
// IDL properties whose content attributes are only defaults, so the loops
|
|
216
|
+
// above never reach the live state; without this a re-patch could not turn a
|
|
217
|
+
// checkbox off or replace a field's text. Writing `value` moves the caret, so a
|
|
218
|
+
// consumer that re-renders mid-keystroke must drive the field from an effect;
|
|
219
|
+
// docs/dom.md records the obligation. canPatch guarantees both nodes share a
|
|
220
|
+
// nodeName, so one side picks the branch.
|
|
221
|
+
if (oldEl.nodeName === "INPUT") {
|
|
222
|
+
const oldInput = oldEl as HTMLInputElement;
|
|
223
|
+
const newInput = newEl as HTMLInputElement;
|
|
224
|
+
// Each `!==` guard is required: a `.value` write sets the dirty-value flag
|
|
225
|
+
// for good, severing the property from its attribute, and on a raw-content
|
|
226
|
+
// field (`type=date`, `number`, `time`, ...) it destroys a half-typed entry.
|
|
227
|
+
// The tests pin all four guards through those flags, not through the caret.
|
|
228
|
+
if (oldInput.checked !== newInput.checked) {
|
|
229
|
+
oldInput.checked = newInput.checked;
|
|
230
|
+
}
|
|
231
|
+
if (oldInput.value !== newInput.value) {
|
|
232
|
+
oldInput.value = newInput.value;
|
|
233
|
+
}
|
|
234
|
+
} else if (oldEl.nodeName === "TEXTAREA") {
|
|
235
|
+
const oldArea = oldEl as HTMLTextAreaElement;
|
|
236
|
+
const newArea = newEl as HTMLTextAreaElement;
|
|
237
|
+
if (oldArea.value !== newArea.value) {
|
|
238
|
+
oldArea.value = newArea.value;
|
|
239
|
+
}
|
|
240
|
+
} else if (oldEl.nodeName === "OPTION") {
|
|
241
|
+
const oldOption = oldEl as HTMLOptionElement;
|
|
242
|
+
const newOption = newEl as HTMLOptionElement;
|
|
243
|
+
if (oldOption.selected !== newOption.selected) {
|
|
244
|
+
oldOption.selected = newOption.selected;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Reconcile on* event handler properties (not reflected as attributes).
|
|
249
|
+
const newKeys = handlerKeysMap.get(newEl);
|
|
250
|
+
const oldKeys = handlerKeysMap.get(oldEl);
|
|
251
|
+
if (oldKeys) {
|
|
252
|
+
for (const key of oldKeys) {
|
|
253
|
+
if (!newKeys?.has(key)) {
|
|
254
|
+
(oldEl as unknown as Record<string, unknown>)[key] = null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (newKeys) {
|
|
259
|
+
const oldRec = oldEl as unknown as Record<string, unknown>;
|
|
260
|
+
const newRec = newEl as unknown as Record<string, unknown>;
|
|
261
|
+
for (const key of newKeys) {
|
|
262
|
+
oldRec[key] = newRec[key];
|
|
263
|
+
}
|
|
264
|
+
handlerKeysMap.set(oldEl, new Set(newKeys));
|
|
265
|
+
} else if (oldKeys) {
|
|
266
|
+
handlerKeysMap.delete(oldEl);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Keyed-list DOM reconciliation with mount/update/onRemove lifecycle.
|
|
2
|
+
// Identity-preserving: existing elements survive across renders.
|
|
3
|
+
|
|
4
|
+
/** Attribute name used to store reconciliation keys on elements. */
|
|
5
|
+
export const KEY_ATTR = "data-reconcile-key";
|
|
6
|
+
|
|
7
|
+
/** Specification for keyed-list reconciliation lifecycle callbacks. */
|
|
8
|
+
export interface ReconcileSpec<T> {
|
|
9
|
+
key: (item: T) => string;
|
|
10
|
+
mount: (item: T) => HTMLElement;
|
|
11
|
+
update?: (el: HTMLElement, item: T) => void;
|
|
12
|
+
onRemove?: (el: HTMLElement, key: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Reconcile a keyed list of items into a parent node, preserving existing elements by key. */
|
|
16
|
+
export function reconcile<T>(
|
|
17
|
+
parent: ParentNode,
|
|
18
|
+
items: readonly T[],
|
|
19
|
+
spec: ReconcileSpec<T>,
|
|
20
|
+
): void {
|
|
21
|
+
const existing = new Map<string, HTMLElement>();
|
|
22
|
+
for (let n = parent.firstChild; n !== null; n = n.nextSibling) {
|
|
23
|
+
if (n.nodeType !== 1) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const el = n as HTMLElement;
|
|
27
|
+
const k = el.getAttribute(KEY_ATTR);
|
|
28
|
+
if (k !== null) {
|
|
29
|
+
existing.set(k, el);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// DEPARTING ELEMENTS GO FIRST, before anything is placed. The walk below runs
|
|
34
|
+
// backwards and skips an element already sitting before the last one it placed; a
|
|
35
|
+
// departing element still in the tree makes every PREDECESSOR's `nextSibling` point at
|
|
36
|
+
// a node about to vanish, so the guard sees a mismatch and re-seats a row nothing
|
|
37
|
+
// changed — which restarts its animations and drops `:hover` and focus. So `onRemove`
|
|
38
|
+
// fires before the survivors are placed, holding its own intact element; only the
|
|
39
|
+
// siblings' positions are not yet final. `key` is still called exactly once per item,
|
|
40
|
+
// and the pairs carry it into the walk so neither loop indexes an array.
|
|
41
|
+
const pairs: [T, string][] = [];
|
|
42
|
+
const wanted = new Set<string>();
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
const k = spec.key(item);
|
|
45
|
+
pairs.push([item, k]);
|
|
46
|
+
wanted.add(k);
|
|
47
|
+
}
|
|
48
|
+
for (const [k, el] of existing) {
|
|
49
|
+
if (!wanted.has(k)) {
|
|
50
|
+
spec.onRemove?.(el, k);
|
|
51
|
+
el.remove();
|
|
52
|
+
existing.delete(k);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let target: Node | null = null;
|
|
57
|
+
for (const [item, k] of pairs.reverse()) {
|
|
58
|
+
let el = existing.get(k);
|
|
59
|
+
if (el === undefined) {
|
|
60
|
+
el = spec.mount(item);
|
|
61
|
+
el.setAttribute(KEY_ATTR, k);
|
|
62
|
+
} else {
|
|
63
|
+
existing.delete(k);
|
|
64
|
+
if (spec.update) {
|
|
65
|
+
spec.update(el, item);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Skip the move when el is already at its target position: re-inserting a node
|
|
69
|
+
// that is already correctly placed still detaches+reattaches it in the DOM,
|
|
70
|
+
// which blurs a focused input / drops a text selection inside that row.
|
|
71
|
+
if (el.parentNode !== parent || el.nextSibling !== target) {
|
|
72
|
+
parent.insertBefore(el, target);
|
|
73
|
+
}
|
|
74
|
+
target = el;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Typed HTTP requests that never throw: each resolves to a result envelope, or to `null` on failure. @module */
|
|
2
|
+
|
|
3
|
+
export { createFetch } from "./instance.js";
|
|
4
|
+
export type { FetchInstance } from "./instance.js";
|
|
5
|
+
|
|
6
|
+
export { API_TIMEOUT_MS, withTimeout } from "./timeout.js";
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
ApiErr,
|
|
10
|
+
ApiOk,
|
|
11
|
+
ApiResult,
|
|
12
|
+
BlobRequestOptions,
|
|
13
|
+
Decoder,
|
|
14
|
+
FailedRequest,
|
|
15
|
+
FetchConfig,
|
|
16
|
+
HttpMethod,
|
|
17
|
+
RequestOptions,
|
|
18
|
+
} from "./types.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { makeRequest, makeRequestRaw } from "./request.js";
|
|
2
|
+
import { makeVerbs } from "./verbs.js";
|
|
3
|
+
import type { FetchVerbs } from "./verbs.js";
|
|
4
|
+
import type { FetchConfig, RequestFn, RequestRawFn } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/** An isolated fetch instance: its own immutable config, request core, and 12
|
|
7
|
+
* verb helpers. Returned by {@link createFetch}. */
|
|
8
|
+
export interface FetchInstance extends FetchVerbs {
|
|
9
|
+
/** The non-throwing request core bound to this instance. */
|
|
10
|
+
requestRaw: RequestRawFn;
|
|
11
|
+
/** The null-collapsing request bound to this instance. */
|
|
12
|
+
request: RequestFn;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create an isolated fetch instance. `config` is shallow-copied and frozen, so
|
|
17
|
+
* two instances never share settings and a changed backend is a new instance;
|
|
18
|
+
* read late-bound state, such as a token, inside `prepareHeaders`.
|
|
19
|
+
* Throws a `TypeError` on a `maxResponseBytes` that is `NaN` or negative.
|
|
20
|
+
*/
|
|
21
|
+
export function createFetch(config: FetchConfig = {}): FetchInstance {
|
|
22
|
+
const max = config.maxResponseBytes;
|
|
23
|
+
if (max !== undefined && !(max >= 0)) {
|
|
24
|
+
throw new TypeError(
|
|
25
|
+
`createFetch: maxResponseBytes must be a byte count of 0 or more (or Infinity for no cap), not ${String(max)}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const cfg: FetchConfig = Object.freeze({ ...config });
|
|
29
|
+
const requestRaw = makeRequestRaw(cfg);
|
|
30
|
+
const request = makeRequest(requestRaw);
|
|
31
|
+
const verbs = makeVerbs(request, requestRaw);
|
|
32
|
+
return {
|
|
33
|
+
...verbs,
|
|
34
|
+
requestRaw,
|
|
35
|
+
request,
|
|
36
|
+
};
|
|
37
|
+
}
|