@betarena/ad-engine 0.2.0 → 0.3.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.js +15 -15
- package/package.json +1 -1
- package/src/lib/Advert-Engine-Widget.svelte +295 -40
package/package.json
CHANGED
|
@@ -141,7 +141,36 @@
|
|
|
141
141
|
* @description
|
|
142
142
|
* 📝 `Map` where, `key=CreativeId` and `value=AdvertObject` (temporary)
|
|
143
143
|
*/
|
|
144
|
-
mapCreative = new Map < number, AdsCreativeMain > ()
|
|
144
|
+
mapCreative = new Map < number, AdsCreativeMain > (),
|
|
145
|
+
/**
|
|
146
|
+
* @description
|
|
147
|
+
* 📝 Shared promise for one-time prerequisite initialization (device type + geolocation).
|
|
148
|
+
* Set on first call to `ensureReady()`; subsequent callers await the same promise.
|
|
149
|
+
*/
|
|
150
|
+
readinessPromise: Promise < void > | null = null,
|
|
151
|
+
/**
|
|
152
|
+
* @description
|
|
153
|
+
* 📝 ID of the deferred `initialize()` timeout, used to cancel it if the component
|
|
154
|
+
* is destroyed before the delay elapses.
|
|
155
|
+
*/
|
|
156
|
+
initTimeoutId: ReturnType < typeof setTimeout > | null = null,
|
|
157
|
+
/**
|
|
158
|
+
* @description
|
|
159
|
+
* 📝 Set to `true` once `onDestroy` fires, so any in-flight async work can bail out.
|
|
160
|
+
*/
|
|
161
|
+
isDestroyed = false,
|
|
162
|
+
/**
|
|
163
|
+
* @description
|
|
164
|
+
* 📝 Incremented on each `refreshAds()` call. Passed into `injectBetarenaAds()`
|
|
165
|
+
* so that a superseded refresh can bail out after its network awaits complete.
|
|
166
|
+
*/
|
|
167
|
+
refreshSerial = 0,
|
|
168
|
+
/**
|
|
169
|
+
* @description
|
|
170
|
+
* 📝 Previous value of `window.betarenaAdEngine` saved before this instance assigns it,
|
|
171
|
+
* so it can be restored on destroy instead of unconditionally deleting.
|
|
172
|
+
*/
|
|
173
|
+
prevWindowApi: unknown = undefined
|
|
145
174
|
;
|
|
146
175
|
|
|
147
176
|
const
|
|
@@ -168,7 +197,7 @@
|
|
|
168
197
|
|
|
169
198
|
/**
|
|
170
199
|
* @author
|
|
171
|
-
* @
|
|
200
|
+
* @jonsnowpt
|
|
172
201
|
* @summary
|
|
173
202
|
* 🟥 MAIN
|
|
174
203
|
* @description
|
|
@@ -177,16 +206,44 @@
|
|
|
177
206
|
*/
|
|
178
207
|
function generateElementMap
|
|
179
208
|
(
|
|
209
|
+
rootElement?: ParentNode
|
|
180
210
|
): void
|
|
181
211
|
{
|
|
212
|
+
mapBetarenaAdvertStandardElement.clear();
|
|
213
|
+
|
|
182
214
|
const
|
|
183
215
|
/**
|
|
184
216
|
* @description
|
|
185
217
|
* 📝 `List` of `HTMLElements` identified on `page` expecting a target `zone` advertisement injection.
|
|
186
218
|
*/
|
|
187
|
-
listElementTarget = document.querySelectorAll('[data-betarena-zone-id]')
|
|
219
|
+
listElementTarget = (rootElement ?? document).querySelectorAll('[data-betarena-zone-id]')
|
|
188
220
|
;
|
|
189
221
|
|
|
222
|
+
// ╭─────
|
|
223
|
+
// │ NOTE:
|
|
224
|
+
// │ |: querySelectorAll only returns descendants — if rootElement itself carries
|
|
225
|
+
// │ |: the zone attribute it would be missed. Include it explicitly.
|
|
226
|
+
// ╰─────
|
|
227
|
+
if (rootElement instanceof Element && rootElement.hasAttribute('data-betarena-zone-id'))
|
|
228
|
+
{
|
|
229
|
+
const
|
|
230
|
+
value = rootElement.getAttribute('data-betarena-zone-id')
|
|
231
|
+
;
|
|
232
|
+
|
|
233
|
+
if (value)
|
|
234
|
+
{
|
|
235
|
+
for (const rawToken of value.split(','))
|
|
236
|
+
{
|
|
237
|
+
const
|
|
238
|
+
token = rawToken.trim(),
|
|
239
|
+
id = Number.parseInt(token, 10)
|
|
240
|
+
;
|
|
241
|
+
if (!token || !Number.isFinite(id)) continue;
|
|
242
|
+
mapBetarenaAdvertStandardElement.set(id, rootElement);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
190
247
|
// [🐞]
|
|
191
248
|
logger
|
|
192
249
|
(
|
|
@@ -219,23 +276,14 @@
|
|
|
219
276
|
]
|
|
220
277
|
);
|
|
221
278
|
|
|
222
|
-
|
|
279
|
+
for (const rawToken of value.split(','))
|
|
223
280
|
{
|
|
224
281
|
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
* 📝 Split value by `,`
|
|
228
|
-
*/
|
|
229
|
-
values = value.split(',')
|
|
282
|
+
token = rawToken.trim(),
|
|
283
|
+
id = Number.parseInt(token, 10)
|
|
230
284
|
;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
mapBetarenaAdvertStandardElement.set(parseInt(val), elem);
|
|
234
|
-
;
|
|
235
|
-
}
|
|
236
|
-
else
|
|
237
|
-
{
|
|
238
|
-
mapBetarenaAdvertStandardElement.set(parseInt(value), elem);
|
|
285
|
+
if (!token || !Number.isFinite(id)) continue;
|
|
286
|
+
mapBetarenaAdvertStandardElement.set(id, elem);
|
|
239
287
|
}
|
|
240
288
|
}
|
|
241
289
|
|
|
@@ -254,7 +302,7 @@
|
|
|
254
302
|
|
|
255
303
|
/**
|
|
256
304
|
* @author
|
|
257
|
-
* @
|
|
305
|
+
* @jonsnowpt
|
|
258
306
|
* @summary
|
|
259
307
|
* 🟦 HELPER
|
|
260
308
|
* @description
|
|
@@ -265,7 +313,9 @@
|
|
|
265
313
|
*/
|
|
266
314
|
async function injectBetarenaAds
|
|
267
315
|
(
|
|
268
|
-
opts: IAdsServiceData['request']['body']
|
|
316
|
+
opts: IAdsServiceData['request']['body'],
|
|
317
|
+
includesGlobalZone: boolean,
|
|
318
|
+
refreshToken: number
|
|
269
319
|
): Promise < void >
|
|
270
320
|
{
|
|
271
321
|
if (!document) return;
|
|
@@ -315,6 +365,8 @@
|
|
|
315
365
|
]
|
|
316
366
|
);
|
|
317
367
|
|
|
368
|
+
if (isDestroyed || refreshToken !== refreshSerial) return;
|
|
369
|
+
|
|
318
370
|
storeSession.updateData
|
|
319
371
|
(
|
|
320
372
|
[
|
|
@@ -363,7 +415,17 @@
|
|
|
363
415
|
// ╰─────
|
|
364
416
|
for (const [zoneId, element] of mapBetarenaAdvertStandardElement)
|
|
365
417
|
{
|
|
366
|
-
|
|
418
|
+
const
|
|
419
|
+
el = element as HTMLElement,
|
|
420
|
+
parent = el.parentElement as HTMLElement | null,
|
|
421
|
+
mountedAttr =
|
|
422
|
+
[
|
|
423
|
+
el.dataset.betarenaAdMounted,
|
|
424
|
+
parent?.dataset?.betarenaAdMounted
|
|
425
|
+
].filter(Boolean).join(','),
|
|
426
|
+
mountedZones = mountedAttr.split(',').map(z => z.trim()).filter(Boolean)
|
|
427
|
+
;
|
|
428
|
+
if (mountedZones.includes(String(zoneId))) continue;
|
|
367
429
|
|
|
368
430
|
const
|
|
369
431
|
/**
|
|
@@ -443,6 +505,11 @@
|
|
|
443
505
|
}
|
|
444
506
|
);
|
|
445
507
|
;
|
|
508
|
+
if (creativeAdData.length > 0)
|
|
509
|
+
{
|
|
510
|
+
mountedZones.push(String(zoneId));
|
|
511
|
+
el.dataset.betarenaAdMounted = mountedZones.join(',');
|
|
512
|
+
}
|
|
446
513
|
}
|
|
447
514
|
else if (zoneId == 2 || zoneId == 3)
|
|
448
515
|
{
|
|
@@ -459,9 +526,16 @@
|
|
|
459
526
|
}
|
|
460
527
|
);
|
|
461
528
|
;
|
|
529
|
+
if (creativeAdData.length > 0)
|
|
530
|
+
{
|
|
531
|
+
mountedZones.push(String(zoneId));
|
|
532
|
+
el.dataset.betarenaAdMounted = mountedZones.join(',');
|
|
533
|
+
}
|
|
462
534
|
}
|
|
463
535
|
else if (zoneId == 4)
|
|
464
536
|
{
|
|
537
|
+
if (!element.parentElement) continue;
|
|
538
|
+
|
|
465
539
|
for (const adData of (creativeAdData ?? []))
|
|
466
540
|
new AdvertLeftSide
|
|
467
541
|
(
|
|
@@ -474,6 +548,29 @@
|
|
|
474
548
|
}
|
|
475
549
|
);
|
|
476
550
|
;
|
|
551
|
+
if (creativeAdData.length > 0)
|
|
552
|
+
{
|
|
553
|
+
mountedZones.push(String(zoneId));
|
|
554
|
+
el.dataset.betarenaAdMounted = mountedZones.join(',');
|
|
555
|
+
// ╭─────
|
|
556
|
+
// │ NOTE:
|
|
557
|
+
// │ |: Also mark the actual injection target (parentElement) so that
|
|
558
|
+
// │ |: if the zone element is replaced while the parent persists,
|
|
559
|
+
// │ |: subsequent refreshAds() calls won't re-inject into the same parent.
|
|
560
|
+
// ╰─────
|
|
561
|
+
if (element.parentElement)
|
|
562
|
+
{
|
|
563
|
+
const
|
|
564
|
+
parentMountedRaw = (element.parentElement as HTMLElement).dataset.betarenaAdMounted ?? '',
|
|
565
|
+
parentMountedZones = parentMountedRaw.split(',').map(z => z.trim()).filter(Boolean)
|
|
566
|
+
;
|
|
567
|
+
if (!parentMountedZones.includes(String(zoneId)))
|
|
568
|
+
{
|
|
569
|
+
parentMountedZones.push(String(zoneId));
|
|
570
|
+
(element.parentElement as HTMLElement).dataset.betarenaAdMounted = parentMountedZones.join(',');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
477
574
|
}
|
|
478
575
|
}
|
|
479
576
|
|
|
@@ -482,8 +579,11 @@
|
|
|
482
579
|
// │ |: for case of injection of 'GLOBAL' placements for adverts,
|
|
483
580
|
// │ |: ⦿ (a.k.a SLIDER / POPUP ads)
|
|
484
581
|
// │ |: ⦿ (a.k.a document.body injections)
|
|
582
|
+
// │ |: `includesGlobalZone` is computed from the caller's original scope in refreshAds()
|
|
583
|
+
// │ |: (not from DOM-filtered targetZoneIds) so global placements inject correctly
|
|
584
|
+
// │ |: even when no zone-1 element exists in the DOM.
|
|
485
585
|
// ╰─────
|
|
486
|
-
if (authorId || authorArticleTagIds.length > 0)
|
|
586
|
+
if (includesGlobalZone && (authorId || authorArticleTagIds.length > 0))
|
|
487
587
|
{
|
|
488
588
|
// ╭─────
|
|
489
589
|
// │ NOTE:
|
|
@@ -658,8 +758,9 @@
|
|
|
658
758
|
// │ |: BUT, data for ADS was still successfully fetched
|
|
659
759
|
// │ |: ⦿ (a.k.a Advert Condition was RETRIEVED/HIT).
|
|
660
760
|
// │ |: Inject STANDARD SLIDER adverts in 'document.body'
|
|
761
|
+
// │ |: Guard with `includesGlobalZone` to avoid body injections on scoped refreshes.
|
|
661
762
|
// ╰─────
|
|
662
|
-
else
|
|
763
|
+
else if (includesGlobalZone)
|
|
663
764
|
{
|
|
664
765
|
// [🐞]
|
|
665
766
|
logger
|
|
@@ -699,7 +800,129 @@
|
|
|
699
800
|
|
|
700
801
|
/**
|
|
701
802
|
* @author
|
|
702
|
-
* @
|
|
803
|
+
* @jonsnowpt
|
|
804
|
+
* @summary
|
|
805
|
+
* 🟦 HELPER
|
|
806
|
+
* @description
|
|
807
|
+
* 📝 Ensures prerequisites (`deviceType`, `geoLocation`) are resolved exactly once.
|
|
808
|
+
* Callers that race — whether `initialize()` or an external `refreshAds()` — all
|
|
809
|
+
* await the same in-flight promise.
|
|
810
|
+
* @returns { Promise < void > }
|
|
811
|
+
*/
|
|
812
|
+
async function ensureReady
|
|
813
|
+
(
|
|
814
|
+
): Promise < void >
|
|
815
|
+
{
|
|
816
|
+
if (readinessPromise) return readinessPromise;
|
|
817
|
+
|
|
818
|
+
readinessPromise =
|
|
819
|
+
(
|
|
820
|
+
async () =>
|
|
821
|
+
{
|
|
822
|
+
try
|
|
823
|
+
{
|
|
824
|
+
deviceType = detectDeviceWithUA() as IDeviceType;
|
|
825
|
+
geoLocation = await getUserLocation();
|
|
826
|
+
}
|
|
827
|
+
catch (e)
|
|
828
|
+
{
|
|
829
|
+
readinessPromise = null;
|
|
830
|
+
throw e;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
)();
|
|
834
|
+
|
|
835
|
+
return readinessPromise;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* @author
|
|
840
|
+
* @jonsnowpt
|
|
841
|
+
* @summary
|
|
842
|
+
* 🟥 MAIN
|
|
843
|
+
* @description
|
|
844
|
+
* 📝 Re-scans the DOM for ad zone elements and injects ads for any that have not
|
|
845
|
+
* yet been mounted. Safe to call multiple times — already-injected nodes store a
|
|
846
|
+
* comma-separated list of mounted zone IDs in `data-betarena-ad-mounted`
|
|
847
|
+
* (e.g. `"1,2"`), and zones whose ID is present in that list are skipped.
|
|
848
|
+
* @param {{ zoneIds?: number[]; rootElement?: HTMLElement }} [opts]
|
|
849
|
+
* 💠 Optional scope: restrict to specific zone IDs and / or a DOM subtree.
|
|
850
|
+
* @returns { Promise < void > }
|
|
851
|
+
*/
|
|
852
|
+
async function refreshAds
|
|
853
|
+
(
|
|
854
|
+
opts?:
|
|
855
|
+
{
|
|
856
|
+
zoneIds?: number[];
|
|
857
|
+
rootElement?: HTMLElement;
|
|
858
|
+
}
|
|
859
|
+
): Promise < void >
|
|
860
|
+
{
|
|
861
|
+
if (isDestroyed) return;
|
|
862
|
+
|
|
863
|
+
const
|
|
864
|
+
mySerial = ++refreshSerial
|
|
865
|
+
;
|
|
866
|
+
|
|
867
|
+
await ensureReady();
|
|
868
|
+
|
|
869
|
+
if (isDestroyed || mySerial !== refreshSerial) return;
|
|
870
|
+
|
|
871
|
+
generateElementMap(opts?.rootElement);
|
|
872
|
+
|
|
873
|
+
let
|
|
874
|
+
/**
|
|
875
|
+
* @description
|
|
876
|
+
* 📝 Zone IDs discovered in the current DOM scan (or filtered subset).
|
|
877
|
+
*/
|
|
878
|
+
targetZoneIds = [...mapBetarenaAdvertStandardElement.keys()]
|
|
879
|
+
;
|
|
880
|
+
|
|
881
|
+
if (opts?.zoneIds && opts.zoneIds.length > 0)
|
|
882
|
+
targetZoneIds = targetZoneIds.filter(id => opts.zoneIds!.includes(id));
|
|
883
|
+
;
|
|
884
|
+
|
|
885
|
+
// ╭─────
|
|
886
|
+
// │ NOTE:
|
|
887
|
+
// │ |: Only tear down previously injected global/body components when the
|
|
888
|
+
// │ |: refresh scope includes zone 1 (the global placement zone) or when
|
|
889
|
+
// │ |: no zone filter was applied (full refresh).
|
|
890
|
+
// │ |: Scoped refreshes that exclude zone 1 leave existing body-mounted
|
|
891
|
+
// │ |: widgets untouched to avoid permanently removing them.
|
|
892
|
+
// ╰─────
|
|
893
|
+
if (!opts?.zoneIds || opts.zoneIds.length === 0 || opts.zoneIds.includes(1))
|
|
894
|
+
{
|
|
895
|
+
for (const item of listAdWidgetElements)
|
|
896
|
+
item.$destroy();
|
|
897
|
+
;
|
|
898
|
+
listAdWidgetElements.length = 0;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
await injectBetarenaAds
|
|
902
|
+
(
|
|
903
|
+
{
|
|
904
|
+
deviceType,
|
|
905
|
+
isoCountryCode: geoLocation?.country_code ?? 'EN',
|
|
906
|
+
authorId,
|
|
907
|
+
tagIds: authorArticleTagIds,
|
|
908
|
+
zoneIds: targetZoneIds
|
|
909
|
+
},
|
|
910
|
+
// ╭─────
|
|
911
|
+
// │ NOTE:
|
|
912
|
+
// │ |: Derived from the caller's originally requested scope, NOT targetZoneIds
|
|
913
|
+
// │ |: (which is DOM-filtered). If the DOM has no zone-1 element, targetZoneIds
|
|
914
|
+
// │ |: won't include 1, but a full/zone-1 refresh should still inject global placements.
|
|
915
|
+
// ╰─────
|
|
916
|
+
!opts?.zoneIds || opts.zoneIds.length === 0 || opts.zoneIds.includes(1),
|
|
917
|
+
mySerial
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* @author
|
|
925
|
+
* @jonsnowpt
|
|
703
926
|
* @summary
|
|
704
927
|
* 🟥 MAIN
|
|
705
928
|
* @description
|
|
@@ -710,10 +933,6 @@
|
|
|
710
933
|
(
|
|
711
934
|
): Promise < void >
|
|
712
935
|
{
|
|
713
|
-
deviceType = detectDeviceWithUA() as IDeviceType;
|
|
714
|
-
geoLocation = await getUserLocation();
|
|
715
|
-
generateElementMap();
|
|
716
|
-
|
|
717
936
|
// [🐞]
|
|
718
937
|
logger
|
|
719
938
|
(
|
|
@@ -722,18 +941,7 @@
|
|
|
722
941
|
]
|
|
723
942
|
);
|
|
724
943
|
|
|
725
|
-
|
|
726
|
-
(
|
|
727
|
-
{
|
|
728
|
-
deviceType,
|
|
729
|
-
// deviceType: 'desktop',
|
|
730
|
-
isoCountryCode: geoLocation.country_code ?? 'EN',
|
|
731
|
-
// isoCountryCode: 'BR',
|
|
732
|
-
authorId,
|
|
733
|
-
tagIds: authorArticleTagIds,
|
|
734
|
-
zoneIds: [...mapBetarenaAdvertStandardElement.keys()]
|
|
735
|
-
}
|
|
736
|
-
);
|
|
944
|
+
await refreshAds();
|
|
737
945
|
|
|
738
946
|
return;
|
|
739
947
|
}
|
|
@@ -756,9 +964,37 @@
|
|
|
756
964
|
betarenaAdEngineStore.useLocalStorage();
|
|
757
965
|
// ╭─────
|
|
758
966
|
// │ NOTE:
|
|
967
|
+
// │ |: Expose public API immediately so host apps can call refreshAds()
|
|
968
|
+
// │ |: without waiting for the delayed initialize().
|
|
969
|
+
// │ |: refreshAds() self-initializes prerequisites via ensureReady() if needed.
|
|
970
|
+
// │ |: Save any existing value so it can be restored when this instance is destroyed.
|
|
971
|
+
// ╰─────
|
|
972
|
+
prevWindowApi = (window as any).betarenaAdEngine;
|
|
973
|
+
(window as any).betarenaAdEngine = { refreshAds };
|
|
974
|
+
// ╭─────
|
|
975
|
+
// │ NOTE:
|
|
759
976
|
// │ |: Delay initialization to ensure all elements are loaded on page
|
|
760
977
|
// ╰─────
|
|
761
|
-
|
|
978
|
+
initTimeoutId = setTimeout
|
|
979
|
+
(
|
|
980
|
+
() =>
|
|
981
|
+
{
|
|
982
|
+
initialize().catch
|
|
983
|
+
(
|
|
984
|
+
(error) =>
|
|
985
|
+
{
|
|
986
|
+
logger
|
|
987
|
+
(
|
|
988
|
+
[
|
|
989
|
+
'🚨 checkpoint ➤ initialize(..) // UNHANDLED ERROR',
|
|
990
|
+
`🔹 [var] ➤ error ${error}`
|
|
991
|
+
]
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
);
|
|
995
|
+
},
|
|
996
|
+
1000
|
|
997
|
+
);
|
|
762
998
|
return;
|
|
763
999
|
}
|
|
764
1000
|
);
|
|
@@ -776,6 +1012,25 @@
|
|
|
776
1012
|
],
|
|
777
1013
|
);
|
|
778
1014
|
|
|
1015
|
+
isDestroyed = true;
|
|
1016
|
+
|
|
1017
|
+
if (initTimeoutId !== null)
|
|
1018
|
+
clearTimeout(initTimeoutId);
|
|
1019
|
+
;
|
|
1020
|
+
|
|
1021
|
+
// ╭─────
|
|
1022
|
+
// │ NOTE:
|
|
1023
|
+
// │ |: Only remove/restore the global if this instance still owns it.
|
|
1024
|
+
// │ |: A later-mounted instance or host code may have overwritten it.
|
|
1025
|
+
// ╰─────
|
|
1026
|
+
if ((window as any).betarenaAdEngine?.refreshAds === refreshAds)
|
|
1027
|
+
{
|
|
1028
|
+
if (prevWindowApi !== undefined)
|
|
1029
|
+
(window as any).betarenaAdEngine = prevWindowApi;
|
|
1030
|
+
else
|
|
1031
|
+
delete (window as any).betarenaAdEngine;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
779
1034
|
// ╭─────
|
|
780
1035
|
// │ NOTE:
|
|
781
1036
|
// │ |: Destroy all dynamically created advert components
|