@akropolys/sdk 1.0.0 → 1.1.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/chunk-QSRD6T65.mjs +1100 -0
- package/dist/chunk-QSRD6T65.mjs.map +1 -0
- package/dist/chunk-UKF434HE.mjs +1100 -0
- package/dist/chunk-UKF434HE.mjs.map +1 -0
- package/dist/commerce.js +133 -160
- package/dist/commerce.js.map +1 -1
- package/dist/commerce.mjs +1 -1
- package/dist/index.d.mts +17 -22
- package/dist/index.d.ts +17 -22
- package/dist/index.js +133 -160
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/property.js +133 -160
- package/dist/property.js.map +1 -1
- package/dist/property.mjs +1 -1
- package/package.json +6 -6
package/dist/property.js
CHANGED
|
@@ -125,7 +125,7 @@ var AkropolysAPI = class {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
async ingest(product) {
|
|
128
|
-
log("info", "ingesting product", product.name);
|
|
128
|
+
log("info", "ingesting product", product.name || product.id || "");
|
|
129
129
|
return this.post("/ingest", { siteId: this.siteId, product });
|
|
130
130
|
}
|
|
131
131
|
async ingestBatch(products) {
|
|
@@ -409,113 +409,6 @@ function getEnvVar(key) {
|
|
|
409
409
|
}
|
|
410
410
|
return void 0;
|
|
411
411
|
}
|
|
412
|
-
function mapRawProduct(input) {
|
|
413
|
-
const name = input.name || input.title || input.productName || "";
|
|
414
|
-
let price = "";
|
|
415
|
-
let priceNumeric = void 0;
|
|
416
|
-
if (input.price !== void 0) {
|
|
417
|
-
if (typeof input.price === "number") {
|
|
418
|
-
priceNumeric = input.price;
|
|
419
|
-
price = String(input.price);
|
|
420
|
-
} else {
|
|
421
|
-
price = input.price;
|
|
422
|
-
const num = parseFloat(input.price.replace(/[^0-9.]/g, ""));
|
|
423
|
-
priceNumeric = isNaN(num) ? void 0 : num;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (input.priceNumeric !== void 0) {
|
|
427
|
-
priceNumeric = input.priceNumeric;
|
|
428
|
-
}
|
|
429
|
-
let url = input.url || "";
|
|
430
|
-
if (!url && typeof window !== "undefined") {
|
|
431
|
-
url = window.location.href;
|
|
432
|
-
}
|
|
433
|
-
let slug = input.slug || input.id || input.productId || "";
|
|
434
|
-
if (!slug && url) {
|
|
435
|
-
slug = url.split("/").filter(Boolean).pop() || "";
|
|
436
|
-
}
|
|
437
|
-
if (!slug && name) {
|
|
438
|
-
slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
439
|
-
}
|
|
440
|
-
let images = [];
|
|
441
|
-
if (input.images) {
|
|
442
|
-
images = input.images;
|
|
443
|
-
} else if (input.image) {
|
|
444
|
-
images = [input.image];
|
|
445
|
-
} else if (input.listing_agent_photo) {
|
|
446
|
-
images = [input.listing_agent_photo];
|
|
447
|
-
} else if (input.thumbnail) {
|
|
448
|
-
images = [input.thumbnail];
|
|
449
|
-
}
|
|
450
|
-
if (!name) {
|
|
451
|
-
console.warn("[Akropolys] Validation warning: Product name/title is missing. Skipping:", input);
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
if (!price) {
|
|
455
|
-
console.warn("[Akropolys] Validation warning: Product price is missing. Skipping:", input);
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
if (!url) {
|
|
459
|
-
console.warn("[Akropolys] Validation warning: Product URL is missing. Skipping:", input);
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
const coreKeys = /* @__PURE__ */ new Set([
|
|
463
|
-
"name",
|
|
464
|
-
"title",
|
|
465
|
-
"productName",
|
|
466
|
-
"price",
|
|
467
|
-
"priceNumeric",
|
|
468
|
-
"url",
|
|
469
|
-
"image",
|
|
470
|
-
"thumbnail",
|
|
471
|
-
"images",
|
|
472
|
-
"slug",
|
|
473
|
-
"id",
|
|
474
|
-
"productId",
|
|
475
|
-
"brand",
|
|
476
|
-
"description",
|
|
477
|
-
"originalPrice",
|
|
478
|
-
"discount",
|
|
479
|
-
"currency",
|
|
480
|
-
"stock",
|
|
481
|
-
"availability",
|
|
482
|
-
"rating",
|
|
483
|
-
"reviewCount",
|
|
484
|
-
"category",
|
|
485
|
-
"subCategory",
|
|
486
|
-
"tags",
|
|
487
|
-
"specs",
|
|
488
|
-
"metadata"
|
|
489
|
-
]);
|
|
490
|
-
const metadata = { ...input.metadata };
|
|
491
|
-
for (const [key, value] of Object.entries(input)) {
|
|
492
|
-
if (!coreKeys.has(key) && value !== void 0) {
|
|
493
|
-
metadata[key] = value;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
return {
|
|
497
|
-
name,
|
|
498
|
-
price,
|
|
499
|
-
url,
|
|
500
|
-
brand: input.brand,
|
|
501
|
-
description: input.description,
|
|
502
|
-
originalPrice: input.originalPrice,
|
|
503
|
-
discount: input.discount,
|
|
504
|
-
currency: input.currency ?? "KES",
|
|
505
|
-
stock: input.stock,
|
|
506
|
-
availability: input.availability,
|
|
507
|
-
rating: input.rating,
|
|
508
|
-
reviewCount: input.reviewCount,
|
|
509
|
-
category: input.category,
|
|
510
|
-
subCategory: input.subCategory,
|
|
511
|
-
tags: input.tags,
|
|
512
|
-
images: images.length > 0 ? images : void 0,
|
|
513
|
-
specs: input.specs,
|
|
514
|
-
priceNumeric,
|
|
515
|
-
slug,
|
|
516
|
-
metadata: Object.keys(metadata).length > 0 ? metadata : void 0
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
412
|
function generateUUID() {
|
|
520
413
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
521
414
|
return crypto.randomUUID();
|
|
@@ -533,6 +426,8 @@ var _AkropolysClient = class _AkropolysClient {
|
|
|
533
426
|
this.ingestedUrls = /* @__PURE__ */ new Set();
|
|
534
427
|
this.onlineHandler = null;
|
|
535
428
|
this.sessionId = "";
|
|
429
|
+
this.isFlushing = false;
|
|
430
|
+
this.retryCount = 0;
|
|
536
431
|
const siteId = config.siteId || getEnvVar("NEXT_PUBLIC_AKROPOLYS_SITE_ID") || "";
|
|
537
432
|
const apiUrl = config.apiUrl || getEnvVar("NEXT_PUBLIC_AKROPOLYS_API_URL") || "";
|
|
538
433
|
const apiToken = config.apiToken || getEnvVar("NEXT_PUBLIC_AKROPOLYS_API_TOKEN") || "";
|
|
@@ -595,6 +490,10 @@ var _AkropolysClient = class _AkropolysClient {
|
|
|
595
490
|
}
|
|
596
491
|
reRegister() {
|
|
597
492
|
instance = this;
|
|
493
|
+
if (typeof window !== "undefined" && !this.onlineHandler) {
|
|
494
|
+
this.onlineHandler = () => this.flushQueue();
|
|
495
|
+
window.addEventListener("online", this.onlineHandler);
|
|
496
|
+
}
|
|
598
497
|
}
|
|
599
498
|
setShopperId(id) {
|
|
600
499
|
this.shopperId = id;
|
|
@@ -641,74 +540,114 @@ var _AkropolysClient = class _AkropolysClient {
|
|
|
641
540
|
}
|
|
642
541
|
if (instance === this) instance = null;
|
|
643
542
|
}
|
|
644
|
-
async queueIngest(
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
if (
|
|
543
|
+
async queueIngest(rawItem) {
|
|
544
|
+
const id = rawItem.id ?? rawItem.productId ?? rawItem.slug ?? rawItem.url ?? rawItem.name ?? "";
|
|
545
|
+
const url = rawItem.url || (typeof window !== "undefined" ? window.location.href : "");
|
|
546
|
+
if (!id && !url) {
|
|
547
|
+
console.warn("[Akropolys] Ingestion warning: Item is missing both a stable identifier and a URL. Skipping.");
|
|
648
548
|
return;
|
|
649
549
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
550
|
+
if (url) {
|
|
551
|
+
if (this.ingestedUrls.has(url)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this.ingestedUrls.add(url);
|
|
555
|
+
this.saveIngestedCache();
|
|
556
|
+
}
|
|
557
|
+
this.ingestQueue.push(rawItem);
|
|
653
558
|
this.scheduleFlush();
|
|
654
559
|
}
|
|
655
|
-
async queueIngestBatch(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
560
|
+
async queueIngestBatch(rawItems) {
|
|
561
|
+
let hasNew = false;
|
|
562
|
+
rawItems.forEach((rawItem) => {
|
|
563
|
+
const id = rawItem.id ?? rawItem.productId ?? rawItem.slug ?? rawItem.url ?? rawItem.name ?? "";
|
|
564
|
+
const url = rawItem.url || (typeof window !== "undefined" ? window.location.href : "");
|
|
565
|
+
if (!id && !url) {
|
|
566
|
+
console.warn("[Akropolys] Ingestion warning: Item is missing both a stable identifier and a URL. Skipping.");
|
|
660
567
|
return;
|
|
661
568
|
}
|
|
662
|
-
|
|
663
|
-
|
|
569
|
+
if (url) {
|
|
570
|
+
if (this.ingestedUrls.has(url)) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
this.ingestedUrls.add(url);
|
|
574
|
+
hasNew = true;
|
|
575
|
+
}
|
|
576
|
+
this.ingestQueue.push(rawItem);
|
|
664
577
|
});
|
|
665
|
-
if (
|
|
578
|
+
if (hasNew) {
|
|
666
579
|
this.saveIngestedCache();
|
|
580
|
+
}
|
|
581
|
+
if (this.ingestQueue.length > 0) {
|
|
667
582
|
this.scheduleFlush();
|
|
668
583
|
}
|
|
669
584
|
}
|
|
670
585
|
scheduleFlush() {
|
|
671
|
-
if (this.ingestTimer) return;
|
|
586
|
+
if (this.ingestTimer || this.isFlushing) return;
|
|
672
587
|
this.ingestTimer = setTimeout(() => {
|
|
673
588
|
this.flushQueue();
|
|
674
589
|
}, 300);
|
|
675
590
|
}
|
|
676
591
|
async flushQueue() {
|
|
677
|
-
this.
|
|
678
|
-
|
|
592
|
+
if (this.isFlushing) return;
|
|
593
|
+
this.isFlushing = true;
|
|
594
|
+
if (this.ingestTimer) {
|
|
595
|
+
clearTimeout(this.ingestTimer);
|
|
596
|
+
this.ingestTimer = null;
|
|
597
|
+
}
|
|
679
598
|
if (this.authLoading) {
|
|
680
599
|
console.log("[Akropolys] Authentication is loading. Deferring ingestion flush.");
|
|
600
|
+
this.isFlushing = false;
|
|
681
601
|
return;
|
|
682
602
|
}
|
|
683
603
|
if (typeof navigator !== "undefined" && !navigator.onLine) {
|
|
684
604
|
console.warn("[Akropolys] Browser offline. Postponing ingestion.");
|
|
605
|
+
this.isFlushing = false;
|
|
685
606
|
return;
|
|
686
607
|
}
|
|
687
|
-
const
|
|
688
|
-
this.ingestQueue = [];
|
|
608
|
+
const maxBatchSize = 50;
|
|
689
609
|
try {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const akropolysError = {
|
|
693
|
-
status: e.status || 500,
|
|
694
|
-
message: e.message || "Unknown network error"
|
|
695
|
-
};
|
|
696
|
-
if (this.onError) {
|
|
610
|
+
while (this.ingestQueue.length > 0) {
|
|
611
|
+
const batch = this.ingestQueue.slice(0, maxBatchSize);
|
|
697
612
|
try {
|
|
698
|
-
this.
|
|
699
|
-
|
|
700
|
-
|
|
613
|
+
await this.api.ingestBatch(batch);
|
|
614
|
+
this.ingestQueue.splice(0, batch.length);
|
|
615
|
+
this.retryCount = 0;
|
|
616
|
+
} catch (e) {
|
|
617
|
+
const status = e.status || 500;
|
|
618
|
+
const message = e.message || "Unknown network error";
|
|
619
|
+
if (this.onError) {
|
|
620
|
+
try {
|
|
621
|
+
this.onError({ status, message });
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.error("[Akropolys] Error inside onError callback:", err);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (status >= 400 && status < 500 && status !== 429) {
|
|
627
|
+
console.error("[Akropolys] Ingestion discarded due to client error:", message);
|
|
628
|
+
this.ingestQueue.splice(0, batch.length);
|
|
629
|
+
continue;
|
|
630
|
+
} else {
|
|
631
|
+
console.warn("[Akropolys] Ingestion temporarily failed. Retrying later.", message);
|
|
632
|
+
this.scheduleFlushWithBackoff();
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
701
635
|
}
|
|
702
636
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
console.warn("[Akropolys] Ingestion failed. Re-queuing to retry.", e);
|
|
708
|
-
this.ingestQueue = [...batch, ...this.ingestQueue];
|
|
709
|
-
this.scheduleFlush();
|
|
637
|
+
} finally {
|
|
638
|
+
this.isFlushing = false;
|
|
710
639
|
}
|
|
711
640
|
}
|
|
641
|
+
scheduleFlushWithBackoff() {
|
|
642
|
+
if (this.ingestTimer) return;
|
|
643
|
+
const baseDelay = 1e3;
|
|
644
|
+
const jitter = Math.random() * 1e3;
|
|
645
|
+
const delay = Math.min(baseDelay * Math.pow(2, this.retryCount), 3e4) + jitter;
|
|
646
|
+
this.retryCount++;
|
|
647
|
+
this.ingestTimer = setTimeout(() => {
|
|
648
|
+
this.flushQueue();
|
|
649
|
+
}, delay);
|
|
650
|
+
}
|
|
712
651
|
};
|
|
713
652
|
_AkropolysClient.INGEST_CACHE_KEY = "akropolys_ingested_v2";
|
|
714
653
|
_AkropolysClient.INGEST_CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
@@ -852,9 +791,40 @@ function useSearch() {
|
|
|
852
791
|
|
|
853
792
|
// src/hooks/useIngest.ts
|
|
854
793
|
var import_react4 = require("react");
|
|
855
|
-
|
|
794
|
+
|
|
795
|
+
// src/utils/TTLCache.ts
|
|
796
|
+
var TTLCache = class {
|
|
797
|
+
constructor(ttlMs = 24 * 60 * 60 * 1e3) {
|
|
798
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
799
|
+
this.ttl = ttlMs;
|
|
800
|
+
}
|
|
801
|
+
add(key) {
|
|
802
|
+
this.cache.set(key, Date.now());
|
|
803
|
+
this.evictExpired();
|
|
804
|
+
}
|
|
805
|
+
has(key) {
|
|
806
|
+
const timestamp = this.cache.get(key);
|
|
807
|
+
if (timestamp === void 0) return false;
|
|
808
|
+
if (Date.now() - timestamp > this.ttl) {
|
|
809
|
+
this.cache.delete(key);
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
evictExpired() {
|
|
815
|
+
const now = Date.now();
|
|
816
|
+
for (const [key, timestamp] of this.cache.entries()) {
|
|
817
|
+
if (now - timestamp > this.ttl) {
|
|
818
|
+
this.cache.delete(key);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
// src/hooks/useIngest.ts
|
|
825
|
+
var recentlyIngested = new TTLCache(24 * 60 * 60 * 1e3);
|
|
856
826
|
function getProductKey(p) {
|
|
857
|
-
return p.
|
|
827
|
+
return p.id || p.productId || p.slug || p.url || p.name || p.title || p.productName || null;
|
|
858
828
|
}
|
|
859
829
|
function useIngest() {
|
|
860
830
|
const client = useAkropolysContext();
|
|
@@ -884,14 +854,14 @@ function useIngest() {
|
|
|
884
854
|
|
|
885
855
|
// src/hooks/useListIngest.ts
|
|
886
856
|
var import_react5 = require("react");
|
|
887
|
-
function useListIngest(
|
|
857
|
+
function useListIngest(items) {
|
|
888
858
|
const { ingestBatch } = useIngest();
|
|
889
859
|
const processedIdsRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
|
|
890
|
-
const listKey = (
|
|
860
|
+
const listKey = (items || []).map((p) => p.id ?? p.productId ?? p.slug ?? p.url ?? p.name ?? "").join(",");
|
|
891
861
|
(0, import_react5.useEffect)(() => {
|
|
892
|
-
if (!
|
|
893
|
-
const newItems =
|
|
894
|
-
const id = item.id
|
|
862
|
+
if (!items || !items.length) return;
|
|
863
|
+
const newItems = items.filter((item) => {
|
|
864
|
+
const id = item.id ?? item.productId ?? item.slug ?? item.url ?? item.name ?? "";
|
|
895
865
|
if (!id) return true;
|
|
896
866
|
if (processedIdsRef.current.has(id)) {
|
|
897
867
|
return false;
|
|
@@ -908,17 +878,20 @@ function useListIngest(products) {
|
|
|
908
878
|
// src/hooks/usePageIngest.ts
|
|
909
879
|
var import_react6 = require("react");
|
|
910
880
|
function usePageIngest(product) {
|
|
911
|
-
const
|
|
881
|
+
const lastIngestedKey = (0, import_react6.useRef)(null);
|
|
882
|
+
const uniqueId = product ? product.id ?? product.productId ?? product.slug ?? product.url ?? "" : "";
|
|
912
883
|
(0, import_react6.useEffect)(() => {
|
|
913
|
-
if (!product) return;
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
ingestedRef.current = url;
|
|
884
|
+
if (!product || !uniqueId) return;
|
|
885
|
+
if (lastIngestedKey.current === uniqueId) return;
|
|
886
|
+
lastIngestedKey.current = uniqueId;
|
|
917
887
|
try {
|
|
918
|
-
getAkropolysClient().queueIngest(
|
|
919
|
-
} catch {
|
|
888
|
+
getAkropolysClient().queueIngest(product);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production") {
|
|
891
|
+
console.warn("[Akropolys] Ingestion failed inside usePageIngest:", err);
|
|
892
|
+
}
|
|
920
893
|
}
|
|
921
|
-
}, [
|
|
894
|
+
}, [uniqueId]);
|
|
922
895
|
}
|
|
923
896
|
|
|
924
897
|
// src/hooks/useKiku.ts
|