@ultdai/pulse 0.1.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/README.md +17 -8
- package/dist/contracts/index.d.ts +248 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +309 -21
- package/dist/sessionDecisionCache.d.ts +28 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,25 +7,32 @@ npm install @ultdai/pulse
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
CloudflareSdkConfigurationCache,
|
|
12
|
+
CloudflareSessionDecisionCache,
|
|
13
|
+
UnlimitedClient,
|
|
14
|
+
} from '@ultdai/pulse'
|
|
11
15
|
|
|
12
16
|
export interface Env { ULTD_API_TOKEN: string }
|
|
13
17
|
|
|
18
|
+
let client: UnlimitedClient | undefined
|
|
19
|
+
|
|
14
20
|
export default {
|
|
15
|
-
async fetch(request: Request, env: Env) {
|
|
21
|
+
async fetch(request: Request, env: Env, context: ExecutionContext) {
|
|
16
22
|
const session = getPlaybackSession(request) // stabilní ID a čas vzniku v Unixových milisekundách
|
|
17
|
-
|
|
23
|
+
client ??= new UnlimitedClient({
|
|
18
24
|
apiToken: env.ULTD_API_TOKEN,
|
|
19
25
|
baseUrl: 'https://api.ultd.tivio.studio',
|
|
20
26
|
cache: new CloudflareSdkConfigurationCache(caches.default),
|
|
27
|
+
sessionCache: new CloudflareSessionDecisionCache(caches.default),
|
|
21
28
|
source: 'stream-worker',
|
|
22
29
|
})
|
|
23
|
-
|
|
30
|
+
context.waitUntil(client.emit({
|
|
24
31
|
occurredAtMs: Date.now(),
|
|
25
32
|
organizationId: 'tivio',
|
|
26
33
|
pulse: {
|
|
27
|
-
type: '
|
|
28
|
-
version:
|
|
34
|
+
type: 'playback.session.playing',
|
|
35
|
+
version: 1,
|
|
29
36
|
id: session.id,
|
|
30
37
|
payload: {
|
|
31
38
|
sessionStartedAtMs: session.startedAtMs,
|
|
@@ -33,7 +40,7 @@ export default {
|
|
|
33
40
|
...(session.channelId ? { channelId: session.channelId } : {}),
|
|
34
41
|
},
|
|
35
42
|
},
|
|
36
|
-
})
|
|
43
|
+
}).catch((error) => console.error('ULTD pulse failed', error)))
|
|
37
44
|
return new Response('ok')
|
|
38
45
|
},
|
|
39
46
|
}
|
|
@@ -41,6 +48,8 @@ export default {
|
|
|
41
48
|
|
|
42
49
|
Klíč patří do serverového secretu, nikdy do klientského JavaScriptu. Konstruktor bez neprázdného `apiToken` skončí chybou ještě před síťovým voláním. Unlimited ověřuje klíč znovu na `GET /v1/sdk-config` i `POST /v1/pulses`; neplatný klíč nemůže zapsat puls.
|
|
43
50
|
|
|
44
|
-
SDK dynamicky načítá seznam PulseTypes, jejich schémata, povolení sběru a sampling politiku při prvním `emit()`. Neznámý typ nebo nevalidní `pulse.payload` SDK odmítne lokálně. Vypnutý nebo nevzorkovaný puls neodešle. `occurredAtMs` a
|
|
51
|
+
SDK dynamicky načítá seznam PulseTypes, jejich schémata, povolení sběru a sampling politiku při prvním `emit()`. Neznámý typ nebo nevalidní `pulse.payload` SDK odmítne lokálně. Vypnutý nebo nevzorkovaný puls neodešle. `occurredAtMs` a `sessionStartedAtMs` jsou Unixové časy v milisekundách. Konfigurace má krátkou platnost a po jejím vypršení se obnovuje; pokud obnovení selže, puls se neodešle. Cloudflare cache je volitelná; bez ní SDK používá paměť instance.
|
|
52
|
+
|
|
53
|
+
U `playback.session.playing@1` SDK při prvním volání pro session odešle idempotentní `playback.session.started@1`. API vrátí rozhodnutí, zda se session dále měří. Vybraná session pak odešle heartbeat nejdříve po 60 sekundách nebo při změně videa; ostatní volání skončí lokálně. Po ztrátě cache SDK zopakuje start se stejným ID a obnoví původní rozhodnutí. Pevné ID session a čas začátku musí dodávat producent. API a Durable Object opakovaný start rozpoznají; cache je pouze optimalizace.
|
|
45
54
|
|
|
46
55
|
SDK obsahuje interní kontrakty přibalené při buildu z monorepa. Aplikace instaluje jen `@ultdai/pulse` a jeho běžnou závislost `zod`; `@ultdai/contracts` se samostatně nevydává. Samotné zveřejnění SDK neposkytuje oprávnění k API.
|
|
@@ -156,6 +156,14 @@ export declare const PulseTypeDefinitionSchema: z.ZodObject<{
|
|
|
156
156
|
reliable: "reliable";
|
|
157
157
|
}>;
|
|
158
158
|
id: z.ZodString;
|
|
159
|
+
lifecycle: z.ZodOptional<z.ZodObject<{
|
|
160
|
+
groupId: z.ZodString;
|
|
161
|
+
phase: z.ZodEnum<{
|
|
162
|
+
observe: "observe";
|
|
163
|
+
start: "start";
|
|
164
|
+
}>;
|
|
165
|
+
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
166
|
+
}, z.core.$strict>>;
|
|
159
167
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
160
168
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
|
161
169
|
sampling: z.ZodOptional<z.ZodObject<{
|
|
@@ -169,16 +177,50 @@ export declare const PulseTypeDefinitionSchema: z.ZodObject<{
|
|
|
169
177
|
version: z.ZodNumber;
|
|
170
178
|
}, z.core.$strict>;
|
|
171
179
|
export declare const SignalReducerSchema: z.ZodEnum<{
|
|
180
|
+
"additive-window": "additive-window";
|
|
172
181
|
"distinct-per-window": "distinct-per-window";
|
|
182
|
+
lifecycle: "lifecycle";
|
|
173
183
|
presence: "presence";
|
|
174
184
|
}>;
|
|
175
185
|
export declare const SignalTypeDefinitionSchema: z.ZodObject<{
|
|
186
|
+
additive: z.ZodOptional<z.ZodObject<{
|
|
187
|
+
baseScopeFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
188
|
+
cohortAtField: z.ZodString;
|
|
189
|
+
dedupeFields: z.ZodArray<z.ZodString>;
|
|
190
|
+
dimensionSets: z.ZodArray<z.ZodObject<{
|
|
191
|
+
kind: z.ZodString;
|
|
192
|
+
fields: z.ZodArray<z.ZodString>;
|
|
193
|
+
}, z.core.$strict>>;
|
|
194
|
+
latenessMs: z.ZodNumber;
|
|
195
|
+
metricRules: z.ZodArray<z.ZodObject<{
|
|
196
|
+
name: z.ZodString;
|
|
197
|
+
op: z.ZodEnum<{
|
|
198
|
+
count: "count";
|
|
199
|
+
sum: "sum";
|
|
200
|
+
}>;
|
|
201
|
+
unitField: z.ZodOptional<z.ZodString>;
|
|
202
|
+
valueField: z.ZodOptional<z.ZodString>;
|
|
203
|
+
when: z.ZodOptional<z.ZodObject<{
|
|
204
|
+
field: z.ZodString;
|
|
205
|
+
equals: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
206
|
+
}, z.core.$strict>>;
|
|
207
|
+
}, z.core.$strict>>;
|
|
208
|
+
primaryMetric: z.ZodString;
|
|
209
|
+
stableScopeField: z.ZodOptional<z.ZodString>;
|
|
210
|
+
}, z.core.$strict>>;
|
|
176
211
|
id: z.ZodString;
|
|
177
212
|
inputPulseTypes: z.ZodArray<z.ZodObject<{
|
|
178
213
|
id: z.ZodString;
|
|
179
214
|
version: z.ZodNumber;
|
|
180
215
|
}, z.core.$strict>>;
|
|
181
216
|
dimensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
217
|
+
lifecycle: z.ZodOptional<z.ZodObject<{
|
|
218
|
+
intervalMs: z.ZodNumber;
|
|
219
|
+
scopeFields: z.ZodArray<z.ZodString>;
|
|
220
|
+
targetSampledPerScopeShard: z.ZodNumber;
|
|
221
|
+
timeoutMs: z.ZodNumber;
|
|
222
|
+
windowMs: z.ZodNumber;
|
|
223
|
+
}, z.core.$strict>>;
|
|
182
224
|
partitionKey: z.ZodOptional<z.ZodString>;
|
|
183
225
|
presence: z.ZodOptional<z.ZodObject<{
|
|
184
226
|
immutableDimensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -187,7 +229,9 @@ export declare const SignalTypeDefinitionSchema: z.ZodObject<{
|
|
|
187
229
|
timeoutMs: z.ZodNumber;
|
|
188
230
|
}, z.core.$strict>>;
|
|
189
231
|
reducer: z.ZodEnum<{
|
|
232
|
+
"additive-window": "additive-window";
|
|
190
233
|
"distinct-per-window": "distinct-per-window";
|
|
234
|
+
lifecycle: "lifecycle";
|
|
191
235
|
presence: "presence";
|
|
192
236
|
}>;
|
|
193
237
|
shardCount: z.ZodNumber;
|
|
@@ -212,6 +256,7 @@ export declare const MetricPulseTargetSchema: z.ZodObject<{
|
|
|
212
256
|
refs: z.ZodArray<z.ZodObject<{
|
|
213
257
|
signalId: z.ZodString;
|
|
214
258
|
dimension: z.ZodString;
|
|
259
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
215
260
|
}, z.core.$strict>>;
|
|
216
261
|
sessionStartedAt: z.ZodString;
|
|
217
262
|
shardCount: z.ZodNumber;
|
|
@@ -272,6 +317,7 @@ export declare const RoutedMetricPulseSchema: z.ZodObject<{
|
|
|
272
317
|
refs: z.ZodArray<z.ZodObject<{
|
|
273
318
|
signalId: z.ZodString;
|
|
274
319
|
dimension: z.ZodString;
|
|
320
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
275
321
|
}, z.core.$strict>>;
|
|
276
322
|
sessionStartedAt: z.ZodString;
|
|
277
323
|
shardCount: z.ZodNumber;
|
|
@@ -282,19 +328,196 @@ export declare const RoutedMetricPulseSchema: z.ZodObject<{
|
|
|
282
328
|
windowStartAt: z.ZodOptional<z.ZodString>;
|
|
283
329
|
}, z.core.$strict>;
|
|
284
330
|
}, z.core.$strict>;
|
|
331
|
+
/** A pulse carries a bounded vector of exact, non-sampled metric additions. */
|
|
332
|
+
export declare const AdditivePulseTargetSchema: z.ZodObject<{
|
|
333
|
+
dedupeKey: z.ZodString;
|
|
334
|
+
generation: z.ZodNumber;
|
|
335
|
+
latenessMs: z.ZodNumber;
|
|
336
|
+
metrics: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
337
|
+
organizationId: z.ZodString;
|
|
338
|
+
ownerId: z.ZodString;
|
|
339
|
+
primaryMetric: z.ZodString;
|
|
340
|
+
refs: z.ZodArray<z.ZodObject<{
|
|
341
|
+
signalId: z.ZodString;
|
|
342
|
+
dimension: z.ZodString;
|
|
343
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
344
|
+
}, z.core.$strict>>;
|
|
345
|
+
shardCount: z.ZodNumber;
|
|
346
|
+
shardIndex: z.ZodNumber;
|
|
347
|
+
signalTypeId: z.ZodString;
|
|
348
|
+
stableScopeKey: z.ZodOptional<z.ZodString>;
|
|
349
|
+
windowEndAt: z.ZodString;
|
|
350
|
+
windowStartAt: z.ZodString;
|
|
351
|
+
}, z.core.$strict>;
|
|
352
|
+
export declare const RoutedAdditivePulseSchema: z.ZodObject<{
|
|
353
|
+
pulse: z.ZodObject<{
|
|
354
|
+
data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
355
|
+
eventId: z.ZodOptional<z.ZodString>;
|
|
356
|
+
id: z.ZodString;
|
|
357
|
+
organizationId: z.ZodString;
|
|
358
|
+
sampling: z.ZodDefault<z.ZodObject<{
|
|
359
|
+
applied: z.ZodBoolean;
|
|
360
|
+
appliedKeepProbability: z.ZodNumber;
|
|
361
|
+
effectiveKeepProbability: z.ZodNumber;
|
|
362
|
+
ingressApplied: z.ZodBoolean;
|
|
363
|
+
ingressKeepProbability: z.ZodNumber;
|
|
364
|
+
ingressMethod: z.ZodEnum<{
|
|
365
|
+
none: "none";
|
|
366
|
+
"pulse-hash-v1": "pulse-hash-v1";
|
|
367
|
+
}>;
|
|
368
|
+
knownKeepProbability: z.ZodNullable<z.ZodNumber>;
|
|
369
|
+
method: z.ZodEnum<{
|
|
370
|
+
none: "none";
|
|
371
|
+
"pulse-hash-v1": "pulse-hash-v1";
|
|
372
|
+
}>;
|
|
373
|
+
policyId: z.ZodNullable<z.ZodString>;
|
|
374
|
+
policyRevision: z.ZodNullable<z.ZodNumber>;
|
|
375
|
+
unit: z.ZodEnum<{
|
|
376
|
+
member: "member";
|
|
377
|
+
pulse: "pulse";
|
|
378
|
+
}>;
|
|
379
|
+
}, z.core.$strict>>;
|
|
380
|
+
source: z.ZodString;
|
|
381
|
+
subject: z.ZodOptional<z.ZodObject<{
|
|
382
|
+
id: z.ZodString;
|
|
383
|
+
type: z.ZodString;
|
|
384
|
+
}, z.core.$strip>>;
|
|
385
|
+
type: z.ZodString;
|
|
386
|
+
typeVersion: z.ZodNumber;
|
|
387
|
+
occurredAt: z.ZodString;
|
|
388
|
+
receivedAt: z.ZodString;
|
|
389
|
+
}, z.core.$strict>;
|
|
390
|
+
target: z.ZodObject<{
|
|
391
|
+
dedupeKey: z.ZodString;
|
|
392
|
+
generation: z.ZodNumber;
|
|
393
|
+
latenessMs: z.ZodNumber;
|
|
394
|
+
metrics: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
395
|
+
organizationId: z.ZodString;
|
|
396
|
+
ownerId: z.ZodString;
|
|
397
|
+
primaryMetric: z.ZodString;
|
|
398
|
+
refs: z.ZodArray<z.ZodObject<{
|
|
399
|
+
signalId: z.ZodString;
|
|
400
|
+
dimension: z.ZodString;
|
|
401
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
402
|
+
}, z.core.$strict>>;
|
|
403
|
+
shardCount: z.ZodNumber;
|
|
404
|
+
shardIndex: z.ZodNumber;
|
|
405
|
+
signalTypeId: z.ZodString;
|
|
406
|
+
stableScopeKey: z.ZodOptional<z.ZodString>;
|
|
407
|
+
windowEndAt: z.ZodString;
|
|
408
|
+
windowStartAt: z.ZodString;
|
|
409
|
+
}, z.core.$strict>;
|
|
410
|
+
}, z.core.$strict>;
|
|
411
|
+
export declare const LifecyclePulseTargetSchema: z.ZodObject<{
|
|
412
|
+
generation: z.ZodNumber;
|
|
413
|
+
groupId: z.ZodString;
|
|
414
|
+
intervalMs: z.ZodNumber;
|
|
415
|
+
organizationId: z.ZodString;
|
|
416
|
+
ownerId: z.ZodString;
|
|
417
|
+
phase: z.ZodEnum<{
|
|
418
|
+
observe: "observe";
|
|
419
|
+
start: "start";
|
|
420
|
+
}>;
|
|
421
|
+
refs: z.ZodArray<z.ZodObject<{
|
|
422
|
+
signalId: z.ZodString;
|
|
423
|
+
dimension: z.ZodString;
|
|
424
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
425
|
+
}, z.core.$strict>>;
|
|
426
|
+
scopeId: z.ZodString;
|
|
427
|
+
sessionStartedAt: z.ZodString;
|
|
428
|
+
shardCount: z.ZodNumber;
|
|
429
|
+
shardIndex: z.ZodNumber;
|
|
430
|
+
targetSampledPerScopeShard: z.ZodNumber;
|
|
431
|
+
timeoutMs: z.ZodNumber;
|
|
432
|
+
windowMs: z.ZodNumber;
|
|
433
|
+
}, z.core.$strict>;
|
|
434
|
+
export declare const RoutedLifecyclePulseSchema: z.ZodObject<{
|
|
435
|
+
pulse: z.ZodObject<{
|
|
436
|
+
data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
437
|
+
eventId: z.ZodOptional<z.ZodString>;
|
|
438
|
+
id: z.ZodString;
|
|
439
|
+
organizationId: z.ZodString;
|
|
440
|
+
sampling: z.ZodDefault<z.ZodObject<{
|
|
441
|
+
applied: z.ZodBoolean;
|
|
442
|
+
appliedKeepProbability: z.ZodNumber;
|
|
443
|
+
effectiveKeepProbability: z.ZodNumber;
|
|
444
|
+
ingressApplied: z.ZodBoolean;
|
|
445
|
+
ingressKeepProbability: z.ZodNumber;
|
|
446
|
+
ingressMethod: z.ZodEnum<{
|
|
447
|
+
none: "none";
|
|
448
|
+
"pulse-hash-v1": "pulse-hash-v1";
|
|
449
|
+
}>;
|
|
450
|
+
knownKeepProbability: z.ZodNullable<z.ZodNumber>;
|
|
451
|
+
method: z.ZodEnum<{
|
|
452
|
+
none: "none";
|
|
453
|
+
"pulse-hash-v1": "pulse-hash-v1";
|
|
454
|
+
}>;
|
|
455
|
+
policyId: z.ZodNullable<z.ZodString>;
|
|
456
|
+
policyRevision: z.ZodNullable<z.ZodNumber>;
|
|
457
|
+
unit: z.ZodEnum<{
|
|
458
|
+
member: "member";
|
|
459
|
+
pulse: "pulse";
|
|
460
|
+
}>;
|
|
461
|
+
}, z.core.$strict>>;
|
|
462
|
+
source: z.ZodString;
|
|
463
|
+
subject: z.ZodOptional<z.ZodObject<{
|
|
464
|
+
id: z.ZodString;
|
|
465
|
+
type: z.ZodString;
|
|
466
|
+
}, z.core.$strip>>;
|
|
467
|
+
type: z.ZodString;
|
|
468
|
+
typeVersion: z.ZodNumber;
|
|
469
|
+
occurredAt: z.ZodString;
|
|
470
|
+
receivedAt: z.ZodString;
|
|
471
|
+
}, z.core.$strict>;
|
|
472
|
+
target: z.ZodObject<{
|
|
473
|
+
generation: z.ZodNumber;
|
|
474
|
+
groupId: z.ZodString;
|
|
475
|
+
intervalMs: z.ZodNumber;
|
|
476
|
+
organizationId: z.ZodString;
|
|
477
|
+
ownerId: z.ZodString;
|
|
478
|
+
phase: z.ZodEnum<{
|
|
479
|
+
observe: "observe";
|
|
480
|
+
start: "start";
|
|
481
|
+
}>;
|
|
482
|
+
refs: z.ZodArray<z.ZodObject<{
|
|
483
|
+
signalId: z.ZodString;
|
|
484
|
+
dimension: z.ZodString;
|
|
485
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
486
|
+
}, z.core.$strict>>;
|
|
487
|
+
scopeId: z.ZodString;
|
|
488
|
+
sessionStartedAt: z.ZodString;
|
|
489
|
+
shardCount: z.ZodNumber;
|
|
490
|
+
shardIndex: z.ZodNumber;
|
|
491
|
+
targetSampledPerScopeShard: z.ZodNumber;
|
|
492
|
+
timeoutMs: z.ZodNumber;
|
|
493
|
+
windowMs: z.ZodNumber;
|
|
494
|
+
}, z.core.$strict>;
|
|
495
|
+
}, z.core.$strict>;
|
|
496
|
+
export declare const LifecycleDecisionSchema: z.ZodObject<{
|
|
497
|
+
accepted: z.ZodBoolean;
|
|
498
|
+
nextHeartbeatAtMs: z.ZodNumber;
|
|
499
|
+
probability: z.ZodNumber;
|
|
500
|
+
selected: z.ZodBoolean;
|
|
501
|
+
}, z.core.$strict>;
|
|
285
502
|
export declare const MetricSnapshotSchema: z.ZodObject<{
|
|
286
503
|
estimatedCount: z.ZodNumber;
|
|
504
|
+
varianceEstimate: z.ZodOptional<z.ZodNumber>;
|
|
287
505
|
generation: z.ZodNumber;
|
|
288
506
|
kind: z.ZodEnum<{
|
|
507
|
+
"additive-window": "additive-window";
|
|
289
508
|
"distinct-per-window": "distinct-per-window";
|
|
290
509
|
presence: "presence";
|
|
291
510
|
}>;
|
|
511
|
+
latenessMs: z.ZodOptional<z.ZodNumber>;
|
|
512
|
+
metrics: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
292
513
|
observedAt: z.ZodString;
|
|
293
514
|
organizationId: z.ZodString;
|
|
294
515
|
originId: z.ZodString;
|
|
295
516
|
recovering: z.ZodBoolean;
|
|
296
517
|
revision: z.ZodNumber;
|
|
297
518
|
sampledCount: z.ZodNumber;
|
|
519
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
520
|
+
scopeKind: z.ZodOptional<z.ZodString>;
|
|
298
521
|
shardCount: z.ZodNumber;
|
|
299
522
|
signalId: z.ZodString;
|
|
300
523
|
signalTypeId: z.ZodString;
|
|
@@ -322,6 +545,14 @@ export declare const SdkPulseTypeSchema: z.ZodObject<{
|
|
|
322
545
|
reliable: "reliable";
|
|
323
546
|
}>;
|
|
324
547
|
id: z.ZodString;
|
|
548
|
+
lifecycle: z.ZodOptional<z.ZodObject<{
|
|
549
|
+
groupId: z.ZodString;
|
|
550
|
+
phase: z.ZodEnum<{
|
|
551
|
+
observe: "observe";
|
|
552
|
+
start: "start";
|
|
553
|
+
}>;
|
|
554
|
+
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
555
|
+
}, z.core.$strict>>;
|
|
325
556
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
326
557
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
|
327
558
|
sampling: z.ZodOptional<z.ZodObject<{
|
|
@@ -359,6 +590,14 @@ export declare const SdkConfigurationSchema: z.ZodObject<{
|
|
|
359
590
|
reliable: "reliable";
|
|
360
591
|
}>;
|
|
361
592
|
id: z.ZodString;
|
|
593
|
+
lifecycle: z.ZodOptional<z.ZodObject<{
|
|
594
|
+
groupId: z.ZodString;
|
|
595
|
+
phase: z.ZodEnum<{
|
|
596
|
+
observe: "observe";
|
|
597
|
+
start: "start";
|
|
598
|
+
}>;
|
|
599
|
+
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
600
|
+
}, z.core.$strict>>;
|
|
362
601
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
363
602
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
|
364
603
|
sampling: z.ZodOptional<z.ZodObject<{
|
|
@@ -394,7 +633,9 @@ export declare const LivePulseTargetSchema: z.ZodObject<{
|
|
|
394
633
|
memberId: z.ZodString;
|
|
395
634
|
organizationId: z.ZodString;
|
|
396
635
|
reducer: z.ZodEnum<{
|
|
636
|
+
"additive-window": "additive-window";
|
|
397
637
|
"distinct-per-window": "distinct-per-window";
|
|
638
|
+
lifecycle: "lifecycle";
|
|
398
639
|
presence: "presence";
|
|
399
640
|
}>;
|
|
400
641
|
shardId: z.ZodString;
|
|
@@ -446,7 +687,9 @@ export declare const RoutedLivePulseSchema: z.ZodObject<{
|
|
|
446
687
|
memberId: z.ZodString;
|
|
447
688
|
organizationId: z.ZodString;
|
|
448
689
|
reducer: z.ZodEnum<{
|
|
690
|
+
"additive-window": "additive-window";
|
|
449
691
|
"distinct-per-window": "distinct-per-window";
|
|
692
|
+
lifecycle: "lifecycle";
|
|
450
693
|
presence: "presence";
|
|
451
694
|
}>;
|
|
452
695
|
shardId: z.ZodString;
|
|
@@ -542,3 +785,8 @@ export type LiveShardSnapshot = z.infer<typeof LiveShardSnapshotSchema>;
|
|
|
542
785
|
export type MetricPulseTarget = z.infer<typeof MetricPulseTargetSchema>;
|
|
543
786
|
export type MetricSnapshot = z.infer<typeof MetricSnapshotSchema>;
|
|
544
787
|
export type RoutedMetricPulse = z.infer<typeof RoutedMetricPulseSchema>;
|
|
788
|
+
export type RoutedAdditivePulse = z.infer<typeof RoutedAdditivePulseSchema>;
|
|
789
|
+
export type AdditivePulseTarget = z.infer<typeof AdditivePulseTargetSchema>;
|
|
790
|
+
export type RoutedLifecyclePulse = z.infer<typeof RoutedLifecyclePulseSchema>;
|
|
791
|
+
export type LifecyclePulseTarget = z.infer<typeof LifecyclePulseTargetSchema>;
|
|
792
|
+
export type LifecycleDecision = z.infer<typeof LifecycleDecisionSchema>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { PulseSubmission } from './contracts/index.js';
|
|
2
2
|
import type { SdkConfigurationCache } from './configurationCache.js';
|
|
3
|
+
import type { SessionDecisionCache } from './sessionDecisionCache.js';
|
|
3
4
|
export { CloudflareSdkConfigurationCache, MemorySdkConfigurationCache } from './configurationCache.js';
|
|
4
5
|
export type { CachedSdkConfiguration, SdkConfigurationCache } from './configurationCache.js';
|
|
6
|
+
export { CloudflareSessionDecisionCache, MemorySessionDecisionCache } from './sessionDecisionCache.js';
|
|
7
|
+
export type { SessionDecision, SessionDecisionCache } from './sessionDecisionCache.js';
|
|
5
8
|
export { CloudflareSamplingPolicyCache, MemorySamplingPolicyCache } from './cache.js';
|
|
6
9
|
export type { CacheApiLike, SamplingPolicyCache } from './cache.js';
|
|
7
10
|
export type SdkPulseInput = Omit<PulseSubmission, 'source' | 'sampling' | 'pulse'> & {
|
|
@@ -14,6 +17,7 @@ export interface UnlimitedClientOptions {
|
|
|
14
17
|
baseUrl: string;
|
|
15
18
|
source: string;
|
|
16
19
|
cache?: SdkConfigurationCache;
|
|
20
|
+
sessionCache?: SessionDecisionCache;
|
|
17
21
|
fetch?: typeof fetch;
|
|
18
22
|
now?: () => Date;
|
|
19
23
|
randomUUID?: () => string;
|
|
@@ -23,12 +27,18 @@ export interface EmitResult {
|
|
|
23
27
|
disabled?: boolean;
|
|
24
28
|
pulseId: string;
|
|
25
29
|
sampledOut: boolean;
|
|
30
|
+
selected?: boolean;
|
|
31
|
+
throttled?: boolean;
|
|
32
|
+
probability?: number;
|
|
33
|
+
nextHeartbeatAtMs?: number;
|
|
26
34
|
}
|
|
27
35
|
export declare class UnlimitedClient {
|
|
28
36
|
private readonly apiToken;
|
|
29
37
|
private readonly baseUrl;
|
|
30
38
|
private readonly source;
|
|
31
39
|
private readonly cache;
|
|
40
|
+
private readonly sessionCache;
|
|
41
|
+
private readonly sessionMemory;
|
|
32
42
|
private readonly fetcher;
|
|
33
43
|
private readonly now;
|
|
34
44
|
private readonly randomUUID;
|
|
@@ -39,5 +49,8 @@ export declare class UnlimitedClient {
|
|
|
39
49
|
private install;
|
|
40
50
|
private refresh;
|
|
41
51
|
private configuration;
|
|
52
|
+
private decisionKey;
|
|
53
|
+
private getDecision;
|
|
54
|
+
private putDecision;
|
|
42
55
|
emit(input: SdkPulseInput): Promise<EmitResult>;
|
|
43
56
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,22 +11,29 @@ var compilePayloadSchema = (value) => {
|
|
|
11
11
|
const required = new Set(schema.required);
|
|
12
12
|
const fields = {};
|
|
13
13
|
for (const [name, field] of Object.entries(schema.properties)) {
|
|
14
|
-
let
|
|
15
|
-
if (field.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
validator = z.enum(field.enum);
|
|
20
|
-
} else if (field.type === "string") {
|
|
14
|
+
let validator2;
|
|
15
|
+
if (field.pattern !== void 0 && field.type !== "string") {
|
|
16
|
+
throw new Error(`Pattern is only supported for string field ${name}.`);
|
|
17
|
+
}
|
|
18
|
+
if (field.type === "string" || field.type === void 0 && field.enum?.every((item) => typeof item === "string")) {
|
|
21
19
|
let string = z.string();
|
|
22
20
|
if (field.minLength !== void 0)
|
|
23
21
|
string = string.min(field.minLength);
|
|
24
22
|
if (field.maxLength !== void 0)
|
|
25
23
|
string = string.max(field.maxLength);
|
|
24
|
+
if (field.pattern !== void 0) {
|
|
25
|
+
let pattern;
|
|
26
|
+
try {
|
|
27
|
+
pattern = new RegExp(field.pattern);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(`Invalid pattern for ${name}.`);
|
|
30
|
+
}
|
|
31
|
+
string = string.regex(pattern);
|
|
32
|
+
}
|
|
26
33
|
if (field.format === "date-time") {
|
|
27
|
-
|
|
34
|
+
validator2 = string.refine((item) => Number.isFinite(Date.parse(item)), "Invalid date-time.");
|
|
28
35
|
} else if (field.format === void 0) {
|
|
29
|
-
|
|
36
|
+
validator2 = string;
|
|
30
37
|
} else {
|
|
31
38
|
throw new Error(`Unsupported string format for ${name}.`);
|
|
32
39
|
}
|
|
@@ -36,20 +43,46 @@ var compilePayloadSchema = (value) => {
|
|
|
36
43
|
number = number.min(field.minimum);
|
|
37
44
|
if (field.maximum !== void 0)
|
|
38
45
|
number = number.max(field.maximum);
|
|
39
|
-
|
|
46
|
+
validator2 = number;
|
|
40
47
|
} else if (field.type === "boolean") {
|
|
41
|
-
|
|
48
|
+
validator2 = z.boolean();
|
|
42
49
|
} else {
|
|
43
50
|
throw new Error(`Unsupported payload field ${name}.`);
|
|
44
51
|
}
|
|
45
|
-
|
|
52
|
+
if (field.enum !== void 0) {
|
|
53
|
+
const enumType = field.type === "number" || field.type === "integer" ? "number" : "string";
|
|
54
|
+
if (field.enum.length === 0 || field.enum.some((item) => typeof item !== enumType)) {
|
|
55
|
+
throw new Error(`Invalid enum for ${name}.`);
|
|
56
|
+
}
|
|
57
|
+
const allowed = new Set(field.enum);
|
|
58
|
+
validator2 = validator2.refine((item) => allowed.has(item), "Invalid enum value.");
|
|
59
|
+
}
|
|
60
|
+
fields[name] = required.has(name) ? validator2 : validator2.optional();
|
|
46
61
|
}
|
|
47
62
|
for (const name of required) {
|
|
48
63
|
if (!fields[name])
|
|
49
64
|
throw new Error(`Required payload field ${name} is undefined.`);
|
|
50
65
|
}
|
|
51
66
|
const object = z.object(fields);
|
|
52
|
-
|
|
67
|
+
const validator = schema.additionalProperties === false ? object.strict() : object.passthrough();
|
|
68
|
+
if (!schema.dependentRequired)
|
|
69
|
+
return validator;
|
|
70
|
+
for (const [field, dependents] of Object.entries(schema.dependentRequired)) {
|
|
71
|
+
if (!fields[field] || !Array.isArray(dependents) || dependents.some((dependent) => !fields[dependent])) {
|
|
72
|
+
throw new Error(`Invalid dependentRequired for ${field}.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return validator.superRefine((payload, context) => {
|
|
76
|
+
for (const [field, dependents] of Object.entries(schema.dependentRequired ?? {})) {
|
|
77
|
+
if (payload[field] === void 0)
|
|
78
|
+
continue;
|
|
79
|
+
for (const dependent of dependents) {
|
|
80
|
+
if (payload[dependent] === void 0) {
|
|
81
|
+
context.addIssue({ code: "custom", path: [dependent], message: `${dependent} is required with ${field}.` });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
53
86
|
};
|
|
54
87
|
|
|
55
88
|
// ../../libs/contracts/dist/index.js
|
|
@@ -178,6 +211,11 @@ var PulseDeliverySchema = z2.enum(["live", "reliable"]);
|
|
|
178
211
|
var PulseTypeDefinitionSchema = z2.object({
|
|
179
212
|
delivery: PulseDeliverySchema,
|
|
180
213
|
id: z2.string().trim().min(1).max(100),
|
|
214
|
+
lifecycle: z2.object({
|
|
215
|
+
groupId: z2.string().trim().min(1).max(100),
|
|
216
|
+
phase: z2.enum(["start", "observe"]),
|
|
217
|
+
intervalMs: z2.number().int().min(1e3).max(864e5).optional()
|
|
218
|
+
}).strict().optional(),
|
|
181
219
|
payloadSchema: z2.record(z2.string(), z2.unknown()),
|
|
182
220
|
requiresExplicitId: z2.boolean().optional(),
|
|
183
221
|
sampling: z2.object({
|
|
@@ -196,15 +234,53 @@ var PulseTypeDefinitionSchema = z2.object({
|
|
|
196
234
|
if (definition.sampling?.unit === "pulse" && definition.sampling.keyField !== null) {
|
|
197
235
|
context.addIssue({ code: "custom", path: ["sampling", "keyField"], message: "Pulse sampling must not define keyField." });
|
|
198
236
|
}
|
|
237
|
+
if (definition.lifecycle && (definition.samplingAllowed || !definition.requiresExplicitId || definition.delivery !== "live" || definition.lifecycle.phase === "observe" !== Boolean(definition.lifecycle.intervalMs))) {
|
|
238
|
+
context.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle pulses need explicit IDs, live delivery, and an interval only for observations." });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
var SignalReducerSchema = z2.enum(["presence", "distinct-per-window", "lifecycle", "additive-window"]);
|
|
242
|
+
var MetricNameSchema = z2.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/).max(64);
|
|
243
|
+
var AdditiveMetricRuleSchema = z2.object({
|
|
244
|
+
name: MetricNameSchema,
|
|
245
|
+
op: z2.enum(["count", "sum"]),
|
|
246
|
+
unitField: z2.string().trim().min(1).max(200).optional(),
|
|
247
|
+
valueField: z2.string().trim().min(1).max(200).optional(),
|
|
248
|
+
when: z2.object({
|
|
249
|
+
field: z2.string().trim().min(1).max(200),
|
|
250
|
+
equals: z2.union([z2.string(), z2.number().finite(), z2.boolean()])
|
|
251
|
+
}).strict().optional()
|
|
252
|
+
}).strict().superRefine((rule, context) => {
|
|
253
|
+
if (rule.op === "sum" !== Boolean(rule.valueField)) {
|
|
254
|
+
context.addIssue({ code: "custom", path: ["valueField"], message: "Sum needs valueField; count must not have one." });
|
|
255
|
+
}
|
|
199
256
|
});
|
|
200
|
-
var SignalReducerSchema = z2.enum(["presence", "distinct-per-window"]);
|
|
201
257
|
var SignalTypeDefinitionSchema = z2.object({
|
|
258
|
+
additive: z2.object({
|
|
259
|
+
baseScopeFields: z2.array(z2.string().trim().min(1).max(100)).max(4).optional(),
|
|
260
|
+
cohortAtField: z2.string().trim().min(1).max(200),
|
|
261
|
+
dedupeFields: z2.array(z2.string().trim().min(1).max(200)).min(1).max(4),
|
|
262
|
+
dimensionSets: z2.array(z2.object({
|
|
263
|
+
kind: z2.string().trim().min(1).max(100),
|
|
264
|
+
fields: z2.array(z2.string().trim().min(1).max(100)).max(4)
|
|
265
|
+
}).strict()).min(1).max(16),
|
|
266
|
+
latenessMs: z2.number().int().min(0).max(36e5),
|
|
267
|
+
metricRules: z2.array(AdditiveMetricRuleSchema).min(1).max(32),
|
|
268
|
+
primaryMetric: MetricNameSchema,
|
|
269
|
+
stableScopeField: z2.string().trim().min(1).max(200).optional()
|
|
270
|
+
}).strict().optional(),
|
|
202
271
|
id: z2.string().trim().min(1).max(100),
|
|
203
272
|
inputPulseTypes: z2.array(z2.object({
|
|
204
273
|
id: z2.string().trim().min(1).max(100),
|
|
205
274
|
version: z2.number().int().positive()
|
|
206
275
|
}).strict()).min(1),
|
|
207
276
|
dimensions: z2.array(z2.string().trim().min(1).max(100)).optional(),
|
|
277
|
+
lifecycle: z2.object({
|
|
278
|
+
intervalMs: z2.number().int().min(1e3).max(864e5),
|
|
279
|
+
scopeFields: z2.array(z2.string().trim().min(1).max(100)).min(1),
|
|
280
|
+
targetSampledPerScopeShard: z2.number().int().positive(),
|
|
281
|
+
timeoutMs: z2.number().int().min(1e3).max(864e5),
|
|
282
|
+
windowMs: z2.number().int().min(1e3).max(864e5)
|
|
283
|
+
}).strict().optional(),
|
|
208
284
|
partitionKey: z2.string().trim().min(1).max(200).optional(),
|
|
209
285
|
presence: z2.object({
|
|
210
286
|
immutableDimensions: z2.array(z2.string().trim().min(1).max(100)).optional(),
|
|
@@ -228,16 +304,38 @@ var SignalTypeDefinitionSchema = z2.object({
|
|
|
228
304
|
if (definition.reducer === "distinct-per-window" && !definition.windowMs) {
|
|
229
305
|
context.addIssue({ code: "custom", path: ["windowMs"], message: "Windowed distinct needs a window." });
|
|
230
306
|
}
|
|
307
|
+
if (definition.reducer === "lifecycle" && (!definition.lifecycle || !definition.partitionKey || !definition.dimensions?.length)) {
|
|
308
|
+
context.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle needs a configuration, partition key and dimensions." });
|
|
309
|
+
}
|
|
310
|
+
if (definition.reducer === "additive-window") {
|
|
311
|
+
const additive = definition.additive;
|
|
312
|
+
if (!additive || !definition.windowMs || !definition.partitionKey) {
|
|
313
|
+
context.addIssue({ code: "custom", path: ["additive"], message: "Additive window needs metrics, windowMs and partitionKey." });
|
|
314
|
+
} else {
|
|
315
|
+
const names = additive.metricRules.map((rule) => rule.name);
|
|
316
|
+
if (new Set(names).size !== names.length || !names.includes(additive.primaryMetric)) {
|
|
317
|
+
context.addIssue({ code: "custom", path: ["additive", "metricRules"], message: "Metric names must be unique and include the primary metric." });
|
|
318
|
+
}
|
|
319
|
+
const kinds = additive.dimensionSets.map((set) => set.kind);
|
|
320
|
+
const sets = additive.dimensionSets.map((set) => JSON.stringify(set.fields));
|
|
321
|
+
if (new Set(sets).size !== sets.length || new Set(kinds).size !== kinds.length) {
|
|
322
|
+
context.addIssue({ code: "custom", path: ["additive", "dimensionSets"], message: "Dimension sets and kinds must be unique." });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else if (definition.additive) {
|
|
326
|
+
context.addIssue({ code: "custom", path: ["additive"], message: "Only additive-window signals may define additive metrics." });
|
|
327
|
+
}
|
|
231
328
|
const schedule = definition.shardSchedule ?? [];
|
|
232
|
-
if (schedule.length > 0 && (!definition.partitionKey || !definition.dimensions?.length)) {
|
|
329
|
+
if (schedule.length > 0 && (!definition.partitionKey || !definition.dimensions?.length && !definition.additive?.dimensionSets.length)) {
|
|
233
330
|
context.addIssue({ code: "custom", path: ["partitionKey"], message: "Scheduled routing needs partitionKey and dimensions." });
|
|
234
331
|
}
|
|
235
332
|
if (schedule.length > 0 && definition.reducer === "presence" && !definition.presence?.startedAtField) {
|
|
236
333
|
context.addIssue({ code: "custom", path: ["presence", "startedAtField"], message: "Scheduled presence needs a stable start time." });
|
|
237
334
|
}
|
|
238
|
-
|
|
335
|
+
const scheduledWindowMs = definition.windowMs ?? definition.lifecycle?.windowMs;
|
|
336
|
+
if (scheduledWindowMs) {
|
|
239
337
|
for (let index = 0; index < schedule.length; index += 1) {
|
|
240
|
-
if (Date.parse(schedule[index].effectiveFrom) %
|
|
338
|
+
if (Date.parse(schedule[index].effectiveFrom) % scheduledWindowMs !== 0) {
|
|
241
339
|
context.addIssue({ code: "custom", path: ["shardSchedule", index], message: "Window generation must start on a window boundary." });
|
|
242
340
|
}
|
|
243
341
|
}
|
|
@@ -250,7 +348,8 @@ var SignalTypeDefinitionSchema = z2.object({
|
|
|
250
348
|
});
|
|
251
349
|
var MetricSignalRefSchema = z2.object({
|
|
252
350
|
signalId: z2.string().trim().min(1).max(500),
|
|
253
|
-
dimension: z2.string().trim().min(1).max(100)
|
|
351
|
+
dimension: z2.string().trim().min(1).max(100),
|
|
352
|
+
scope: z2.record(z2.string().trim().min(1).max(100), z2.string().trim().min(1).max(200)).optional()
|
|
254
353
|
}).strict();
|
|
255
354
|
var MetricPulseTargetSchema = z2.object({
|
|
256
355
|
generation: z2.number().int().positive(),
|
|
@@ -282,25 +381,99 @@ var RoutedMetricPulseSchema = z2.object({
|
|
|
282
381
|
pulse: RawPulseSchema,
|
|
283
382
|
target: MetricPulseTargetSchema
|
|
284
383
|
}).strict();
|
|
384
|
+
var AdditivePulseTargetSchema = z2.object({
|
|
385
|
+
dedupeKey: z2.string().trim().min(1).max(500),
|
|
386
|
+
generation: z2.number().int().positive(),
|
|
387
|
+
latenessMs: z2.number().int().min(0).max(36e5),
|
|
388
|
+
metrics: z2.record(MetricNameSchema, z2.number().int().safe().nonnegative()),
|
|
389
|
+
organizationId: z2.string().trim().min(1).max(100),
|
|
390
|
+
ownerId: z2.string().trim().min(1).max(500),
|
|
391
|
+
primaryMetric: MetricNameSchema,
|
|
392
|
+
refs: z2.array(MetricSignalRefSchema).min(1).max(16),
|
|
393
|
+
shardCount: z2.number().int().min(1).max(65536),
|
|
394
|
+
shardIndex: z2.number().int().nonnegative(),
|
|
395
|
+
signalTypeId: z2.string().trim().min(1).max(100),
|
|
396
|
+
stableScopeKey: z2.string().trim().min(1).max(500).optional(),
|
|
397
|
+
windowEndAt: isoDateTime,
|
|
398
|
+
windowStartAt: isoDateTime
|
|
399
|
+
}).strict().superRefine((target, context) => {
|
|
400
|
+
if (target.shardIndex >= target.shardCount || Date.parse(target.windowStartAt) >= Date.parse(target.windowEndAt)) {
|
|
401
|
+
context.addIssue({ code: "custom", path: ["shardIndex"], message: "Invalid additive shard or window." });
|
|
402
|
+
}
|
|
403
|
+
if (!Object.hasOwn(target.metrics, target.primaryMetric)) {
|
|
404
|
+
context.addIssue({ code: "custom", path: ["metrics"], message: "Additive metrics must include the primary metric." });
|
|
405
|
+
}
|
|
406
|
+
if (new Set(target.refs.map((ref) => ref.signalId)).size !== target.refs.length) {
|
|
407
|
+
context.addIssue({ code: "custom", path: ["refs"], message: "Additive signal references must be unique." });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
var RoutedAdditivePulseSchema = z2.object({ pulse: RawPulseSchema, target: AdditivePulseTargetSchema }).strict();
|
|
411
|
+
var LifecyclePulseTargetSchema = z2.object({
|
|
412
|
+
generation: z2.number().int().positive(),
|
|
413
|
+
groupId: z2.string().trim().min(1).max(100),
|
|
414
|
+
intervalMs: z2.number().int().positive(),
|
|
415
|
+
organizationId: z2.string().trim().min(1).max(100),
|
|
416
|
+
ownerId: z2.string().trim().min(1).max(500),
|
|
417
|
+
phase: z2.enum(["start", "observe"]),
|
|
418
|
+
refs: z2.array(MetricSignalRefSchema).min(1),
|
|
419
|
+
scopeId: z2.string().trim().min(1).max(500),
|
|
420
|
+
sessionStartedAt: isoDateTime,
|
|
421
|
+
shardCount: z2.number().int().min(1).max(65536),
|
|
422
|
+
shardIndex: z2.number().int().nonnegative(),
|
|
423
|
+
targetSampledPerScopeShard: z2.number().int().positive(),
|
|
424
|
+
timeoutMs: z2.number().int().positive(),
|
|
425
|
+
windowMs: z2.number().int().positive()
|
|
426
|
+
}).strict().superRefine((target, context) => {
|
|
427
|
+
if (target.shardIndex >= target.shardCount) {
|
|
428
|
+
context.addIssue({ code: "custom", path: ["shardIndex"], message: "Shard index exceeds shard count." });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
var RoutedLifecyclePulseSchema = z2.object({
|
|
432
|
+
pulse: RawPulseSchema,
|
|
433
|
+
target: LifecyclePulseTargetSchema
|
|
434
|
+
}).strict();
|
|
435
|
+
var LifecycleDecisionSchema = z2.object({
|
|
436
|
+
accepted: z2.boolean(),
|
|
437
|
+
nextHeartbeatAtMs: z2.number().int().nonnegative(),
|
|
438
|
+
probability: z2.number().positive().max(1),
|
|
439
|
+
selected: z2.boolean()
|
|
440
|
+
}).strict();
|
|
285
441
|
var MetricSnapshotSchema = z2.object({
|
|
286
442
|
estimatedCount: z2.number().finite().nonnegative(),
|
|
443
|
+
varianceEstimate: z2.number().finite().nonnegative().optional(),
|
|
287
444
|
generation: z2.number().int().positive(),
|
|
288
|
-
kind: z2.enum(["presence", "distinct-per-window"]),
|
|
445
|
+
kind: z2.enum(["presence", "distinct-per-window", "additive-window"]),
|
|
446
|
+
latenessMs: z2.number().int().min(0).max(36e5).optional(),
|
|
447
|
+
metrics: z2.record(MetricNameSchema, z2.number().int().safe().nonnegative()).optional(),
|
|
289
448
|
observedAt: isoDateTime,
|
|
290
449
|
organizationId: z2.string().trim().min(1).max(100),
|
|
291
450
|
originId: z2.string().trim().min(1).max(200),
|
|
292
451
|
recovering: z2.boolean(),
|
|
293
452
|
revision: z2.number().int().nonnegative(),
|
|
294
453
|
sampledCount: z2.number().int().nonnegative(),
|
|
454
|
+
scope: z2.record(z2.string().trim().min(1).max(100), z2.string().trim().min(1).max(200)).optional(),
|
|
455
|
+
scopeKind: z2.string().trim().min(1).max(100).optional(),
|
|
295
456
|
shardCount: z2.number().int().min(1).max(65536),
|
|
296
457
|
signalId: z2.string().trim().min(1).max(500),
|
|
297
458
|
signalTypeId: z2.string().trim().min(1).max(100),
|
|
298
459
|
windowEndAt: isoDateTime.optional(),
|
|
299
460
|
windowStartAt: isoDateTime.optional()
|
|
300
461
|
}).strict().superRefine((snapshot, context) => {
|
|
301
|
-
if (snapshot.kind === "distinct-per-window" && (!snapshot.windowStartAt || !snapshot.windowEndAt)) {
|
|
462
|
+
if ((snapshot.kind === "distinct-per-window" || snapshot.kind === "additive-window") && (!snapshot.windowStartAt || !snapshot.windowEndAt)) {
|
|
302
463
|
context.addIssue({ code: "custom", path: ["windowStartAt"], message: "Distinct snapshot needs a window." });
|
|
303
464
|
}
|
|
465
|
+
if (snapshot.kind === "additive-window" && !snapshot.metrics) {
|
|
466
|
+
context.addIssue({ code: "custom", path: ["metrics"], message: "Additive snapshot needs metrics." });
|
|
467
|
+
}
|
|
468
|
+
if (snapshot.kind === "additive-window" && snapshot.latenessMs === void 0) {
|
|
469
|
+
context.addIssue({ code: "custom", path: ["latenessMs"], message: "Additive snapshot needs its lateness bound." });
|
|
470
|
+
}
|
|
471
|
+
if (snapshot.kind === "additive-window" && (!snapshot.scope || !snapshot.scopeKind)) {
|
|
472
|
+
context.addIssue({ code: "custom", path: ["scope"], message: "Additive snapshot needs a structured scope." });
|
|
473
|
+
}
|
|
474
|
+
if (snapshot.kind !== "additive-window" && snapshot.metrics) {
|
|
475
|
+
context.addIssue({ code: "custom", path: ["metrics"], message: "Only additive snapshots carry metrics." });
|
|
476
|
+
}
|
|
304
477
|
if (snapshot.kind === "presence" && (snapshot.windowStartAt || snapshot.windowEndAt)) {
|
|
305
478
|
context.addIssue({ code: "custom", path: ["windowStartAt"], message: "Presence snapshot must not have a window." });
|
|
306
479
|
}
|
|
@@ -435,6 +608,47 @@ var CloudflareSdkConfigurationCache = class {
|
|
|
435
608
|
}
|
|
436
609
|
};
|
|
437
610
|
|
|
611
|
+
// src/sessionDecisionCache.ts
|
|
612
|
+
var MemorySessionDecisionCache = class {
|
|
613
|
+
constructor(maxEntries = 1e4) {
|
|
614
|
+
this.maxEntries = maxEntries;
|
|
615
|
+
}
|
|
616
|
+
maxEntries;
|
|
617
|
+
values = /* @__PURE__ */ new Map();
|
|
618
|
+
async get(key) {
|
|
619
|
+
return this.values.get(key);
|
|
620
|
+
}
|
|
621
|
+
async set(key, decision) {
|
|
622
|
+
this.values.delete(key);
|
|
623
|
+
this.values.set(key, decision);
|
|
624
|
+
if (this.values.size > this.maxEntries) this.values.delete(this.values.keys().next().value);
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
var CloudflareSessionDecisionCache = class {
|
|
628
|
+
constructor(cache, namespace = "https://session-decision-cache.ultd.invalid") {
|
|
629
|
+
this.cache = cache;
|
|
630
|
+
this.namespace = namespace;
|
|
631
|
+
}
|
|
632
|
+
cache;
|
|
633
|
+
namespace;
|
|
634
|
+
request(key) {
|
|
635
|
+
return new Request(`${this.namespace}/${key}`);
|
|
636
|
+
}
|
|
637
|
+
async get(key) {
|
|
638
|
+
const response = await this.cache.match(this.request(key));
|
|
639
|
+
if (!response) return void 0;
|
|
640
|
+
const value = await response.json();
|
|
641
|
+
if (typeof value.expiresAtMs !== "number" || typeof value.nextHeartbeatAtMs !== "number" || typeof value.probability !== "number" || value.probability <= 0 || value.probability > 1 || typeof value.selected !== "boolean") return void 0;
|
|
642
|
+
return value;
|
|
643
|
+
}
|
|
644
|
+
async set(key, decision) {
|
|
645
|
+
const seconds = Math.max(1, Math.floor((decision.expiresAtMs - Date.now()) / 1e3));
|
|
646
|
+
await this.cache.put(this.request(key), new Response(JSON.stringify(decision), {
|
|
647
|
+
headers: { "cache-control": `public, max-age=${seconds}`, "content-type": "application/json" }
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
438
652
|
// src/cache.ts
|
|
439
653
|
var MemorySamplingPolicyCache = class {
|
|
440
654
|
policies = /* @__PURE__ */ new Map();
|
|
@@ -499,6 +713,8 @@ var UnlimitedClient = class {
|
|
|
499
713
|
baseUrl;
|
|
500
714
|
source;
|
|
501
715
|
cache;
|
|
716
|
+
sessionCache;
|
|
717
|
+
sessionMemory = new MemorySessionDecisionCache();
|
|
502
718
|
fetcher;
|
|
503
719
|
now;
|
|
504
720
|
randomUUID;
|
|
@@ -516,6 +732,7 @@ var UnlimitedClient = class {
|
|
|
516
732
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
517
733
|
this.source = options.source;
|
|
518
734
|
this.cache = options.cache ?? new MemorySdkConfigurationCache();
|
|
735
|
+
this.sessionCache = options.sessionCache ?? this.sessionMemory;
|
|
519
736
|
this.fetcher = options.fetch ?? fetch;
|
|
520
737
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
521
738
|
this.randomUUID = options.randomUUID ?? (() => crypto.randomUUID());
|
|
@@ -584,6 +801,28 @@ var UnlimitedClient = class {
|
|
|
584
801
|
});
|
|
585
802
|
return this.refreshing;
|
|
586
803
|
}
|
|
804
|
+
async decisionKey(groupId, organizationId, sessionId) {
|
|
805
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(
|
|
806
|
+
`${await this.cacheKey}\0${groupId}\0${organizationId}\0${sessionId}`
|
|
807
|
+
));
|
|
808
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
809
|
+
}
|
|
810
|
+
async getDecision(key) {
|
|
811
|
+
const memory = await this.sessionMemory.get(key);
|
|
812
|
+
if (memory && memory.expiresAtMs > this.now().getTime()) return memory;
|
|
813
|
+
const cached = await this.sessionCache.get(key).catch(() => void 0);
|
|
814
|
+
if (cached && cached.expiresAtMs > this.now().getTime()) {
|
|
815
|
+
await this.sessionMemory.set(key, cached);
|
|
816
|
+
return cached;
|
|
817
|
+
}
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
async putDecision(key, decision) {
|
|
821
|
+
await this.sessionMemory.set(key, decision);
|
|
822
|
+
if (this.sessionCache !== this.sessionMemory) {
|
|
823
|
+
await this.sessionCache.set(key, decision).catch(() => void 0);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
587
826
|
async emit(input) {
|
|
588
827
|
const loaded = await this.configuration();
|
|
589
828
|
const type = loaded.types.get(typeKey(input.pulse.type, input.pulse.version));
|
|
@@ -602,9 +841,36 @@ var UnlimitedClient = class {
|
|
|
602
841
|
if (!type.entry.enabled) {
|
|
603
842
|
return { accepted: false, disabled: true, pulseId: id, sampledOut: false };
|
|
604
843
|
}
|
|
844
|
+
const lifecycle = type.entry.definition.lifecycle;
|
|
845
|
+
const decisionKey = lifecycle ? await this.decisionKey(lifecycle.groupId, input.organizationId, id) : void 0;
|
|
846
|
+
if (lifecycle?.phase === "observe" && decisionKey) {
|
|
847
|
+
const decision = await this.getDecision(decisionKey);
|
|
848
|
+
if (!decision) {
|
|
849
|
+
const start = [...loaded.types.values()].find((candidate) => candidate.entry.definition.lifecycle?.groupId === lifecycle.groupId && candidate.entry.definition.lifecycle.phase === "start");
|
|
850
|
+
if (!start) throw new Error(`Lifecycle group ${lifecycle.groupId} has no start PulseType.`);
|
|
851
|
+
return this.emit({
|
|
852
|
+
...input,
|
|
853
|
+
pulse: { ...input.pulse, type: start.entry.definition.id, version: start.entry.definition.version }
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
if (decision && !decision.selected) {
|
|
857
|
+
return { accepted: false, pulseId: id, sampledOut: true, selected: false, probability: decision.probability };
|
|
858
|
+
}
|
|
859
|
+
if (decision && this.now().getTime() < decision.nextHeartbeatAtMs && decision.lastVideoId === payload.videoId) {
|
|
860
|
+
return {
|
|
861
|
+
accepted: false,
|
|
862
|
+
pulseId: id,
|
|
863
|
+
sampledOut: false,
|
|
864
|
+
throttled: true,
|
|
865
|
+
selected: true,
|
|
866
|
+
probability: decision.probability,
|
|
867
|
+
nextHeartbeatAtMs: decision.nextHeartbeatAtMs
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
}
|
|
605
871
|
const policy = type.entry.samplingPolicy;
|
|
606
872
|
let sampling = NoSamplingContext;
|
|
607
|
-
if (policy) {
|
|
873
|
+
if (policy && !lifecycle) {
|
|
608
874
|
sampling = {
|
|
609
875
|
applied: policy.keepProbability < 1,
|
|
610
876
|
appliedKeepProbability: policy.keepProbability,
|
|
@@ -639,6 +905,26 @@ var UnlimitedClient = class {
|
|
|
639
905
|
});
|
|
640
906
|
if (!response.ok) throw new Error(`Pulse request failed with ${response.status}.`);
|
|
641
907
|
const body = await response.json();
|
|
908
|
+
if (lifecycle && decisionKey) {
|
|
909
|
+
if (typeof body.selected !== "boolean" || typeof body.probability !== "number" || body.probability <= 0 || body.probability > 1 || typeof body.nextHeartbeatAtMs !== "number") {
|
|
910
|
+
throw new Error("Lifecycle response did not contain a valid session decision.");
|
|
911
|
+
}
|
|
912
|
+
await this.putDecision(decisionKey, {
|
|
913
|
+
selected: body.selected,
|
|
914
|
+
probability: body.probability,
|
|
915
|
+
nextHeartbeatAtMs: body.nextHeartbeatAtMs,
|
|
916
|
+
lastVideoId: body.selected && typeof payload.videoId === "string" ? payload.videoId : void 0,
|
|
917
|
+
expiresAtMs: this.now().getTime() + 24 * 60 * 60 * 1e3
|
|
918
|
+
});
|
|
919
|
+
return {
|
|
920
|
+
accepted: body.accepted !== false,
|
|
921
|
+
pulseId: id,
|
|
922
|
+
sampledOut: lifecycle.phase === "observe" && !body.selected,
|
|
923
|
+
selected: body.selected,
|
|
924
|
+
probability: body.probability,
|
|
925
|
+
nextHeartbeatAtMs: body.nextHeartbeatAtMs
|
|
926
|
+
};
|
|
927
|
+
}
|
|
642
928
|
return {
|
|
643
929
|
accepted: body.accepted !== false,
|
|
644
930
|
...body.disabled === true ? { disabled: true } : {},
|
|
@@ -650,7 +936,9 @@ var UnlimitedClient = class {
|
|
|
650
936
|
export {
|
|
651
937
|
CloudflareSamplingPolicyCache,
|
|
652
938
|
CloudflareSdkConfigurationCache,
|
|
939
|
+
CloudflareSessionDecisionCache,
|
|
653
940
|
MemorySamplingPolicyCache,
|
|
654
941
|
MemorySdkConfigurationCache,
|
|
942
|
+
MemorySessionDecisionCache,
|
|
655
943
|
UnlimitedClient
|
|
656
944
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CacheApiLike } from './cache.js';
|
|
2
|
+
export interface SessionDecision {
|
|
3
|
+
expiresAtMs: number;
|
|
4
|
+
lastVideoId?: string;
|
|
5
|
+
nextHeartbeatAtMs: number;
|
|
6
|
+
probability: number;
|
|
7
|
+
selected: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface SessionDecisionCache {
|
|
10
|
+
get(key: string): Promise<SessionDecision | undefined>;
|
|
11
|
+
set(key: string, decision: SessionDecision): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare class MemorySessionDecisionCache implements SessionDecisionCache {
|
|
14
|
+
private readonly maxEntries;
|
|
15
|
+
private readonly values;
|
|
16
|
+
constructor(maxEntries?: number);
|
|
17
|
+
get(key: string): Promise<SessionDecision | undefined>;
|
|
18
|
+
set(key: string, decision: SessionDecision): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** Optional L2 cache; the Durable Object remains authoritative on a miss. */
|
|
21
|
+
export declare class CloudflareSessionDecisionCache implements SessionDecisionCache {
|
|
22
|
+
private readonly cache;
|
|
23
|
+
private readonly namespace;
|
|
24
|
+
constructor(cache: CacheApiLike, namespace?: string);
|
|
25
|
+
private request;
|
|
26
|
+
get(key: string): Promise<SessionDecision | undefined>;
|
|
27
|
+
set(key: string, decision: SessionDecision): Promise<void>;
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultdai/pulse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": ["dist", "README.md"],
|
|
6
6
|
"publishConfig": { "access": "public" },
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json --emitDeclarationOnly && node scripts/bundle.mjs",
|
|
17
|
-
"prepack": "pnpm --dir ../../libs/contracts run build && pnpm run build",
|
|
17
|
+
"prepack": "corepack pnpm --dir ../../libs/contracts run build && corepack pnpm run build",
|
|
18
18
|
"test": "vitest run test --exclude 'dist/**'",
|
|
19
19
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
20
20
|
},
|