@granular-software/sdk 0.4.19 → 0.4.20

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.mjs CHANGED
@@ -11229,6 +11229,28 @@ var STANDARD_MODULES_OPERATIONS = [
11229
11229
  var BUILTIN_MODULES = {
11230
11230
  "standard_modules": STANDARD_MODULES_OPERATIONS
11231
11231
  };
11232
+ var DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE = 100;
11233
+ var MAX_RECORD_OBJECTS_CONCURRENCY = 16;
11234
+ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT = 3;
11235
+ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS = 1e3;
11236
+ function planRecordObjectsChunks(records, batchSize) {
11237
+ const total = records.length;
11238
+ const size = Math.max(1, Math.min(batchSize, total));
11239
+ const chunkCount = Math.ceil(total / size);
11240
+ const plans = [];
11241
+ for (let offset = 0, chunkIndex = 0; offset < total; offset += size, chunkIndex += 1) {
11242
+ const slice = records.slice(offset, offset + size);
11243
+ plans.push({ chunkIndex, chunkCount, offset, slice });
11244
+ }
11245
+ return plans;
11246
+ }
11247
+ function sleep(ms) {
11248
+ return new Promise((resolve) => setTimeout(resolve, ms));
11249
+ }
11250
+ function isRetryableRecordObjectsError(error) {
11251
+ const message = error instanceof Error ? error.message : String(error);
11252
+ return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(message);
11253
+ }
11232
11254
  function computeEffectKey2(effect) {
11233
11255
  const attachedClass = effect.className?.trim();
11234
11256
  if (!attachedClass) {
@@ -12186,24 +12208,101 @@ var Environment = class extends Session {
12186
12208
  /**
12187
12209
  * Batch version of `recordObject()`.
12188
12210
  *
12189
- * Sends several upserts through the control-plane batch endpoint so the
12190
- * server can collapse the graph mutations into far fewer round trips.
12211
+ * Sends rows through the control-plane **`/records/batch`** endpoint in **chunks** (default
12212
+ * **100** records per HTTP request) so individual requests stay bounded and gateway timeouts are
12213
+ * unlikely. Each chunk is retried on transient network / worker errors.
12214
+ *
12215
+ * Use the optional second argument to:
12216
+ * - **`batchSize`** — rows per POST (smaller = more progress events; larger = fewer round trips).
12217
+ * - **`concurrency`** — run up to N chunk POSTs in parallel (capped at 16) when you want lower wall time.
12218
+ * - **`onChunkComplete`** — hook for UIs after each chunk succeeds (row order in the returned array
12219
+ * always matches `records`; chunk **completion** order may differ when `concurrency > 1`).
12220
+ *
12221
+ * For **asynchronous** ingestion with worker-side batching and aggregate counters (`queued`,
12222
+ * `completed`, …), use **`enqueueRecordImport`** and poll **`getRecordImport`** /
12223
+ * **`getRecordImportSummary`** — best for very large fire-and-forget loads when immediate
12224
+ * synchronous commit of every row is not required.
12191
12225
  */
12192
- async recordObjects(records) {
12226
+ async recordObjects(records, options) {
12193
12227
  if (!Array.isArray(records) || records.length === 0) {
12194
12228
  return [];
12195
12229
  }
12196
- const response = await this.controlPlaneRequest(
12197
- `/control/environments/${this.environmentId}/records/batch`,
12198
- {
12199
- method: "POST",
12200
- body: JSON.stringify({ records })
12201
- }
12230
+ const batchSize = Math.max(
12231
+ 1,
12232
+ Math.min(
12233
+ records.length,
12234
+ options?.batchSize ?? DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE
12235
+ )
12202
12236
  );
12203
- return Array.isArray(response.items) ? response.items : [];
12237
+ const concurrency = Math.min(
12238
+ MAX_RECORD_OBJECTS_CONCURRENCY,
12239
+ Math.max(1, options?.concurrency ?? 1)
12240
+ );
12241
+ const plans = planRecordObjectsChunks(records, batchSize);
12242
+ const total = records.length;
12243
+ const results = new Array(total);
12244
+ const onChunk = options?.onChunkComplete;
12245
+ for (let waveStart = 0; waveStart < plans.length; waveStart += concurrency) {
12246
+ const wave = plans.slice(waveStart, waveStart + concurrency);
12247
+ await Promise.all(
12248
+ wave.map(async (plan) => {
12249
+ const { items, durationMs } = await this.executeRecordObjectsChunk(plan.slice);
12250
+ if (items.length !== plan.slice.length) {
12251
+ throw new Error(
12252
+ `recordObjects: chunk ${plan.chunkIndex + 1} returned ${items.length} results, expected ${plan.slice.length}`
12253
+ );
12254
+ }
12255
+ for (let index = 0; index < items.length; index += 1) {
12256
+ results[plan.offset + index] = items[index];
12257
+ }
12258
+ if (onChunk) {
12259
+ const info = {
12260
+ chunkIndex: plan.chunkIndex,
12261
+ totalChunks: plan.chunkCount,
12262
+ offset: plan.offset,
12263
+ recordCount: plan.slice.length,
12264
+ durationMs,
12265
+ results: items
12266
+ };
12267
+ await onChunk(info);
12268
+ }
12269
+ })
12270
+ );
12271
+ }
12272
+ return results;
12273
+ }
12274
+ async executeRecordObjectsChunk(chunk) {
12275
+ const wallStart = Date.now();
12276
+ let lastError;
12277
+ for (let attempt = 1; attempt <= DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT; attempt += 1) {
12278
+ try {
12279
+ const response = await this.controlPlaneRequest(
12280
+ `/control/environments/${this.environmentId}/records/batch`,
12281
+ {
12282
+ method: "POST",
12283
+ body: JSON.stringify({ records: chunk })
12284
+ }
12285
+ );
12286
+ const items = Array.isArray(response.items) ? response.items : [];
12287
+ return { items, durationMs: Date.now() - wallStart };
12288
+ } catch (error) {
12289
+ lastError = error;
12290
+ if (!isRetryableRecordObjectsError(error) || attempt === DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT) {
12291
+ throw error;
12292
+ }
12293
+ await sleep(DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS * attempt);
12294
+ }
12295
+ }
12296
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
12204
12297
  }
12205
12298
  /**
12206
- * Queue a background record import for this environment.
12299
+ * Queue a background record import for this environment (async worker pipeline).
12300
+ *
12301
+ * **vs `recordObjects`:** this path accepts the full payload in one request, returns an
12302
+ * **`importId`**, and processes rows in the background — use **`getRecordImport`** /
12303
+ * **`getRecordImportSummary`** for progress. Choose it for large bulk loads where you do not
12304
+ * need every row committed before the HTTP call returns. Use **`recordObjects`** when you need
12305
+ * synchronous upserts and fine-grained chunk progress via **`onChunkComplete`**.
12207
12306
  */
12208
12307
  async enqueueRecordImport(records, options = {}) {
12209
12308
  return this.controlPlaneRequest(