@porulle/plugin-channel-connector 0.47.0 → 0.48.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/service.d.ts +12 -0
- package/dist/service.js +21 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +2 -2
- package/src/index.ts +14 -0
- package/src/service.ts +54 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/plugin-channel-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@hono/zod-openapi": "^1.2.2",
|
|
21
21
|
"hono": "^4.12.5",
|
|
22
|
-
"@porulle/core": "0.
|
|
22
|
+
"@porulle/core": "0.48.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^24.5.2",
|
package/src/index.ts
CHANGED
|
@@ -104,11 +104,25 @@ export const CHANNEL_MAX_BATCHES_PER_SWEEP = 5_000;
|
|
|
104
104
|
|
|
105
105
|
/** What one bounded batch reports back: whether the store is drained, how much work it did, and
|
|
106
106
|
* where it stopped. Everything here crosses a durable-step boundary, so it must stay JSON. */
|
|
107
|
+
import type { CatalogConvergenceFailure } from "./service.js";
|
|
108
|
+
|
|
107
109
|
interface BatchOutcome {
|
|
108
110
|
exhausted: boolean;
|
|
109
111
|
counted: number;
|
|
110
112
|
cursor: unknown;
|
|
111
113
|
warnings?: string[];
|
|
114
|
+
/**
|
|
115
|
+
* The entities this batch committed, in input order, failures excluded — so the caller can emit
|
|
116
|
+
* ONE message naming the page instead of one enqueue per product.
|
|
117
|
+
*
|
|
118
|
+
* Optional in the type and unconditional in the value the service returns. `undefined` means no
|
|
119
|
+
* batch produced an outcome (the initial `last` below, or a plugin build that predates this
|
|
120
|
+
* field); `[]` means a batch ran and committed nothing. A caller that collapses those two enqueues
|
|
121
|
+
* nothing and reports success.
|
|
122
|
+
*/
|
|
123
|
+
entityIds?: string[];
|
|
124
|
+
/** The items that did not land, so the caller can record them against the run rather than drop them. */
|
|
125
|
+
failures?: CatalogConvergenceFailure[];
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
/**
|
package/src/service.ts
CHANGED
|
@@ -324,6 +324,26 @@ interface CatalogConvergenceStats {
|
|
|
324
324
|
* REJECT_FAILED_ROWS as against REJECT_EVERYTHING; this is the former.
|
|
325
325
|
*/
|
|
326
326
|
failures: CatalogConvergenceFailure[];
|
|
327
|
+
/**
|
|
328
|
+
* The entities this batch actually COMMITTED, in input order, with failures excluded.
|
|
329
|
+
*
|
|
330
|
+
* It exists so a caller can name the page it just converged — one queue message carrying these
|
|
331
|
+
* ids instead of one enqueue per product. Everything about that use makes the exact membership
|
|
332
|
+
* load-bearing, so the ways it can be wrong are worth stating rather than discovering:
|
|
333
|
+
*
|
|
334
|
+
* - **A failed item must not appear.** Every failure path above `continue`s before the push, so
|
|
335
|
+
* an id is added only after the item's last write. An id here that was never committed makes
|
|
336
|
+
* the consumer pay a model call for a row that does not exist.
|
|
337
|
+
* - **Order is input order.** The page message is rebuilt from this array, so a stable order is
|
|
338
|
+
* what makes the same page produce the same message on a Workflow retry.
|
|
339
|
+
* - **No duplicates.** A connector that returns one `externalId` twice in a page would otherwise
|
|
340
|
+
* put the same entity in the message twice, and the consumer would pay for it twice. The push
|
|
341
|
+
* is de-duplicated on first occurrence.
|
|
342
|
+
* - **It must stay JSON.** This crosses a durable step boundary as part of `BatchOutcome`.
|
|
343
|
+
* - **It is per BATCH, never accumulated across a walk.** `walkBatches` keeps only `last`, so a
|
|
344
|
+
* 3,000-product store never builds a 3,000-element array against the 1 MiB step-result limit.
|
|
345
|
+
*/
|
|
346
|
+
entityIds: string[];
|
|
327
347
|
}
|
|
328
348
|
|
|
329
349
|
type ImportResumePosition = { pageCursor: string | null; offset: number };
|
|
@@ -2420,6 +2440,18 @@ export class ChannelConnectorService {
|
|
|
2420
2440
|
conflicts?: CatalogFieldConflict[];
|
|
2421
2441
|
warnings?: string[];
|
|
2422
2442
|
failures?: CatalogConvergenceFailure[];
|
|
2443
|
+
/**
|
|
2444
|
+
* The entities this batch committed — see `CatalogConvergenceStats.entityIds`.
|
|
2445
|
+
*
|
|
2446
|
+
* Declared on the BOUNDED overload only. This is the one a per-batch step wrapper calls, so it
|
|
2447
|
+
* is the one with a page to name. The unbounded overload walks a whole catalogue and would hand
|
|
2448
|
+
* back thousands of ids across a durable step boundary, which is the opposite of the point.
|
|
2449
|
+
*
|
|
2450
|
+
* Required rather than optional here, unlike its siblings, for the reason the implementation
|
|
2451
|
+
* states: a caller must be able to tell "this batch committed nothing" from "this build does
|
|
2452
|
+
* not report entities", and one of those is `[]` while the other is `undefined`.
|
|
2453
|
+
*/
|
|
2454
|
+
entityIds: string[];
|
|
2423
2455
|
}>>;
|
|
2424
2456
|
async importCatalog(
|
|
2425
2457
|
orgId: string,
|
|
@@ -2447,6 +2479,7 @@ export class ChannelConnectorService {
|
|
|
2447
2479
|
conflicts?: CatalogFieldConflict[];
|
|
2448
2480
|
warnings?: string[];
|
|
2449
2481
|
failures?: CatalogConvergenceFailure[];
|
|
2482
|
+
entityIds?: string[];
|
|
2450
2483
|
}>> {
|
|
2451
2484
|
const store = await this.getStoreRecord(orgId, storeId);
|
|
2452
2485
|
if (!store || store.status !== "connected") {
|
|
@@ -2494,6 +2527,7 @@ export class ChannelConnectorService {
|
|
|
2494
2527
|
const conflicts: CatalogFieldConflict[] = [];
|
|
2495
2528
|
const warnings: string[] = [];
|
|
2496
2529
|
const failures: CatalogConvergenceFailure[] = [];
|
|
2530
|
+
const entityIds: string[] = [];
|
|
2497
2531
|
|
|
2498
2532
|
while (remaining > 0) {
|
|
2499
2533
|
const page = await connector.importCatalog(store as ChannelStore, pageCursor ?? undefined);
|
|
@@ -2523,6 +2557,7 @@ export class ChannelConnectorService {
|
|
|
2523
2557
|
conflicts.push(...result.value.conflicts);
|
|
2524
2558
|
warnings.push(...result.value.warnings);
|
|
2525
2559
|
failures.push(...result.value.failures);
|
|
2560
|
+
entityIds.push(...result.value.entityIds);
|
|
2526
2561
|
offset += result.value.consumed;
|
|
2527
2562
|
|
|
2528
2563
|
if (offset >= pageItems.length) {
|
|
@@ -2558,6 +2593,13 @@ export class ChannelConnectorService {
|
|
|
2558
2593
|
// Always surfaced when non-empty. A silently dropped product is worse than the halt this
|
|
2559
2594
|
// replaced: the caller must be able to see which externalIds did not land.
|
|
2560
2595
|
...(failures.length > 0 ? { failures } : {}),
|
|
2596
|
+
// UNCONDITIONAL, unlike every optional field above it, and the asymmetry is deliberate.
|
|
2597
|
+
// The caller turns this into one queue message naming the page it just converged. If the key
|
|
2598
|
+
// were omitted when empty, a caller reading `outcome.entityIds` could not tell "this batch
|
|
2599
|
+
// committed nothing" from "this plugin version does not report entities" — both read as
|
|
2600
|
+
// `undefined`, and the second one silently produces an import that enqueues nothing. An
|
|
2601
|
+
// empty array says the first; a missing key says the second. They deserve different answers.
|
|
2602
|
+
entityIds,
|
|
2561
2603
|
});
|
|
2562
2604
|
}
|
|
2563
2605
|
|
|
@@ -2746,6 +2788,7 @@ export class ChannelConnectorService {
|
|
|
2746
2788
|
conflicts: [],
|
|
2747
2789
|
warnings: [],
|
|
2748
2790
|
failures: [], // a dry run converges nothing, so it can fail nothing
|
|
2791
|
+
entityIds: [], // ...and commits nothing, so it names nothing
|
|
2749
2792
|
};
|
|
2750
2793
|
const assets = await this.db.select().from(mediaAssets).where(eq(mediaAssets.organizationId, orgId));
|
|
2751
2794
|
for (const item of items) {
|
|
@@ -2894,6 +2937,8 @@ export class ChannelConnectorService {
|
|
|
2894
2937
|
const conflicts: CatalogFieldConflict[] = [];
|
|
2895
2938
|
const warnings: string[] = [];
|
|
2896
2939
|
const failures: CatalogConvergenceFailure[] = [];
|
|
2940
|
+
const entityIds: string[] = [];
|
|
2941
|
+
const committed = new Set<string>();
|
|
2897
2942
|
for (const item of items) {
|
|
2898
2943
|
consumed += 1;
|
|
2899
2944
|
try {
|
|
@@ -3122,6 +3167,14 @@ export class ChannelConnectorService {
|
|
|
3122
3167
|
eq(channelEntityMap.entityId, entityId),
|
|
3123
3168
|
eq(channelEntityMap.kind, "variant"),
|
|
3124
3169
|
));
|
|
3170
|
+
// LAST statement of the try, and that position is the whole guarantee: every failure path
|
|
3171
|
+
// above reaches `continue` before here, so an id is recorded only once the item's writes are
|
|
3172
|
+
// done. De-duplicated because a connector returning one externalId twice in a page would
|
|
3173
|
+
// otherwise have the consumer pay for the same entity twice.
|
|
3174
|
+
if (!committed.has(entityId)) {
|
|
3175
|
+
committed.add(entityId);
|
|
3176
|
+
entityIds.push(entityId);
|
|
3177
|
+
}
|
|
3125
3178
|
} catch (error) {
|
|
3126
3179
|
// Anything the stages throw rather than returning. Same disposition: record it against the
|
|
3127
3180
|
// item and carry on, so an unforeseen throw costs one product and not the remaining page.
|
|
@@ -3141,6 +3194,7 @@ export class ChannelConnectorService {
|
|
|
3141
3194
|
conflicts,
|
|
3142
3195
|
warnings,
|
|
3143
3196
|
failures,
|
|
3197
|
+
entityIds,
|
|
3144
3198
|
});
|
|
3145
3199
|
}
|
|
3146
3200
|
|