@magicpages/ghost-typesense-core 1.9.1 → 1.9.2

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/dist/index.d.mts CHANGED
@@ -35,6 +35,26 @@ declare class GhostTypesenseManager {
35
35
  * Fetch all posts from Ghost and index them in Typesense
36
36
  */
37
37
  indexAllPosts(): Promise<void>;
38
+ /**
39
+ * Index documents in batches with retry logic and backpressure handling
40
+ * @private
41
+ */
42
+ private indexDocumentsBatched;
43
+ /**
44
+ * Process a single batch with retry logic and backpressure handling
45
+ * @private
46
+ */
47
+ private processBatchWithRetry;
48
+ /**
49
+ * Retry failed batches with smaller batch sizes
50
+ * @private
51
+ */
52
+ private retryFailedBatches;
53
+ /**
54
+ * Sleep utility for backoff delays
55
+ * @private
56
+ */
57
+ private sleep;
38
58
  /**
39
59
  * Index a single post in Typesense
40
60
  */
package/dist/index.d.ts CHANGED
@@ -35,6 +35,26 @@ declare class GhostTypesenseManager {
35
35
  * Fetch all posts from Ghost and index them in Typesense
36
36
  */
37
37
  indexAllPosts(): Promise<void>;
38
+ /**
39
+ * Index documents in batches with retry logic and backpressure handling
40
+ * @private
41
+ */
42
+ private indexDocumentsBatched;
43
+ /**
44
+ * Process a single batch with retry logic and backpressure handling
45
+ * @private
46
+ */
47
+ private processBatchWithRetry;
48
+ /**
49
+ * Retry failed batches with smaller batch sizes
50
+ * @private
51
+ */
52
+ private retryFailedBatches;
53
+ /**
54
+ * Sleep utility for backoff delays
55
+ * @private
56
+ */
57
+ private sleep;
38
58
  /**
39
59
  * Index a single post in Typesense
40
60
  */
package/dist/index.js CHANGED
@@ -41,9 +41,10 @@ var GhostTypesenseManager = class {
41
41
  this.typesense = new import_typesense.Client({
42
42
  nodes: config.typesense.nodes,
43
43
  apiKey: config.typesense.apiKey,
44
- connectionTimeoutSeconds: config.typesense.connectionTimeoutSeconds,
45
- retryIntervalSeconds: config.typesense.retryIntervalSeconds,
46
- numRetries: 3
44
+ connectionTimeoutSeconds: config.typesense.connectionTimeoutSeconds || 3600,
45
+ // 60 minutes for bulk operations
46
+ retryIntervalSeconds: config.typesense.retryIntervalSeconds || 2,
47
+ numRetries: 5
47
48
  });
48
49
  }
49
50
  /**
@@ -163,23 +164,154 @@ var GhostTypesenseManager = class {
163
164
  }
164
165
  console.log(`Found ${allPosts.length} posts to index`);
165
166
  const documents = allPosts.map((post) => this.transformPost(post));
166
- try {
167
- const collection = this.typesense.collections(this.collectionName);
168
- const results = await Promise.all(
169
- documents.map(
170
- (doc) => collection.documents().upsert(doc).then(() => ({ success: true, id: doc.id })).catch((error) => ({ success: false, id: doc.id, error: error.message }))
171
- )
172
- );
173
- const succeeded = results.filter((r) => r.success).length;
174
- const failed = results.filter((r) => !r.success).length;
175
- console.log(`Indexing complete: ${succeeded} succeeded, ${failed} failed`);
176
- if (failed > 0) {
177
- console.log("Failed documents:", results.filter((r) => !r.success));
167
+ await this.indexDocumentsBatched(documents);
168
+ }
169
+ /**
170
+ * Index documents in batches with retry logic and backpressure handling
171
+ * @private
172
+ */
173
+ async indexDocumentsBatched(documents) {
174
+ const batchSize = this.config.typesense.batchSize || 200;
175
+ const maxConcurrentBatches = this.config.typesense.maxConcurrentBatches || 12;
176
+ const batches = [];
177
+ for (let i = 0; i < documents.length; i += batchSize) {
178
+ batches.push(documents.slice(i, i + batchSize));
179
+ }
180
+ console.log(`Processing ${documents.length} documents in ${batches.length} batches (batch size: ${batchSize})`);
181
+ const collection = this.typesense.collections(this.collectionName);
182
+ let totalSucceeded = 0;
183
+ let totalFailed = 0;
184
+ const failedBatches = [];
185
+ for (let i = 0; i < batches.length; i += maxConcurrentBatches) {
186
+ const batchGroup = batches.slice(i, i + maxConcurrentBatches);
187
+ const batchPromises = batchGroup.map(async (batch, batchIndex) => {
188
+ const actualBatchIndex = i + batchIndex;
189
+ return this.processBatchWithRetry(collection, batch, actualBatchIndex, batches.length);
190
+ });
191
+ const results = await Promise.allSettled(batchPromises);
192
+ results.forEach((result, batchIndex) => {
193
+ const actualBatchIndex = i + batchIndex;
194
+ if (result.status === "fulfilled") {
195
+ totalSucceeded += result.value.succeeded;
196
+ totalFailed += result.value.failed;
197
+ if (result.value.error) {
198
+ failedBatches.push({
199
+ batchIndex: actualBatchIndex,
200
+ documents: batchGroup[batchIndex],
201
+ error: result.value.error
202
+ });
203
+ }
204
+ } else {
205
+ const batchSize2 = batchGroup[batchIndex].length;
206
+ totalFailed += batchSize2;
207
+ failedBatches.push({
208
+ batchIndex: actualBatchIndex,
209
+ documents: batchGroup[batchIndex],
210
+ error: result.reason?.message || "Unknown batch error"
211
+ });
212
+ }
213
+ });
214
+ console.log(`Progress: ${Math.min(i + maxConcurrentBatches, batches.length)}/${batches.length} batch groups processed`);
215
+ }
216
+ console.log(`Indexing complete: ${totalSucceeded} succeeded, ${totalFailed} failed`);
217
+ if (failedBatches.length > 0) {
218
+ console.log(`Retrying ${failedBatches.length} failed batches with smaller batch size...`);
219
+ const retryResults = await this.retryFailedBatches(collection, failedBatches);
220
+ totalSucceeded += retryResults.succeeded;
221
+ totalFailed = totalFailed - retryResults.retryAttempted + retryResults.failed;
222
+ console.log(`Final result: ${totalSucceeded} succeeded, ${totalFailed} failed`);
223
+ }
224
+ if (totalFailed > 0) {
225
+ console.log(`\u26A0\uFE0F ${totalFailed} documents failed to index. Consider running sync again or checking server capacity.`);
226
+ }
227
+ }
228
+ /**
229
+ * Process a single batch with retry logic and backpressure handling
230
+ * @private
231
+ */
232
+ async processBatchWithRetry(collection, documents, batchIndex, totalBatches) {
233
+ const maxRetries = 3;
234
+ let lastError = "";
235
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
236
+ try {
237
+ console.log(`Processing batch ${batchIndex + 1}/${totalBatches} (${documents.length} docs) - attempt ${attempt}`);
238
+ const result = await collection.documents().import(documents, {
239
+ action: "upsert",
240
+ batch_size: documents.length,
241
+ return_doc: false,
242
+ return_id: false
243
+ });
244
+ const succeeded = result.filter((r) => r.success === true).length;
245
+ const failed = documents.length - succeeded;
246
+ if (failed > 0) {
247
+ console.log(`Batch ${batchIndex + 1}: ${succeeded} succeeded, ${failed} failed`);
248
+ }
249
+ return { succeeded, failed };
250
+ } catch (error) {
251
+ lastError = error.message || error;
252
+ if (error.httpStatus === 503 || lastError.includes("503") || lastError.includes("Not Ready")) {
253
+ const backoffDelay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
254
+ console.log(`Batch ${batchIndex + 1}: Server overload (503), retrying in ${backoffDelay}ms...`);
255
+ await this.sleep(backoffDelay);
256
+ continue;
257
+ }
258
+ if (lastError.includes("timeout") || lastError.includes("ECONNABORTED")) {
259
+ const backoffDelay = Math.min(2e3 * attempt, 8e3);
260
+ console.log(`Batch ${batchIndex + 1}: Timeout error, retrying in ${backoffDelay}ms...`);
261
+ await this.sleep(backoffDelay);
262
+ continue;
263
+ }
264
+ if (attempt < maxRetries) {
265
+ const backoffDelay = 1e3 * attempt;
266
+ console.log(`Batch ${batchIndex + 1}: Error (${lastError}), retrying in ${backoffDelay}ms...`);
267
+ await this.sleep(backoffDelay);
268
+ continue;
269
+ }
178
270
  }
179
- } catch (error) {
180
- console.error("Indexing error:", error);
181
- throw error;
182
271
  }
272
+ console.error(`Batch ${batchIndex + 1} failed after ${maxRetries} attempts: ${lastError}`);
273
+ return { succeeded: 0, failed: documents.length, error: lastError };
274
+ }
275
+ /**
276
+ * Retry failed batches with smaller batch sizes
277
+ * @private
278
+ */
279
+ async retryFailedBatches(collection, failedBatches) {
280
+ let succeeded = 0;
281
+ let failed = 0;
282
+ let retryAttempted = 0;
283
+ for (const failedBatch of failedBatches) {
284
+ retryAttempted += failedBatch.documents.length;
285
+ const smallBatches = [];
286
+ for (let i = 0; i < failedBatch.documents.length; i += 50) {
287
+ smallBatches.push(failedBatch.documents.slice(i, i + 50));
288
+ }
289
+ for (const smallBatch of smallBatches) {
290
+ try {
291
+ const result = await collection.documents().import(smallBatch, {
292
+ action: "upsert",
293
+ batch_size: smallBatch.length,
294
+ return_doc: false,
295
+ return_id: false
296
+ });
297
+ const batchSucceeded = result.filter((r) => r.success === true).length;
298
+ succeeded += batchSucceeded;
299
+ failed += smallBatch.length - batchSucceeded;
300
+ } catch (error) {
301
+ console.error(`Small batch retry failed: ${error.message || error}`);
302
+ failed += smallBatch.length;
303
+ }
304
+ await this.sleep(500);
305
+ }
306
+ }
307
+ return { succeeded, failed, retryAttempted };
308
+ }
309
+ /**
310
+ * Sleep utility for backoff delays
311
+ * @private
312
+ */
313
+ sleep(ms) {
314
+ return new Promise((resolve) => setTimeout(resolve, ms));
183
315
  }
184
316
  /**
185
317
  * Index a single post in Typesense
package/dist/index.mjs CHANGED
@@ -17,9 +17,10 @@ var GhostTypesenseManager = class {
17
17
  this.typesense = new Client({
18
18
  nodes: config.typesense.nodes,
19
19
  apiKey: config.typesense.apiKey,
20
- connectionTimeoutSeconds: config.typesense.connectionTimeoutSeconds,
21
- retryIntervalSeconds: config.typesense.retryIntervalSeconds,
22
- numRetries: 3
20
+ connectionTimeoutSeconds: config.typesense.connectionTimeoutSeconds || 3600,
21
+ // 60 minutes for bulk operations
22
+ retryIntervalSeconds: config.typesense.retryIntervalSeconds || 2,
23
+ numRetries: 5
23
24
  });
24
25
  }
25
26
  /**
@@ -139,23 +140,154 @@ var GhostTypesenseManager = class {
139
140
  }
140
141
  console.log(`Found ${allPosts.length} posts to index`);
141
142
  const documents = allPosts.map((post) => this.transformPost(post));
142
- try {
143
- const collection = this.typesense.collections(this.collectionName);
144
- const results = await Promise.all(
145
- documents.map(
146
- (doc) => collection.documents().upsert(doc).then(() => ({ success: true, id: doc.id })).catch((error) => ({ success: false, id: doc.id, error: error.message }))
147
- )
148
- );
149
- const succeeded = results.filter((r) => r.success).length;
150
- const failed = results.filter((r) => !r.success).length;
151
- console.log(`Indexing complete: ${succeeded} succeeded, ${failed} failed`);
152
- if (failed > 0) {
153
- console.log("Failed documents:", results.filter((r) => !r.success));
143
+ await this.indexDocumentsBatched(documents);
144
+ }
145
+ /**
146
+ * Index documents in batches with retry logic and backpressure handling
147
+ * @private
148
+ */
149
+ async indexDocumentsBatched(documents) {
150
+ const batchSize = this.config.typesense.batchSize || 200;
151
+ const maxConcurrentBatches = this.config.typesense.maxConcurrentBatches || 12;
152
+ const batches = [];
153
+ for (let i = 0; i < documents.length; i += batchSize) {
154
+ batches.push(documents.slice(i, i + batchSize));
155
+ }
156
+ console.log(`Processing ${documents.length} documents in ${batches.length} batches (batch size: ${batchSize})`);
157
+ const collection = this.typesense.collections(this.collectionName);
158
+ let totalSucceeded = 0;
159
+ let totalFailed = 0;
160
+ const failedBatches = [];
161
+ for (let i = 0; i < batches.length; i += maxConcurrentBatches) {
162
+ const batchGroup = batches.slice(i, i + maxConcurrentBatches);
163
+ const batchPromises = batchGroup.map(async (batch, batchIndex) => {
164
+ const actualBatchIndex = i + batchIndex;
165
+ return this.processBatchWithRetry(collection, batch, actualBatchIndex, batches.length);
166
+ });
167
+ const results = await Promise.allSettled(batchPromises);
168
+ results.forEach((result, batchIndex) => {
169
+ const actualBatchIndex = i + batchIndex;
170
+ if (result.status === "fulfilled") {
171
+ totalSucceeded += result.value.succeeded;
172
+ totalFailed += result.value.failed;
173
+ if (result.value.error) {
174
+ failedBatches.push({
175
+ batchIndex: actualBatchIndex,
176
+ documents: batchGroup[batchIndex],
177
+ error: result.value.error
178
+ });
179
+ }
180
+ } else {
181
+ const batchSize2 = batchGroup[batchIndex].length;
182
+ totalFailed += batchSize2;
183
+ failedBatches.push({
184
+ batchIndex: actualBatchIndex,
185
+ documents: batchGroup[batchIndex],
186
+ error: result.reason?.message || "Unknown batch error"
187
+ });
188
+ }
189
+ });
190
+ console.log(`Progress: ${Math.min(i + maxConcurrentBatches, batches.length)}/${batches.length} batch groups processed`);
191
+ }
192
+ console.log(`Indexing complete: ${totalSucceeded} succeeded, ${totalFailed} failed`);
193
+ if (failedBatches.length > 0) {
194
+ console.log(`Retrying ${failedBatches.length} failed batches with smaller batch size...`);
195
+ const retryResults = await this.retryFailedBatches(collection, failedBatches);
196
+ totalSucceeded += retryResults.succeeded;
197
+ totalFailed = totalFailed - retryResults.retryAttempted + retryResults.failed;
198
+ console.log(`Final result: ${totalSucceeded} succeeded, ${totalFailed} failed`);
199
+ }
200
+ if (totalFailed > 0) {
201
+ console.log(`\u26A0\uFE0F ${totalFailed} documents failed to index. Consider running sync again or checking server capacity.`);
202
+ }
203
+ }
204
+ /**
205
+ * Process a single batch with retry logic and backpressure handling
206
+ * @private
207
+ */
208
+ async processBatchWithRetry(collection, documents, batchIndex, totalBatches) {
209
+ const maxRetries = 3;
210
+ let lastError = "";
211
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
212
+ try {
213
+ console.log(`Processing batch ${batchIndex + 1}/${totalBatches} (${documents.length} docs) - attempt ${attempt}`);
214
+ const result = await collection.documents().import(documents, {
215
+ action: "upsert",
216
+ batch_size: documents.length,
217
+ return_doc: false,
218
+ return_id: false
219
+ });
220
+ const succeeded = result.filter((r) => r.success === true).length;
221
+ const failed = documents.length - succeeded;
222
+ if (failed > 0) {
223
+ console.log(`Batch ${batchIndex + 1}: ${succeeded} succeeded, ${failed} failed`);
224
+ }
225
+ return { succeeded, failed };
226
+ } catch (error) {
227
+ lastError = error.message || error;
228
+ if (error.httpStatus === 503 || lastError.includes("503") || lastError.includes("Not Ready")) {
229
+ const backoffDelay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
230
+ console.log(`Batch ${batchIndex + 1}: Server overload (503), retrying in ${backoffDelay}ms...`);
231
+ await this.sleep(backoffDelay);
232
+ continue;
233
+ }
234
+ if (lastError.includes("timeout") || lastError.includes("ECONNABORTED")) {
235
+ const backoffDelay = Math.min(2e3 * attempt, 8e3);
236
+ console.log(`Batch ${batchIndex + 1}: Timeout error, retrying in ${backoffDelay}ms...`);
237
+ await this.sleep(backoffDelay);
238
+ continue;
239
+ }
240
+ if (attempt < maxRetries) {
241
+ const backoffDelay = 1e3 * attempt;
242
+ console.log(`Batch ${batchIndex + 1}: Error (${lastError}), retrying in ${backoffDelay}ms...`);
243
+ await this.sleep(backoffDelay);
244
+ continue;
245
+ }
154
246
  }
155
- } catch (error) {
156
- console.error("Indexing error:", error);
157
- throw error;
158
247
  }
248
+ console.error(`Batch ${batchIndex + 1} failed after ${maxRetries} attempts: ${lastError}`);
249
+ return { succeeded: 0, failed: documents.length, error: lastError };
250
+ }
251
+ /**
252
+ * Retry failed batches with smaller batch sizes
253
+ * @private
254
+ */
255
+ async retryFailedBatches(collection, failedBatches) {
256
+ let succeeded = 0;
257
+ let failed = 0;
258
+ let retryAttempted = 0;
259
+ for (const failedBatch of failedBatches) {
260
+ retryAttempted += failedBatch.documents.length;
261
+ const smallBatches = [];
262
+ for (let i = 0; i < failedBatch.documents.length; i += 50) {
263
+ smallBatches.push(failedBatch.documents.slice(i, i + 50));
264
+ }
265
+ for (const smallBatch of smallBatches) {
266
+ try {
267
+ const result = await collection.documents().import(smallBatch, {
268
+ action: "upsert",
269
+ batch_size: smallBatch.length,
270
+ return_doc: false,
271
+ return_id: false
272
+ });
273
+ const batchSucceeded = result.filter((r) => r.success === true).length;
274
+ succeeded += batchSucceeded;
275
+ failed += smallBatch.length - batchSucceeded;
276
+ } catch (error) {
277
+ console.error(`Small batch retry failed: ${error.message || error}`);
278
+ failed += smallBatch.length;
279
+ }
280
+ await this.sleep(500);
281
+ }
282
+ }
283
+ return { succeeded, failed, retryAttempted };
284
+ }
285
+ /**
286
+ * Sleep utility for backoff delays
287
+ * @private
288
+ */
289
+ sleep(ms) {
290
+ return new Promise((resolve) => setTimeout(resolve, ms));
159
291
  }
160
292
  /**
161
293
  * Index a single post in Typesense
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magicpages/ghost-typesense-core",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "Core functionality for Ghost-Typesense integration",
5
5
  "author": "MagicPages",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  "typecheck": "tsc --noEmit"
25
25
  },
26
26
  "dependencies": {
27
- "@magicpages/ghost-typesense-config": "^1.9.1",
27
+ "@magicpages/ghost-typesense-config": "^1.9.2",
28
28
  "@ts-ghost/content-api": "^4.0.6",
29
29
  "@ts-ghost/core-api": "^4.0.6",
30
30
  "typesense": "^1.7.2",