@lde/search-typesense 0.2.0 → 0.4.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.
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,WAAW,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,wBAAwB,CAAC;AAKhC;8EAC8E;AAC9E,MAAM,WAAW,cAAe,SAAQ,uBAAuB;IAC7D,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,OAAO,CAAC,QAAQ,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAC3D,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,aAAa,CAAC,QAAQ,CAAC,EAClC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAwC1D"}
package/dist/adapter.js DELETED
@@ -1,214 +0,0 @@
1
- import { buildCollectionSchema, } from './collection-schema.js';
2
- const LOCK_COLLECTION = 'rebuild_locks';
3
- const DEFAULT_LOCK_TTL_MS = 10 * 60 * 1000;
4
- /**
5
- * Blue/green-rebuild the search index `options.name` from one declarative
6
- * source: the collection schema is derived from `searchType`
7
- * ({@link buildCollectionSchema}) and the documents are streamed in — one call
8
- * from declaration to live index.
9
- *
10
- * 1. create a fresh versioned collection (`${name}_<timestamp>`)
11
- * 2. stream `documents` into it in batches
12
- * 3. atomically repoint the `name` alias to the new collection, then
13
- * drop the collection it superseded. The caller passes only the logical
14
- * index `name`; the versioned collection name and the alias are managed here.
15
- *
16
- * The rebuild is **single-flight per index**: it first takes a lock (a marker
17
- * document in a `rebuild_locks` collection, created on demand) via Typesense’s
18
- * atomic create, so concurrent callers across pods never rebuild the same index
19
- * at once. This keeps blue/green safe under replication.
20
- *
21
- * `documents` is an async iterable (e.g. a streaming projection); only one
22
- * `batchSize`-sized chunk is held in memory at a time. On any failure before the
23
- * swap nothing is repointed, so the live alias never points at a partial build,
24
- * and the orphaned half-built collection is dropped.
25
- *
26
- * @returns the live collection name and the number of documents imported, or
27
- * `null` when the rebuild was skipped because another rebuild for the same index
28
- * was already running.
29
- *
30
- * Limitations:
31
- * - **Advisory, not a strict mutex.** The lock is built on Typesense, not a
32
- * consensus store. Under a TTL-reclaim race two rebuilds can briefly run at
33
- * once; this is safe because blue/green is idempotent (worst case: redundant
34
- * work and a transient orphaned collection).
35
- * - **Single-flight, not coalescing.** A call made while a rebuild is in flight
36
- * is skipped (returns `null`), not queued. If you must capture state that
37
- * changed mid-build, re-trigger after the running rebuild finishes.
38
- * - **Lock TTL.** A rebuild that runs longer than `lockTtlMs` (default 10
39
- * minutes) can be reclaimed by another caller and run concurrently; size the
40
- * TTL above your longest rebuild.
41
- */
42
- export async function rebuild(client, documents, searchType, options) {
43
- const { batchSize = 1000, lockTtlMs = DEFAULT_LOCK_TTL_MS, ...schemaOptions } = options;
44
- const schema = buildCollectionSchema(searchType, schemaOptions);
45
- const name = schemaOptions.name;
46
- if (!(await acquireLock(client, name, lockTtlMs))) {
47
- return null;
48
- }
49
- const collection = `${name}_${Date.now()}`;
50
- try {
51
- const previous = await aliasTarget(client, name);
52
- await client.collections().create({ ...schema, name: collection });
53
- let imported;
54
- try {
55
- imported = await importStreamed(client, collection, documents, batchSize);
56
- await client.aliases().upsert(name, { collection_name: collection });
57
- }
58
- catch (error) {
59
- // The build failed before the swap: the live alias is untouched, so just
60
- // drop the orphaned half-built collection rather than let it accumulate.
61
- await client
62
- .collections(collection)
63
- .delete()
64
- .catch(() => undefined);
65
- throw error;
66
- }
67
- if (previous !== undefined && previous !== collection) {
68
- await client
69
- .collections(previous)
70
- .delete()
71
- .catch(() => undefined);
72
- }
73
- return { collection, imported };
74
- }
75
- finally {
76
- await releaseLock(client, name);
77
- }
78
- }
79
- /** The collection an alias currently points at, or `undefined` if unset. */
80
- async function aliasTarget(client, alias) {
81
- try {
82
- const { collection_name } = await client.aliases(alias).retrieve();
83
- return collection_name;
84
- }
85
- catch (error) {
86
- if (httpStatus(error) === 404) {
87
- return undefined;
88
- }
89
- throw error;
90
- }
91
- }
92
- /** Upsert a stream of documents in `batchSize` chunks; returns the count. */
93
- async function importStreamed(client, collection, documents, batchSize) {
94
- let imported = 0;
95
- let batch = [];
96
- for await (const document of documents) {
97
- batch.push(document);
98
- if (batch.length >= batchSize) {
99
- await importBatch(client, collection, batch);
100
- imported += batch.length;
101
- batch = [];
102
- }
103
- }
104
- if (batch.length > 0) {
105
- await importBatch(client, collection, batch);
106
- imported += batch.length;
107
- }
108
- return imported;
109
- }
110
- /**
111
- * Create-or-replace one batch of whole documents, keyed on `id`, throwing if any
112
- * individual document fails (Typesense’s bulk import otherwise reports
113
- * per-document failures without rejecting).
114
- */
115
- async function importBatch(client, collection, batch) {
116
- const results = (await client
117
- .collections(collection)
118
- .documents()
119
- .import(batch, {
120
- action: 'upsert',
121
- // Collect per-document outcomes instead of throwing the client’s opaque
122
- // ImportError, so we can report which documents failed and why.
123
- throwOnFail: false,
124
- }));
125
- const failures = results.filter((result) => !result.success);
126
- if (failures.length > 0) {
127
- throw new Error(`Typesense upsert into “${collection}” failed for ${failures.length}/${results.length} documents: ${failures
128
- .map((failure) => failure.error)
129
- .join('; ')}`);
130
- }
131
- }
132
- /**
133
- * Take the per-alias rebuild lock via an atomic create, reclaiming it if the
134
- * current holder is older than `ttlMs`. Returns `false` if another caller holds
135
- * a fresh lock.
136
- */
137
- async function acquireLock(client, alias, ttlMs) {
138
- await ensureLockCollection(client);
139
- try {
140
- await client
141
- .collections(LOCK_COLLECTION)
142
- .documents()
143
- .create({ id: alias, acquired_at: Date.now() });
144
- return true;
145
- }
146
- catch (error) {
147
- if (httpStatus(error) === 409) {
148
- return reclaimIfStale(client, alias, ttlMs);
149
- }
150
- throw error;
151
- }
152
- }
153
- /** Take over the lock if its holder has not refreshed it within `ttlMs`. */
154
- async function reclaimIfStale(client, alias, ttlMs) {
155
- let held;
156
- try {
157
- held = (await client
158
- .collections(LOCK_COLLECTION)
159
- .documents(alias)
160
- .retrieve());
161
- }
162
- catch (error) {
163
- // Released between our create and this read — leave it for the next try.
164
- if (httpStatus(error) === 404) {
165
- return false;
166
- }
167
- throw error;
168
- }
169
- if (Date.now() - held.acquired_at <= ttlMs) {
170
- return false;
171
- }
172
- await client
173
- .collections(LOCK_COLLECTION)
174
- .documents()
175
- .upsert({ id: alias, acquired_at: Date.now() });
176
- return true;
177
- }
178
- /** Release the per-alias lock; a no-op when it is not currently held. */
179
- async function releaseLock(client, alias) {
180
- try {
181
- await client.collections(LOCK_COLLECTION).documents(alias).delete();
182
- }
183
- catch (error) {
184
- if (httpStatus(error) !== 404) {
185
- throw error;
186
- }
187
- }
188
- }
189
- /** Create the lock collection on demand, tolerating a concurrent creator. */
190
- async function ensureLockCollection(client) {
191
- try {
192
- await client.collections(LOCK_COLLECTION).retrieve();
193
- return;
194
- }
195
- catch (error) {
196
- if (httpStatus(error) !== 404) {
197
- throw error;
198
- }
199
- }
200
- try {
201
- await client.collections().create({
202
- name: LOCK_COLLECTION,
203
- fields: [{ name: 'acquired_at', type: 'int64' }],
204
- });
205
- }
206
- catch (error) {
207
- if (httpStatus(error) !== 409) {
208
- throw error;
209
- }
210
- }
211
- }
212
- function httpStatus(error) {
213
- return error.httpStatus;
214
- }