@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/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Neverbland
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kairos-es/feed-algolia
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An Algolia sink for the kairos-es change feed (`@kairos-es/feed`): it keeps one Algolia index in step with a read model, sending only the records that changed, each at its newest state, and rebuilding the index into a fresh one that is swapped in whole when the read model is rebuilt.
|
|
4
|
+
|
|
5
|
+
It talks to Algolia's REST API through `effect`'s own `HttpClient`, so it adds no SDK dependency. You choose the HTTP transport by providing the client.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
`@kairos-es/feed` and `effect` are peer dependencies:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @kairos-es/feed-algolia@alpha @kairos-es/feed@alpha effect@^4.0.5
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`@kairos-es/feed` peers `@kairos-es/core` and `@kairos-es/read` in turn; its README says what to install alongside it.
|
|
16
|
+
|
|
17
|
+
## Wiring
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { runFeedSync, SinkName } from '@kairos-es/feed'
|
|
21
|
+
import {
|
|
22
|
+
AlgoliaAppId,
|
|
23
|
+
AlgoliaIndexName,
|
|
24
|
+
makeAlgoliaSink,
|
|
25
|
+
} from '@kairos-es/feed-algolia'
|
|
26
|
+
import { Config, Effect } from 'effect'
|
|
27
|
+
import { FetchHttpClient } from 'effect/http'
|
|
28
|
+
|
|
29
|
+
const program = Effect.gen(function* () {
|
|
30
|
+
const sink = yield* makeAlgoliaSink({
|
|
31
|
+
name: SinkName.make('search'),
|
|
32
|
+
appId: AlgoliaAppId.make(yield* Config.String('ALGOLIA_APP_ID')),
|
|
33
|
+
adminKey: yield* Config.Redacted('ALGOLIA_ADMIN_KEY'),
|
|
34
|
+
index: AlgoliaIndexName.make('courses'),
|
|
35
|
+
doc: CourseDoc, // the Schema your read model writes into the feed
|
|
36
|
+
}).pipe(Effect.provide(FetchHttpClient.layer))
|
|
37
|
+
yield* Effect.forkScoped(runFeedSync({ source, sink, store }))
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The admin key is held as `Redacted` and never logged. If you use a scoped key instead, give it exactly these ACLs, on the index and on the resync indices named after it (`<index>__kairos_g…`):
|
|
42
|
+
|
|
43
|
+
| ACL | What the sink uses it for |
|
|
44
|
+
| :-- | :-- |
|
|
45
|
+
| `addObject` | writing records, copying the served index's settings into a resync index, and moving that index over the served one |
|
|
46
|
+
| `deleteObject` | deleting records |
|
|
47
|
+
| `deleteIndex` | deleting the resync indices an abandoned rebuild left behind |
|
|
48
|
+
| `settings` | reading an index's settings, to check a resync index before it is moved |
|
|
49
|
+
| `editSettings` | marking a resync index, and taking the mark off the served index after the move |
|
|
50
|
+
| `browse` | listing the served index's object ids, for reconcile |
|
|
51
|
+
| `listIndexes` | listing indices, to find the resync indices to delete |
|
|
52
|
+
|
|
53
|
+
`listIndexes` is required. A key without it stops the worker with a `SinkHalted` (a 403 on `GET /1/indexes`) at the start of every rebuild, before anything is created. The sink does not skip the clean-up when the key cannot list: the clean-up is also what stops a worker that has lost its lease from moving its own half-built index over the served one.
|
|
54
|
+
|
|
55
|
+
## What it does
|
|
56
|
+
|
|
57
|
+
- **Writes.** Each batch is one Algolia `batch` call of full-record writes: `addObject` with `objectID` set to the feed key, or `deleteObject`. Nothing partial is ever sent, so resending a batch after a crash is harmless.
|
|
58
|
+
- **Confirmation.** The feed's cursor moves only once Algolia has published the batch's task. `taskPolling` sets how the task is polled (default: from 100 ms, doubling to 2 s, giving up after 60 s, after which the batch is resent).
|
|
59
|
+
- **Oversized records.** Algolia refuses a whole batch for one record over the plan's size limit. The sink measures every record first (its JSON in UTF-8 bytes, `objectID` included) and refuses the oversized ones itself, so they are parked and the rest of the batch goes through. The default limit, `maxRecordBytes: 10_000`, is the smallest plan's, so out of the box nothing is parked that Algolia would have taken. On a plan with a larger record limit, raise `maxRecordBytes` to that limit (for example `100_000` for 100 KB records). Do not set it above your plan's limit: such a record then costs a round trip that Algolia refuses before it is parked.
|
|
60
|
+
- **Hosts.** Unless you set `host`, the sink talks to `<appId>.algolia.net` and, when that host does not answer in time, answers with a 5xx, or answers with a 1xx or 3xx, tries the same request on Algolia's fallback hosts `<appId>-1.algolianet.com` to `<appId>-3.algolianet.com`, in random order. A host that failed that way is skipped by the sink for two minutes, so a host that has gone quiet costs one timeout rather than one per call; when every host is down, all of them are tried again. `timeouts` sets how long one request to one host may take, as `connect`, `read` and `write` (defaults 2 s, 5 s and 30 s, the official JavaScript client's); a read (a `GET`, or a `browse`) may take `connect` plus `read`, and every other call `connect` plus `write`. Setting `host` sends every call to that one base URL and turns this failover off.
|
|
61
|
+
- **Failures.** A 429, or a 5xx or no answer from every host tried, is retried after a back-off that honours `Retry-After`. A 429 is not tried on another host, since it is your application's rate limit; a 429 whose message names a quota (your record quota, or your allowance of indexing by file size) stops the worker instead, with Algolia's message, since waiting does not lift it. A 400 that names a record parks that record. Any other 4xx (bad credentials, a missing ACL) stops the worker, with Algolia's message.
|
|
62
|
+
- **Rebuilds.** When the read model is rebuilt, the sink builds a fresh index named `<index>__kairos_g<generation>_<attempt>`, where `<attempt>` is random and new on every rebuild attempt, copying only the served index's settings, synonyms and rules into it. It loads every record there and then moves it over the served index. Search keeps serving the old index until the move. The rebuild marks its fresh index with one key, `kairosResyncAttempt`, in the index's `userData` setting. Just after the move it puts the served index's settings back exactly as they were before the rebuild began, by copying them from a snapshot it took then (`<index>__kairos_g<generation>_<attempt>__settings`, deleted afterwards). So your own `userData` is kept as it was, and an index that had none still has none. Algolia keeps a `userData: null` as a stored value, so a settings write could not do that. The one exception: if a restore fails, the sink logs a warning and leaves the mark on the served index, and the next rebuild takes it off. If the mark was all the `userData` there was, that index then ends with `userData: null`. Your `userData` must therefore be a JSON object, or absent: a rebuild of an index whose `userData` is anything else stops the worker. A rebuild whose fresh index was deleted under it (by a newer rebuild) is refused rather than moved over the served index, whether it was written to again or deleted just before the move; the sink checks that the served index carries the move's mark before it reports the rebuild published. Starting a rebuild also deletes every other `<index>__kairos_g…` index an earlier, abandoned rebuild left behind, so an interrupted rebuild does not keep holding records against your quota.
|
|
63
|
+
- **Reconcile.** The sink can list the served index's object ids (by `browse`, retrieving the ids alone), so the feed can delete records the index holds and the feed no longer has.
|
|
64
|
+
|
|
65
|
+
## Replicas
|
|
66
|
+
|
|
67
|
+
Algolia does not document how replicas behave across a scoped copy and a move. The expectation this package is built on is that the resync index is not given the served index's replicas, and that the served index keeps its replicas across the move and across the settings restore that follows it. The package's own tests check both against a real application when credentials are available. Until you have seen that hold for your application, check your replicas after a first rebuild.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.unreadableAnswer = exports.transportFailure = exports.parseRetryAfter = exports.objectIdNamedIn = exports.classifyAnswer = exports.QUOTA_429 = void 0;
|
|
7
|
+
var _feed = require("@kairos-es/feed");
|
|
8
|
+
var _effect = require("effect");
|
|
9
|
+
/**
|
|
10
|
+
* How an Algolia answer becomes one of the feed's three sink failures.
|
|
11
|
+
*
|
|
12
|
+
* The driver's whole behaviour on failure follows from which of the three a sink
|
|
13
|
+
* names (`@kairos-es/feed`'s `errors.ts` owns that), so this mapping is the most
|
|
14
|
+
* consequential thing in the package and it is kept pure, in one place, and tested
|
|
15
|
+
* on its own. The rules:
|
|
16
|
+
*
|
|
17
|
+
* - **A 429 is `SinkRetryable`**, carrying `Retry-After` when Algolia sends one,
|
|
18
|
+
* so the driver backs off no less than Algolia asked — UNLESS its message names
|
|
19
|
+
* a QUOTA rather than a rate: Algolia answers 429 for an application over its
|
|
20
|
+
* record quota or its indexing file-size allowance too, and neither lifts by
|
|
21
|
+
* waiting, so retrying would only hold the feed until supervision reported it
|
|
22
|
+
* stalled, with Algolia's reason lost. Such a 429 is `SinkHalted`, carrying
|
|
23
|
+
* Algolia's message. The wordings recognised are {@link QUOTA_429}; a 429 whose
|
|
24
|
+
* wording is not among them stays retryable, which is the safe side to err on.
|
|
25
|
+
* - **Every 5xx is `SinkRetryable`, and so is a 1xx or 3xx**, a status no call
|
|
26
|
+
* here expects and one the transport has already tried on every host, as the
|
|
27
|
+
* official client does. A transport failure — no answer at all — is retryable
|
|
28
|
+
* too: the request may never have arrived, and resending is safe because every
|
|
29
|
+
* write this sink makes is a full-record replace or a delete.
|
|
30
|
+
* - **A 400 that names an `objectID` in the batch is `SinkRejected` for that key.**
|
|
31
|
+
* Algolia refuses a whole batch for one bad record, and names the record in its
|
|
32
|
+
* message (`… objectID=<id> …`). Parking just that key lets the driver send the
|
|
33
|
+
* rest again without it, which is the difference between one record missing from
|
|
34
|
+
* search and the whole feed stopping. The key has to be one the batch carried: an
|
|
35
|
+
* id the batch did not send is not a record the driver can park, so such an
|
|
36
|
+
* answer falls through to the next rule. The message does not delimit the id, and
|
|
37
|
+
* a feed key may hold a space, a comma or anything else, so the id is not parsed
|
|
38
|
+
* out of the message: the batch's own keys are matched against what follows
|
|
39
|
+
* `objectID=` (`objectIdNamedIn`).
|
|
40
|
+
* - **Every other 4xx is `SinkHalted`.** Bad credentials, a key without the right
|
|
41
|
+
* ACL, an index name Algolia will not accept, a request this package built wrong:
|
|
42
|
+
* none of them is mended by sending again, so the worker stops and supervision
|
|
43
|
+
* reports it.
|
|
44
|
+
*
|
|
45
|
+
* Nothing here reads the request, and nothing here sees a header but `Retry-After`,
|
|
46
|
+
* so no classification can carry the admin key into a reason string.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
const OBJECT_ID_MARKER = 'objectID=';
|
|
50
|
+
/** What may follow an id in Algolia's message: the end, a space or punctuation. */
|
|
51
|
+
const ID_ENDS = /^(?:$|[\s,;.:)])/;
|
|
52
|
+
/**
|
|
53
|
+
* The key of `batchKeys` an Algolia error message names, when it names one.
|
|
54
|
+
*
|
|
55
|
+
* Algolia's record-level refusals all carry `objectID=<id>` (the record-too-big
|
|
56
|
+
* message is the common one), with nothing marking where the id ends. So each
|
|
57
|
+
* batch key is tried against the text after each `objectID=`, and it matches when
|
|
58
|
+
* that text starts with the key and the key is followed by the end of the message,
|
|
59
|
+
* a space or punctuation; the longest key that matches wins, so of `big` and
|
|
60
|
+
* `big one` the message `objectID=big one is too big` names the second.
|
|
61
|
+
*/
|
|
62
|
+
const objectIdNamedIn = (message, batchKeys) => {
|
|
63
|
+
let named;
|
|
64
|
+
let from = message.indexOf(OBJECT_ID_MARKER);
|
|
65
|
+
while (from >= 0) {
|
|
66
|
+
const after = message.slice(from + OBJECT_ID_MARKER.length);
|
|
67
|
+
for (const key of batchKeys) {
|
|
68
|
+
if (after.startsWith(key) && ID_ENDS.test(after.slice(key.length)) && (named === undefined || key.length > named.length)) {
|
|
69
|
+
named = key;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
from = message.indexOf(OBJECT_ID_MARKER, from + 1);
|
|
73
|
+
}
|
|
74
|
+
return _effect.Option.fromUndefinedOr(named);
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The 429 wordings that name a quota, not a rate, matched case-insensitively
|
|
78
|
+
* anywhere in Algolia's message: an application over its record quota, or over
|
|
79
|
+
* its allowance of indexing by file size. See the module doc for why each halts.
|
|
80
|
+
*/
|
|
81
|
+
exports.objectIdNamedIn = objectIdNamedIn;
|
|
82
|
+
const QUOTA_429 = exports.QUOTA_429 = [/quota/i, /file size/i];
|
|
83
|
+
/**
|
|
84
|
+
* `Retry-After` as a duration, given the current time.
|
|
85
|
+
*
|
|
86
|
+
* The header is either a whole number of seconds or an HTTP date. A date is turned
|
|
87
|
+
* into a wait against `now`, which the caller reads from `Clock` so a test's
|
|
88
|
+
* `TestClock` governs it; a date already past is a wait of zero. Anything else is
|
|
89
|
+
* ignored rather than guessed at, and the driver's own back-off applies.
|
|
90
|
+
*/
|
|
91
|
+
const parseRetryAfter = (header, nowMillis) => {
|
|
92
|
+
const trimmed = header.trim();
|
|
93
|
+
if (/^\d+$/.test(trimmed)) {
|
|
94
|
+
return _effect.Option.some(_effect.Duration.seconds(Number(trimmed)));
|
|
95
|
+
}
|
|
96
|
+
// A bare number with a fraction or a sign is neither form; do not let the date
|
|
97
|
+
// parser read it as a year.
|
|
98
|
+
if (/^[-+\d.]+$/.test(trimmed)) {
|
|
99
|
+
return _effect.Option.none();
|
|
100
|
+
}
|
|
101
|
+
return _effect.Option.map(_effect.DateTime.make(trimmed), at => _effect.Duration.millis(Math.max(0, _effect.DateTime.toEpochMillis(at) - nowMillis)));
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Classify an Algolia error answer; see the module doc for the rules.
|
|
105
|
+
*
|
|
106
|
+
* `batchKeys` is the set of keys the request carried, empty for a call that sends
|
|
107
|
+
* no records, so only a write can ever produce a `SinkRejected`.
|
|
108
|
+
*/
|
|
109
|
+
exports.parseRetryAfter = parseRetryAfter;
|
|
110
|
+
const classifyAnswer = (call, answer, batchKeys, nowMillis) => {
|
|
111
|
+
const reason = `Algolia answered ${answer.status} to ${call.method} ${call.path}: ${answer.message}`;
|
|
112
|
+
if (answer.status === 429 && QUOTA_429.some(wording => wording.test(answer.message))) {
|
|
113
|
+
return new _feed.SinkHalted({
|
|
114
|
+
reason
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (answer.status === 429 || answer.status >= 500 || answer.status < 200 || answer.status >= 300 && answer.status < 400) {
|
|
118
|
+
const retryAfter = _effect.Option.flatMap(answer.retryAfter, header => parseRetryAfter(header, nowMillis));
|
|
119
|
+
return new _feed.SinkRetryable({
|
|
120
|
+
reason,
|
|
121
|
+
...(_effect.Option.isSome(retryAfter) ? {
|
|
122
|
+
retryAfter: retryAfter.value
|
|
123
|
+
} : {})
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (answer.status === 400) {
|
|
127
|
+
const named = objectIdNamedIn(answer.message, batchKeys);
|
|
128
|
+
if (_effect.Option.isSome(named)) {
|
|
129
|
+
const key = named.value;
|
|
130
|
+
return new _feed.SinkRejected({
|
|
131
|
+
keys: [key],
|
|
132
|
+
reasons: new Map([[key, answer.message]])
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return new _feed.SinkHalted({
|
|
137
|
+
reason
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* A request that got no answer at all. Retryable: see the module doc for why a
|
|
142
|
+
* resend is safe.
|
|
143
|
+
*/
|
|
144
|
+
exports.classifyAnswer = classifyAnswer;
|
|
145
|
+
const transportFailure = (call, detail) => new _feed.SinkRetryable({
|
|
146
|
+
reason: `no answer from Algolia to ${call.method} ${call.path}: ${detail}`
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* An answer this package cannot read: a success whose body is not the shape
|
|
150
|
+
* Algolia documents. Halted, because sending again will get the same body back.
|
|
151
|
+
*/
|
|
152
|
+
exports.transportFailure = transportFailure;
|
|
153
|
+
const unreadableAnswer = (call, detail) => new _feed.SinkHalted({
|
|
154
|
+
reason: `unreadable answer from Algolia to ${call.method} ${call.path}: ${detail}`
|
|
155
|
+
});
|
|
156
|
+
exports.unreadableAnswer = unreadableAnswer;
|
|
157
|
+
//# sourceMappingURL=classify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.js","names":["_feed","require","_effect","OBJECT_ID_MARKER","ID_ENDS","objectIdNamedIn","message","batchKeys","named","from","indexOf","after","slice","length","key","startsWith","test","undefined","Option","fromUndefinedOr","exports","QUOTA_429","parseRetryAfter","header","nowMillis","trimmed","trim","some","Duration","seconds","Number","none","map","DateTime","make","at","millis","Math","max","toEpochMillis","classifyAnswer","call","answer","reason","status","method","path","wording","SinkHalted","retryAfter","flatMap","SinkRetryable","isSome","value","SinkRejected","keys","reasons","Map","transportFailure","detail","unreadableAnswer"],"sources":["../../src/classify.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.AlgoliaTaskId = exports.AlgoliaIndexName = exports.AlgoliaAppId = void 0;
|
|
7
|
+
var _effect = require("effect");
|
|
8
|
+
/**
|
|
9
|
+
* The Algolia sink's branded identifiers.
|
|
10
|
+
*
|
|
11
|
+
* Three values cross this package's boundary that would otherwise be bare
|
|
12
|
+
* strings and numbers, and each is easy to pass in the place of another: an
|
|
13
|
+
* application id where an index name belongs, a feed key where an index name
|
|
14
|
+
* belongs, a task id from one index polled against another. Branding them is the
|
|
15
|
+
* repository's standing rule for a domain identifier, and here it is also what
|
|
16
|
+
* keeps a `Receipt` — an index and a task id — from being assembled by hand out of
|
|
17
|
+
* values that never belonged together.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The Algolia application id: the first half of the credentials, and the name of
|
|
22
|
+
* the application's hosts. Not a secret — it is sent beside every search a browser
|
|
23
|
+
* makes — so it is a plain branded string, unlike the admin key.
|
|
24
|
+
*/
|
|
25
|
+
const AlgoliaAppId = exports.AlgoliaAppId = /*#__PURE__*/_effect.Schema.String.pipe(/*#__PURE__*/_effect.Schema.check(/*#__PURE__*/_effect.Schema.isNonEmpty()), /*#__PURE__*/_effect.Schema.brand('AlgoliaAppId'));
|
|
26
|
+
/**
|
|
27
|
+
* The name of an Algolia index. The sink's SERVED index is one; a resync builds into
|
|
28
|
+
* another, named after the served one, the generation it rebuilds and the attempt.
|
|
29
|
+
*/
|
|
30
|
+
const AlgoliaIndexName = exports.AlgoliaIndexName = /*#__PURE__*/_effect.Schema.String.pipe(/*#__PURE__*/_effect.Schema.check(/*#__PURE__*/_effect.Schema.isNonEmpty()), /*#__PURE__*/_effect.Schema.brand('AlgoliaIndexName'));
|
|
31
|
+
/**
|
|
32
|
+
* An Algolia task id: what every write returns, and what `getTask` is polled with
|
|
33
|
+
* until the write is published. A task belongs to the index it was issued on, and
|
|
34
|
+
* is polled there and nowhere else — which is why a receipt carries the index
|
|
35
|
+
* beside it.
|
|
36
|
+
*/
|
|
37
|
+
const AlgoliaTaskId = exports.AlgoliaTaskId = /*#__PURE__*/_effect.Schema.Number.pipe(/*#__PURE__*/_effect.Schema.check(/*#__PURE__*/_effect.Schema.isInt(), /*#__PURE__*/_effect.Schema.isGreaterThanOrEqualTo(0)), /*#__PURE__*/_effect.Schema.brand('AlgoliaTaskId'));
|
|
38
|
+
//# sourceMappingURL=identifiers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identifiers.js","names":["_effect","require","AlgoliaAppId","exports","Schema","String","pipe","check","isNonEmpty","brand","AlgoliaIndexName","AlgoliaTaskId","Number","isInt","isGreaterThanOrEqualTo"],"sources":["../../src/identifiers.ts"],"sourcesContent":[null],"mappings":";;;;;;AAWA,IAAAA,OAAA,GAAAC,OAAA;AAXA;;;;;;;;;;;;AAaA;;;;;AAKO,MAAMC,YAAY,GAAAC,OAAA,CAAAD,YAAA,gBAAGE,cAAM,CAACC,MAAM,CAACC,IAAI,cAC5CF,cAAM,CAACG,KAAK,cAACH,cAAM,CAACI,UAAU,EAAE,CAAC,eACjCJ,cAAM,CAACK,KAAK,CAAC,cAAc,CAAC,CAC7B;AAGD;;;;AAIO,MAAMC,gBAAgB,GAAAP,OAAA,CAAAO,gBAAA,gBAAGN,cAAM,CAACC,MAAM,CAACC,IAAI,cAChDF,cAAM,CAACG,KAAK,cAACH,cAAM,CAACI,UAAU,EAAE,CAAC,eACjCJ,cAAM,CAACK,KAAK,CAAC,kBAAkB,CAAC,CACjC;AAGD;;;;;;AAMO,MAAME,aAAa,GAAAR,OAAA,CAAAQ,aAAA,gBAAGP,cAAM,CAACQ,MAAM,CAACN,IAAI,cAC7CF,cAAM,CAACG,KAAK,cAACH,cAAM,CAACS,KAAK,EAAE,eAAET,cAAM,CAACU,sBAAsB,CAAC,CAAC,CAAC,CAAC,eAC9DV,cAAM,CAACK,KAAK,CAAC,eAAe,CAAC,CAC9B","ignoreList":[]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "AlgoliaAppId", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _identifiers.AlgoliaAppId;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "AlgoliaIndexName", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _identifiers.AlgoliaIndexName;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "AlgoliaTaskId", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _identifiers.AlgoliaTaskId;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "DEFAULT_MAX_RECORD_BYTES", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () {
|
|
27
|
+
return _records.DEFAULT_MAX_RECORD_BYTES;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(exports, "makeAlgoliaSink", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () {
|
|
33
|
+
return _sink.makeAlgoliaSink;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
var _identifiers = require("./identifiers.js");
|
|
37
|
+
var _records = require("./records.js");
|
|
38
|
+
var _sink = require("./sink.js");
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["_identifiers","require","_records","_sink"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,IAAAA,YAAA,GAAAC,OAAA;AAKA,IAAAC,QAAA,GAAAD,OAAA;AAEA,IAAAE,KAAA,GAAAF,OAAA","ignoreList":[]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.recordBytes = exports.encodeBatch = exports.DEFAULT_MAX_RECORD_BYTES = void 0;
|
|
7
|
+
var _feed = require("@kairos-es/feed");
|
|
8
|
+
var _effect = require("effect");
|
|
9
|
+
/**
|
|
10
|
+
* Turning a batch of feed changes into Algolia batch operations, and refusing the
|
|
11
|
+
* records Algolia would refuse BEFORE they are sent.
|
|
12
|
+
*
|
|
13
|
+
* ## Full-record replace, and nothing partial
|
|
14
|
+
*
|
|
15
|
+
* An upsert is `addObject` with the record's `objectID` set to the feed key, which
|
|
16
|
+
* replaces whatever Algolia held under that id; a delete is `deleteObject`. Both
|
|
17
|
+
* leave the index the same however many times they are sent, which is the
|
|
18
|
+
* idempotence the driver relies on when it resends a batch it could not confirm.
|
|
19
|
+
* Algolia's partial operations (`partialUpdateObject`, the built-in `Increment`
|
|
20
|
+
* and its kin) do not: applied twice they apply twice. They are never used.
|
|
21
|
+
*
|
|
22
|
+
* The key is written LAST, over the encoded document, so a document that carries an
|
|
23
|
+
* `objectID` field of its own cannot point a record at another key.
|
|
24
|
+
*
|
|
25
|
+
* ## Why oversized records are refused here
|
|
26
|
+
*
|
|
27
|
+
* Algolia refuses a whole `batch` when one record in it is over the plan's record
|
|
28
|
+
* size limit. Its answer does name the record, and `classify.ts` turns that into a
|
|
29
|
+
* rejection of that one key — but only after a round trip that wrote nothing, and
|
|
30
|
+
* only for the first oversized record the batch held. Checking every record's size
|
|
31
|
+
* up front rejects all of them at once, so the driver's resend of the rest goes
|
|
32
|
+
* through first time. The 400 path stays as the backstop for a limit configured
|
|
33
|
+
* higher than the plan's.
|
|
34
|
+
*
|
|
35
|
+
* The size is the record's JSON in UTF-8 bytes, `objectID` included, which is how
|
|
36
|
+
* Algolia measures it. The default limit is the smallest Algolia plan's (10 KB);
|
|
37
|
+
* an application on a larger plan raises it.
|
|
38
|
+
*
|
|
39
|
+
* ## A document that does not encode
|
|
40
|
+
*
|
|
41
|
+
* The document schema is the read model's, and a document it cannot encode — or one
|
|
42
|
+
* that encodes to something other than a JSON object, which is all an Algolia
|
|
43
|
+
* record can be — is a defect in the read model, not a record the sink refuses. It
|
|
44
|
+
* dies, loudly, rather than being parked where nobody looks.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/** The smallest Algolia plan's record size limit, in bytes. */
|
|
48
|
+
const DEFAULT_MAX_RECORD_BYTES = exports.DEFAULT_MAX_RECORD_BYTES = 10_000;
|
|
49
|
+
const utf8 = /*#__PURE__*/new TextEncoder();
|
|
50
|
+
/** A record's size as Algolia measures it: its JSON, in UTF-8 bytes. */
|
|
51
|
+
const recordBytes = record => utf8.encode(JSON.stringify(record)).length;
|
|
52
|
+
exports.recordBytes = recordBytes;
|
|
53
|
+
const isJsonObject = u => typeof u === 'object' && u !== null && !Array.isArray(u);
|
|
54
|
+
/**
|
|
55
|
+
* Encode a batch, failing `SinkRejected` with EVERY oversized key when there is
|
|
56
|
+
* one; see the module doc.
|
|
57
|
+
*/
|
|
58
|
+
const encodeBatch = (doc, maxRecordBytes, batch) => _effect.Effect.gen(function* () {
|
|
59
|
+
const encode = _effect.Schema.encodeEffect(doc);
|
|
60
|
+
const operations = [];
|
|
61
|
+
const keys = new Set();
|
|
62
|
+
const reasons = new Map();
|
|
63
|
+
for (const change of batch) {
|
|
64
|
+
keys.add(change.key);
|
|
65
|
+
if (change._tag === 'Delete') {
|
|
66
|
+
operations.push({
|
|
67
|
+
action: 'deleteObject',
|
|
68
|
+
body: {
|
|
69
|
+
objectID: change.key
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const encoded = yield* _effect.Effect.orDie(encode(change.doc));
|
|
75
|
+
if (!isJsonObject(encoded)) {
|
|
76
|
+
return yield* _effect.Effect.die(new Error(`feed-algolia: the document for key '${change.key}' encodes to ${encoded === null ? 'null' : typeof encoded}, and an Algolia record must be a JSON object`));
|
|
77
|
+
}
|
|
78
|
+
const record = {
|
|
79
|
+
...encoded,
|
|
80
|
+
objectID: change.key
|
|
81
|
+
};
|
|
82
|
+
const size = recordBytes(record);
|
|
83
|
+
if (size > maxRecordBytes) {
|
|
84
|
+
reasons.set(change.key, `the record is ${size} bytes, over the configured maxRecordBytes of ${maxRecordBytes}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
operations.push({
|
|
88
|
+
action: 'addObject',
|
|
89
|
+
body: record
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const rejected = Array.from(reasons.keys());
|
|
93
|
+
if (_effect.Array.isReadonlyArrayNonEmpty(rejected)) {
|
|
94
|
+
return yield* _effect.Effect.fail(new _feed.SinkRejected({
|
|
95
|
+
keys: rejected,
|
|
96
|
+
reasons
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
operations,
|
|
101
|
+
keys
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
exports.encodeBatch = encodeBatch;
|
|
105
|
+
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.js","names":["_feed","require","_effect","DEFAULT_MAX_RECORD_BYTES","exports","utf8","TextEncoder","recordBytes","record","encode","JSON","stringify","length","isJsonObject","u","Array","isArray","encodeBatch","doc","maxRecordBytes","batch","Effect","gen","Schema","encodeEffect","operations","keys","Set","reasons","Map","change","add","key","_tag","push","action","body","objectID","encoded","orDie","die","Error","size","set","rejected","from","Arr","isReadonlyArrayNonEmpty","fail","SinkRejected"],"sources":["../../src/records.ts"],"sourcesContent":[null],"mappings":";;;;;;AAqCA,IAAAA,KAAA,GAAAC,OAAA;AACA,IAAAC,OAAA,GAAAD,OAAA;AAtCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA;AACO,MAAME,wBAAwB,GAAAC,OAAA,CAAAD,wBAAA,GAAG,MAAM;AAE9C,MAAME,IAAI,gBAAG,IAAIC,WAAW,EAAE;AAE9B;AACO,MAAMC,WAAW,GACtBC,MAAyC,IAC9BH,IAAI,CAACI,MAAM,CAACC,IAAI,CAACC,SAAS,CAACH,MAAM,CAAC,CAAC,CAACI,MAAM;AAAAR,OAAA,CAAAG,WAAA,GAAAA,WAAA;AAQvD,MAAMM,YAAY,GAAIC,CAAU,IAC9B,OAAOA,CAAC,KAAK,QAAQ,IAAIA,CAAC,KAAK,IAAI,IAAI,CAACC,KAAK,CAACC,OAAO,CAACF,CAAC,CAAC;AAE1D;;;;AAIO,MAAMG,WAAW,GAAGA,CACzBC,GAAuB,EACvBC,cAAsB,EACtBC,KAA2C,KAE3CC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMb,MAAM,GAAGc,cAAM,CAACC,YAAY,CAACN,GAAG,CAAC;EACvC,MAAMO,UAAU,GAA0B,EAAE;EAC5C,MAAMC,IAAI,GAAG,IAAIC,GAAG,EAAW;EAC/B,MAAMC,OAAO,GAAG,IAAIC,GAAG,EAAmB;EAC1C,KAAK,MAAMC,MAAM,IAAIV,KAAK,EAAE;IAC1BM,IAAI,CAACK,GAAG,CAACD,MAAM,CAACE,GAAG,CAAC;IACpB,IAAIF,MAAM,CAACG,IAAI,KAAK,QAAQ,EAAE;MAC5BR,UAAU,CAACS,IAAI,CAAC;QACdC,MAAM,EAAE,cAAc;QACtBC,IAAI,EAAE;UAAEC,QAAQ,EAAEP,MAAM,CAACE;QAAG;OAC7B,CAAC;MACF;IACF;IACA,MAAMM,OAAO,GAAY,OAAOjB,cAAM,CAACkB,KAAK,CAAC9B,MAAM,CAACqB,MAAM,CAACZ,GAAG,CAAC,CAAC;IAChE,IAAI,CAACL,YAAY,CAACyB,OAAO,CAAC,EAAE;MAC1B,OAAO,OAAOjB,cAAM,CAACmB,GAAG,CACtB,IAAIC,KAAK,CACP,uCAAuCX,MAAM,CAACE,GAAG,gBAAgBM,OAAO,KAAK,IAAI,GAAG,MAAM,GAAG,OAAOA,OAAO,+CAA+C,CAC3J,CACF;IACH;IACA,MAAM9B,MAAM,GAAG;MAAE,GAAG8B,OAAO;MAAED,QAAQ,EAAEP,MAAM,CAACE;IAAG,CAAE;IACnD,MAAMU,IAAI,GAAGnC,WAAW,CAACC,MAAM,CAAC;IAChC,IAAIkC,IAAI,GAAGvB,cAAc,EAAE;MACzBS,OAAO,CAACe,GAAG,CACTb,MAAM,CAACE,GAAG,EACV,iBAAiBU,IAAI,iDAAiDvB,cAAc,EAAE,CACvF;MACD;IACF;IACAM,UAAU,CAACS,IAAI,CAAC;MAAEC,MAAM,EAAE,WAAW;MAAEC,IAAI,EAAE5B;IAAM,CAAE,CAAC;EACxD;EACA,MAAMoC,QAAQ,GAAG7B,KAAK,CAAC8B,IAAI,CAACjB,OAAO,CAACF,IAAI,EAAE,CAAC;EAC3C,IAAIoB,aAAG,CAACC,uBAAuB,CAACH,QAAQ,CAAC,EAAE;IACzC,OAAO,OAAOvB,cAAM,CAAC2B,IAAI,CAAC,IAAIC,kBAAY,CAAC;MAAEvB,IAAI,EAAEkB,QAAQ;MAAEhB;IAAO,CAAE,CAAC,CAAC;EAC1E;EACA,OAAO;IAAEH,UAAU;IAAEC;EAAI,CAAE;AAC7B,CAAC,CAAC;AAAAtB,OAAA,CAAAa,WAAA,GAAAA,WAAA","ignoreList":[]}
|