@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
package/dist/cjs/sink.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.resyncIndexName = exports.makeAlgoliaSink = void 0;
|
|
7
|
+
var _feed = require("@kairos-es/feed");
|
|
8
|
+
var _effect = require("effect");
|
|
9
|
+
var _identifiers = require("./identifiers.js");
|
|
10
|
+
var _records = require("./records.js");
|
|
11
|
+
var _rest = require("./rest.js");
|
|
12
|
+
/**
|
|
13
|
+
* `makeAlgoliaSink` — a `FeedSink` onto one served Algolia index, with both optional
|
|
14
|
+
* parts: a `FeedResync` built on Algolia's index copy and move, and a
|
|
15
|
+
* `FeedReconcile` built on `browse`.
|
|
16
|
+
*
|
|
17
|
+
* ## The steady state
|
|
18
|
+
*
|
|
19
|
+
* `apply` is ONE `batch` to the served index (`records.ts` owns what goes in it and
|
|
20
|
+
* which records are refused before it is sent), and its receipt is the index and
|
|
21
|
+
* the task Algolia returned. `confirm` polls `getTask` on that index until the task
|
|
22
|
+
* is `published`, which is the moment Algolia serves the batch — the moment the
|
|
23
|
+
* port's `confirm` promises. The poll runs on an Effect `Schedule`: it backs off
|
|
24
|
+
* from `taskPolling.initialDelay`, doubling to `taskPolling.maxDelay`, and no poll
|
|
25
|
+
* STARTS after `taskPolling.timeout` has passed since the wait began — the last
|
|
26
|
+
* wait is cut short to land on that deadline rather than overrun it by up to
|
|
27
|
+
* `maxDelay`. A task still unpublished then is a `SinkRetryable`: the driver
|
|
28
|
+
* resends the batch, which is safe, rather than waiting on a task without bound.
|
|
29
|
+
* A poll that has started can still outlast the deadline by its own request
|
|
30
|
+
* timeout (`rest.ts`). A `Schedule` rather than a hand-written loop because the
|
|
31
|
+
* curve is then one declared value, run under the `Clock`, so a test's
|
|
32
|
+
* `TestClock` drives it like any other wait.
|
|
33
|
+
*
|
|
34
|
+
* ## Resync: a fresh index, swapped in whole
|
|
35
|
+
*
|
|
36
|
+
* Algolia's `operationIndex` move replaces the destination index atomically, which
|
|
37
|
+
* is exactly the swap `FeedResync` asks for. So:
|
|
38
|
+
*
|
|
39
|
+
* - `begin(generation)` names the target `<served>__kairos_g<generation>_<attempt>`,
|
|
40
|
+
* where `<attempt>` is drawn from Effect's `Random` afresh on every call, and
|
|
41
|
+
* then COPIES the served index into it with `scope` limited to settings, synonyms
|
|
42
|
+
* and rules. The fresh index then ranks and matches exactly as the served one does,
|
|
43
|
+
* and holds none of its records. A served index that has never been written has no
|
|
44
|
+
* scope to copy, and that is not an error: `begin` reads the served index's
|
|
45
|
+
* settings first and makes no copy when it does not exist. Algolia would copy a
|
|
46
|
+
* missing source as an empty index, but the copy's task is polled on the served
|
|
47
|
+
* index, and `getTask` on an index that does not exist answers 404, so skipping
|
|
48
|
+
* the copy is what keeps that wait off a missing index. The name is the ATTEMPT's, not the
|
|
49
|
+
* generation's, because two workers can be resyncing one generation at once —
|
|
50
|
+
* one of them having lost its lease without knowing it yet — and a target they
|
|
51
|
+
* shared is one either could publish half-built; `runFeedSync.ts` owns the
|
|
52
|
+
* argument. Random rather than the lease holder's id, because the port hands a
|
|
53
|
+
* sink no lease, and a draw needs none.
|
|
54
|
+
* - `begin` then MARKS the target as its attempt's own: it writes the target's
|
|
55
|
+
* `userData` setting as whatever the copy brought from the served index, plus one
|
|
56
|
+
* key, `kairosResyncAttempt`, holding the attempt. Algolia creates an index on a
|
|
57
|
+
* settings write, so this is also what makes the target EXIST whatever the copy
|
|
58
|
+
* did — over a served index never written, and for a rebuild that loads nothing.
|
|
59
|
+
* The mark is what `publish` checks, below. It goes in `userData` because that
|
|
60
|
+
* is the one setting Algolia keeps for the application's own data and reads
|
|
61
|
+
* nothing from, so the mark changes nothing about how the target ranks or
|
|
62
|
+
* matches.
|
|
63
|
+
* - `begin` first drops every OTHER resync index of this served index it can list,
|
|
64
|
+
* whatever its generation, settings snapshots included. Each was left by an attempt that will never publish
|
|
65
|
+
* it now — one that crashed, one abandoned because the generation moved, or one
|
|
66
|
+
* a worker that lost its lease is still loading — and every such index holds
|
|
67
|
+
* records against the application's quota; an ephemeral feed, whose generation
|
|
68
|
+
* is new on every boot, would otherwise leave one behind per interrupted boot.
|
|
69
|
+
* Dropping the one a stale worker is loading is also what makes that worker's
|
|
70
|
+
* `publish` fail rather than land. Listing needs the key to carry Algolia's
|
|
71
|
+
* `listIndexes` ACL, and the sink REQUIRES it rather than skipping the drop when
|
|
72
|
+
* it is missing: without the drop, a stale worker's `publish` could land and
|
|
73
|
+
* abandoned targets would pile up. A key without it is refused like any other
|
|
74
|
+
* 4xx, a `SinkHalted` at the start of the resync, before the copy, so nothing is
|
|
75
|
+
* created.
|
|
76
|
+
* - `load` is `apply`, against the target. It does not check the mark: that would
|
|
77
|
+
* cost a settings read per page and still race the drop, and `publish` is where
|
|
78
|
+
* a target is served, so `publish` is where the check has to hold.
|
|
79
|
+
* - `publish` reads the target's settings and refuses, `SinkRetryable`, a target
|
|
80
|
+
* that is missing or does not carry this attempt's mark; only then does it move
|
|
81
|
+
* the target over the served index and wait on the move's task. Then it reads the
|
|
82
|
+
* SERVED index's settings and reports success only if the served index now
|
|
83
|
+
* carries this attempt's mark, which only this move could have put there;
|
|
84
|
+
* otherwise it fails `SinkRetryable`. That read is the authority, and the
|
|
85
|
+
* move's own answer is not: Algolia ignores a move of a missing index "without
|
|
86
|
+
* returning an error", so a target dropped after the check gets a task back, the
|
|
87
|
+
* task publishes, and the served index still holds the old copy. Taking that for
|
|
88
|
+
* success would let the driver save its cursor at the new generation over an
|
|
89
|
+
* index that never got it. For the same
|
|
90
|
+
* reason a 404 on the move's task poll — `getTask` on an index that no longer
|
|
91
|
+
* exists — ends the wait rather than halting, and the read decides. An empty rebuild
|
|
92
|
+
* is a move like any other, of a target that holds no records, and it leaves the
|
|
93
|
+
* served index empty. A target can go missing before its publish in two ways: a
|
|
94
|
+
* newer `begin` dropped it, or something moved it over the served index first.
|
|
95
|
+
* Only the second could ever land a half-built target, and the per-attempt name
|
|
96
|
+
* is what rules it out: the one `publish` that names this target is this
|
|
97
|
+
* attempt's own, so no other worker's can move it. What is left is the first,
|
|
98
|
+
* and it has a second form, which is why the mark exists: a worker that lost its
|
|
99
|
+
* lease without knowing it goes on LOADING into its dropped target, and Algolia
|
|
100
|
+
* creates an index on a batch write, so the target comes back holding the stale
|
|
101
|
+
* worker's later pages alone. A name alone cannot tell that index from the one
|
|
102
|
+
* `begin` made; the missing mark can, since only `begin` writes it. Either way
|
|
103
|
+
* `publish` fails `SinkRetryable` and the resync starts again. It never touches
|
|
104
|
+
* the served index on that account, since the worker that finds its target gone
|
|
105
|
+
* is the one most likely to have lost the lease. An index a stale load brought
|
|
106
|
+
* back is left where it is, and the next `begin` drops it with the rest.
|
|
107
|
+
* - Before it marks the target, `begin` takes a SNAPSHOT of the served index's
|
|
108
|
+
* settings into `<target>__settings`, a settings-scoped copy, or an empty
|
|
109
|
+
* settings write when the served index does not exist. After the move has landed,
|
|
110
|
+
* `publish` takes the mark back off the served index by copying the snapshot's
|
|
111
|
+
* settings over it, scoped to settings, then drops the snapshot. The drop is a
|
|
112
|
+
* best effort of its own, and runs whether or not the restore landed. A copy is
|
|
113
|
+
* the one write that REMOVES a setting: Algolia stores a `null` written to `userData` as
|
|
114
|
+
* the value `null`, and `{}` or `resetToDefault` leave an empty object, so a
|
|
115
|
+
* rebuild of an index that had no `userData` would otherwise leave one behind.
|
|
116
|
+
* The snapshot is a second copy of the same served index's settings, made just
|
|
117
|
+
* after the target's, so the restore changes nothing else unless those settings
|
|
118
|
+
* changed between the two copies, when it applies the later ones. Its scope is
|
|
119
|
+
* SETTINGS ONLY, and must stay so: the snapshot holds no synonyms or rules, so a
|
|
120
|
+
* wider restore would wipe the served index's. Records are untouched, and so are the
|
|
121
|
+
* served index's replicas (see below). A mark an earlier failed restore left on the
|
|
122
|
+
* served index is taken off the snapshot as it is made, so it cannot come
|
|
123
|
+
* back; when it was all the `userData` there was, a settings write is the only
|
|
124
|
+
* way to take it off, and the `null` Algolia stores for it is what remains.
|
|
125
|
+
* The move and the restore are two steps, so a search index briefly carries the
|
|
126
|
+
* key, which nothing reads. The restore is BEST-EFFORT: the swap has
|
|
127
|
+
* already landed, and failing `publish` now would tell the driver it had not. A
|
|
128
|
+
* snapshot that is gone (a newer `begin` dropped it) is not copied, since a copy
|
|
129
|
+
* from a missing index changes nothing and its task never publishes; the mark
|
|
130
|
+
* then stays until the next rebuild's restore, and a warning says so. One that
|
|
131
|
+
* goes between that check and the copy is reported the same way, once the wait
|
|
132
|
+
* on the copy's task ends: at the first poll, if `getTask` on the missing index
|
|
133
|
+
* answers 404 as above, and at `pollTimeout` if it does not. The swap has landed
|
|
134
|
+
* by then, so the worst cost is a `publish` that returns late, not a wrong one.
|
|
135
|
+
*
|
|
136
|
+
* Check-then-move leaves no gap a stale load can use: the only worker that loads
|
|
137
|
+
* into this target is the one publishing it, and its loads have finished. A drop
|
|
138
|
+
* between the check and the move makes the move a no-op, which the read of the
|
|
139
|
+
* served index after it turns into a `SinkRetryable`, as above. Taking the mark
|
|
140
|
+
* off the TARGET before the move would open one: a drop between that write and
|
|
141
|
+
* the move, and the write itself would bring back an empty target for the move to
|
|
142
|
+
* land.
|
|
143
|
+
*
|
|
144
|
+
* Replicas are the one thing this does not settle by construction: a copy with
|
|
145
|
+
* `scope` is not documented to carry a primary's replicas to the target, nor a
|
|
146
|
+
* move over a primary, or the restore's settings-scoped copy over it, to keep
|
|
147
|
+
* them. Measured against a real application, all three leave them where they
|
|
148
|
+
* were, and the package's live test checks it after `publish` when credentials
|
|
149
|
+
* are present.
|
|
150
|
+
*
|
|
151
|
+
* ## Reconcile
|
|
152
|
+
*
|
|
153
|
+
* `listKeys` browses the served index page by page, retrieving `objectID` and nothing
|
|
154
|
+
* else, so listing a large index costs the ids and not the records.
|
|
155
|
+
*
|
|
156
|
+
* ## The key's ACLs
|
|
157
|
+
*
|
|
158
|
+
* A scoped key needs `addObject` (record writes, and the copy and the move),
|
|
159
|
+
* `deleteObject`, `deleteIndex` (the drop), `settings` (the mark check),
|
|
160
|
+
* `editSettings` (writing and removing the mark), `browse` (reconcile) and
|
|
161
|
+
* `listIndexes` (the drop's listing), on the served index and its resync indices.
|
|
162
|
+
* The README states the same set, ACL by ACL, for a consumer.
|
|
163
|
+
*
|
|
164
|
+
* ## What the sink holds
|
|
165
|
+
*
|
|
166
|
+
* Nothing between calls but its options and the captured `HttpClient`: no queue, no
|
|
167
|
+
* cursor, no buffered batch. Every piece of progress is the driver's, which is what
|
|
168
|
+
* lets a resent batch and a restarted resync be safe.
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The resync target's name for one attempt at `generation`. PACKAGE-INTERNAL, and
|
|
173
|
+
* kept off the entry point deliberately: `attempt` is what `begin` draws for each
|
|
174
|
+
* call, so a real target's name is the one `begin` returns, and a consumer has no
|
|
175
|
+
* use for composing one.
|
|
176
|
+
*
|
|
177
|
+
* It does not check `attempt`. Every caller here passes lower-case base-36
|
|
178
|
+
* digits, which `begin` draws and the tests write by hand, and that shape is all
|
|
179
|
+
* `resyncGenerationOf` below recognises: a suffix of any other shape would name an
|
|
180
|
+
* index that `begin` never drops and `publish` never accepts.
|
|
181
|
+
*/
|
|
182
|
+
const resyncIndexName = (served, generation, attempt) => _identifiers.AlgoliaIndexName.make(`${served}__kairos_g${generation}_${attempt}`);
|
|
183
|
+
/**
|
|
184
|
+
* The generation `name` rebuilds, when it is a resync target of `served` for SOME
|
|
185
|
+
* generation and attempt, and `undefined` otherwise: the prefix, the decimal digits
|
|
186
|
+
* a `Generation` prints as, an underscore, then base-36 digits and nothing else,
|
|
187
|
+
* so a served index that merely starts with this one's name plus `__kairos_g` is not
|
|
188
|
+
* mistaken for one of its targets.
|
|
189
|
+
*/
|
|
190
|
+
exports.resyncIndexName = resyncIndexName;
|
|
191
|
+
const resyncGenerationOf = (served, name) => {
|
|
192
|
+
const prefix = `${served}__kairos_g`;
|
|
193
|
+
if (!name.startsWith(prefix)) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return /^(\d+)_[0-9a-z]+$/.exec(name.slice(prefix.length))?.[1];
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* The settings snapshot `begin` takes for `target`: the served index's settings as
|
|
200
|
+
* the target got them, before the mark. Named after its target, so it is a resync
|
|
201
|
+
* index of the same served index and `begin`'s drop of leftovers takes a snapshot an
|
|
202
|
+
* interrupted attempt left, as it takes the target.
|
|
203
|
+
*/
|
|
204
|
+
const settingsSnapshotName = target => _identifiers.AlgoliaIndexName.make(`${target}__settings`);
|
|
205
|
+
/**
|
|
206
|
+
* Whether `name` is any resync index of `served`: a target, or a target's settings
|
|
207
|
+
* snapshot. What `begin` drops; `resyncGenerationOf` alone is what a target must be.
|
|
208
|
+
*/
|
|
209
|
+
const isResyncIndexOf = (served, name) => resyncGenerationOf(served, name.replace(/__settings$/, '')) !== undefined;
|
|
210
|
+
/**
|
|
211
|
+
* The attempt a resync target's name ends in: the base-36 digits after its last
|
|
212
|
+
* underscore. Called only on a name `targetIndex` has already checked.
|
|
213
|
+
*/
|
|
214
|
+
const attemptOf = name => /_([0-9a-z]+)$/.exec(name)?.[1] ?? '';
|
|
215
|
+
/** The `userData` key `begin` marks its target with; see the module doc. */
|
|
216
|
+
const ATTEMPT_MARK = 'kairosResyncAttempt';
|
|
217
|
+
/** Digits in a resync target's attempt suffix: 12 base-36 digits, about 62 bits. */
|
|
218
|
+
const ATTEMPT_DIGITS = 12;
|
|
219
|
+
/** One base-36 digit's worth of `Random`, `ATTEMPT_DIGITS` times. */
|
|
220
|
+
const drawAttempt = /*#__PURE__*/_effect.Effect.map(/*#__PURE__*/_effect.Effect.all(/*#__PURE__*/Array.from({
|
|
221
|
+
length: ATTEMPT_DIGITS
|
|
222
|
+
}, () => _effect.Random.nextIntBetween(0, 35))), digits => digits.map(d => d.toString(36)).join(''));
|
|
223
|
+
const RESYNC_SCOPE = ['settings', 'synonyms', 'rules'];
|
|
224
|
+
/** Index names per `listIndices` page; Algolia's own default. */
|
|
225
|
+
const LIST_PAGE_SIZE = 100;
|
|
226
|
+
const DEFAULT_POLL_INITIAL = /*#__PURE__*/_effect.Duration.millis(100);
|
|
227
|
+
const DEFAULT_POLL_MAX = /*#__PURE__*/_effect.Duration.seconds(2);
|
|
228
|
+
const DEFAULT_POLL_TIMEOUT = /*#__PURE__*/_effect.Duration.seconds(60);
|
|
229
|
+
const DEFAULT_BROWSE_PAGE = 1_000;
|
|
230
|
+
/** A positive duration from an option, or the default; dies on nonsense. */
|
|
231
|
+
const durationOption = (label, input, fallback) => {
|
|
232
|
+
if (input === undefined) {
|
|
233
|
+
return fallback;
|
|
234
|
+
}
|
|
235
|
+
const decoded = _effect.Duration.fromInputUnsafe(input);
|
|
236
|
+
if (!_effect.Duration.isFinite(decoded) || _effect.Duration.toMillis(decoded) <= 0) {
|
|
237
|
+
throw new Error(`feed-algolia: ${label} must be a positive, finite duration`);
|
|
238
|
+
}
|
|
239
|
+
return decoded;
|
|
240
|
+
};
|
|
241
|
+
const positiveInteger = (label, value, fallback) => {
|
|
242
|
+
if (value === undefined) {
|
|
243
|
+
return fallback;
|
|
244
|
+
}
|
|
245
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
246
|
+
throw new Error(`feed-algolia: ${label} must be a positive integer (got ${value})`);
|
|
247
|
+
}
|
|
248
|
+
return value;
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* An index's `userData` setting as an object to add a key to or take one from:
|
|
252
|
+
* empty when the index has none. A `userData` that is not a JSON object leaves no
|
|
253
|
+
* room for the mark beside it, and is a halt rather than something to overwrite.
|
|
254
|
+
*/
|
|
255
|
+
const userDataOf = (index, settings) => {
|
|
256
|
+
const userData = _effect.Option.isSome(settings) ? settings.value.userData : undefined;
|
|
257
|
+
if (userData === undefined || userData === null) {
|
|
258
|
+
return _effect.Effect.succeed({});
|
|
259
|
+
}
|
|
260
|
+
return typeof userData === 'object' && !Array.isArray(userData) ? _effect.Effect.succeed(userData) : _effect.Effect.fail(new _feed.SinkHalted({
|
|
261
|
+
reason: `feed-algolia: the userData setting of '${index}' is not a JSON object, so a resync cannot mark its target beside it`
|
|
262
|
+
}));
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* The resync mark an index's settings carry, if any. Unlike {@link userDataOf} it
|
|
266
|
+
* never halts: after a move, a `userData` that is not an object only means the
|
|
267
|
+
* move did not land, which is the retryable case.
|
|
268
|
+
*/
|
|
269
|
+
const attemptMarkOf = settings => {
|
|
270
|
+
const userData = _effect.Option.isSome(settings) ? settings.value.userData : undefined;
|
|
271
|
+
return typeof userData === 'object' && userData !== null && !Array.isArray(userData) ? userData[ATTEMPT_MARK] : undefined;
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Narrow a call that sends no records. Only a write can be refused per record, so
|
|
275
|
+
* a `SinkRejected` from anything else is a bug in this package's classification;
|
|
276
|
+
* it is reported as a halt rather than silently dropped.
|
|
277
|
+
*/
|
|
278
|
+
const noRejection = effect => _effect.Effect.mapError(effect, failure => failure._tag === 'SinkRejected' ? new _feed.SinkHalted({
|
|
279
|
+
reason: `feed-algolia: a call that sends no records was refused per record: ${failure.message}`
|
|
280
|
+
}) : failure);
|
|
281
|
+
/**
|
|
282
|
+
* Build an Algolia sink over the `HttpClient` in context; see the module doc.
|
|
283
|
+
* Dies at once on options out of range.
|
|
284
|
+
*/
|
|
285
|
+
const makeAlgoliaSink = options => _effect.Effect.gen(function* () {
|
|
286
|
+
const settings = yield* _effect.Effect.try({
|
|
287
|
+
try: () => ({
|
|
288
|
+
maxRecordBytes: positiveInteger('maxRecordBytes', options.maxRecordBytes, _records.DEFAULT_MAX_RECORD_BYTES),
|
|
289
|
+
browsePageSize: positiveInteger('browsePageSize', options.browsePageSize, DEFAULT_BROWSE_PAGE),
|
|
290
|
+
pollInitial: durationOption('taskPolling.initialDelay', options.taskPolling?.initialDelay, DEFAULT_POLL_INITIAL),
|
|
291
|
+
pollMax: durationOption('taskPolling.maxDelay', options.taskPolling?.maxDelay, DEFAULT_POLL_MAX),
|
|
292
|
+
pollTimeout: durationOption('taskPolling.timeout', options.taskPolling?.timeout, DEFAULT_POLL_TIMEOUT)
|
|
293
|
+
}),
|
|
294
|
+
catch: error => error
|
|
295
|
+
}).pipe(_effect.Effect.orDie);
|
|
296
|
+
const rest = yield* (0, _rest.makeAlgoliaRest)(options);
|
|
297
|
+
const served = options.index;
|
|
298
|
+
const pollTimeoutMillis = _effect.Duration.toMillis(settings.pollTimeout);
|
|
299
|
+
/**
|
|
300
|
+
* The curve a task poll waits on between polls: doubling from the first delay,
|
|
301
|
+
* capped, and never past `deadline` — the last delay is cut to land on it, and
|
|
302
|
+
* nothing recurs once it is reached. Built per wait, since the deadline is
|
|
303
|
+
* the wait's own; the curve is the same for every one.
|
|
304
|
+
*/
|
|
305
|
+
const pollSchedule = deadline => _effect.Schedule.min([_effect.Schedule.exponential(settings.pollInitial, 2), _effect.Schedule.spaced(settings.pollMax)]).pipe(_effect.Schedule.while(({
|
|
306
|
+
now
|
|
307
|
+
}) => now < deadline), _effect.Schedule.modifyDelay(({
|
|
308
|
+
now,
|
|
309
|
+
duration
|
|
310
|
+
}) => _effect.Effect.succeed(_effect.Duration.millis(Math.min(_effect.Duration.toMillis(duration), deadline - now)))));
|
|
311
|
+
/**
|
|
312
|
+
* Poll until `poll` is done; see the module doc for the curve. `poll` answers
|
|
313
|
+
* whether to stop, which for every wait but one is "the task is published".
|
|
314
|
+
*/
|
|
315
|
+
const pollUntil = (index, taskID, poll) => _effect.Effect.gen(function* () {
|
|
316
|
+
const deadline = (yield* _effect.Clock.currentTimeMillis) + pollTimeoutMillis;
|
|
317
|
+
const done = yield* poll.pipe(_effect.Effect.repeat({
|
|
318
|
+
schedule: pollSchedule(deadline),
|
|
319
|
+
until: d => d
|
|
320
|
+
}));
|
|
321
|
+
if (!done) {
|
|
322
|
+
return yield* _effect.Effect.fail(new _feed.SinkRetryable({
|
|
323
|
+
reason: `Algolia task ${taskID} on index '${index}' was not published within ${_effect.Duration.format(settings.pollTimeout)}`
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
/** Poll a task until it is published; see the module doc for the curve. */
|
|
328
|
+
const awaitTask = (index, taskID) => pollUntil(index, taskID, noRejection(rest.taskPublished(index, taskID)));
|
|
329
|
+
/** Wait on a task an index operation may or may not have returned. */
|
|
330
|
+
const awaitIfAny = (index, task) => _effect.Option.match(task, {
|
|
331
|
+
onNone: () => _effect.Effect.void,
|
|
332
|
+
onSome: taskID => awaitTask(index, taskID)
|
|
333
|
+
});
|
|
334
|
+
const send = (index, batch) => _effect.Effect.gen(function* () {
|
|
335
|
+
const encoded = yield* (0, _records.encodeBatch)(options.doc, settings.maxRecordBytes, batch);
|
|
336
|
+
const taskID = yield* rest.batch(index, encoded.operations, encoded.keys);
|
|
337
|
+
return {
|
|
338
|
+
index,
|
|
339
|
+
taskID
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
/**
|
|
343
|
+
* The target's index name, refusing a target this sink did not begin: a
|
|
344
|
+
* target is named after the served index, its generation and its attempt, and a
|
|
345
|
+
* load into any other index would write somewhere no publish of this sink will
|
|
346
|
+
* move.
|
|
347
|
+
*/
|
|
348
|
+
const targetIndex = target => resyncGenerationOf(served, target.name) === `${target.generation}` ? _effect.Effect.succeed(_identifiers.AlgoliaIndexName.make(target.name)) : _effect.Effect.die(new Error(`feed-algolia: the resync target '${target.name}' is not one of this sink's targets for generation ${target.generation} ('${served}__kairos_g${target.generation}_<attempt>')`));
|
|
349
|
+
/** Every name `listIndices` returns, page by page. */
|
|
350
|
+
const allIndexNames = _effect.Stream.paginate(0, page => _effect.Effect.map(noRejection(rest.listIndexNames(page, LIST_PAGE_SIZE)), answer => [answer.names, answer.more ? _effect.Option.some(page + 1) : _effect.Option.none()]));
|
|
351
|
+
/** Drop every resync target of `served` but `keep`; see the module doc. */
|
|
352
|
+
const dropAbandonedTargets = keep => _effect.Effect.gen(function* () {
|
|
353
|
+
const names = yield* _effect.Stream.runCollect(allIndexNames);
|
|
354
|
+
for (const name of names) {
|
|
355
|
+
if (name === keep || !isResyncIndexOf(served, name)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const index = _identifiers.AlgoliaIndexName.make(name);
|
|
359
|
+
yield* awaitIfAny(index, yield* noRejection(rest.deleteIndex(index)));
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
const resync = {
|
|
363
|
+
begin: generation => _effect.Effect.gen(function* () {
|
|
364
|
+
const attempt = yield* drawAttempt;
|
|
365
|
+
const target = resyncIndexName(served, generation, attempt);
|
|
366
|
+
yield* dropAbandonedTargets(target);
|
|
367
|
+
const snapshot = settingsSnapshotName(target);
|
|
368
|
+
// No copy of a served index that does not exist; see the module doc.
|
|
369
|
+
if (_effect.Option.isSome(yield* noRejection(rest.readSettings(served)))) {
|
|
370
|
+
yield* awaitTask(served, yield* noRejection(rest.operation(served, {
|
|
371
|
+
operation: 'copy',
|
|
372
|
+
destination: target,
|
|
373
|
+
scope: RESYNC_SCOPE
|
|
374
|
+
})));
|
|
375
|
+
yield* awaitTask(served, yield* noRejection(rest.operation(served, {
|
|
376
|
+
operation: 'copy',
|
|
377
|
+
destination: snapshot,
|
|
378
|
+
scope: ['settings']
|
|
379
|
+
})));
|
|
380
|
+
// A mark a failed restore left on the served index must not ride into
|
|
381
|
+
// the snapshot, or every restore would put it back.
|
|
382
|
+
const {
|
|
383
|
+
[ATTEMPT_MARK]: stale,
|
|
384
|
+
...kept
|
|
385
|
+
} = yield* userDataOf(snapshot, yield* noRejection(rest.readSettings(snapshot)));
|
|
386
|
+
if (stale !== undefined) {
|
|
387
|
+
yield* awaitTask(snapshot, yield* noRejection(rest.writeSettings(snapshot, {
|
|
388
|
+
userData: Object.keys(kept).length === 0 ? null : kept
|
|
389
|
+
})));
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
// An index with every setting at its default, which is what the
|
|
393
|
+
// served index has; an empty settings write creates it.
|
|
394
|
+
yield* awaitTask(snapshot, yield* noRejection(rest.writeSettings(snapshot, {})));
|
|
395
|
+
}
|
|
396
|
+
const userData = yield* userDataOf(target, yield* noRejection(rest.readSettings(target)));
|
|
397
|
+
yield* awaitTask(target, yield* noRejection(rest.writeSettings(target, {
|
|
398
|
+
userData: {
|
|
399
|
+
...userData,
|
|
400
|
+
[ATTEMPT_MARK]: attempt
|
|
401
|
+
}
|
|
402
|
+
})));
|
|
403
|
+
return {
|
|
404
|
+
generation,
|
|
405
|
+
name: target
|
|
406
|
+
};
|
|
407
|
+
}),
|
|
408
|
+
load: (target, batch) => _effect.Effect.flatMap(targetIndex(target), index => send(index, batch)),
|
|
409
|
+
publish: target => _effect.Effect.gen(function* () {
|
|
410
|
+
const index = yield* targetIndex(target);
|
|
411
|
+
const found = yield* noRejection(rest.readSettings(index));
|
|
412
|
+
const userData = yield* userDataOf(index, found);
|
|
413
|
+
if (userData[ATTEMPT_MARK] !== attemptOf(index)) {
|
|
414
|
+
return yield* _effect.Effect.fail(new _feed.SinkRetryable({
|
|
415
|
+
reason: _effect.Option.isNone(found) ? `the resync index '${index}' vanished before it could be moved over '${served}'` : `the resync index '${index}' is not the one its begin made (a load after a newer begin dropped it brought it back), so it is not moved over '${served}'`
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
const moved = yield* noRejection(rest.operation(index, {
|
|
419
|
+
operation: 'move',
|
|
420
|
+
destination: served
|
|
421
|
+
}));
|
|
422
|
+
// A 404 on the poll ends the wait; the read below decides.
|
|
423
|
+
yield* pollUntil(index, moved, _effect.Effect.map(noRejection(rest.taskStateUnlessIndexMissing(index, moved)), state => state !== 'pending'));
|
|
424
|
+
const landed = yield* noRejection(rest.readSettings(served));
|
|
425
|
+
if (attemptMarkOf(landed) !== attemptOf(index)) {
|
|
426
|
+
return yield* _effect.Effect.fail(new _feed.SinkRetryable({
|
|
427
|
+
reason: `the move of the resync index '${index}' over '${served}' did not land: '${served}' does not carry its mark, so the index was dropped after publish checked it, or another attempt's move landed over it`
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
// Best-effort, after the swap has landed; see the module doc.
|
|
431
|
+
const snapshot = settingsSnapshotName(index);
|
|
432
|
+
yield* _effect.Effect.gen(function* () {
|
|
433
|
+
// A copy from a missing snapshot changes nothing, and its task never
|
|
434
|
+
// publishes, so a snapshot a newer `begin` dropped is not waited on,
|
|
435
|
+
// unless it goes between this read and the copy; see the module doc.
|
|
436
|
+
if (_effect.Option.isNone(yield* noRejection(rest.readSettings(snapshot)))) {
|
|
437
|
+
return yield* _effect.Effect.fail(new _feed.SinkRetryable({
|
|
438
|
+
reason: `its settings snapshot '${snapshot}' is gone`
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
yield* awaitTask(snapshot, yield* noRejection(rest.operation(snapshot, {
|
|
442
|
+
operation: 'copy',
|
|
443
|
+
destination: served,
|
|
444
|
+
scope: ['settings']
|
|
445
|
+
})));
|
|
446
|
+
}).pipe(_effect.Effect.catch(failure => _effect.Effect.logWarning(`feed-algolia: the resync mark was left in the userData of '${served}', and the next rebuild will take it off: ${failure.reason}`)));
|
|
447
|
+
// Separately, so a failed drop is not reported as a failed restore.
|
|
448
|
+
yield* _effect.Effect.flatMap(noRejection(rest.deleteIndex(snapshot)), task => awaitIfAny(snapshot, task)).pipe(_effect.Effect.catch(failure => _effect.Effect.logWarning(`feed-algolia: the settings snapshot '${snapshot}' was left behind, and the next rebuild will drop it: ${failure.reason}`)));
|
|
449
|
+
})
|
|
450
|
+
};
|
|
451
|
+
const reconcile = {
|
|
452
|
+
listKeys: _effect.Stream.paginate(_effect.Option.none(), cursor => _effect.Effect.map(noRejection(rest.browseObjectIds(served, cursor, settings.browsePageSize)), page => [page.objectIDs.filter(id => id.length > 0).map(id => _feed.FeedKey.make(id)), _effect.Option.map(page.cursor, _effect.Option.some)]))
|
|
453
|
+
};
|
|
454
|
+
return {
|
|
455
|
+
name: options.name,
|
|
456
|
+
apply: batch => send(served, batch),
|
|
457
|
+
confirm: receipt => awaitTask(receipt.index, receipt.taskID),
|
|
458
|
+
resync,
|
|
459
|
+
reconcile
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
exports.makeAlgoliaSink = makeAlgoliaSink;
|
|
463
|
+
//# sourceMappingURL=sink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sink.js","names":["_feed","require","_effect","_identifiers","_records","_rest","resyncIndexName","served","generation","attempt","AlgoliaIndexName","make","exports","resyncGenerationOf","name","prefix","startsWith","undefined","exec","slice","length","settingsSnapshotName","target","isResyncIndexOf","replace","attemptOf","ATTEMPT_MARK","ATTEMPT_DIGITS","drawAttempt","Effect","map","all","Array","from","Random","nextIntBetween","digits","d","toString","join","RESYNC_SCOPE","LIST_PAGE_SIZE","DEFAULT_POLL_INITIAL","Duration","millis","DEFAULT_POLL_MAX","seconds","DEFAULT_POLL_TIMEOUT","DEFAULT_BROWSE_PAGE","durationOption","label","input","fallback","decoded","fromInputUnsafe","isFinite","toMillis","Error","positiveInteger","value","Number","isInteger","userDataOf","index","settings","userData","Option","isSome","succeed","isArray","fail","SinkHalted","reason","attemptMarkOf","noRejection","effect","mapError","failure","_tag","message","makeAlgoliaSink","options","gen","try","maxRecordBytes","DEFAULT_MAX_RECORD_BYTES","browsePageSize","pollInitial","taskPolling","initialDelay","pollMax","maxDelay","pollTimeout","timeout","catch","error","pipe","orDie","rest","makeAlgoliaRest","pollTimeoutMillis","pollSchedule","deadline","Schedule","min","exponential","spaced","while","now","modifyDelay","duration","Math","pollUntil","taskID","poll","Clock","currentTimeMillis","done","repeat","schedule","until","SinkRetryable","format","awaitTask","taskPublished","awaitIfAny","task","match","onNone","void","onSome","send","batch","encoded","encodeBatch","doc","operations","keys","targetIndex","die","allIndexNames","Stream","paginate","page","listIndexNames","answer","names","more","some","none","dropAbandonedTargets","keep","runCollect","deleteIndex","resync","begin","snapshot","readSettings","operation","destination","scope","stale","kept","writeSettings","Object","load","flatMap","publish","found","isNone","moved","taskStateUnlessIndexMissing","state","landed","logWarning","reconcile","listKeys","cursor","browseObjectIds","objectIDs","filter","id","FeedKey","apply","confirm","receipt"],"sources":["../../src/sink.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -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"}
|