@proulr/bff-core 0.1.0 → 0.1.2
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/package.json +1 -1
- package/src/index.ts +40 -7
- package/src/native-ad-slot-insertion.ts +90 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -5,11 +5,17 @@ import { cors } from 'hono/cors';
|
|
|
5
5
|
import type { SduiScreenBuilder, SduiScreenBuilderContext } from '@proulr/sdui-contract';
|
|
6
6
|
import { createBffMetrics, isBffMetricsEnabled } from './bff-metrics';
|
|
7
7
|
|
|
8
|
+
export type SduiActionHandler = (
|
|
9
|
+
ctx: SduiScreenBuilderContext,
|
|
10
|
+
body: Record<string, unknown>,
|
|
11
|
+
) => Promise<Record<string, unknown>>;
|
|
12
|
+
|
|
8
13
|
export type CreateSduiBffOptions = {
|
|
9
14
|
name: string;
|
|
10
15
|
port: number;
|
|
11
16
|
bffId: string;
|
|
12
17
|
screens: Record<string, SduiScreenBuilder>;
|
|
18
|
+
actions?: Record<string, SduiActionHandler>;
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
function buildContext(
|
|
@@ -107,13 +113,34 @@ export function createSduiBff(options: CreateSduiBffOptions) {
|
|
|
107
113
|
|
|
108
114
|
app.post('/v1/actions/:actionId', async (c) => {
|
|
109
115
|
const actionId = c.req.param('actionId');
|
|
110
|
-
const body = await c.req.json().catch(() => ({}))
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<string, unknown>;
|
|
117
|
+
const handler = options.actions?.[actionId];
|
|
118
|
+
|
|
119
|
+
if (!handler) {
|
|
120
|
+
return c.json({
|
|
121
|
+
ok: false,
|
|
122
|
+
action_id: actionId,
|
|
123
|
+
error: 'action_not_found',
|
|
124
|
+
message: 'Action endpoint stub — register handler in BFF actions map.',
|
|
125
|
+
}, 404);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const authToken = readAuthToken(c.req.header('Authorization'));
|
|
130
|
+
const result = await handler(buildContext({}, {}, authToken), body);
|
|
131
|
+
return c.json({ ok: true, action_id: actionId, ...result });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.warn(`[${options.bffId}] action ${actionId} failed`, error);
|
|
134
|
+
return c.json(
|
|
135
|
+
{
|
|
136
|
+
ok: false,
|
|
137
|
+
action_id: actionId,
|
|
138
|
+
error: 'action_failed',
|
|
139
|
+
message: error instanceof Error ? error.message : 'action_failed',
|
|
140
|
+
},
|
|
141
|
+
500,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
117
144
|
});
|
|
118
145
|
|
|
119
146
|
return {
|
|
@@ -128,3 +155,9 @@ export function createSduiBff(options: CreateSduiBffOptions) {
|
|
|
128
155
|
|
|
129
156
|
export * from '@proulr/sdui-contract';
|
|
130
157
|
export { createBffSupabaseClient, isBffSupabaseConfigured } from './bff-supabase-client';
|
|
158
|
+
export {
|
|
159
|
+
buildNativeAdInsertionSeed,
|
|
160
|
+
insertRandomNativeAdSlots,
|
|
161
|
+
type InsertRandomNativeAdSlotsOptions,
|
|
162
|
+
type NativeAdSlotWire,
|
|
163
|
+
} from './native-ad-slot-insertion';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type NativeAdSlotWire = {
|
|
2
|
+
kind: 'ad';
|
|
3
|
+
id: string;
|
|
4
|
+
placement: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type InsertRandomNativeAdSlotsOptions = {
|
|
8
|
+
placement: string;
|
|
9
|
+
seed: string;
|
|
10
|
+
minContentBeforeFirstAd?: number;
|
|
11
|
+
minGap?: number;
|
|
12
|
+
maxGap?: number;
|
|
13
|
+
maxAds?: number;
|
|
14
|
+
idPrefix?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function hashSeed(seed: string): number {
|
|
18
|
+
let hash = 2166136261;
|
|
19
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
20
|
+
hash ^= seed.charCodeAt(index);
|
|
21
|
+
hash = Math.imul(hash, 16777619);
|
|
22
|
+
}
|
|
23
|
+
return hash >>> 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createSeededRandom(seed: number): () => number {
|
|
27
|
+
let state = seed || 1;
|
|
28
|
+
return () => {
|
|
29
|
+
state = (state + 0x6d2b79f5) | 0;
|
|
30
|
+
let t = Math.imul(state ^ (state >>> 15), 1 | state);
|
|
31
|
+
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
|
|
32
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function randomInt(random: () => number, min: number, max: number): number {
|
|
37
|
+
if (max <= min) return min;
|
|
38
|
+
return min + Math.floor(random() * (max - min + 1));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildNativeAdInsertionSeed(parts: string[]): string {
|
|
42
|
+
return parts.filter(Boolean).join(':');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function insertRandomNativeAdSlots<T>(
|
|
46
|
+
items: T[],
|
|
47
|
+
options: InsertRandomNativeAdSlotsOptions,
|
|
48
|
+
): Array<T | NativeAdSlotWire> {
|
|
49
|
+
const minContentBeforeFirstAd = options.minContentBeforeFirstAd ?? 2;
|
|
50
|
+
const minGap = options.minGap ?? 4;
|
|
51
|
+
const maxGap = options.maxGap ?? 8;
|
|
52
|
+
const maxAds = options.maxAds ?? Math.max(1, Math.floor(items.length / 12));
|
|
53
|
+
const idPrefix = options.idPrefix ?? options.placement;
|
|
54
|
+
|
|
55
|
+
if (!items.length || items.length < minContentBeforeFirstAd + 1) {
|
|
56
|
+
return [...items];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const random = createSeededRandom(hashSeed(options.seed));
|
|
60
|
+
const result: Array<T | NativeAdSlotWire> = [];
|
|
61
|
+
let sourceIndex = 0;
|
|
62
|
+
let adsInserted = 0;
|
|
63
|
+
let contentSinceLastAd = 0;
|
|
64
|
+
let nextAdAfter = randomInt(random, minGap, maxGap);
|
|
65
|
+
|
|
66
|
+
while (sourceIndex < items.length) {
|
|
67
|
+
const shouldInsertAd =
|
|
68
|
+
adsInserted < maxAds &&
|
|
69
|
+
contentSinceLastAd >= nextAdAfter &&
|
|
70
|
+
sourceIndex >= minContentBeforeFirstAd;
|
|
71
|
+
|
|
72
|
+
if (shouldInsertAd) {
|
|
73
|
+
result.push({
|
|
74
|
+
kind: 'ad',
|
|
75
|
+
id: `${idPrefix}-ad-${adsInserted}`,
|
|
76
|
+
placement: options.placement,
|
|
77
|
+
});
|
|
78
|
+
adsInserted += 1;
|
|
79
|
+
contentSinceLastAd = 0;
|
|
80
|
+
nextAdAfter = randomInt(random, minGap, maxGap);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
result.push(items[sourceIndex]);
|
|
85
|
+
sourceIndex += 1;
|
|
86
|
+
contentSinceLastAd += 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|