@proulr/bff-core 0.1.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proulr/bff-core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
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
- return c.json({
112
- ok: true,
113
- action_id: actionId,
114
- received: body,
115
- message: 'Action endpoint stub — implement per domain in each BFF.',
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
+ }