@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.
Files changed (60) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +66 -2
  3. package/dist/cjs/classify.js +157 -0
  4. package/dist/cjs/classify.js.map +1 -0
  5. package/dist/cjs/identifiers.js +38 -0
  6. package/dist/cjs/identifiers.js.map +1 -0
  7. package/dist/cjs/index.js +39 -0
  8. package/dist/cjs/index.js.map +1 -0
  9. package/dist/cjs/records.js +105 -0
  10. package/dist/cjs/records.js.map +1 -0
  11. package/dist/cjs/rest.js +293 -0
  12. package/dist/cjs/rest.js.map +1 -0
  13. package/dist/cjs/sink.js +463 -0
  14. package/dist/cjs/sink.js.map +1 -0
  15. package/dist/dts-cjs/classify.d.ts +101 -0
  16. package/dist/dts-cjs/classify.d.ts.map +1 -0
  17. package/dist/dts-cjs/identifiers.d.ts +34 -0
  18. package/dist/dts-cjs/identifiers.d.ts.map +1 -0
  19. package/dist/dts-cjs/index.d.ts +39 -0
  20. package/dist/dts-cjs/index.d.ts.map +1 -0
  21. package/dist/dts-cjs/package.json +3 -0
  22. package/dist/dts-cjs/records.d.ts +56 -0
  23. package/dist/dts-cjs/records.d.ts.map +1 -0
  24. package/dist/dts-cjs/rest.d.ts +215 -0
  25. package/dist/dts-cjs/rest.d.ts.map +1 -0
  26. package/dist/dts-cjs/sink.d.ts +220 -0
  27. package/dist/dts-cjs/sink.d.ts.map +1 -0
  28. package/dist/dts-esm/classify.d.ts +101 -0
  29. package/dist/dts-esm/classify.d.ts.map +1 -0
  30. package/dist/dts-esm/identifiers.d.ts +34 -0
  31. package/dist/dts-esm/identifiers.d.ts.map +1 -0
  32. package/dist/dts-esm/index.d.ts +39 -0
  33. package/dist/dts-esm/index.d.ts.map +1 -0
  34. package/dist/dts-esm/package.json +3 -0
  35. package/dist/dts-esm/records.d.ts +56 -0
  36. package/dist/dts-esm/records.d.ts.map +1 -0
  37. package/dist/dts-esm/rest.d.ts +215 -0
  38. package/dist/dts-esm/rest.d.ts.map +1 -0
  39. package/dist/dts-esm/sink.d.ts +220 -0
  40. package/dist/dts-esm/sink.d.ts.map +1 -0
  41. package/dist/esm/classify.js +145 -0
  42. package/dist/esm/classify.js.map +1 -0
  43. package/dist/esm/identifiers.js +31 -0
  44. package/dist/esm/identifiers.js.map +1 -0
  45. package/dist/esm/index.js +38 -0
  46. package/dist/esm/index.js.map +1 -0
  47. package/dist/esm/package.json +4 -0
  48. package/dist/esm/records.js +96 -0
  49. package/dist/esm/records.js.map +1 -0
  50. package/dist/esm/rest.js +285 -0
  51. package/dist/esm/rest.js.map +1 -0
  52. package/dist/esm/sink.js +454 -0
  53. package/dist/esm/sink.js.map +1 -0
  54. package/package.json +33 -3
  55. package/src/classify.ts +201 -0
  56. package/src/identifiers.ts +45 -0
  57. package/src/index.ts +48 -0
  58. package/src/records.ts +109 -0
  59. package/src/rest.ts +773 -0
  60. package/src/sink.ts +733 -0
