@porulle/plugin-channel-connector 0.48.1 → 0.49.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/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { type ChannelConnectorPluginOptions } from "./service.js";
2
2
  export { mockChannelConnector } from "./mock-connector.js";
3
3
  export type { MockChannelConnectorOptions } from "./mock-connector.js";
4
- export { ChannelConnectorService, CATALOG_OUTBOUND_SUPPRESSION_WINDOW_MS, CATALOG_PUSH_BATCH_SIZES, CATALOG_PUSH_MAX_ATTEMPTS, canCatalogPushTransition, canExportTransition, catalogPushConcurrencyKey, catalogPushRetryDelayMs, CHANNEL_INVENTORY_MAX_ITEMS_PER_INVOCATION, isCatalogPushBreakerOpen, } from "./service.js";
4
+ export { ChannelConnectorService, HERO_IMAGE_BYTE_CAP, selectImportImages, CATALOG_OUTBOUND_SUPPRESSION_WINDOW_MS, CATALOG_PUSH_BATCH_SIZES, CATALOG_PUSH_MAX_ATTEMPTS, canCatalogPushTransition, canExportTransition, catalogPushConcurrencyKey, catalogPushRetryDelayMs, CHANNEL_INVENTORY_MAX_ITEMS_PER_INVOCATION, isCatalogPushBreakerOpen, } from "./service.js";
5
5
  export { isValidCatalogMappingFieldPath, matchFieldPath, mergeCatalogFieldMapping, normalizeCatalogFieldMapping, compareCatalogFieldMappingSpecificity, providerCatalogFieldMappingDefaults, selectCatalogFieldMapping, validateCatalogMappingRow, } from "./catalog-field-mapping.js";
6
+ export type { CatalogDeferredMedia, CatalogMediaFailure, CatalogMediaFailureReason, CatalogPageConvergence, ImportImageSelection, } from "./service.js";
6
7
  export type { CatalogFieldMapping, CatalogFieldMappingInput, CatalogFieldMappingRow, CatalogFieldTarget, } from "./catalog-field-mapping.js";
7
8
  /** ~320 Neon HTTP subrequests per product against a 10,000 per-invocation cap → hard ceiling near 31; 20 leaves margin for heavier products. */
8
9
  export declare const CHANNEL_IMPORT_MAX_ITEMS_PER_INVOCATION = 20;
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { ChannelConnectorService, catalogPushConcurrencyKey, CHANNEL_INVENTORY_M
8
8
  import { buildHooks } from "./hooks.js";
9
9
  import { oauthStateEventId, signState, verifyState } from "./oauth-state.js";
10
10
  export { mockChannelConnector } from "./mock-connector.js";
11
- export { ChannelConnectorService, CATALOG_OUTBOUND_SUPPRESSION_WINDOW_MS, CATALOG_PUSH_BATCH_SIZES, CATALOG_PUSH_MAX_ATTEMPTS, canCatalogPushTransition, canExportTransition, catalogPushConcurrencyKey, catalogPushRetryDelayMs, CHANNEL_INVENTORY_MAX_ITEMS_PER_INVOCATION, isCatalogPushBreakerOpen, } from "./service.js";
11
+ export { ChannelConnectorService, HERO_IMAGE_BYTE_CAP, selectImportImages, CATALOG_OUTBOUND_SUPPRESSION_WINDOW_MS, CATALOG_PUSH_BATCH_SIZES, CATALOG_PUSH_MAX_ATTEMPTS, canCatalogPushTransition, canExportTransition, catalogPushConcurrencyKey, catalogPushRetryDelayMs, CHANNEL_INVENTORY_MAX_ITEMS_PER_INVOCATION, isCatalogPushBreakerOpen, } from "./service.js";
12
12
  export { isValidCatalogMappingFieldPath, matchFieldPath, mergeCatalogFieldMapping, normalizeCatalogFieldMapping, compareCatalogFieldMappingSpecificity, providerCatalogFieldMappingDefaults, selectCatalogFieldMapping, validateCatalogMappingRow, } from "./catalog-field-mapping.js";
13
13
  /** ~320 Neon HTTP subrequests per product against a 10,000 per-invocation cap → hard ceiling near 31; 20 leaves margin for heavier products. */
14
14
  export const CHANNEL_IMPORT_MAX_ITEMS_PER_INVOCATION = 20;
package/dist/service.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Actor, ChannelConnector, ChannelOrderSlice, ChannelPushCatalogField, ChannelPushCatalogImage, ChannelPushCatalogItem, ChannelPushCatalogItemOutcome, ChannelPushCatalogResult, PluginDb, PluginResult, PluginTxFn } from "@porulle/core";
1
+ import type { Actor, ChannelCatalogItem, ChannelConnector, ChannelOrderSlice, ChannelPushCatalogField, ChannelPushCatalogImage, ChannelPushCatalogItem, ChannelPushCatalogItemOutcome, ChannelPushCatalogResult, PluginDb, PluginResult, PluginTxFn } from "@porulle/core";
2
+ import type { ChannelCatalogImage } from "@porulle/core";
2
3
  import type { FieldOwner, FieldPath } from "@porulle/core";
