@kairos-es/feed-algolia 0.0.0-stage → 0.0.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.
- package/LICENSE +28 -0
- package/README.md +66 -2
- package/dist/cjs/classify.js +157 -0
- package/dist/cjs/classify.js.map +1 -0
- package/dist/cjs/identifiers.js +38 -0
- package/dist/cjs/identifiers.js.map +1 -0
- package/dist/cjs/index.js +39 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/records.js +105 -0
- package/dist/cjs/records.js.map +1 -0
- package/dist/cjs/rest.js +293 -0
- package/dist/cjs/rest.js.map +1 -0
- package/dist/cjs/sink.js +463 -0
- package/dist/cjs/sink.js.map +1 -0
- package/dist/dts-cjs/classify.d.ts +101 -0
- package/dist/dts-cjs/classify.d.ts.map +1 -0
- package/dist/dts-cjs/identifiers.d.ts +34 -0
- package/dist/dts-cjs/identifiers.d.ts.map +1 -0
- package/dist/dts-cjs/index.d.ts +39 -0
- package/dist/dts-cjs/index.d.ts.map +1 -0
- package/dist/dts-cjs/package.json +3 -0
- package/dist/dts-cjs/records.d.ts +56 -0
- package/dist/dts-cjs/records.d.ts.map +1 -0
- package/dist/dts-cjs/rest.d.ts +215 -0
- package/dist/dts-cjs/rest.d.ts.map +1 -0
- package/dist/dts-cjs/sink.d.ts +220 -0
- package/dist/dts-cjs/sink.d.ts.map +1 -0
- package/dist/dts-esm/classify.d.ts +101 -0
- package/dist/dts-esm/classify.d.ts.map +1 -0
- package/dist/dts-esm/identifiers.d.ts +34 -0
- package/dist/dts-esm/identifiers.d.ts.map +1 -0
- package/dist/dts-esm/index.d.ts +39 -0
- package/dist/dts-esm/index.d.ts.map +1 -0
- package/dist/dts-esm/package.json +3 -0
- package/dist/dts-esm/records.d.ts +56 -0
- package/dist/dts-esm/records.d.ts.map +1 -0
- package/dist/dts-esm/rest.d.ts +215 -0
- package/dist/dts-esm/rest.d.ts.map +1 -0
- package/dist/dts-esm/sink.d.ts +220 -0
- package/dist/dts-esm/sink.d.ts.map +1 -0
- package/dist/esm/classify.js +145 -0
- package/dist/esm/classify.js.map +1 -0
- package/dist/esm/identifiers.js +31 -0
- package/dist/esm/identifiers.js.map +1 -0
- package/dist/esm/index.js +38 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/records.js +96 -0
- package/dist/esm/records.js.map +1 -0
- package/dist/esm/rest.js +285 -0
- package/dist/esm/rest.js.map +1 -0
- package/dist/esm/sink.js +454 -0
- package/dist/esm/sink.js.map +1 -0
- package/package.json +33 -3
- package/src/classify.ts +201 -0
- package/src/identifiers.ts +45 -0
- package/src/index.ts +48 -0
- package/src/records.ts +109 -0
- package/src/rest.ts +773 -0
- package/src/sink.ts +733 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an Algolia answer becomes one of the feed's three sink failures.
|
|
3
|
+
*
|
|
4
|
+
* The driver's whole behaviour on failure follows from which of the three a sink
|
|
5
|
+
* names (`@kairos-es/feed`'s `errors.ts` owns that), so this mapping is the most
|
|
6
|
+
* consequential thing in the package and it is kept pure, in one place, and tested
|
|
7
|
+
* on its own. The rules:
|
|
8
|
+
*
|
|
9
|
+
* - **A 429 is `SinkRetryable`**, carrying `Retry-After` when Algolia sends one,
|
|
10
|
+
* so the driver backs off no less than Algolia asked — UNLESS its message names
|
|
11
|
+
* a QUOTA rather than a rate: Algolia answers 429 for an application over its
|
|
12
|
+
* record quota or its indexing file-size allowance too, and neither lifts by
|
|
13
|
+
* waiting, so retrying would only hold the feed until supervision reported it
|
|
14
|
+
* stalled, with Algolia's reason lost. Such a 429 is `SinkHalted`, carrying
|
|
15
|
+
* Algolia's message. The wordings recognised are {@link QUOTA_429}; a 429 whose
|
|
16
|
+
* wording is not among them stays retryable, which is the safe side to err on.
|
|
17
|
+
* - **Every 5xx is `SinkRetryable`, and so is a 1xx or 3xx**, a status no call
|
|
18
|
+
* here expects and one the transport has already tried on every host, as the
|
|
19
|
+
* official client does. A transport failure — no answer at all — is retryable
|
|
20
|
+
* too: the request may never have arrived, and resending is safe because every
|
|
21
|
+
* write this sink makes is a full-record replace or a delete.
|
|
22
|
+
* - **A 400 that names an `objectID` in the batch is `SinkRejected` for that key.**
|
|
23
|
+
* Algolia refuses a whole batch for one bad record, and names the record in its
|
|
24
|
+
* message (`… objectID=<id> …`). Parking just that key lets the driver send the
|
|
25
|
+
* rest again without it, which is the difference between one record missing from
|
|
26
|
+
* search and the whole feed stopping. The key has to be one the batch carried: an
|
|
27
|
+
* id the batch did not send is not a record the driver can park, so such an
|
|
28
|
+
* answer falls through to the next rule. The message does not delimit the id, and
|
|
29
|
+
* a feed key may hold a space, a comma or anything else, so the id is not parsed
|
|
30
|
+
* out of the message: the batch's own keys are matched against what follows
|
|
31
|
+
* `objectID=` (`objectIdNamedIn`).
|
|
32
|
+
* - **Every other 4xx is `SinkHalted`.** Bad credentials, a key without the right
|
|
33
|
+
* ACL, an index name Algolia will not accept, a request this package built wrong:
|
|
34
|
+
* none of them is mended by sending again, so the worker stops and supervision
|
|
35
|
+
* reports it.
|
|
36
|
+
*
|
|
37
|
+
* Nothing here reads the request, and nothing here sees a header but `Retry-After`,
|
|
38
|
+
* so no classification can carry the admin key into a reason string.
|
|
39
|
+
*/
|
|
40
|
+
import type { FeedKey } from '@kairos-es/feed';
|
|
41
|
+
import { SinkHalted, SinkRejected, SinkRetryable } from '@kairos-es/feed';
|
|
42
|
+
import { Duration, Option } from 'effect';
|
|
43
|
+
/** Every failure an Algolia call can end in, already in the feed's vocabulary. */
|
|
44
|
+
export type AlgoliaFailure = SinkRetryable | SinkRejected | SinkHalted;
|
|
45
|
+
/** An Algolia answer that was not a success, reduced to what classifying needs. */
|
|
46
|
+
export interface AlgoliaErrorAnswer {
|
|
47
|
+
readonly status: number;
|
|
48
|
+
/** Algolia's `message`, or the raw body when it sent none. */
|
|
49
|
+
readonly message: string;
|
|
50
|
+
/** The `Retry-After` header, verbatim, when there was one. */
|
|
51
|
+
readonly retryAfter: Option.Option<string>;
|
|
52
|
+
}
|
|
53
|
+
/** What the call was, for the reason string: never its headers. */
|
|
54
|
+
export interface AlgoliaCallLabel {
|
|
55
|
+
readonly method: string;
|
|
56
|
+
readonly path: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The key of `batchKeys` an Algolia error message names, when it names one.
|
|
60
|
+
*
|
|
61
|
+
* Algolia's record-level refusals all carry `objectID=<id>` (the record-too-big
|
|
62
|
+
* message is the common one), with nothing marking where the id ends. So each
|
|
63
|
+
* batch key is tried against the text after each `objectID=`, and it matches when
|
|
64
|
+
* that text starts with the key and the key is followed by the end of the message,
|
|
65
|
+
* a space or punctuation; the longest key that matches wins, so of `big` and
|
|
66
|
+
* `big one` the message `objectID=big one is too big` names the second.
|
|
67
|
+
*/
|
|
68
|
+
export declare const objectIdNamedIn: (message: string, batchKeys: ReadonlySet<FeedKey>) => Option.Option<FeedKey>;
|
|
69
|
+
/**
|
|
70
|
+
* The 429 wordings that name a quota, not a rate, matched case-insensitively
|
|
71
|
+
* anywhere in Algolia's message: an application over its record quota, or over
|
|
72
|
+
* its allowance of indexing by file size. See the module doc for why each halts.
|
|
73
|
+
*/
|
|
74
|
+
export declare const QUOTA_429: ReadonlyArray<RegExp>;
|
|
75
|
+
/**
|
|
76
|
+
* `Retry-After` as a duration, given the current time.
|
|
77
|
+
*
|
|
78
|
+
* The header is either a whole number of seconds or an HTTP date. A date is turned
|
|
79
|
+
* into a wait against `now`, which the caller reads from `Clock` so a test's
|
|
80
|
+
* `TestClock` governs it; a date already past is a wait of zero. Anything else is
|
|
81
|
+
* ignored rather than guessed at, and the driver's own back-off applies.
|
|
82
|
+
*/
|
|
83
|
+
export declare const parseRetryAfter: (header: string, nowMillis: number) => Option.Option<Duration.Duration>;
|
|
84
|
+
/**
|
|
85
|
+
* Classify an Algolia error answer; see the module doc for the rules.
|
|
86
|
+
*
|
|
87
|
+
* `batchKeys` is the set of keys the request carried, empty for a call that sends
|
|
88
|
+
* no records, so only a write can ever produce a `SinkRejected`.
|
|
89
|
+
*/
|
|
90
|
+
export declare const classifyAnswer: (call: AlgoliaCallLabel, answer: AlgoliaErrorAnswer, batchKeys: ReadonlySet<FeedKey>, nowMillis: number) => AlgoliaFailure;
|
|
91
|
+
/**
|
|
92
|
+
* A request that got no answer at all. Retryable: see the module doc for why a
|
|
93
|
+
* resend is safe.
|
|
94
|
+
*/
|
|
95
|
+
export declare const transportFailure: (call: AlgoliaCallLabel, detail: string) => AlgoliaFailure;
|
|
96
|
+
/**
|
|
97
|
+
* An answer this package cannot read: a success whose body is not the shape
|
|
98
|
+
* Algolia documents. Halted, because sending again will get the same body back.
|
|
99
|
+
*/
|
|
100
|
+
export declare const unreadableAnswer: (call: AlgoliaCallLabel, detail: string) => AlgoliaFailure;
|
|
101
|
+
//# sourceMappingURL=classify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACzE,OAAO,EAAY,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEnD,kFAAkF;AAClF,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,YAAY,GAAG,UAAU,CAAA;AAEtE,mFAAmF;AACnF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CAC3C;AAED,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAOD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,EACf,WAAW,WAAW,CAAC,OAAO,CAAC,KAC9B,MAAM,CAAC,MAAM,CAAC,OAAO,CAiBvB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,aAAa,CAAC,MAAM,CAA4B,CAAA;AAExE;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAC1B,QAAQ,MAAM,EACd,WAAW,MAAM,KAChB,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAajC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,gBAAgB,EACtB,QAAQ,kBAAkB,EAC1B,WAAW,WAAW,CAAC,OAAO,CAAC,EAC/B,WAAW,MAAM,KAChB,cAiCF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,gBAAgB,EACtB,QAAQ,MAAM,KACb,cAGC,CAAA;AAEJ;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,gBAAgB,EACtB,QAAQ,MAAM,KACb,cAGC,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Algolia sink's branded identifiers.
|
|
3
|
+
*
|
|
4
|
+
* Three values cross this package's boundary that would otherwise be bare
|
|
5
|
+
* strings and numbers, and each is easy to pass in the place of another: an
|
|
6
|
+
* application id where an index name belongs, a feed key where an index name
|
|
7
|
+
* belongs, a task id from one index polled against another. Branding them is the
|
|
8
|
+
* repository's standing rule for a domain identifier, and here it is also what
|
|
9
|
+
* keeps a `Receipt` — an index and a task id — from being assembled by hand out of
|
|
10
|
+
* values that never belonged together.
|
|
11
|
+
*/
|
|
12
|
+
import { Schema } from 'effect';
|
|
13
|
+
/**
|
|
14
|
+
* The Algolia application id: the first half of the credentials, and the name of
|
|
15
|
+
* the application's hosts. Not a secret — it is sent beside every search a browser
|
|
16
|
+
* makes — so it is a plain branded string, unlike the admin key.
|
|
17
|
+
*/
|
|
18
|
+
export declare const AlgoliaAppId: Schema.brand<Schema.String, "AlgoliaAppId">;
|
|
19
|
+
export type AlgoliaAppId = typeof AlgoliaAppId.Type;
|
|
20
|
+
/**
|
|
21
|
+
* The name of an Algolia index. The sink's SERVED index is one; a resync builds into
|
|
22
|
+
* another, named after the served one, the generation it rebuilds and the attempt.
|
|
23
|
+
*/
|
|
24
|
+
export declare const AlgoliaIndexName: Schema.brand<Schema.String, "AlgoliaIndexName">;
|
|
25
|
+
export type AlgoliaIndexName = typeof AlgoliaIndexName.Type;
|
|
26
|
+
/**
|
|
27
|
+
* An Algolia task id: what every write returns, and what `getTask` is polled with
|
|
28
|
+
* until the write is published. A task belongs to the index it was issued on, and
|
|
29
|
+
* is polled there and nowhere else — which is why a receipt carries the index
|
|
30
|
+
* beside it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const AlgoliaTaskId: Schema.brand<Schema.Number, "AlgoliaTaskId">;
|
|
33
|
+
export type AlgoliaTaskId = typeof AlgoliaTaskId.Type;
|
|
34
|
+
//# sourceMappingURL=identifiers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identifiers.d.ts","sourceRoot":"","sources":["../../src/identifiers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;;GAIG;AACH,eAAO,MAAM,YAAY,6CAGxB,CAAA;AACD,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA;AAEnD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,iDAG5B,CAAA;AACD,MAAM,MAAM,gBAAgB,GAAG,OAAO,gBAAgB,CAAC,IAAI,CAAA;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,aAAa,8CAGzB,CAAA;AACD,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC,IAAI,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kairos-es/feed-algolia — a `FeedSink` onto an Algolia index, with both of the
|
|
3
|
+
* port's optional parts: a `FeedResync` that rebuilds into a fresh index and moves
|
|
4
|
+
* it over the served one, and a `FeedReconcile` that lists the served index's ids.
|
|
5
|
+
*
|
|
6
|
+
* It talks to Algolia's REST API through `effect`'s own `HttpClient`, so it peers
|
|
7
|
+
* `effect` and `@kairos-es/feed` and nothing else, and the application chooses the
|
|
8
|
+
* HTTP transport by providing the client — `FetchHttpClient.layer` from
|
|
9
|
+
* `effect/http`, say.
|
|
10
|
+
*
|
|
11
|
+
* Where each part's argument lives:
|
|
12
|
+
*
|
|
13
|
+
* - `makeAlgoliaSink` (`sink.ts`): the steady state, resync and reconcile, and how
|
|
14
|
+
* `confirm` waits.
|
|
15
|
+
* - Encoding and the size pre-check (`records.ts`): why only full-record writes,
|
|
16
|
+
* and why oversized records are refused before they are sent.
|
|
17
|
+
* - Error classification (`classify.ts`): which Algolia answers are retryable,
|
|
18
|
+
* which refuse one record, and which stop the worker.
|
|
19
|
+
* - The REST calls (`rest.ts`): why no SDK, the host failover kept from it, and
|
|
20
|
+
* how the admin key is kept out of anything printed.
|
|
21
|
+
*
|
|
22
|
+
* Wiring:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* const sink = yield* makeAlgoliaSink({
|
|
26
|
+
* name: SinkName.make('search'),
|
|
27
|
+
* appId: AlgoliaAppId.make(config.appId),
|
|
28
|
+
* adminKey: config.adminKey, // a Redacted<string>, from the application's config
|
|
29
|
+
* index: AlgoliaIndexName.make('courses'),
|
|
30
|
+
* doc: CourseDoc,
|
|
31
|
+
* }).pipe(Effect.provide(FetchHttpClient.layer))
|
|
32
|
+
* yield* Effect.forkScoped(runFeedSync({ source, sink, store }))
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export { AlgoliaAppId, AlgoliaIndexName, AlgoliaTaskId, } from './identifiers.js';
|
|
36
|
+
export { DEFAULT_MAX_RECORD_BYTES } from './records.js';
|
|
37
|
+
export type { AlgoliaConnection, AlgoliaTimeouts } from './rest.js';
|
|
38
|
+
export { type AlgoliaReceipt, type AlgoliaSinkOptions, type AlgoliaTaskPolling, makeAlgoliaSink, } from './sink.js';
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,aAAa,GACd,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AACpD,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAA;AAChE,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,eAAe,GAChB,MAAM,QAAQ,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a batch of feed changes into Algolia batch operations, and refusing the
|
|
3
|
+
* records Algolia would refuse BEFORE they are sent.
|
|
4
|
+
*
|
|
5
|
+
* ## Full-record replace, and nothing partial
|
|
6
|
+
*
|
|
7
|
+
* An upsert is `addObject` with the record's `objectID` set to the feed key, which
|
|
8
|
+
* replaces whatever Algolia held under that id; a delete is `deleteObject`. Both
|
|
9
|
+
* leave the index the same however many times they are sent, which is the
|
|
10
|
+
* idempotence the driver relies on when it resends a batch it could not confirm.
|
|
11
|
+
* Algolia's partial operations (`partialUpdateObject`, the built-in `Increment`
|
|
12
|
+
* and its kin) do not: applied twice they apply twice. They are never used.
|
|
13
|
+
*
|
|
14
|
+
* The key is written LAST, over the encoded document, so a document that carries an
|
|
15
|
+
* `objectID` field of its own cannot point a record at another key.
|
|
16
|
+
*
|
|
17
|
+
* ## Why oversized records are refused here
|
|
18
|
+
*
|
|
19
|
+
* Algolia refuses a whole `batch` when one record in it is over the plan's record
|
|
20
|
+
* size limit. Its answer does name the record, and `classify.ts` turns that into a
|
|
21
|
+
* rejection of that one key — but only after a round trip that wrote nothing, and
|
|
22
|
+
* only for the first oversized record the batch held. Checking every record's size
|
|
23
|
+
* up front rejects all of them at once, so the driver's resend of the rest goes
|
|
24
|
+
* through first time. The 400 path stays as the backstop for a limit configured
|
|
25
|
+
* higher than the plan's.
|
|
26
|
+
*
|
|
27
|
+
* The size is the record's JSON in UTF-8 bytes, `objectID` included, which is how
|
|
28
|
+
* Algolia measures it. The default limit is the smallest Algolia plan's (10 KB);
|
|
29
|
+
* an application on a larger plan raises it.
|
|
30
|
+
*
|
|
31
|
+
* ## A document that does not encode
|
|
32
|
+
*
|
|
33
|
+
* The document schema is the read model's, and a document it cannot encode — or one
|
|
34
|
+
* that encodes to something other than a JSON object, which is all an Algolia
|
|
35
|
+
* record can be — is a defect in the read model, not a record the sink refuses. It
|
|
36
|
+
* dies, loudly, rather than being parked where nobody looks.
|
|
37
|
+
*/
|
|
38
|
+
import { type FeedChange, type FeedKey, SinkRejected } from '@kairos-es/feed';
|
|
39
|
+
import { Effect, Schema } from 'effect';
|
|
40
|
+
import type { NonEmptyReadonlyArray } from 'effect/Array';
|
|
41
|
+
import type { BatchOperation } from './rest.js';
|
|
42
|
+
/** The smallest Algolia plan's record size limit, in bytes. */
|
|
43
|
+
export declare const DEFAULT_MAX_RECORD_BYTES = 10000;
|
|
44
|
+
/** A record's size as Algolia measures it: its JSON, in UTF-8 bytes. */
|
|
45
|
+
export declare const recordBytes: (record: Readonly<Record<string, unknown>>) => number;
|
|
46
|
+
/** A batch ready to send: its operations, and the keys it carries. */
|
|
47
|
+
export interface EncodedBatch {
|
|
48
|
+
readonly operations: ReadonlyArray<BatchOperation>;
|
|
49
|
+
readonly keys: ReadonlySet<FeedKey>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Encode a batch, failing `SinkRejected` with EVERY oversized key when there is
|
|
53
|
+
* one; see the module doc.
|
|
54
|
+
*/
|
|
55
|
+
export declare const encodeBatch: <D, I>(doc: Schema.Codec<D, I>, maxRecordBytes: number, batch: NonEmptyReadonlyArray<FeedChange<D>>) => Effect.Effect<EncodedBatch, SinkRejected>;
|
|
56
|
+
//# sourceMappingURL=records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../src/records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC7E,OAAO,EAAgB,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE5C,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,QAAS,CAAA;AAI9C,wEAAwE;AACxE,eAAO,MAAM,WAAW,GACtB,QAAQ,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KACxC,MAAoD,CAAA;AAEvD,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;IAClD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;CACpC;AAKD;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,EAAE,CAAC,EAC9B,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EACvB,gBAAgB,MAAM,EACtB,OAAO,qBAAqB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAC1C,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,CAuCvC,CAAA"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of Algolia's REST API this sink speaks, over `effect`'s own
|
|
3
|
+
* `HttpClient`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why no SDK
|
|
6
|
+
*
|
|
7
|
+
* The surface the sink needs is eight calls — `batch`, `getTask`, `operationIndex`
|
|
8
|
+
* (copy and move), `deleteIndex`, `browse`, `listIndices`, and a settings read and
|
|
9
|
+
* write, which is how a resync target is made to exist and marked as its attempt's
|
|
10
|
+
* own — and each is one JSON request. Peering `algoliasearch` would bring a large
|
|
11
|
+
* non-Effect dependency for that, and its retries would sit underneath the feed
|
|
12
|
+
* driver's own supervision, which already owns retrying. Over `HttpClient` the
|
|
13
|
+
* sink is Effect-native, adds no peer beyond `effect`, and is faked in tests by a
|
|
14
|
+
* stub client with no network at all.
|
|
15
|
+
*
|
|
16
|
+
* ## Host failover
|
|
17
|
+
*
|
|
18
|
+
* The one piece of the SDK's transport kept here is the part the driver cannot do
|
|
19
|
+
* for itself: trying ANOTHER HOST. Algolia serves an application from
|
|
20
|
+
* `<appId>.algolia.net` and documents three fallbacks on separate infrastructure,
|
|
21
|
+
* `<appId>-1.algolianet.com` to `<appId>-3.algolianet.com`, to be tried when the
|
|
22
|
+
* first does not answer. A request that gets no answer (a transport failure, or no
|
|
23
|
+
* answer within its timeout), a 5xx, or a 1xx or 3xx — a status no call here
|
|
24
|
+
* expects, on which the official JavaScript client tries the next host too — is
|
|
25
|
+
* sent again at once to the next host, and only when every host it tried has
|
|
26
|
+
* failed is the call `SinkRetryable`, carrying the last host's failure. Without
|
|
27
|
+
* it, one unreachable host would hold the feed in the driver's back-off for as
|
|
28
|
+
* long as the host stayed down, with three healthy hosts unasked.
|
|
29
|
+
*
|
|
30
|
+
* - Hosts are STATEFUL, one state per client and so per sink, in the shape the
|
|
31
|
+
* official JavaScript client gives them. A host that failed in one of those ways
|
|
32
|
+
* is marked DOWN for two minutes, by `Clock`, and skipped by every call while it
|
|
33
|
+
* is down; a host that answers is marked up again. Without that memory a primary
|
|
34
|
+
* that drops packets would cost a whole timeout on EVERY call, each `getTask`
|
|
35
|
+
* poll included, so a single wait for a task could spend its whole
|
|
36
|
+
* `taskPolling.timeout` on the dead host. When every host is down, every host is
|
|
37
|
+
* tried again, in order, rather than none.
|
|
38
|
+
* - The fallbacks are SHUFFLED, once, when the client is built, from Effect's
|
|
39
|
+
* `Random`, so many workers do not all fail over onto the same host; the primary
|
|
40
|
+
* goes first whenever it is up.
|
|
41
|
+
* - Each hand-off to the next host is logged at DEBUG level, with the call, the
|
|
42
|
+
* host that failed, the host tried next and why. A call that lands after a
|
|
43
|
+
* failover looks like any other, so that line is the only trace one leaves.
|
|
44
|
+
* Hosts carry the application id, which is not a secret; the key travels in a
|
|
45
|
+
* header and reaches no log.
|
|
46
|
+
* - Three timeouts, as the official client has: `connect`, `read` and `write`,
|
|
47
|
+
* defaulting to its 2, 5 and 30 seconds. A read is a `GET` or a `browse`, and
|
|
48
|
+
* every other call is a write. `HttpClient` does not show this module the moment
|
|
49
|
+
* a connection is made, so one request's budget on one host is `connect` plus
|
|
50
|
+
* `read`, or `connect` plus `write`, over the whole exchange. A transport that
|
|
51
|
+
* enforces a connect timeout of its own fails sooner, and that failure fails
|
|
52
|
+
* over the same way.
|
|
53
|
+
* - Every call uses the same list, reads included. The SDK sends reads to
|
|
54
|
+
* `<appId>-dsn.algolia.net` first, which is the search-serving network; every
|
|
55
|
+
* read here is an admin read (a task's status, a listing, a settings read, a
|
|
56
|
+
* browse for reconcile) that wants the cluster the writes went to.
|
|
57
|
+
* - A 429 is NOT retried on another host: it is the application's rate limit or
|
|
58
|
+
* quota, not a host's health (`classify.ts` owns which). Nor is any other 4xx,
|
|
59
|
+
* which another host would answer the same.
|
|
60
|
+
* - A request can be sent more than once this way, and that is safe for the
|
|
61
|
+
* reason every resend here is (`classify.ts`): every write is a full-record
|
|
62
|
+
* replace, a delete, an index operation or a settings write, each of which
|
|
63
|
+
* leaves the application the same however often it lands.
|
|
64
|
+
* - The worst case for one call is therefore its budget times the number of hosts
|
|
65
|
+
* that are up when it starts.
|
|
66
|
+
* - An explicit `host` turns failover OFF: the caller has named the one place to
|
|
67
|
+
* talk to (a proxy, a test double), and there are no fallbacks of it to guess.
|
|
68
|
+
* That one host is never skipped, since every host being down tries it again.
|
|
69
|
+
*
|
|
70
|
+
* ## The admin key
|
|
71
|
+
*
|
|
72
|
+
* It is held as `Redacted` and unwrapped only into the request header, at the
|
|
73
|
+
* moment the request is built. Two things keep it out of anything printed: no
|
|
74
|
+
* failure this module produces carries the request or its headers — only the
|
|
75
|
+
* method, the path and Algolia's own message (`classify.ts`) — and every request
|
|
76
|
+
* runs with Algolia's key header added to the names `effect`'s `Headers` redact,
|
|
77
|
+
* so a request that is logged or traced by something further down shows the
|
|
78
|
+
* header as redacted.
|
|
79
|
+
*
|
|
80
|
+
* ## Not found
|
|
81
|
+
*
|
|
82
|
+
* Three of the calls meet a missing index as an ordinary outcome rather than a
|
|
83
|
+
* failure — dropping a leftover resync index that is not there, browsing an index
|
|
84
|
+
* that does not exist yet, reading the settings of an index that does not exist —
|
|
85
|
+
* so they answer a 404 as `Option.none()` instead of a `SinkHalted`. Every other
|
|
86
|
+
* call halts on a 404, with Algolia's own message in the reason. An index
|
|
87
|
+
* operation is one of those: Algolia documents a copy of a missing source as
|
|
88
|
+
* creating an empty destination, and a move of one as ignored without an error,
|
|
89
|
+
* so neither is a 404 (the sink's `publish` owns what that costs). A 404 is never
|
|
90
|
+
* failed over: another host would answer the same.
|
|
91
|
+
*/
|
|
92
|
+
import type { FeedKey } from '@kairos-es/feed';
|
|
93
|
+
import { Duration, Effect, Option, Redacted } from 'effect';
|
|
94
|
+
import { HttpClient, type HttpMethod } from 'effect/http';
|
|
95
|
+
import { type AlgoliaFailure } from './classify.js';
|
|
96
|
+
import { type AlgoliaAppId, type AlgoliaIndexName, AlgoliaTaskId } from './identifiers.js';
|
|
97
|
+
/** Where and as whom the sink talks to Algolia. */
|
|
98
|
+
export interface AlgoliaConnection {
|
|
99
|
+
readonly appId: AlgoliaAppId;
|
|
100
|
+
/** The admin API key. Never logged; see the module doc. */
|
|
101
|
+
readonly adminKey: Redacted.Redacted<string>;
|
|
102
|
+
/**
|
|
103
|
+
* ONE base URL to send every call to, with no failover. Leave it unset to talk
|
|
104
|
+
* to Algolia itself: `https://<appId>.algolia.net`, failing over to the three
|
|
105
|
+
* documented fallback hosts (see the module doc). Set it for a proxy or a test
|
|
106
|
+
* double, whose fallbacks there is no way to know.
|
|
107
|
+
*/
|
|
108
|
+
readonly host?: string;
|
|
109
|
+
/**
|
|
110
|
+
* How long one request to one host may take before it counts as unanswered,
|
|
111
|
+
* that host is marked down, and the next is tried; see the module doc.
|
|
112
|
+
*/
|
|
113
|
+
readonly timeouts?: AlgoliaTimeouts;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The three per-host timeouts, with the official JavaScript client's defaults. A
|
|
117
|
+
* request's budget on one host is `connect` plus `read` for a read, or `connect`
|
|
118
|
+
* plus `write` for a write; see the module doc for why they are added.
|
|
119
|
+
*/
|
|
120
|
+
export interface AlgoliaTimeouts {
|
|
121
|
+
/** Default 2 seconds. */
|
|
122
|
+
readonly connect?: Duration.Input;
|
|
123
|
+
/** For a `GET` or a `browse`. Default 5 seconds. */
|
|
124
|
+
readonly read?: Duration.Input;
|
|
125
|
+
/** For every other call. Default 30 seconds. */
|
|
126
|
+
readonly write?: Duration.Input;
|
|
127
|
+
}
|
|
128
|
+
/** One batch operation, as Algolia's `batch` takes it. Full-record writes only. */
|
|
129
|
+
export type BatchOperation = {
|
|
130
|
+
readonly action: 'addObject';
|
|
131
|
+
readonly body: Readonly<Record<string, unknown>>;
|
|
132
|
+
} | {
|
|
133
|
+
readonly action: 'deleteObject';
|
|
134
|
+
readonly body: {
|
|
135
|
+
readonly objectID: string;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
/** What an index-level `operationIndex` call asks for. */
|
|
139
|
+
export interface IndexOperation {
|
|
140
|
+
readonly operation: 'copy' | 'move';
|
|
141
|
+
readonly destination: AlgoliaIndexName;
|
|
142
|
+
readonly scope?: ReadonlyArray<'settings' | 'synonyms' | 'rules'>;
|
|
143
|
+
}
|
|
144
|
+
/** One page of a `browse`: the ids on it, and the cursor to the next, if any. */
|
|
145
|
+
export interface BrowsePage {
|
|
146
|
+
readonly objectIDs: ReadonlyArray<string>;
|
|
147
|
+
readonly cursor: Option.Option<string>;
|
|
148
|
+
}
|
|
149
|
+
/** One page of `listIndices`: the index names on it, and whether more follow. */
|
|
150
|
+
export interface IndexListPage {
|
|
151
|
+
readonly names: ReadonlyArray<string>;
|
|
152
|
+
readonly more: boolean;
|
|
153
|
+
}
|
|
154
|
+
/** One raw call: everything the typed calls below are built from. */
|
|
155
|
+
export interface AlgoliaCall {
|
|
156
|
+
readonly method: HttpMethod.HttpMethod;
|
|
157
|
+
/** The path, already encoded: `/1/indexes/<name>/...`. */
|
|
158
|
+
readonly path: string;
|
|
159
|
+
readonly body?: unknown;
|
|
160
|
+
/** The keys the request carries, so a 400 naming one can be a rejection. */
|
|
161
|
+
readonly batchKeys?: ReadonlySet<FeedKey>;
|
|
162
|
+
/**
|
|
163
|
+
* A read whose method is not `GET` (a `browse`), so it gets the read timeout.
|
|
164
|
+
* Every `GET` is a read already.
|
|
165
|
+
*/
|
|
166
|
+
readonly read?: true;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Where a task stands, as a caller that tolerates a missing index sees it: still
|
|
170
|
+
* running, published, or unanswerable because its index does not exist.
|
|
171
|
+
*/
|
|
172
|
+
export type AlgoliaTaskState = 'pending' | 'published' | 'indexMissing';
|
|
173
|
+
/** The Algolia calls the sink makes. Every failure is already classified. */
|
|
174
|
+
export interface AlgoliaRest {
|
|
175
|
+
/** A raw call; `Option.none()` on a 404. Exposed for the package's own tests. */
|
|
176
|
+
readonly call: (call: AlgoliaCall) => Effect.Effect<Option.Option<unknown>, AlgoliaFailure>;
|
|
177
|
+
readonly batch: (index: AlgoliaIndexName, operations: ReadonlyArray<BatchOperation>, batchKeys: ReadonlySet<FeedKey>) => Effect.Effect<AlgoliaTaskId, AlgoliaFailure>;
|
|
178
|
+
readonly taskPublished: (index: AlgoliaIndexName, taskID: AlgoliaTaskId) => Effect.Effect<boolean, AlgoliaFailure>;
|
|
179
|
+
/**
|
|
180
|
+
* `taskPublished`, as a state, with a 404 — Algolia's answer to `getTask` on an
|
|
181
|
+
* index that does not exist — as `'indexMissing'` rather than a halt, for a
|
|
182
|
+
* caller that has another way to learn what the task did.
|
|
183
|
+
*/
|
|
184
|
+
readonly taskStateUnlessIndexMissing: (index: AlgoliaIndexName, taskID: AlgoliaTaskId) => Effect.Effect<AlgoliaTaskState, AlgoliaFailure>;
|
|
185
|
+
/**
|
|
186
|
+
* A copy or a move. Algolia answers a missing source with a task too: a copy
|
|
187
|
+
* creates an empty destination, and a move does nothing (see the module doc).
|
|
188
|
+
*/
|
|
189
|
+
readonly operation: (source: AlgoliaIndexName, operation: IndexOperation) => Effect.Effect<AlgoliaTaskId, AlgoliaFailure>;
|
|
190
|
+
/** `Option.none()` when the index does not exist. */
|
|
191
|
+
readonly deleteIndex: (index: AlgoliaIndexName) => Effect.Effect<Option.Option<AlgoliaTaskId>, AlgoliaFailure>;
|
|
192
|
+
/** An index's settings; `Option.none()` when the index does not exist. */
|
|
193
|
+
readonly readSettings: (index: AlgoliaIndexName) => Effect.Effect<Option.Option<Readonly<Record<string, unknown>>>, AlgoliaFailure>;
|
|
194
|
+
/**
|
|
195
|
+
* A settings write of the settings named, and only those; a `null` resets one
|
|
196
|
+
* to its default. Algolia creates an index on a settings write, so this is also
|
|
197
|
+
* how an index is made to exist without a record.
|
|
198
|
+
*/
|
|
199
|
+
readonly writeSettings: (index: AlgoliaIndexName, settings: Readonly<Record<string, unknown>>) => Effect.Effect<AlgoliaTaskId, AlgoliaFailure>;
|
|
200
|
+
/**
|
|
201
|
+
* One page of the application's index names, `page` counting from 0. Every
|
|
202
|
+
* index the key may see is listed, so a caller filters by name.
|
|
203
|
+
*/
|
|
204
|
+
readonly listIndexNames: (page: number, hitsPerPage: number) => Effect.Effect<IndexListPage, AlgoliaFailure>;
|
|
205
|
+
/** A missing index browses as one empty last page. */
|
|
206
|
+
readonly browseObjectIds: (index: AlgoliaIndexName, cursor: Option.Option<string>, hitsPerPage: number) => Effect.Effect<BrowsePage, AlgoliaFailure>;
|
|
207
|
+
}
|
|
208
|
+
/** The path of an index, or of something under it. */
|
|
209
|
+
export declare const indexPath: (index: AlgoliaIndexName, rest?: string) => string;
|
|
210
|
+
/**
|
|
211
|
+
* Build the REST client over the `HttpClient` in context. The client is captured
|
|
212
|
+
* here, so every call the sink makes later needs nothing from its caller.
|
|
213
|
+
*/
|
|
214
|
+
export declare const makeAlgoliaRest: (connection: AlgoliaConnection) => Effect.Effect<AlgoliaRest, never, HttpClient.HttpClient>;
|
|
215
|
+
//# sourceMappingURL=rest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/rest.ts"],"names":[],"mappings":"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"}
|