@@ -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,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -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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"}
@@ -0,0 +1,220 @@
1
+ /**
2
+ * `makeAlgoliaSink` — a `FeedSink` onto one served Algolia index, with both optional
3
+ * parts: a `FeedResync` built on Algolia's index copy and move, and a
4
+ * `FeedReconcile` built on `browse`.
5
+ *
6
+ * ## The steady state
7
+ *
8
+ * `apply` is ONE `batch` to the served index (`records.ts` owns what goes in it and
9
+ * which records are refused before it is sent), and its receipt is the index and
10
+ * the task Algolia returned. `confirm` polls `getTask` on that index until the task
11
+ * is `published`, which is the moment Algolia serves the batch — the moment the
12
+ * port's `confirm` promises. The poll runs on an Effect `Schedule`: it backs off
13
+ * from `taskPolling.initialDelay`, doubling to `taskPolling.maxDelay`, and no poll
14
+ * STARTS after `taskPolling.timeout` has passed since the wait began — the last
15
+ * wait is cut short to land on that deadline rather than overrun it by up to
16
+ * `maxDelay`. A task still unpublished then is a `SinkRetryable`: the driver
17
+ * resends the batch, which is safe, rather than waiting on a task without bound.
18
+ * A poll that has started can still outlast the deadline by its own request
19
+ * timeout (`rest.ts`). A `Schedule` rather than a hand-written loop because the
20
+ * curve is then one declared value, run under the `Clock`, so a test's
21
+ * `TestClock` drives it like any other wait.
22
+ *
23
+ * ## Resync: a fresh index, swapped in whole
24
+ *
25
+ * Algolia's `operationIndex` move replaces the destination index atomically, which
26
+ * is exactly the swap `FeedResync` asks for. So:
27
+ *
28
+ * - `begin(generation)` names the target `<served>__kairos_g<generation>_<attempt>`,
29
+ * where `<attempt>` is drawn from Effect's `Random` afresh on every call, and
30
+ * then COPIES the served index into it with `scope` limited to settings, synonyms
31
+ * and rules. The fresh index then ranks and matches exactly as the served one does,
32
+ * and holds none of its records. A served index that has never been written has no
33
+ * scope to copy, and that is not an error: `begin` reads the served index's
34
+ * settings first and makes no copy when it does not exist. Algolia would copy a
35
+ * missing source as an empty index, but the copy's task is polled on the served
36
+ * index, and `getTask` on an index that does not exist answers 404, so skipping
37
+ * the copy is what keeps that wait off a missing index. The name is the ATTEMPT's, not the
38
+ * generation's, because two workers can be resyncing one generation at once —
39
+ * one of them having lost its lease without knowing it yet — and a target they
40
+ * shared is one either could publish half-built; `runFeedSync.ts` owns the
41
+ * argument. Random rather than the lease holder's id, because the port hands a
42
+ * sink no lease, and a draw needs none.
43
+ * - `begin` then MARKS the target as its attempt's own: it writes the target's
44
+ * `userData` setting as whatever the copy brought from the served index, plus one
45
+ * key, `kairosResyncAttempt`, holding the attempt. Algolia creates an index on a
46
+ * settings write, so this is also what makes the target EXIST whatever the copy
47
+ * did — over a served index never written, and for a rebuild that loads nothing.
48
+ * The mark is what `publish` checks, below. It goes in `userData` because that
49
+ * is the one setting Algolia keeps for the application's own data and reads
50
+ * nothing from, so the mark changes nothing about how the target ranks or
51
+ * matches.
52
+ * - `begin` first drops every OTHER resync index of this served index it can list,
53
+ * whatever its generation, settings snapshots included. Each was left by an attempt that will never publish
54
+ * it now — one that crashed, one abandoned because the generation moved, or one
55
+ * a worker that lost its lease is still loading — and every such index holds
56
+ * records against the application's quota; an ephemeral feed, whose generation
57
+ * is new on every boot, would otherwise leave one behind per interrupted boot.
58
+ * Dropping the one a stale worker is loading is also what makes that worker's
59
+ * `publish` fail rather than land. Listing needs the key to carry Algolia's
60
+ * `listIndexes` ACL, and the sink REQUIRES it rather than skipping the drop when
61
+ * it is missing: without the drop, a stale worker's `publish` could land and
62
+ * abandoned targets would pile up. A key without it is refused like any other
63
+ * 4xx, a `SinkHalted` at the start of the resync, before the copy, so nothing is
64
+ * created.
65
+ * - `load` is `apply`, against the target. It does not check the mark: that would
66
+ * cost a settings read per page and still race the drop, and `publish` is where
67
+ * a target is served, so `publish` is where the check has to hold.
68
+ * - `publish` reads the target's settings and refuses, `SinkRetryable`, a target
69
+ * that is missing or does not carry this attempt's mark; only then does it move
70
+ * the target over the served index and wait on the move's task. Then it reads the
71
+ * SERVED index's settings and reports success only if the served index now
72
+ * carries this attempt's mark, which only this move could have put there;
73
+ * otherwise it fails `SinkRetryable`. That read is the authority, and the
74
+ * move's own answer is not: Algolia ignores a move of a missing index "without
75
+ * returning an error", so a target dropped after the check gets a task back, the
76
+ * task publishes, and the served index still holds the old copy. Taking that for
77
+ * success would let the driver save its cursor at the new generation over an
78
+ * index that never got it. For the same
79
+ * reason a 404 on the move's task poll — `getTask` on an index that no longer
80
+ * exists — ends the wait rather than halting, and the read decides. An empty rebuild
81
+ * is a move like any other, of a target that holds no records, and it leaves the
82
+ * served index empty. A target can go missing before its publish in two ways: a
83
+ * newer `begin` dropped it, or something moved it over the served index first.
84
+ * Only the second could ever land a half-built target, and the per-attempt name
85
+ * is what rules it out: the one `publish` that names this target is this
86
+ * attempt's own, so no other worker's can move it. What is left is the first,
87
+ * and it has a second form, which is why the mark exists: a worker that lost its
88
+ * lease without knowing it goes on LOADING into its dropped target, and Algolia
89
+ * creates an index on a batch write, so the target comes back holding the stale
90
+ * worker's later pages alone. A name alone cannot tell that index from the one
91
+ * `begin` made; the missing mark can, since only `begin` writes it. Either way
92
+ * `publish` fails `SinkRetryable` and the resync starts again. It never touches
93
+ * the served index on that account, since the worker that finds its target gone
94
+ * is the one most likely to have lost the lease. An index a stale load brought
95
+ * back is left where it is, and the next `begin` drops it with the rest.
96
+ * - Before it marks the target, `begin` takes a SNAPSHOT of the served index's
97
+ * settings into `<target>__settings`, a settings-scoped copy, or an empty
98
+ * settings write when the served index does not exist. After the move has landed,
99
+ * `publish` takes the mark back off the served index by copying the snapshot's
100
+ * settings over it, scoped to settings, then drops the snapshot. The drop is a
101
+ * best effort of its own, and runs whether or not the restore landed. A copy is
102
+ * the one write that REMOVES a setting: Algolia stores a `null` written to `userData` as
103
+ * the value `null`, and `{}` or `resetToDefault` leave an empty object, so a
104
+ * rebuild of an index that had no `userData` would otherwise leave one behind.
105
+ * The snapshot is a second copy of the same served index's settings, made just
106
+ * after the target's, so the restore changes nothing else unless those settings
107
+ * changed between the two copies, when it applies the later ones. Its scope is
108
+ * SETTINGS ONLY, and must stay so: the snapshot holds no synonyms or rules, so a
109
+ * wider restore would wipe the served index's. Records are untouched, and so are the
110
+ * served index's replicas (see below). A mark an earlier failed restore left on the
111
+ * served index is taken off the snapshot as it is made, so it cannot come
112
+ * back; when it was all the `userData` there was, a settings write is the only
113
+ * way to take it off, and the `null` Algolia stores for it is what remains.
114
+ * The move and the restore are two steps, so a search index briefly carries the
115
+ * key, which nothing reads. The restore is BEST-EFFORT: the swap has
116
+ * already landed, and failing `publish` now would tell the driver it had not. A
117
+ * snapshot that is gone (a newer `begin` dropped it) is not copied, since a copy
118
+ * from a missing index changes nothing and its task never publishes; the mark
119
+ * then stays until the next rebuild's restore, and a warning says so. One that
120
+ * goes between that check and the copy is reported the same way, once the wait
121
+ * on the copy's task ends: at the first poll, if `getTask` on the missing index
122
+ * answers 404 as above, and at `pollTimeout` if it does not. The swap has landed
123
+ * by then, so the worst cost is a `publish` that returns late, not a wrong one.
124
+ *
125
+ * Check-then-move leaves no gap a stale load can use: the only worker that loads
126
+ * into this target is the one publishing it, and its loads have finished. A drop
127
+ * between the check and the move makes the move a no-op, which the read of the
128
+ * served index after it turns into a `SinkRetryable`, as above. Taking the mark
129
+ * off the TARGET before the move would open one: a drop between that write and
130
+ * the move, and the write itself would bring back an empty target for the move to
131
+ * land.
132
+ *
133
+ * Replicas are the one thing this does not settle by construction: a copy with
134
+ * `scope` is not documented to carry a primary's replicas to the target, nor a
135
+ * move over a primary, or the restore's settings-scoped copy over it, to keep
136
+ * them. Measured against a real application, all three leave them where they
137
+ * were, and the package's live test checks it after `publish` when credentials
138
+ * are present.
139
+ *
140
+ * ## Reconcile
141
+ *
142
+ * `listKeys` browses the served index page by page, retrieving `objectID` and nothing
143
+ * else, so listing a large index costs the ids and not the records.
144
+ *
145
+ * ## The key's ACLs
146
+ *
147
+ * A scoped key needs `addObject` (record writes, and the copy and the move),
148
+ * `deleteObject`, `deleteIndex` (the drop), `settings` (the mark check),
149
+ * `editSettings` (writing and removing the mark), `browse` (reconcile) and
150
+ * `listIndexes` (the drop's listing), on the served index and its resync indices.
151
+ * The README states the same set, ACL by ACL, for a consumer.
152
+ *
153
+ * ## What the sink holds
154
+ *
155
+ * Nothing between calls but its options and the captured `HttpClient`: no queue, no
156
+ * cursor, no buffered batch. Every piece of progress is the driver's, which is what
157
+ * lets a resent batch and a restarted resync be safe.
158
+ */
159
+ import { type FeedSink, type Generation, type SinkName } from '@kairos-es/feed';
160
+ import { Duration, Effect, type Schema } from 'effect';
161
+ import type { HttpClient } from 'effect/http';
162
+ import { AlgoliaIndexName, type AlgoliaTaskId } from './identifiers.js';
163
+ import { type AlgoliaConnection } from './rest.js';
164
+ /** What `apply` and `load` return: the index the batch went to, and its task. */
165
+ export interface AlgoliaReceipt {
166
+ readonly index: AlgoliaIndexName;
167
+ readonly taskID: AlgoliaTaskId;
168
+ }
169
+ /** How `confirm`, and every wait on an index operation, polls `getTask`. */
170
+ export interface AlgoliaTaskPolling {
171
+ /** The first wait between polls. Default 100 ms. */
172
+ readonly initialDelay?: Duration.Input;
173
+ /** The cap the wait doubles up to. Default 2 seconds. */
174
+ readonly maxDelay?: Duration.Input;
175
+ /** How long to poll before giving up, retryably. Default 60 seconds. */
176
+ readonly timeout?: Duration.Input;
177
+ }
178
+ /** Options for {@link makeAlgoliaSink}. */
179
+ export interface AlgoliaSinkOptions<D, I> extends AlgoliaConnection {
180
+ /** The sink's name: one cursor per feed and sink. */
181
+ readonly name: SinkName;
182
+ /** The SERVED index: the one search reads. */
183
+ readonly index: AlgoliaIndexName;
184
+ /** The document's schema; a record is its encoded form plus `objectID`. */
185
+ readonly doc: Schema.Codec<D, I>;
186
+ /**
187
+ * Records larger than this, in bytes of their JSON with `objectID`, are refused
188
+ * (parked) before sending. Default 10 000 bytes — {@link DEFAULT_MAX_RECORD_BYTES}
189
+ * — which is the record size limit of Algolia's smallest plan, so the default
190
+ * refuses nothing Algolia would take on any plan.
191
+ *
192
+ * On a plan with a larger limit, raise this to that limit (100 000 for a plan
193
+ * whose records may be 100 KB, say); records between the two are then sent
194
+ * rather than parked. Set it no higher than the plan's own limit: a record over
195
+ * the plan's limit that passes this check costs a round trip, which Algolia
196
+ * refuses whole, before it is parked by the answer naming it.
197
+ */
198
+ readonly maxRecordBytes?: number;
199
+ readonly taskPolling?: AlgoliaTaskPolling;
200
+ /** Ids per `browse` page when listing keys. Default 1 000, Algolia's maximum. */
201
+ readonly browsePageSize?: number;
202
+ }
203
+ /**
204
+ * The resync target's name for one attempt at `generation`. PACKAGE-INTERNAL, and
205
+ * kept off the entry point deliberately: `attempt` is what `begin` draws for each
206
+ * call, so a real target's name is the one `begin` returns, and a consumer has no
207
+ * use for composing one.
208
+ *
209
+ * It does not check `attempt`. Every caller here passes lower-case base-36
210
+ * digits, which `begin` draws and the tests write by hand, and that shape is all
211
+ * `resyncGenerationOf` below recognises: a suffix of any other shape would name an
212
+ * index that `begin` never drops and `publish` never accepts.
213
+ */
214
+ export declare const resyncIndexName: (served: AlgoliaIndexName, generation: Generation, attempt: string) => AlgoliaIndexName;
215
+ /**
216
+ * Build an Algolia sink over the `HttpClient` in context; see the module doc.
217
+ * Dies at once on options out of range.
218
+ */
219
+ export declare const makeAlgoliaSink: <D, I>(options: AlgoliaSinkOptions<D, I>) => Effect.Effect<FeedSink<D, AlgoliaReceipt>, never, HttpClient.HttpClient>;
220
+ //# sourceMappingURL=sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sink.d.ts","sourceRoot":"","sources":["../../src/sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6JG;AACH,OAAO,EAIL,KAAK,QAAQ,EACb,KAAK,UAAU,EAGf,KAAK,QAAQ,EAEd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAEL,QAAQ,EACR,MAAM,EAIN,KAAK,MAAM,EAEZ,MAAM,QAAQ,CAAA;AACf,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,OAAO,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAA;AAEpE,OAAO,EAAE,KAAK,iBAAiB,EAAmB,MAAM,QAAQ,CAAA;AAEhE,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAA;IAChC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAC/B;AAED,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,oDAAoD;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAA;IACtC,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAA;IAClC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAA;CAClC;AAED,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAE,SAAQ,iBAAiB;IACjE,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAA;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAChC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IACzC,iFAAiF;IACjF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CACjC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAC1B,QAAQ,gBAAgB,EACxB,YAAY,UAAU,EACtB,SAAS,MAAM,KACd,gBACmE,CAAA;AA2JtE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,CAAC,EAClC,SAAS,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,KAChC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,cAAc,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,CAwUtE,CAAA"}