@connextar/house 0.6.1 → 0.8.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/README.md +57 -1
- package/dist/ux/index.d.ts +1 -1
- package/dist/ux/index.d.ts.map +1 -1
- package/dist/ux/index.js +1 -1
- package/dist/ux/index.js.map +1 -1
- package/dist/ux/optimistic.d.ts +70 -0
- package/dist/ux/optimistic.d.ts.map +1 -1
- package/dist/ux/optimistic.js +107 -0
- package/dist/ux/optimistic.js.map +1 -1
- package/dist/ux/use-optimistic-value.d.ts +14 -6
- package/dist/ux/use-optimistic-value.d.ts.map +1 -1
- package/dist/ux/use-optimistic-value.js +39 -36
- package/dist/ux/use-optimistic-value.js.map +1 -1
- package/dist/web-search/index.d.ts +3 -0
- package/dist/web-search/index.d.ts.map +1 -0
- package/dist/web-search/index.js +3 -0
- package/dist/web-search/index.js.map +1 -0
- package/dist/web-search/providers.d.ts +33 -0
- package/dist/web-search/providers.d.ts.map +1 -0
- package/dist/web-search/providers.js +161 -0
- package/dist/web-search/providers.js.map +1 -0
- package/dist/web-search/types.d.ts +82 -0
- package/dist/web-search/types.d.ts.map +1 -0
- package/dist/web-search/types.js +29 -0
- package/dist/web-search/types.js.map +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -276,7 +276,8 @@ library, no HTTP client.
|
|
|
276
276
|
change as a _patch_ beside them, folded in on render. `insert`, `update`,
|
|
277
277
|
`remove`, `move` and `bulk` each take a commit function; `isPending(key)` marks
|
|
278
278
|
one row without disabling the table. `useOptimisticValue` is the same for a
|
|
279
|
-
switch, a status or a settings record
|
|
279
|
+
switch, a status or a settings record — a queue there too, not one held value;
|
|
280
|
+
`useAction` and `useKeyedAction` are for
|
|
280
281
|
work that should not be guessed at — a payment, a model call, a sign-in — and
|
|
281
282
|
give a control the `pending` flag it needs and nothing more.
|
|
282
283
|
|
|
@@ -301,6 +302,17 @@ which case they win and the screen corrects itself. Evidence, never a timer —
|
|
|
301
302
|
screen that does not refetch has no truth to fall back to, and snapping to stale
|
|
302
303
|
rows after N seconds is a bug that only appears on a slow connection.
|
|
303
304
|
|
|
305
|
+
**Two writes at once.** Each one lands or rolls back on its own, which is why
|
|
306
|
+
both hooks hold a queue rather than the current answer. A slow first write that
|
|
307
|
+
comes back after a second one keeps its place in the queue instead of winning
|
|
308
|
+
it, and a refusal takes down the change that was refused and nothing else.
|
|
309
|
+
Holding a single value gets both of these backwards: the first write's answer
|
|
310
|
+
reappears on screen a second after the person watched the second one save, and a
|
|
311
|
+
failed early write rolls back a later change nobody asked to undo. `merge` takes
|
|
312
|
+
the same discipline — two fields changed in the same breath both survive,
|
|
313
|
+
because a merge is held as its fields rather than as a copy of the value it was
|
|
314
|
+
read from.
|
|
315
|
+
|
|
304
316
|
**The three seams that keep it portable.** A commit is
|
|
305
317
|
`() => Promise<Row | void>` that **throws** on failure, which is the one failure
|
|
306
318
|
contract every client already has; an app whose client returns a result object
|
|
@@ -578,6 +590,50 @@ filter is dropped rather than throwing when the menu entry is picked. Where view
|
|
|
578
590
|
are stored — a table keyed by account, or the browser — is the app's decision,
|
|
579
591
|
because only the app knows whether a view should follow someone to their phone.
|
|
580
592
|
|
|
593
|
+
### `@connextar/house/web-search`
|
|
594
|
+
|
|
595
|
+
Google's organic results for a query, from whichever search API a deployment
|
|
596
|
+
has a key for — **Serper** or **VALUE SERP** — in one shape.
|
|
597
|
+
|
|
598
|
+
This one is here by decision rather than by count. AltEd is the first app to
|
|
599
|
+
search the web, but which search API a deployment pays for is a cost choice the
|
|
600
|
+
owner makes across the portfolio — two providers whose prices cross over at a
|
|
601
|
+
few thousand searches a month — and that choice should be one setting, not a
|
|
602
|
+
rewrite in every app that later needs it. What differs between the providers is
|
|
603
|
+
where the key goes, what the fields are called and how they bill; what an app
|
|
604
|
+
wants is identical. So the module is a factory per provider and one picker:
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
import { webSearchProviderFromEnv, WebSearchError } from "@connextar/house/web-search";
|
|
608
|
+
|
|
609
|
+
const search = webSearchProviderFromEnv(); // null without a key
|
|
610
|
+
const { provider, results } = await search!.search(
|
|
611
|
+
{ q: "chemistry tutor Leeds", country: "gb", language: "en", num: 20 },
|
|
612
|
+
{ timeoutMs: 12_000 },
|
|
613
|
+
);
|
|
614
|
+
// results: [{ position, title, url, snippet }]
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
`webSearchProviderFromEnv` reads `SERPER_API_KEY` and `VALUESERP_API_KEY`,
|
|
618
|
+
uses `WEB_SEARCH_PROVIDER` (`serper` or `valueserp`) to choose when both are
|
|
619
|
+
set, and otherwise prefers Serper. A named provider without a key falls back to
|
|
620
|
+
the other rather than to no search at all. `SERPER_URL` and `VALUESERP_URL`
|
|
621
|
+
point either at a stand-in for local checks. `provider.label` is the name to
|
|
622
|
+
list in a privacy policy.
|
|
623
|
+
|
|
624
|
+
**A failure is a `WebSearchError` with a code** — `unauthorised`,
|
|
625
|
+
`rate_limited`, `timeout`, `unreachable` or `bad_response` — and the HTTP status
|
|
626
|
+
where there was one. **It never carries the request's address, headers or
|
|
627
|
+
response body.** VALUE SERP takes the key in the query string, so a message that
|
|
628
|
+
quoted the address, or a body that echoed it, would put a live key into whatever
|
|
629
|
+
log caught the error; the tests assert the key appears nowhere in the message or
|
|
630
|
+
the stack for every status and both providers. Serper takes it in a header.
|
|
631
|
+
|
|
632
|
+
Billing differs and is the caller's to weigh: Serper charges one credit for up to
|
|
633
|
+
ten results and two for eleven to a hundred; VALUE SERP charges per request.
|
|
634
|
+
There is no caching here — what may be stored, and for how long, is each app's
|
|
635
|
+
decision and each provider's terms.
|
|
636
|
+
|
|
581
637
|
### Styles — `@connextar/house/house.css`
|
|
582
638
|
|
|
583
639
|
What the components need that utility classes cannot say: rich text, the editor's
|
package/dist/ux/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export { ActionError, errorMessage } from "./errors.js";
|
|
15
15
|
export { ActionFeedbackProvider, useActionReporter, type ActionFeedback, type ActionReporter } from "./feedback.js";
|
|
16
|
-
export { applyPatches, isRedundant, isTempKey, reconcile, sameRow, settlePatch, shouldDropSettled, supersede, tempKey, type Patch, type PatchKind, type RowKey, } from "./optimistic.js";
|
|
16
|
+
export { applyPatches, applyValuePatches, isRedundant, isRedundantValue, isTempKey, reconcile, reconcileValue, sameRow, settlePatch, settleValuePatch, shouldDropSettled, shouldDropSettledValue, supersede, supersedeValue, tempKey, type Patch, type PatchKind, type RowKey, type ValuePatch, } from "./optimistic.js";
|
|
17
17
|
export { useAction, useKeyedAction, type Action, type ActionOptions, type KeyedAction } from "./use-action.js";
|
|
18
18
|
export { useOptimisticList, type OptimisticList, type OptimisticListOptions, type WriteOptions, } from "./use-optimistic-list.js";
|
|
19
19
|
export { useOptimisticValue, type OptimisticValue, type OptimisticValueOptions } from "./use-optimistic-value.js";
|
package/dist/ux/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ux/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACpH,OAAO,EACL,YAAY,EACZ,WAAW,EACX,SAAS,EACT,SAAS,EACT,OAAO,EACP,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ux/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACpH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,cAAc,EACd,OAAO,EACP,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,SAAS,EACT,cAAc,EACd,OAAO,EACP,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,UAAU,GAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC/G,OAAO,EACL,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,YAAY,GAClB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,2BAA2B,CAAC"}
|
package/dist/ux/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export { ActionError, errorMessage } from "./errors.js";
|
|
15
15
|
export { ActionFeedbackProvider, useActionReporter } from "./feedback.js";
|
|
16
|
-
export { applyPatches, isRedundant, isTempKey, reconcile, sameRow, settlePatch, shouldDropSettled, supersede, tempKey, } from "./optimistic.js";
|
|
16
|
+
export { applyPatches, applyValuePatches, isRedundant, isRedundantValue, isTempKey, reconcile, reconcileValue, sameRow, settlePatch, settleValuePatch, shouldDropSettled, shouldDropSettledValue, supersede, supersedeValue, tempKey, } from "./optimistic.js";
|
|
17
17
|
export { useAction, useKeyedAction } from "./use-action.js";
|
|
18
18
|
export { useOptimisticList, } from "./use-optimistic-list.js";
|
|
19
19
|
export { useOptimisticValue } from "./use-optimistic-value.js";
|
package/dist/ux/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ux/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAA4C,MAAM,eAAe,CAAC;AACpH,OAAO,EACL,YAAY,EACZ,WAAW,EACX,SAAS,EACT,SAAS,EACT,OAAO,EACP,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ux/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAA4C,MAAM,eAAe,CAAC;AACpH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,cAAc,EACd,OAAO,EACP,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,SAAS,EACT,cAAc,EACd,OAAO,GAKR,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAqD,MAAM,iBAAiB,CAAC;AAC/G,OAAO,EACL,iBAAiB,GAIlB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAqD,MAAM,2BAA2B,CAAC"}
|
package/dist/ux/optimistic.d.ts
CHANGED
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
* A settled patch is dropped when fresh rows arrive that account for it. That
|
|
37
37
|
* is the whole reconciliation rule, and it is deliberately evidence-based
|
|
38
38
|
* rather than timed: see `shouldDropSettled`.
|
|
39
|
+
*
|
|
40
|
+
* The same rules apply to a single value — a switch, a status, a settings
|
|
41
|
+
* record — under the `…Value` names at the foot of this file. A queue there
|
|
42
|
+
* too, and for the sharper of the two reasons: see `ValuePatch`.
|
|
39
43
|
*/
|
|
40
44
|
/** How a row is identified. Ids are strings here; numbers stringify fine. */
|
|
41
45
|
export type RowKey = string;
|
|
@@ -136,4 +140,70 @@ export declare function supersede<T>(patches: readonly Patch<T>[], keys: Readonl
|
|
|
136
140
|
export declare function tempKey(prefix?: string): string;
|
|
137
141
|
/** Whether a key came from `tempKey` — a row that is still being created. */
|
|
138
142
|
export declare function isTempKey(key: RowKey): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* A change to a single value — a switch, a status, a settings record.
|
|
145
|
+
*
|
|
146
|
+
* The same queue as above, for the case where the base is one thing instead of
|
|
147
|
+
* an array of them. It is a queue and not one held value for the reason a list
|
|
148
|
+
* is: two writes can be in flight at once, and each has to be able to land or
|
|
149
|
+
* roll back **on its own**. A single slot makes whichever request answers last
|
|
150
|
+
* the winner, so a slow first write landing after a second one quietly puts the
|
|
151
|
+
* first answer back on screen — a change the person watched save, undone by
|
|
152
|
+
* nothing they did.
|
|
153
|
+
*/
|
|
154
|
+
export interface ValuePatch<T> {
|
|
155
|
+
readonly id: string;
|
|
156
|
+
/** `set` replaces the value; `merge` lays fields over whatever is beneath it. */
|
|
157
|
+
readonly kind: "set" | "merge";
|
|
158
|
+
/** `set`: the value to show. */
|
|
159
|
+
readonly value?: T;
|
|
160
|
+
/** `merge`: the fields to lay over it. */
|
|
161
|
+
readonly fields?: Partial<T>;
|
|
162
|
+
readonly settled: boolean;
|
|
163
|
+
/** What the base was when the write landed — see `shouldDropSettledValue`. */
|
|
164
|
+
readonly witness?: T;
|
|
165
|
+
}
|
|
166
|
+
/** The value to render: the server's, with every change laid over it in order. */
|
|
167
|
+
export declare function applyValuePatches<T>(base: T, patches: readonly ValuePatch<T>[]): T;
|
|
168
|
+
/** Does the base already say what this change says? */
|
|
169
|
+
export declare function isRedundantValue<T>(patch: ValuePatch<T>, base: T): boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Whether a settled change has been overtaken by a fresh value — the same two
|
|
172
|
+
* endings as `shouldDropSettled`: the base agrees, so the change has nothing
|
|
173
|
+
* left to show, or the base has moved since the write landed, in which case it
|
|
174
|
+
* wins whatever it says.
|
|
175
|
+
*/
|
|
176
|
+
export declare function shouldDropSettledValue<T>(patch: ValuePatch<T>, base: T): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* The queue after a fresh value arrived.
|
|
179
|
+
*
|
|
180
|
+
* Writes in flight are left alone. Settled ones go **together**, judged by the
|
|
181
|
+
* last of them: unlike a list, every change here is about the same one thing,
|
|
182
|
+
* so once fresh data has overtaken the newest of them the older ones have
|
|
183
|
+
* nothing to say either — and keeping some while dropping others is how a
|
|
184
|
+
* half-applied value would get on screen.
|
|
185
|
+
*/
|
|
186
|
+
export declare function reconcileValue<T>(patches: readonly ValuePatch<T>[], base: T): readonly ValuePatch<T>[];
|
|
187
|
+
/**
|
|
188
|
+
* Mark a landed write as settled, taking the server's own answer where it sent
|
|
189
|
+
* one and noting what the base said, so the next refresh can be recognised.
|
|
190
|
+
*
|
|
191
|
+
* A `set` takes the answer whole, because the caller declared the value's own
|
|
192
|
+
* shape by passing it. A `merge` takes only the fields it wrote, for the reason
|
|
193
|
+
* `settlePatch` does: a write route often answers with a thinner record than
|
|
194
|
+
* the read did, and laying that over the value blanks everything the write said
|
|
195
|
+
* nothing about.
|
|
196
|
+
*/
|
|
197
|
+
export declare function settleValuePatch<T>(patch: ValuePatch<T>, answer: T | undefined, base: T): ValuePatch<T>;
|
|
198
|
+
/**
|
|
199
|
+
* Drop what a write that has just landed supersedes: settled changes queued
|
|
200
|
+
* *before* a landed `set`, which replaced the whole value and so answered for
|
|
201
|
+
* them. Writes still in flight stay — each has a request that can still come
|
|
202
|
+
* back refused — and a landed `merge` supersedes nothing, because the changes
|
|
203
|
+
* under it may carry fields it says nothing about.
|
|
204
|
+
*
|
|
205
|
+
* Pruning only, never order: a landed change keeps its place in the queue, or a
|
|
206
|
+
* slow first write would jump ahead of the second one on landing and win.
|
|
207
|
+
*/
|
|
208
|
+
export declare function supersedeValue<T>(patches: readonly ValuePatch<T>[], landed: ValuePatch<T>): readonly ValuePatch<T>[];
|
|
139
209
|
//# sourceMappingURL=optimistic.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optimistic.d.ts","sourceRoot":"","sources":["../../src/ux/optimistic.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"optimistic.d.ts","sourceRoot":"","sources":["../../src/ux/optimistic.ts"],"names":[],"mappings":"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"}
|
package/dist/ux/optimistic.js
CHANGED
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
* A settled patch is dropped when fresh rows arrive that account for it. That
|
|
37
37
|
* is the whole reconciliation rule, and it is deliberately evidence-based
|
|
38
38
|
* rather than timed: see `shouldDropSettled`.
|
|
39
|
+
*
|
|
40
|
+
* The same rules apply to a single value — a switch, a status, a settings
|
|
41
|
+
* record — under the `…Value` names at the foot of this file. A queue there
|
|
42
|
+
* too, and for the sharper of the two reasons: see `ValuePatch`.
|
|
39
43
|
*/
|
|
40
44
|
let counter = 0;
|
|
41
45
|
/** A patch id. Unique within a session; never stored or sent anywhere. */
|
|
@@ -239,4 +243,107 @@ export function tempKey(prefix = "tmp") {
|
|
|
239
243
|
export function isTempKey(key) {
|
|
240
244
|
return key.startsWith("tmp:");
|
|
241
245
|
}
|
|
246
|
+
/** The value to render: the server's, with every change laid over it in order. */
|
|
247
|
+
export function applyValuePatches(base, patches) {
|
|
248
|
+
if (patches.length === 0)
|
|
249
|
+
return base;
|
|
250
|
+
let value = base;
|
|
251
|
+
for (const patch of patches) {
|
|
252
|
+
if (patch.kind === "set") {
|
|
253
|
+
value = patch.value;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
// Fields need something to go over. A merge against a value that is not an
|
|
257
|
+
// object has nothing to lay them on and is dropped rather than turned into
|
|
258
|
+
// an object that was never that shape.
|
|
259
|
+
if (typeof value === "object" && value !== null) {
|
|
260
|
+
value = { ...value, ...patch.fields };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
/** Does the base already say what this change says? */
|
|
266
|
+
export function isRedundantValue(patch, base) {
|
|
267
|
+
if (patch.kind === "set")
|
|
268
|
+
return sameRow(base, patch.value);
|
|
269
|
+
if (typeof base !== "object" || base === null)
|
|
270
|
+
return true;
|
|
271
|
+
const fields = (patch.fields ?? {});
|
|
272
|
+
const current = base;
|
|
273
|
+
return Object.keys(fields).every((key) => Object.is(current[key], fields[key]));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Whether a settled change has been overtaken by a fresh value — the same two
|
|
277
|
+
* endings as `shouldDropSettled`: the base agrees, so the change has nothing
|
|
278
|
+
* left to show, or the base has moved since the write landed, in which case it
|
|
279
|
+
* wins whatever it says.
|
|
280
|
+
*/
|
|
281
|
+
export function shouldDropSettledValue(patch, base) {
|
|
282
|
+
return isRedundantValue(patch, base) || !sameRow(base, patch.witness);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The queue after a fresh value arrived.
|
|
286
|
+
*
|
|
287
|
+
* Writes in flight are left alone. Settled ones go **together**, judged by the
|
|
288
|
+
* last of them: unlike a list, every change here is about the same one thing,
|
|
289
|
+
* so once fresh data has overtaken the newest of them the older ones have
|
|
290
|
+
* nothing to say either — and keeping some while dropping others is how a
|
|
291
|
+
* half-applied value would get on screen.
|
|
292
|
+
*/
|
|
293
|
+
export function reconcileValue(patches, base) {
|
|
294
|
+
if (patches.length === 0)
|
|
295
|
+
return patches;
|
|
296
|
+
let newest = null;
|
|
297
|
+
for (const patch of patches)
|
|
298
|
+
if (patch.settled)
|
|
299
|
+
newest = patch;
|
|
300
|
+
if (newest === null || !shouldDropSettledValue(newest, base))
|
|
301
|
+
return patches;
|
|
302
|
+
return patches.filter((patch) => !patch.settled);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Mark a landed write as settled, taking the server's own answer where it sent
|
|
306
|
+
* one and noting what the base said, so the next refresh can be recognised.
|
|
307
|
+
*
|
|
308
|
+
* A `set` takes the answer whole, because the caller declared the value's own
|
|
309
|
+
* shape by passing it. A `merge` takes only the fields it wrote, for the reason
|
|
310
|
+
* `settlePatch` does: a write route often answers with a thinner record than
|
|
311
|
+
* the read did, and laying that over the value blanks everything the write said
|
|
312
|
+
* nothing about.
|
|
313
|
+
*/
|
|
314
|
+
export function settleValuePatch(patch, answer, base) {
|
|
315
|
+
const settled = { ...patch, settled: true, witness: base };
|
|
316
|
+
if (answer === null || answer === undefined)
|
|
317
|
+
return settled;
|
|
318
|
+
if (patch.kind === "set")
|
|
319
|
+
return { ...settled, value: answer };
|
|
320
|
+
if (typeof answer !== "object")
|
|
321
|
+
return settled;
|
|
322
|
+
const sent = answer;
|
|
323
|
+
const corrected = { ...(patch.fields ?? {}) };
|
|
324
|
+
for (const field of Object.keys(corrected)) {
|
|
325
|
+
if (field in sent)
|
|
326
|
+
corrected[field] = sent[field];
|
|
327
|
+
}
|
|
328
|
+
return { ...settled, fields: corrected };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Drop what a write that has just landed supersedes: settled changes queued
|
|
332
|
+
* *before* a landed `set`, which replaced the whole value and so answered for
|
|
333
|
+
* them. Writes still in flight stay — each has a request that can still come
|
|
334
|
+
* back refused — and a landed `merge` supersedes nothing, because the changes
|
|
335
|
+
* under it may carry fields it says nothing about.
|
|
336
|
+
*
|
|
337
|
+
* Pruning only, never order: a landed change keeps its place in the queue, or a
|
|
338
|
+
* slow first write would jump ahead of the second one on landing and win.
|
|
339
|
+
*/
|
|
340
|
+
export function supersedeValue(patches, landed) {
|
|
341
|
+
if (landed.kind !== "set")
|
|
342
|
+
return patches;
|
|
343
|
+
const at = patches.findIndex((patch) => patch.id === landed.id);
|
|
344
|
+
if (at <= 0)
|
|
345
|
+
return patches;
|
|
346
|
+
const kept = patches.filter((patch, index) => index >= at || !patch.settled);
|
|
347
|
+
return kept.length === patches.length ? patches : kept;
|
|
348
|
+
}
|
|
242
349
|
//# sourceMappingURL=optimistic.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optimistic.js","sourceRoot":"","sources":["../../src/ux/optimistic.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"optimistic.js","sourceRoot":"","sources":["../../src/ux/optimistic.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,7 +20,12 @@ export interface OptimisticValue<T> {
|
|
|
20
20
|
success?: string;
|
|
21
21
|
error?: string;
|
|
22
22
|
}) => Promise<boolean>;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The same for an object value: lay these fields over it, whatever else is
|
|
25
|
+
* held. Two fields changed in the same breath both survive, and the commit's
|
|
26
|
+
* answer corrects only the fields this write set — a thinner record than the
|
|
27
|
+
* read sent cannot blank the rest.
|
|
28
|
+
*/
|
|
24
29
|
merge: (fields: Partial<T>, commit: () => Promise<T | void>, options?: {
|
|
25
30
|
success?: string;
|
|
26
31
|
error?: string;
|
|
@@ -30,11 +35,14 @@ export interface OptimisticValue<T> {
|
|
|
30
35
|
* One value the screen can change before the server has agreed: a switch, a
|
|
31
36
|
* status, a counter, a settings record.
|
|
32
37
|
*
|
|
33
|
-
* Same rules as `useOptimisticList` and for the same reasons —
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
+
* Same rules as `useOptimisticList` and for the same reasons — changes are held
|
|
39
|
+
* beside the server's value rather than replacing it, so a failure rolls back to
|
|
40
|
+
* something real and a refresh always wins. A switch is the case where getting
|
|
41
|
+
* this wrong is most obvious: flipping back a second after it was flipped, with
|
|
42
|
+
* no explanation, is worse than not moving at all.
|
|
43
|
+
*
|
|
44
|
+
* Changes are a queue, not one slot, so two overlapping writes each land or roll
|
|
45
|
+
* back on their own. See `ValuePatch` for what a single slot got wrong.
|
|
38
46
|
*/
|
|
39
47
|
export declare function useOptimisticValue<T>(base: T, options?: OptimisticValueOptions): OptimisticValue<T>;
|
|
40
48
|
//# sourceMappingURL=use-optimistic-value.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-optimistic-value.d.ts","sourceRoot":"","sources":["../../src/ux/use-optimistic-value.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-optimistic-value.d.ts","sourceRoot":"","sources":["../../src/ux/use-optimistic-value.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,sBAAsB;IACrC,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,+EAA+E;IAC/E,KAAK,EAAE,CAAC,CAAC;IACT,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;OAMG;IACH,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpH;;;;;OAKG;IACH,KAAK,EAAE,CACL,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAClB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAC/B,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAC3C,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAAC,CAAC,CAAC,CAwEvG"}
|
|
@@ -2,50 +2,49 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { errorMessage } from "./errors.js";
|
|
4
4
|
import { useActionReporter } from "./feedback.js";
|
|
5
|
-
import {
|
|
5
|
+
import { applyValuePatches, nextPatchId, reconcileValue, settleValuePatch, supersedeValue, } from "./optimistic.js";
|
|
6
|
+
const NO_PATCHES = [];
|
|
6
7
|
/**
|
|
7
8
|
* One value the screen can change before the server has agreed: a switch, a
|
|
8
9
|
* status, a counter, a settings record.
|
|
9
10
|
*
|
|
10
|
-
* Same rules as `useOptimisticList` and for the same reasons —
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Same rules as `useOptimisticList` and for the same reasons — changes are held
|
|
12
|
+
* beside the server's value rather than replacing it, so a failure rolls back to
|
|
13
|
+
* something real and a refresh always wins. A switch is the case where getting
|
|
14
|
+
* this wrong is most obvious: flipping back a second after it was flipped, with
|
|
15
|
+
* no explanation, is worse than not moving at all.
|
|
16
|
+
*
|
|
17
|
+
* Changes are a queue, not one slot, so two overlapping writes each land or roll
|
|
18
|
+
* back on their own. See `ValuePatch` for what a single slot got wrong.
|
|
15
19
|
*/
|
|
16
20
|
export function useOptimisticValue(base, options = {}) {
|
|
17
21
|
const report = useActionReporter();
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
// each render rather than stored, so an object rebuilt by the parent on
|
|
22
|
-
// every render cannot start a render loop.
|
|
23
|
-
const [held, setHeld] = React.useState(null);
|
|
24
|
-
const latest = React.useRef({ options, report });
|
|
25
|
-
React.useEffect(() => {
|
|
26
|
-
latest.current = { options, report };
|
|
27
|
-
});
|
|
28
|
-
// Nothing is stored when this flips: the next `set` replaces the hold
|
|
29
|
-
// anyway, and clearing it from an effect would only add a render.
|
|
30
|
-
const overtaken = held !== null && held.settled && !sameRow(held.witness, base);
|
|
31
|
-
const value = held && !overtaken ? held.value : base;
|
|
32
|
-
const valueRef = React.useRef(value);
|
|
33
|
-
const baseRef = React.useRef(base);
|
|
22
|
+
const [patches, setPatches] = React.useState(NO_PATCHES);
|
|
23
|
+
// Everything the asynchronous half needs, read at the time it runs.
|
|
24
|
+
const latest = React.useRef({ base, options, report });
|
|
34
25
|
React.useEffect(() => {
|
|
35
|
-
|
|
36
|
-
baseRef.current = base;
|
|
26
|
+
latest.current = { base, options, report };
|
|
37
27
|
});
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
// Worked out on each render rather than stored, so a value the parent rebuilds
|
|
29
|
+
// every render cannot start a render loop. A settled change knows what the
|
|
30
|
+
// base said when it landed, which is how "nothing has refreshed yet" is told
|
|
31
|
+
// apart from "the server now says something else".
|
|
32
|
+
const live = reconcileValue(patches, base);
|
|
33
|
+
const value = applyValuePatches(base, live);
|
|
34
|
+
const write = React.useCallback(async (patch, commit, per) => {
|
|
35
|
+
// Queued at the end, where it wins on render, and *without* clearing what
|
|
36
|
+
// is already held. Reconciling here is the only pruning the stored queue
|
|
37
|
+
// gets; render works it out without touching state, as it must.
|
|
38
|
+
setPatches((previous) => [...reconcileValue(previous, latest.current.base), patch]);
|
|
40
39
|
const { options: opts, report: tell } = latest.current;
|
|
41
40
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
const answer = (await commit());
|
|
42
|
+
setPatches((previous) => {
|
|
43
|
+
// In place, keeping its position: a slow first write that lands after
|
|
44
|
+
// a second one must not jump the queue and win.
|
|
45
|
+
const next = previous.map((held) => held.id === patch.id ? settleValuePatch(held, answer ?? undefined, latest.current.base) : held);
|
|
46
|
+
const landed = next.find((held) => held.id === patch.id);
|
|
47
|
+
return landed ? supersedeValue(next, landed) : next;
|
|
49
48
|
});
|
|
50
49
|
if (per?.success)
|
|
51
50
|
tell({ tone: "success", title: per.success });
|
|
@@ -53,7 +52,10 @@ export function useOptimisticValue(base, options = {}) {
|
|
|
53
52
|
return true;
|
|
54
53
|
}
|
|
55
54
|
catch (thrown) {
|
|
56
|
-
|
|
55
|
+
// Roll back this write and only this one. A later change still in flight
|
|
56
|
+
// is a change the person can see, and taking it down because an earlier
|
|
57
|
+
// request was refused undoes something they never asked to undo.
|
|
58
|
+
setPatches((previous) => previous.filter((held) => held.id !== patch.id));
|
|
57
59
|
tell({
|
|
58
60
|
tone: "error",
|
|
59
61
|
title: per?.error ?? opts.error ?? "That didn't save",
|
|
@@ -62,7 +64,8 @@ export function useOptimisticValue(base, options = {}) {
|
|
|
62
64
|
return false;
|
|
63
65
|
}
|
|
64
66
|
}, []);
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
+
const set = React.useCallback((next, commit, per) => write({ id: nextPatchId(), kind: "set", value: next, settled: false }, commit, per), [write]);
|
|
68
|
+
const merge = React.useCallback((fields, commit, per) => write({ id: nextPatchId(), kind: "merge", fields, settled: false }, commit, per), [write]);
|
|
69
|
+
return { value, pending: live.some((patch) => !patch.settled), set, merge };
|
|
67
70
|
}
|
|
68
71
|
//# sourceMappingURL=use-optimistic-value.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-optimistic-value.js","sourceRoot":"","sources":["../../src/ux/use-optimistic-value.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,
|
|
1
|
+
{"version":3,"file":"use-optimistic-value.js","sourceRoot":"","sources":["../../src/ux/use-optimistic-value.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { WebSearchError, type FetchLike, type OrganicResult, type ProviderOptions, type WebSearchCallOptions, type WebSearchErrorCode, type WebSearchProvider, type WebSearchProviderName, type WebSearchRequest, type WebSearchResponse, } from "./types.js";
|
|
2
|
+
export { serper, valueSerp, webSearchProviderFromEnv, type WebSearchEnv } from "./providers.js";
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web-search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,wBAAwB,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/web-search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAUf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,wBAAwB,EAAqB,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type FetchLike, type ProviderOptions, type WebSearchProvider } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Serper (serper.dev). The key goes in a header, never the address. One credit
|
|
4
|
+
* buys up to ten results; eleven to a hundred cost two.
|
|
5
|
+
*/
|
|
6
|
+
export declare function serper(options: ProviderOptions): WebSearchProvider;
|
|
7
|
+
/**
|
|
8
|
+
* VALUE SERP (Traject Data). **The key goes in the query string** — that is
|
|
9
|
+
* their API, not a choice — so this module never lets the address reach an
|
|
10
|
+
* error message.
|
|
11
|
+
*/
|
|
12
|
+
export declare function valueSerp(options: ProviderOptions): WebSearchProvider;
|
|
13
|
+
/** What a deployment's environment says about search. */
|
|
14
|
+
export interface WebSearchEnv {
|
|
15
|
+
/** `serper` or `valueserp`, to choose when both keys are set. */
|
|
16
|
+
WEB_SEARCH_PROVIDER?: string;
|
|
17
|
+
SERPER_API_KEY?: string;
|
|
18
|
+
SERPER_URL?: string;
|
|
19
|
+
VALUESERP_API_KEY?: string;
|
|
20
|
+
VALUESERP_URL?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The provider a deployment is set up for, or null when it has no key.
|
|
24
|
+
*
|
|
25
|
+
* `WEB_SEARCH_PROVIDER` decides when it names a provider that has a key;
|
|
26
|
+
* otherwise Serper is used if its key is set, then VALUE SERP. A named provider
|
|
27
|
+
* without a key falls back rather than failing, so a half-finished change to
|
|
28
|
+
* the environment degrades to the other provider instead of to no search.
|
|
29
|
+
*/
|
|
30
|
+
export declare function webSearchProviderFromEnv(env?: WebSearchEnv, options?: {
|
|
31
|
+
fetch?: FetchLike;
|
|
32
|
+
}): WebSearchProvider | null;
|
|
33
|
+
//# sourceMappingURL=providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/web-search/providers.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAEd,KAAK,eAAe,EAEpB,KAAK,iBAAiB,EAIvB,MAAM,YAAY,CAAC;AA4DpB;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,iBAAiB,CAsBlE;AAWD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,eAAe,GAAG,iBAAiB,CA0BrE;AAED,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,GAAE,YAA0C,EAC/C,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAA;CAAO,GAClC,iBAAiB,GAAG,IAAI,CAe1B"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { WebSearchError, } from "./types.js";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 12_000;
|
|
3
|
+
function signalFor(options) {
|
|
4
|
+
const timeout = AbortSignal.timeout(options?.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
5
|
+
return options?.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
6
|
+
}
|
|
7
|
+
/** The status, as a code — and nothing of the response body, which can echo the request. */
|
|
8
|
+
function codeFor(status) {
|
|
9
|
+
if (status === 401 || status === 403)
|
|
10
|
+
return "unauthorised";
|
|
11
|
+
if (status === 402 || status === 429)
|
|
12
|
+
return "rate_limited";
|
|
13
|
+
return "bad_response";
|
|
14
|
+
}
|
|
15
|
+
async function call(provider, doFetch, url, init) {
|
|
16
|
+
let response;
|
|
17
|
+
try {
|
|
18
|
+
response = await doFetch(url, init);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// Never the error itself: a fetch failure can quote the address.
|
|
22
|
+
const aborted = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
23
|
+
throw new WebSearchError(provider, aborted ? "timeout" : "unreachable");
|
|
24
|
+
}
|
|
25
|
+
if (!response.ok)
|
|
26
|
+
throw new WebSearchError(provider, codeFor(response.status), response.status);
|
|
27
|
+
try {
|
|
28
|
+
return await response.json();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new WebSearchError(provider, "bad_response", response.status);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const text = (value) => (typeof value === "string" && value.trim() ? value.trim() : null);
|
|
35
|
+
/** Rows with a title and an address, numbered as the engine ranked them. */
|
|
36
|
+
function organic(rows) {
|
|
37
|
+
if (!Array.isArray(rows))
|
|
38
|
+
return [];
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const [index, row] of rows.entries()) {
|
|
41
|
+
if (!row || typeof row !== "object")
|
|
42
|
+
continue;
|
|
43
|
+
const r = row;
|
|
44
|
+
const title = text(r.title);
|
|
45
|
+
const url = text(r.link);
|
|
46
|
+
if (!title || !url)
|
|
47
|
+
continue;
|
|
48
|
+
out.push({
|
|
49
|
+
position: typeof r.position === "number" ? r.position : index + 1,
|
|
50
|
+
title,
|
|
51
|
+
url,
|
|
52
|
+
snippet: text(r.snippet),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Serper (serper.dev). The key goes in a header, never the address. One credit
|
|
59
|
+
* buys up to ten results; eleven to a hundred cost two.
|
|
60
|
+
*/
|
|
61
|
+
export function serper(options) {
|
|
62
|
+
const doFetch = options.fetch ?? fetch;
|
|
63
|
+
const endpoint = new URL("search", `${(options.baseUrl ?? "https://google.serper.dev").replace(/\/+$/, "")}/`);
|
|
64
|
+
return {
|
|
65
|
+
name: "serper",
|
|
66
|
+
label: "Serper",
|
|
67
|
+
async search(request, callOptions) {
|
|
68
|
+
const body = { q: request.q };
|
|
69
|
+
if (request.country)
|
|
70
|
+
body.gl = request.country.toLowerCase();
|
|
71
|
+
if (request.language)
|
|
72
|
+
body.hl = request.language.toLowerCase();
|
|
73
|
+
if (request.num)
|
|
74
|
+
body.num = request.num;
|
|
75
|
+
const data = await call("serper", doFetch, endpoint, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "X-API-KEY": options.apiKey, "Content-Type": "application/json", Accept: "application/json" },
|
|
78
|
+
body: JSON.stringify(body),
|
|
79
|
+
signal: signalFor(callOptions),
|
|
80
|
+
});
|
|
81
|
+
const answer = data;
|
|
82
|
+
if (!answer || !Array.isArray(answer.organic))
|
|
83
|
+
throw new WebSearchError("serper", "bad_response");
|
|
84
|
+
return { provider: "serper", results: organic(answer.organic) };
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** The Google domain for a country, so ValueSERP searches the same Google a person there would. */
|
|
89
|
+
function googleDomain(country) {
|
|
90
|
+
if (!country)
|
|
91
|
+
return undefined;
|
|
92
|
+
const code = country.toLowerCase();
|
|
93
|
+
if (code === "gb" || code === "uk")
|
|
94
|
+
return "google.co.uk";
|
|
95
|
+
if (code === "us")
|
|
96
|
+
return "google.com";
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* VALUE SERP (Traject Data). **The key goes in the query string** — that is
|
|
101
|
+
* their API, not a choice — so this module never lets the address reach an
|
|
102
|
+
* error message.
|
|
103
|
+
*/
|
|
104
|
+
export function valueSerp(options) {
|
|
105
|
+
const doFetch = options.fetch ?? fetch;
|
|
106
|
+
const base = options.baseUrl ?? "https://api.valueserp.com/search";
|
|
107
|
+
return {
|
|
108
|
+
name: "valueserp",
|
|
109
|
+
label: "VALUE SERP",
|
|
110
|
+
async search(request, callOptions) {
|
|
111
|
+
const url = new URL(base);
|
|
112
|
+
url.searchParams.set("api_key", options.apiKey);
|
|
113
|
+
url.searchParams.set("q", request.q);
|
|
114
|
+
if (request.country)
|
|
115
|
+
url.searchParams.set("gl", request.country.toLowerCase());
|
|
116
|
+
if (request.language)
|
|
117
|
+
url.searchParams.set("hl", request.language.toLowerCase());
|
|
118
|
+
const domain = googleDomain(request.country);
|
|
119
|
+
if (domain)
|
|
120
|
+
url.searchParams.set("google_domain", domain);
|
|
121
|
+
if (request.num)
|
|
122
|
+
url.searchParams.set("num", String(request.num));
|
|
123
|
+
url.searchParams.set("output", "json");
|
|
124
|
+
const data = await call("valueserp", doFetch, url, {
|
|
125
|
+
headers: { Accept: "application/json" },
|
|
126
|
+
signal: signalFor(callOptions),
|
|
127
|
+
});
|
|
128
|
+
const answer = data;
|
|
129
|
+
if (!answer || answer.request_info?.success === false)
|
|
130
|
+
throw new WebSearchError("valueserp", "bad_response");
|
|
131
|
+
// A search with no results has no `organic_results` at all.
|
|
132
|
+
return { provider: "valueserp", results: organic(answer.organic_results ?? []) };
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The provider a deployment is set up for, or null when it has no key.
|
|
138
|
+
*
|
|
139
|
+
* `WEB_SEARCH_PROVIDER` decides when it names a provider that has a key;
|
|
140
|
+
* otherwise Serper is used if its key is set, then VALUE SERP. A named provider
|
|
141
|
+
* without a key falls back rather than failing, so a half-finished change to
|
|
142
|
+
* the environment degrades to the other provider instead of to no search.
|
|
143
|
+
*/
|
|
144
|
+
export function webSearchProviderFromEnv(env = process.env, options = {}) {
|
|
145
|
+
const keys = {
|
|
146
|
+
serper: env.SERPER_API_KEY?.trim() || undefined,
|
|
147
|
+
valueserp: env.VALUESERP_API_KEY?.trim() || undefined,
|
|
148
|
+
};
|
|
149
|
+
const wanted = env.WEB_SEARCH_PROVIDER?.trim().toLowerCase();
|
|
150
|
+
const order = wanted === "valueserp" ? ["valueserp", "serper"] : ["serper", "valueserp"];
|
|
151
|
+
for (const name of order) {
|
|
152
|
+
const apiKey = keys[name];
|
|
153
|
+
if (!apiKey)
|
|
154
|
+
continue;
|
|
155
|
+
return name === "serper"
|
|
156
|
+
? serper({ apiKey, baseUrl: env.SERPER_URL?.trim() || undefined, fetch: options.fetch })
|
|
157
|
+
: valueSerp({ apiKey, baseUrl: env.VALUESERP_URL?.trim() || undefined, fetch: options.fetch });
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=providers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/web-search/providers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One search engine, whichever company answers it.
|
|
3
|
+
*
|
|
4
|
+
* An app that looks further than its own database — AltEd's tutors from the
|
|
5
|
+
* wider web was the first — wants the same thing from a search API: Google's
|
|
6
|
+
* organic results for a query, in a country and a language, quickly, and an
|
|
7
|
+
* error that says what went wrong without repeating the key it was sent with. Providers differ in everything else —
|
|
8
|
+
* where the key goes, what the fields are called, what a result costs — and
|
|
9
|
+
* that is exactly what this module hides.
|
|
10
|
+
*/
|
|
11
|
+
/** The providers this module speaks to. */
|
|
12
|
+
export type WebSearchProviderName = "serper" | "valueserp";
|
|
13
|
+
/** What to search for. Everything but the words is optional. */
|
|
14
|
+
export interface WebSearchRequest {
|
|
15
|
+
/** The words, as they would be typed into the engine. */
|
|
16
|
+
q: string;
|
|
17
|
+
/** Two-letter country the results are for — `gl`: "gb", "us". */
|
|
18
|
+
country?: string;
|
|
19
|
+
/** Two-letter interface language — `hl`: "en". */
|
|
20
|
+
language?: string;
|
|
21
|
+
/** How many results to ask for. Providers bill differently above ten; see each provider. */
|
|
22
|
+
num?: number;
|
|
23
|
+
}
|
|
24
|
+
/** One organic result, the same shape from every provider. */
|
|
25
|
+
export interface OrganicResult {
|
|
26
|
+
/** 1-based, as the engine ranked it. */
|
|
27
|
+
position: number;
|
|
28
|
+
title: string;
|
|
29
|
+
/** The page's address, exactly as the engine gave it. */
|
|
30
|
+
url: string;
|
|
31
|
+
/** The engine's extract of the page, if it gave one. */
|
|
32
|
+
snippet: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface WebSearchResponse {
|
|
35
|
+
provider: WebSearchProviderName;
|
|
36
|
+
results: OrganicResult[];
|
|
37
|
+
}
|
|
38
|
+
export interface WebSearchCallOptions {
|
|
39
|
+
/** Abandon the request after this long. Default 12 seconds. */
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
/** The caller's own cancellation, combined with the timeout. */
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}
|
|
44
|
+
export interface WebSearchProvider {
|
|
45
|
+
readonly name: WebSearchProviderName;
|
|
46
|
+
/** A person-facing name, for a privacy policy's list of processors. */
|
|
47
|
+
readonly label: string;
|
|
48
|
+
search(request: WebSearchRequest, options?: WebSearchCallOptions): Promise<WebSearchResponse>;
|
|
49
|
+
}
|
|
50
|
+
/** Why a search failed, as a code an app can branch on. */
|
|
51
|
+
export type WebSearchErrorCode =
|
|
52
|
+
/** The key was refused. */
|
|
53
|
+
"unauthorised"
|
|
54
|
+
/** The provider's rate limit or the account's credit ran out. */
|
|
55
|
+
| "rate_limited"
|
|
56
|
+
/** The request never got an answer in time. */
|
|
57
|
+
| "timeout"
|
|
58
|
+
/** The provider could not be reached at all. */
|
|
59
|
+
| "unreachable"
|
|
60
|
+
/** It answered, but not with results. */
|
|
61
|
+
| "bad_response";
|
|
62
|
+
/**
|
|
63
|
+
* A failed search. **Its message never contains the request's address or
|
|
64
|
+
* headers**: one provider takes the key in the query string, and an error that
|
|
65
|
+
* quoted the address would put a live key into whatever log caught it.
|
|
66
|
+
*/
|
|
67
|
+
export declare class WebSearchError extends Error {
|
|
68
|
+
readonly provider: WebSearchProviderName;
|
|
69
|
+
readonly code: WebSearchErrorCode;
|
|
70
|
+
/** The HTTP status, when there was one. */
|
|
71
|
+
readonly status: number | undefined;
|
|
72
|
+
constructor(provider: WebSearchProviderName, code: WebSearchErrorCode, status?: number);
|
|
73
|
+
}
|
|
74
|
+
/** Injected for tests and for runtimes with their own `fetch`. */
|
|
75
|
+
export type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
76
|
+
export interface ProviderOptions {
|
|
77
|
+
apiKey: string;
|
|
78
|
+
/** Point at a stand-in for local checks. */
|
|
79
|
+
baseUrl?: string;
|
|
80
|
+
fetch?: FetchLike;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/web-search/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,2CAA2C;AAC3C,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE3D,gEAAgE;AAChE,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,CAAC,EAAE,MAAM,CAAC;IACV,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,wDAAwD;IACxD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/F;AAED,2DAA2D;AAC3D,MAAM,MAAM,kBAAkB;AAC5B,2BAA2B;AACzB,cAAc;AAChB,iEAAiE;GAC/D,cAAc;AAChB,+CAA+C;GAC7C,SAAS;AACX,gDAAgD;GAC9C,aAAa;AACf,yCAAyC;GACvC,cAAc,CAAC;AAEnB;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;gBAExB,QAAQ,EAAE,qBAAqB,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,MAAM;CAOvF;AAED,kEAAkE;AAClE,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One search engine, whichever company answers it.
|
|
3
|
+
*
|
|
4
|
+
* An app that looks further than its own database — AltEd's tutors from the
|
|
5
|
+
* wider web was the first — wants the same thing from a search API: Google's
|
|
6
|
+
* organic results for a query, in a country and a language, quickly, and an
|
|
7
|
+
* error that says what went wrong without repeating the key it was sent with. Providers differ in everything else —
|
|
8
|
+
* where the key goes, what the fields are called, what a result costs — and
|
|
9
|
+
* that is exactly what this module hides.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* A failed search. **Its message never contains the request's address or
|
|
13
|
+
* headers**: one provider takes the key in the query string, and an error that
|
|
14
|
+
* quoted the address would put a live key into whatever log caught it.
|
|
15
|
+
*/
|
|
16
|
+
export class WebSearchError extends Error {
|
|
17
|
+
provider;
|
|
18
|
+
code;
|
|
19
|
+
/** The HTTP status, when there was one. */
|
|
20
|
+
status;
|
|
21
|
+
constructor(provider, code, status) {
|
|
22
|
+
super(`The ${provider} search failed (${code}${status ? `, HTTP ${status}` : ""}).`);
|
|
23
|
+
this.name = "WebSearchError";
|
|
24
|
+
this.provider = provider;
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.status = status;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/web-search/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA4DH;;;;GAIG;AACH,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,QAAQ,CAAwB;IAChC,IAAI,CAAqB;IAClC,2CAA2C;IAClC,MAAM,CAAqB;IAEpC,YAAY,QAA+B,EAAE,IAAwB,EAAE,MAAe;QACpF,KAAK,CAAC,OAAO,QAAQ,mBAAmB,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,UAAU,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACrF,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@connextar/house",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "The pieces every app we build needs and none of them should own a copy of.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,6 +54,10 @@
|
|
|
54
54
|
"types": "./dist/grid/index.d.ts",
|
|
55
55
|
"default": "./dist/grid/index.js"
|
|
56
56
|
},
|
|
57
|
+
"./web-search": {
|
|
58
|
+
"types": "./dist/web-search/index.d.ts",
|
|
59
|
+
"default": "./dist/web-search/index.js"
|
|
60
|
+
},
|
|
57
61
|
"./wizard": {
|
|
58
62
|
"types": "./dist/wizard/index.d.ts",
|
|
59
63
|
"default": "./dist/wizard/index.js"
|