3
4
  import type { JobsAdapter } from "@porulle/core";
4
5
  import { type ChannelCatalogPush, type ChannelCatalogConflict, type ChannelOrderExport, type ChannelRefundRequest, type ConnectedStore } from "./schema.js";
@@ -231,6 +232,50 @@ export interface CatalogPushPreviewResult {
231
232
  export declare function canExportTransition(from: ExportState, to: ExportState): boolean;
232
233
  export declare function canCatalogPushTransition(from: CatalogPushState, to: CatalogPushState): boolean;
233
234
  export declare const CATALOG_OUTBOUND_SUPPRESSION_WINDOW_MS: number;
235
+ /**
236
+ * A hero is streamed inside the page's own invocation, so it is bounded: a 30 MB TIFF a merchant
237
+ * uploaded by mistake must not buffer into a 128 MiB isolate. Anything larger is reported, not
238
+ * stored, and the product still lands — the index reads text first and media later.
239
+ */
240
+ export declare const HERO_IMAGE_BYTE_CAP: number;
241
+ export type CatalogMediaFailureReason = "too-large" | "download-failed" | "unsupported" | "storage";
242
+ export interface CatalogMediaFailure {
243
+ externalId: string;
244
+ imageExternalId?: string;
245
+ url: string;
246
+ reason: CatalogMediaFailureReason;
247
+ detail: string;
248
+ }
249
+ /** Media the page did NOT fetch: the first photo of each variant the hero does not show. */
250
+ export interface CatalogDeferredMedia {
251
+ externalId: string;
252
+ entityId: string;
253
+ images: ChannelCatalogImage[];
254
+ }
255
+ export interface CatalogPageConvergence extends Record<string, unknown> {
256
+ created: number;
257
+ unchanged: number;
258
+ updated: number;
259
+ /** Input order, failures excluded, no duplicates — the page message is rebuilt from this. */
260
+ entityIds: string[];
261
+ failures: CatalogConvergenceFailure[];
262
+ heroesImported: number;
263
+ mediaFailures: CatalogMediaFailure[];
264
+ deferredMedia: CatalogDeferredMedia[];
265
+ warnings: string[];
266
+ }
267
+ export interface ImportImageSelection {
268
+ hero: ChannelCatalogImage | null;
269
+ /** In variant order; one image per variant the hero does not cover; no url twice. */
270
+ perVariant: ChannelCatalogImage[];
271
+ }
272
+ /**
273
+ * Ruling 2026-09-22: import the hero plus the FIRST photo of each other variant, nothing more.
274
+ * A "blue long dress" query must be able to show the blue variant, and a fourth photo of the red
275
+ * one adds nothing the index can use. Variants are read off the images' own variant references,
276
+ * so a connector that lists images against variants it does not enumerate still gets one each.
277
+ */
278
+ export declare function selectImportImages(item: ChannelCatalogItem): ImportImageSelection;
234
279
  export declare class ChannelConnectorService {
235
280
  private readonly db;
236
281
  private readonly services;
@@ -256,6 +301,28 @@ export declare class ChannelConnectorService {
256
301
  private setCatalogAttributesIfWritable;
257
302
  private upsertOptionAxes;
258
303
  private upsertVariants;
304
+ /**
305
+ * The organization's categories, brands and tags, read ONCE per converge run instead of once per
306
+ * product.
307
+ *
308
+ * `applyTaxonomy` takes an `entityId` and is called unconditionally for every item, and each of
309
+ * its three lookups was a whole-table read filtered by `organization_id`. Measured on the deployed
310
+ * Worker: 1.0 call per product per class, three classes, every product — an organization's whole
311
+ * category list re-read for each of twenty products in a batch that cannot have changed it.
312
+ *
313
+ * Rows created DURING the run are appended by the callers below, exactly as they were appended to
314
+ * the per-product arrays before, so an item that introduces a category is still seen by the next
315
+ * item. The cache is cleared at the top of `convergeCatalogItems`, so its lifetime is one converge
316
+ * rather than the lifetime of the service.
317
+ *
318
+ * The staleness window widens from one product to one batch: a category created by ANOTHER process
319
+ * mid-batch is not seen here. That was already true within a product — these lists were always a
320
+ * snapshot — and the create paths below go through `this.catalog`, which refuses a duplicate slug
321
+ * rather than writing one. So the failure mode is unchanged in kind and wider in window, which is
322
+ * the trade this comment exists to state rather than hide.
323
+ */
324
+ private taxonomyCache;
325
+ private taxonomyFor;
259
326
  private applyTaxonomy;
260
327
  private applyMedia;
261
328
  private getStoreRecord;
@@ -281,6 +348,27 @@ export declare class ChannelConnectorService {
281
348
  getStore(orgId: string, id: string): Promise<PluginResult<PublicConnectedStore>>;
282
349
  listStores(orgId: string, context?: StoreReadContext): Promise<PluginResult<PublicConnectedStore[]>>;
283
350
  validateLineStock(orgId: string, lines: ChannelStockLine[], timeoutMs?: number): Promise<void>;
351
+ /**
352
+ * One page from the connector, nothing written. The host lands the page durably (R2 + its
353
+ * ledger) and hands it to `convergeCatalogPage` from a queue consumer; the two halves are
354
+ * separate so a consumer retry never re-fetches the merchant's API.
355
+ */
356
+ fetchCatalogPage(orgId: string, storeId: string, cursor: string | null): Promise<PluginResult<{
357
+ items: ChannelCatalogItem[];
358
+ nextCursor: string | null;
359
+ }>>;
360
+ /**
361
+ * Converges a page: items this store has never mapped take the import fast path
362
+ * (`catalog.importProducts`, one transaction, multi-row writes); items already mapped and
363
+ * unchanged cost nothing; items mapped-but-changed, and orphans (an entity of this store with
364
+ * the item's slug but no map row), take the editor path, which owns ownership and conflicts.
365
+ *
366
+ * Media: only each new item's hero is fetched here, streamed under `HERO_IMAGE_BYTE_CAP`, and
367
+ * linked at entity level as `primary` plus to the variants it shows. The first photo of every
368
+ * other variant comes back in `deferredMedia` for the host to land later.
369
+ */
370
+ convergeCatalogPage(orgId: string, storeId: string, items: ChannelCatalogItem[], actor: Actor): Promise<PluginResult<CatalogPageConvergence>>;
371
+ private importHeroes;
284
372
  importCatalog(orgId: string, storeId: string, actor: Actor, options: {
285
373
  maxItems: number;
286
374
  }): Promise<PluginResult<{