@porulle/plugin-channel-connector 0.78.0 → 0.79.1
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/service.d.ts +13 -5
- package/dist/service.js +33 -15
- package/package.json +2 -2
- package/src/service.ts +33 -14
package/dist/service.d.ts
CHANGED
|
@@ -355,6 +355,10 @@ export declare const channelOrderAddressSchema: z.ZodObject<{
|
|
|
355
355
|
* A hero is streamed inside the page's own invocation, so it is bounded: a 30 MB TIFF a merchant
|
|
356
356
|
* uploaded by mistake must not buffer into a 128 MiB isolate. Anything larger is reported, not
|
|
357
357
|
* stored, and the product still lands — the index reads text first and media later.
|
|
358
|
+
*
|
|
359
|
+
* 4 MiB, not 1: at 1 MiB the cap refused ordinary product photographs — 16 of 100 Kelly Felder
|
|
360
|
+
* heroes and 11 of 100 Arienti, all between 1 and 2 MiB, measured 2026-10-06 — and a product with
|
|
361
|
+
* no photo is left out of every agent feed. Three in flight at 4 MiB stays far under the isolate.
|
|
358
362
|
*/
|
|
359
363
|
export declare const HERO_IMAGE_BYTE_CAP: number;
|
|
360
364
|
export type CatalogMediaFailureReason = "too-large" | "download-failed" | "unsupported" | "storage";
|
|
@@ -391,12 +395,16 @@ export interface ImportImageSelection {
|
|
|
391
395
|
hero: ChannelCatalogImage | null;
|
|
392
396
|
/** In variant order; one image per variant the hero does not cover; no url twice. */
|
|
393
397
|
perVariant: ChannelCatalogImage[];
|
|
398
|
+
/** The store's further photos in its order, as entity-level `gallery` images; no url twice. */
|
|
399
|
+
gallery: ChannelCatalogImage[];
|
|
394
400
|
}
|
|
395
401
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
402
|
+
* The hero, then the first photo of each other variant (a "blue long dress" query must be able to
|
|
403
|
+
* show the blue variant), then the store's further photos as the gallery, up to
|
|
404
|
+
* `IMPORT_IMAGE_LIMIT` in all. Agent feeds publish the gallery as additional images and enrichment
|
|
405
|
+
* reads it; only the hero is embedded, so a gallery photo costs an upload and no model call.
|
|
406
|
+
* Variants are read off the images' own variant references, so a connector that lists images
|
|
407
|
+
* against variants it does not enumerate still gets one each.
|
|
400
408
|
*/
|
|
401
409
|
export declare function selectImportImages(item: ChannelCatalogItem): ImportImageSelection;
|
|
402
410
|
export declare class ChannelConnectorService {
|
|
@@ -598,7 +606,7 @@ export declare class ChannelConnectorService {
|
|
|
598
606
|
*
|
|
599
607
|
* Media: only each new item's hero is fetched here, streamed under `HERO_IMAGE_BYTE_CAP`, and
|
|
600
608
|
* linked at entity level as `primary` plus to the variants it shows. The first photo of every
|
|
601
|
-
* other variant
|
|
609
|
+
* other variant, then the bounded gallery, come back in `deferredMedia` for the host to land later.
|
|
602
610
|
*/
|
|
603
611
|
convergeCatalogPage(orgId: string, storeId: string, rawItems: ChannelCatalogItem[], actor: Actor): Promise<PluginResult<CatalogPageConvergence>>;
|
|
604
612
|
private importHeroes;
|
package/dist/service.js
CHANGED
|
@@ -507,22 +507,30 @@ function redactStore(store) {
|
|
|
507
507
|
* A hero is streamed inside the page's own invocation, so it is bounded: a 30 MB TIFF a merchant
|
|
508
508
|
* uploaded by mistake must not buffer into a 128 MiB isolate. Anything larger is reported, not
|
|
509
509
|
* stored, and the product still lands — the index reads text first and media later.
|
|
510
|
+
*
|
|
511
|
+
* 4 MiB, not 1: at 1 MiB the cap refused ordinary product photographs — 16 of 100 Kelly Felder
|
|
512
|
+
* heroes and 11 of 100 Arienti, all between 1 and 2 MiB, measured 2026-10-06 — and a product with
|
|
513
|
+
* no photo is left out of every agent feed. Three in flight at 4 MiB stays far under the isolate.
|
|
510
514
|
*/
|
|
511
|
-
export const HERO_IMAGE_BYTE_CAP = 1024 * 1024;
|
|
515
|
+
export const HERO_IMAGE_BYTE_CAP = 4 * 1024 * 1024;
|
|
516
|
+
/** The hero, the variant photos and the gallery together never exceed this — what a projection reads. */
|
|
517
|
+
const IMPORT_IMAGE_LIMIT = 6;
|
|
512
518
|
function imageOrder(a, b) {
|
|
513
519
|
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
|
514
520
|
}
|
|
515
521
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
522
|
+
* The hero, then the first photo of each other variant (a "blue long dress" query must be able to
|
|
523
|
+
* show the blue variant), then the store's further photos as the gallery, up to
|
|
524
|
+
* `IMPORT_IMAGE_LIMIT` in all. Agent feeds publish the gallery as additional images and enrichment
|
|
525
|
+
* reads it; only the hero is embedded, so a gallery photo costs an upload and no model call.
|
|
526
|
+
* Variants are read off the images' own variant references, so a connector that lists images
|
|
527
|
+
* against variants it does not enumerate still gets one each.
|
|
520
528
|
*/
|
|
521
529
|
export function selectImportImages(item) {
|
|
522
530
|
const images = [...(item.images ?? [])].sort(imageOrder);
|
|
523
531
|
const hero = images.find((image) => image.role === "primary") ?? images[0] ?? null;
|
|
524
532
|
if (!hero)
|
|
525
|
-
return { hero: null, perVariant: [] };
|
|
533
|
+
return { hero: null, perVariant: [], gallery: [] };
|
|
526
534
|
const covered = new Set(hero.variantExternalIds ?? []);
|
|
527
535
|
const usedUrls = new Set([hero.url]);
|
|
528
536
|
const perVariant = [];
|
|
@@ -539,7 +547,17 @@ export function selectImportImages(item) {
|
|
|
539
547
|
usedUrls.add(image.url);
|
|
540
548
|
perVariant.push(image);
|
|
541
549
|
}
|
|
542
|
-
|
|
550
|
+
const gallery = [];
|
|
551
|
+
for (const image of images) {
|
|
552
|
+
if (1 + perVariant.length + gallery.length >= IMPORT_IMAGE_LIMIT)
|
|
553
|
+
break;
|
|
554
|
+
if (usedUrls.has(image.url))
|
|
555
|
+
continue;
|
|
556
|
+
usedUrls.add(image.url);
|
|
557
|
+
// A further photo of a variant already shown is a product photo, not that variant's image.
|
|
558
|
+
gallery.push({ ...image, role: "gallery", variantExternalIds: [] });
|
|
559
|
+
}
|
|
560
|
+
return { hero, perVariant, gallery };
|
|
543
561
|
}
|
|
544
562
|
/** Streams a response and refuses mid-stream past `cap`; a lying `content-length` cannot get around it. */
|
|
545
563
|
async function fetchBounded(url, cap) {
|
|
@@ -1555,12 +1573,11 @@ export class ChannelConnectorService {
|
|
|
1555
1573
|
async applyMedia(orgId, entityId, item, variantIds, actor, warnings, owners) {
|
|
1556
1574
|
// Uploads what is missing and PLANS the entity's media links; `commitEntityLinks` writes them.
|
|
1557
1575
|
//
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
// gallery the fast path had deliberately left out: an upload, an embed and a bump per photo.
|
|
1576
|
+
// The SAME `selectImportImages` the page fast path uses — hero, variant photos, bounded gallery.
|
|
1577
|
+
// This path once attached EVERY image the item listed, so a product's first price change
|
|
1578
|
+
// pulled in photos the fast path had left out: an upload and an entity bump per photo.
|
|
1562
1579
|
const selection = selectImportImages(item);
|
|
1563
|
-
const images = selection.hero === null ? [] : [selection.hero, ...selection.perVariant];
|
|
1580
|
+
const images = selection.hero === null ? [] : [selection.hero, ...selection.perVariant, ...selection.gallery];
|
|
1564
1581
|
const externalIds = [...new Set(images.map((image) => image.externalId).filter((id) => id != null))];
|
|
1565
1582
|
const urlHashes = [...new Set(images.map((image) => hash(image.url)))];
|
|
1566
1583
|
const keyPredicates = [];
|
|
@@ -2550,7 +2567,7 @@ export class ChannelConnectorService {
|
|
|
2550
2567
|
*
|
|
2551
2568
|
* Media: only each new item's hero is fetched here, streamed under `HERO_IMAGE_BYTE_CAP`, and
|
|
2552
2569
|
* linked at entity level as `primary` plus to the variants it shows. The first photo of every
|
|
2553
|
-
* other variant
|
|
2570
|
+
* other variant, then the bounded gallery, come back in `deferredMedia` for the host to land later.
|
|
2554
2571
|
*/
|
|
2555
2572
|
async convergeCatalogPage(orgId, storeId, rawItems, actor) {
|
|
2556
2573
|
// A host may hand items it never read through this service's intake; the rule is idempotent.
|
|
@@ -2702,8 +2719,9 @@ export class ChannelConnectorService {
|
|
|
2702
2719
|
const deferredMedia = [];
|
|
2703
2720
|
const selections = createdItems.flatMap(({ item, entityId, variantIds }) => {
|
|
2704
2721
|
const selection = selectImportImages(item);
|
|
2705
|
-
|
|
2706
|
-
|
|
2722
|
+
const deferred = [...selection.perVariant, ...selection.gallery];
|
|
2723
|
+
if (deferred.length > 0)
|
|
2724
|
+
deferredMedia.push({ externalId: item.externalId, entityId, images: deferred });
|
|
2707
2725
|
return selection.hero ? [{ item, entityId, variantIds, hero: selection.hero }] : [];
|
|
2708
2726
|
});
|
|
2709
2727
|
if (selections.length === 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/plugin-channel-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@hono/zod-openapi": "^1.2.2",
|
|
24
24
|
"hono": "^4.12.5",
|
|
25
|
-
"@porulle/core": "0.
|
|
25
|
+
"@porulle/core": "0.79.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^24.5.2",
|
package/src/service.ts
CHANGED
|
@@ -1135,8 +1135,12 @@ function redactStore(store: ConnectedStore): PublicConnectedStore {
|
|
|
1135
1135
|
* A hero is streamed inside the page's own invocation, so it is bounded: a 30 MB TIFF a merchant
|
|
1136
1136
|
* uploaded by mistake must not buffer into a 128 MiB isolate. Anything larger is reported, not
|
|
1137
1137
|
* stored, and the product still lands — the index reads text first and media later.
|
|
1138
|
+
*
|
|
1139
|
+
* 4 MiB, not 1: at 1 MiB the cap refused ordinary product photographs — 16 of 100 Kelly Felder
|
|
1140
|
+
* heroes and 11 of 100 Arienti, all between 1 and 2 MiB, measured 2026-10-06 — and a product with
|
|
1141
|
+
* no photo is left out of every agent feed. Three in flight at 4 MiB stays far under the isolate.
|
|
1138
1142
|
*/
|
|
1139
|
-
export const HERO_IMAGE_BYTE_CAP = 1024 * 1024;
|
|
1143
|
+
export const HERO_IMAGE_BYTE_CAP = 4 * 1024 * 1024;
|
|
1140
1144
|
|
|
1141
1145
|
export type CatalogMediaFailureReason = "too-large" | "download-failed" | "unsupported" | "storage";
|
|
1142
1146
|
|
|
@@ -1176,22 +1180,29 @@ export interface ImportImageSelection {
|
|
|
1176
1180
|
hero: ChannelCatalogImage | null;
|
|
1177
1181
|
/** In variant order; one image per variant the hero does not cover; no url twice. */
|
|
1178
1182
|
perVariant: ChannelCatalogImage[];
|
|
1183
|
+
/** The store's further photos in its order, as entity-level `gallery` images; no url twice. */
|
|
1184
|
+
gallery: ChannelCatalogImage[];
|
|
1179
1185
|
}
|
|
1180
1186
|
|
|
1187
|
+
/** The hero, the variant photos and the gallery together never exceed this — what a projection reads. */
|
|
1188
|
+
const IMPORT_IMAGE_LIMIT = 6;
|
|
1189
|
+
|
|
1181
1190
|
function imageOrder(a: ChannelCatalogImage, b: ChannelCatalogImage): number {
|
|
1182
1191
|
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
|
|
1183
1192
|
}
|
|
1184
1193
|
|
|
1185
1194
|
/**
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1195
|
+
* The hero, then the first photo of each other variant (a "blue long dress" query must be able to
|
|
1196
|
+
* show the blue variant), then the store's further photos as the gallery, up to
|
|
1197
|
+
* `IMPORT_IMAGE_LIMIT` in all. Agent feeds publish the gallery as additional images and enrichment
|
|
1198
|
+
* reads it; only the hero is embedded, so a gallery photo costs an upload and no model call.
|
|
1199
|
+
* Variants are read off the images' own variant references, so a connector that lists images
|
|
1200
|
+
* against variants it does not enumerate still gets one each.
|
|
1190
1201
|
*/
|
|
1191
1202
|
export function selectImportImages(item: ChannelCatalogItem): ImportImageSelection {
|
|
1192
1203
|
const images = [...(item.images ?? [])].sort(imageOrder);
|
|
1193
1204
|
const hero = images.find((image) => image.role === "primary") ?? images[0] ?? null;
|
|
1194
|
-
if (!hero) return { hero: null, perVariant: [] };
|
|
1205
|
+
if (!hero) return { hero: null, perVariant: [], gallery: [] };
|
|
1195
1206
|
const covered = new Set(hero.variantExternalIds ?? []);
|
|
1196
1207
|
const usedUrls = new Set([hero.url]);
|
|
1197
1208
|
const perVariant: ChannelCatalogImage[] = [];
|
|
@@ -1205,7 +1216,15 @@ export function selectImportImages(item: ChannelCatalogItem): ImportImageSelecti
|
|
|
1205
1216
|
usedUrls.add(image.url);
|
|
1206
1217
|
perVariant.push(image);
|
|
1207
1218
|
}
|
|
1208
|
-
|
|
1219
|
+
const gallery: ChannelCatalogImage[] = [];
|
|
1220
|
+
for (const image of images) {
|
|
1221
|
+
if (1 + perVariant.length + gallery.length >= IMPORT_IMAGE_LIMIT) break;
|
|
1222
|
+
if (usedUrls.has(image.url)) continue;
|
|
1223
|
+
usedUrls.add(image.url);
|
|
1224
|
+
// A further photo of a variant already shown is a product photo, not that variant's image.
|
|
1225
|
+
gallery.push({ ...image, role: "gallery", variantExternalIds: [] });
|
|
1226
|
+
}
|
|
1227
|
+
return { hero, perVariant, gallery };
|
|
1209
1228
|
}
|
|
1210
1229
|
|
|
1211
1230
|
type BoundedFetch =
|
|
@@ -2400,12 +2419,11 @@ export class ChannelConnectorService {
|
|
|
2400
2419
|
}>> {
|
|
2401
2420
|
// Uploads what is missing and PLANS the entity's media links; `commitEntityLinks` writes them.
|
|
2402
2421
|
//
|
|
2403
|
-
//
|
|
2404
|
-
//
|
|
2405
|
-
//
|
|
2406
|
-
// gallery the fast path had deliberately left out: an upload, an embed and a bump per photo.
|
|
2422
|
+
// The SAME `selectImportImages` the page fast path uses — hero, variant photos, bounded gallery.
|
|
2423
|
+
// This path once attached EVERY image the item listed, so a product's first price change
|
|
2424
|
+
// pulled in photos the fast path had left out: an upload and an entity bump per photo.
|
|
2407
2425
|
const selection = selectImportImages(item);
|
|
2408
|
-
const images = selection.hero === null ? [] : [selection.hero, ...selection.perVariant];
|
|
2426
|
+
const images = selection.hero === null ? [] : [selection.hero, ...selection.perVariant, ...selection.gallery];
|
|
2409
2427
|
const externalIds = [...new Set(images.map((image) => image.externalId).filter((id): id is string => id != null))];
|
|
2410
2428
|
const urlHashes = [...new Set(images.map((image) => hash(image.url)))];
|
|
2411
2429
|
const keyPredicates = [];
|
|
@@ -3501,7 +3519,7 @@ export class ChannelConnectorService {
|
|
|
3501
3519
|
*
|
|
3502
3520
|
* Media: only each new item's hero is fetched here, streamed under `HERO_IMAGE_BYTE_CAP`, and
|
|
3503
3521
|
* linked at entity level as `primary` plus to the variants it shows. The first photo of every
|
|
3504
|
-
* other variant
|
|
3522
|
+
* other variant, then the bounded gallery, come back in `deferredMedia` for the host to land later.
|
|
3505
3523
|
*/
|
|
3506
3524
|
async convergeCatalogPage(
|
|
3507
3525
|
orgId: string,
|
|
@@ -3669,7 +3687,8 @@ export class ChannelConnectorService {
|
|
|
3669
3687
|
const deferredMedia: CatalogDeferredMedia[] = [];
|
|
3670
3688
|
const selections = createdItems.flatMap(({ item, entityId, variantIds }) => {
|
|
3671
3689
|
const selection = selectImportImages(item);
|
|
3672
|
-
|
|
3690
|
+
const deferred = [...selection.perVariant, ...selection.gallery];
|
|
3691
|
+
if (deferred.length > 0) deferredMedia.push({ externalId: item.externalId, entityId, images: deferred });
|
|
3673
3692
|
return selection.hero ? [{ item, entityId, variantIds, hero: selection.hero }] : [];
|
|
3674
3693
|
});
|
|
3675
3694
|
if (selections.length === 0) return { heroesImported: 0, mediaFailures, deferredMedia };
|