@ultdai/pulse 0.1.0 → 0.2.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 +128 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +169 -4
- package/dist/sessionDecisionCache.d.ts +28 -0
- package/package.json +1 -1
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<{
|
|
@@ -170,6 +178,7 @@ export declare const PulseTypeDefinitionSchema: z.ZodObject<{
|
|
|
170
178
|
}, z.core.$strict>;
|
|
171
179
|
export declare const SignalReducerSchema: z.ZodEnum<{
|
|
172
180
|
"distinct-per-window": "distinct-per-window";
|
|
181
|
+
lifecycle: "lifecycle";
|
|
173
182
|
presence: "presence";
|
|
174
183
|
}>;
|
|
175
184
|
export declare const SignalTypeDefinitionSchema: z.ZodObject<{
|
|
@@ -179,6 +188,13 @@ export declare const SignalTypeDefinitionSchema: z.ZodObject<{
|
|
|
179
188
|
version: z.ZodNumber;
|
|
180
189
|
}, z.core.$strict>>;
|
|
181
190
|
dimensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
191
|
+
lifecycle: z.ZodOptional<z.ZodObject<{
|
|
192
|
+
intervalMs: z.ZodNumber;
|
|
193
|
+
scopeFields: z.ZodArray<z.ZodString>;
|
|
194
|
+
targetSampledPerScopeShard: z.ZodNumber;
|
|
195
|
+
timeoutMs: z.ZodNumber;
|
|
196
|
+
windowMs: z.ZodNumber;
|
|
197
|
+
}, z.core.$strict>>;
|
|
182
198
|
partitionKey: z.ZodOptional<z.ZodString>;
|
|
183
199
|
presence: z.ZodOptional<z.ZodObject<{
|
|
184
200
|
immutableDimensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -188,6 +204,7 @@ export declare const SignalTypeDefinitionSchema: z.ZodObject<{
|
|
|
188
204
|
}, z.core.$strict>>;
|
|
189
205
|
reducer: z.ZodEnum<{
|
|
190
206
|
"distinct-per-window": "distinct-per-window";
|
|
207
|
+
lifecycle: "lifecycle";
|
|
191
208
|
presence: "presence";
|
|
192
209
|
}>;
|
|
193
210
|
shardCount: z.ZodNumber;
|
|
@@ -282,8 +299,98 @@ export declare const RoutedMetricPulseSchema: z.ZodObject<{
|
|
|
282
299
|
windowStartAt: z.ZodOptional<z.ZodString>;
|
|
283
300
|
}, z.core.$strict>;
|
|
284
301
|
}, z.core.$strict>;
|
|
302
|
+
export declare const LifecyclePulseTargetSchema: z.ZodObject<{
|
|
303
|
+
generation: z.ZodNumber;
|
|
304
|
+
groupId: z.ZodString;
|
|
305
|
+
intervalMs: z.ZodNumber;
|
|
306
|
+
organizationId: z.ZodString;
|
|
307
|
+
ownerId: z.ZodString;
|
|
308
|
+
phase: z.ZodEnum<{
|
|
309
|
+
observe: "observe";
|
|
310
|
+
start: "start";
|
|
311
|
+
}>;
|
|
312
|
+
refs: z.ZodArray<z.ZodObject<{
|
|
313
|
+
signalId: z.ZodString;
|
|
314
|
+
dimension: z.ZodString;
|
|
315
|
+
}, z.core.$strict>>;
|
|
316
|
+
scopeId: z.ZodString;
|
|
317
|
+
sessionStartedAt: z.ZodString;
|
|
318
|
+
shardCount: z.ZodNumber;
|
|
319
|
+
shardIndex: z.ZodNumber;
|
|
320
|
+
targetSampledPerScopeShard: z.ZodNumber;
|
|
321
|
+
timeoutMs: z.ZodNumber;
|
|
322
|
+
windowMs: z.ZodNumber;
|
|
323
|
+
}, z.core.$strict>;
|
|
324
|
+
export declare const RoutedLifecyclePulseSchema: z.ZodObject<{
|
|
325
|
+
pulse: z.ZodObject<{
|
|
326
|
+
data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
327
|
+
eventId: z.ZodOptional<z.ZodString>;
|
|
328
|
+
id: z.ZodString;
|
|
329
|
+
organizationId: z.ZodString;
|
|
330
|
+
sampling: z.ZodDefault<z.ZodObject<{
|
|
331
|
+
applied: z.ZodBoolean;
|
|
332
|
+
appliedKeepProbability: z.ZodNumber;
|
|
333
|
+
effectiveKeepProbability: z.ZodNumber;
|
|
334
|
+
ingressApplied: z.ZodBoolean;
|
|
335
|
+
ingressKeepProbability: z.ZodNumber;
|
|
336
|
+
ingressMethod: z.ZodEnum<{
|
|
337
|
+
none: "none";
|
|
338
|
+
"pulse-hash-v1": "pulse-hash-v1";
|
|
339
|
+
}>;
|
|
340
|
+
knownKeepProbability: z.ZodNullable<z.ZodNumber>;
|
|
341
|
+
method: z.ZodEnum<{
|
|
342
|
+
none: "none";
|
|
343
|
+
"pulse-hash-v1": "pulse-hash-v1";
|
|
344
|
+
}>;
|
|
345
|
+
policyId: z.ZodNullable<z.ZodString>;
|
|
346
|
+
policyRevision: z.ZodNullable<z.ZodNumber>;
|
|
347
|
+
unit: z.ZodEnum<{
|
|
348
|
+
member: "member";
|
|
349
|
+
pulse: "pulse";
|
|
350
|
+
}>;
|
|
351
|
+
}, z.core.$strict>>;
|
|
352
|
+
source: z.ZodString;
|
|
353
|
+
subject: z.ZodOptional<z.ZodObject<{
|
|
354
|
+
id: z.ZodString;
|
|
355
|
+
type: z.ZodString;
|
|
356
|
+
}, z.core.$strip>>;
|
|
357
|
+
type: z.ZodString;
|
|
358
|
+
typeVersion: z.ZodNumber;
|
|
359
|
+
occurredAt: z.ZodString;
|
|
360
|
+
receivedAt: z.ZodString;
|
|
361
|
+
}, z.core.$strict>;
|
|
362
|
+
target: z.ZodObject<{
|
|
363
|
+
generation: z.ZodNumber;
|
|
364
|
+
groupId: z.ZodString;
|
|
365
|
+
intervalMs: z.ZodNumber;
|
|
366
|
+
organizationId: z.ZodString;
|
|
367
|
+
ownerId: z.ZodString;
|
|
368
|
+
phase: z.ZodEnum<{
|
|
369
|
+
observe: "observe";
|
|
370
|
+
start: "start";
|
|
371
|
+
}>;
|
|
372
|
+
refs: z.ZodArray<z.ZodObject<{
|
|
373
|
+
signalId: z.ZodString;
|
|
374
|
+
dimension: z.ZodString;
|
|
375
|
+
}, z.core.$strict>>;
|
|
376
|
+
scopeId: z.ZodString;
|
|
377
|
+
sessionStartedAt: z.ZodString;
|
|
378
|
+
shardCount: z.ZodNumber;
|
|
379
|
+
shardIndex: z.ZodNumber;
|
|
380
|
+
targetSampledPerScopeShard: z.ZodNumber;
|
|
381
|
+
timeoutMs: z.ZodNumber;
|
|
382
|
+
windowMs: z.ZodNumber;
|
|
383
|
+
}, z.core.$strict>;
|
|
384
|
+
}, z.core.$strict>;
|
|
385
|
+
export declare const LifecycleDecisionSchema: z.ZodObject<{
|
|
386
|
+
accepted: z.ZodBoolean;
|
|
387
|
+
nextHeartbeatAtMs: z.ZodNumber;
|
|
388
|
+
probability: z.ZodNumber;
|
|
389
|
+
selected: z.ZodBoolean;
|
|
390
|
+
}, z.core.$strict>;
|
|
285
391
|
export declare const MetricSnapshotSchema: z.ZodObject<{
|
|
286
392
|
estimatedCount: z.ZodNumber;
|
|
393
|
+
varianceEstimate: z.ZodOptional<z.ZodNumber>;
|
|
287
394
|
generation: z.ZodNumber;
|
|
288
395
|
kind: z.ZodEnum<{
|
|
289
396
|
"distinct-per-window": "distinct-per-window";
|
|
@@ -322,6 +429,14 @@ export declare const SdkPulseTypeSchema: z.ZodObject<{
|
|
|
322
429
|
reliable: "reliable";
|
|
323
430
|
}>;
|
|
324
431
|
id: z.ZodString;
|
|
432
|
+
lifecycle: z.ZodOptional<z.ZodObject<{
|
|
433
|
+
groupId: z.ZodString;
|
|
434
|
+
phase: z.ZodEnum<{
|
|
435
|
+
observe: "observe";
|
|
436
|
+
start: "start";
|
|
437
|
+
}>;
|
|
438
|
+
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
439
|
+
}, z.core.$strict>>;
|
|
325
440
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
326
441
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
|
327
442
|
sampling: z.ZodOptional<z.ZodObject<{
|
|
@@ -359,6 +474,14 @@ export declare const SdkConfigurationSchema: z.ZodObject<{
|
|
|
359
474
|
reliable: "reliable";
|
|
360
475
|
}>;
|
|
361
476
|
id: z.ZodString;
|
|
477
|
+
lifecycle: z.ZodOptional<z.ZodObject<{
|
|
478
|
+
groupId: z.ZodString;
|
|
479
|
+
phase: z.ZodEnum<{
|
|
480
|
+
observe: "observe";
|
|
481
|
+
start: "start";
|
|
482
|
+
}>;
|
|
483
|
+
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
484
|
+
}, z.core.$strict>>;
|
|
362
485
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
363
486
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
|
364
487
|
sampling: z.ZodOptional<z.ZodObject<{
|
|
@@ -395,6 +518,7 @@ export declare const LivePulseTargetSchema: z.ZodObject<{
|
|
|
395
518
|
organizationId: z.ZodString;
|
|
396
519
|
reducer: z.ZodEnum<{
|
|
397
520
|
"distinct-per-window": "distinct-per-window";
|
|
521
|
+
lifecycle: "lifecycle";
|
|
398
522
|
presence: "presence";
|
|
399
523
|
}>;
|
|
400
524
|
shardId: z.ZodString;
|
|
@@ -447,6 +571,7 @@ export declare const RoutedLivePulseSchema: z.ZodObject<{
|
|
|
447
571
|
organizationId: z.ZodString;
|
|
448
572
|
reducer: z.ZodEnum<{
|
|
449
573
|
"distinct-per-window": "distinct-per-window";
|
|
574
|
+
lifecycle: "lifecycle";
|
|
450
575
|
presence: "presence";
|
|
451
576
|
}>;
|
|
452
577
|
shardId: z.ZodString;
|
|
@@ -542,3 +667,6 @@ export type LiveShardSnapshot = z.infer<typeof LiveShardSnapshotSchema>;
|
|
|
542
667
|
export type MetricPulseTarget = z.infer<typeof MetricPulseTargetSchema>;
|
|
543
668
|
export type MetricSnapshot = z.infer<typeof MetricSnapshotSchema>;
|
|
544
669
|
export type RoutedMetricPulse = z.infer<typeof RoutedMetricPulseSchema>;
|
|
670
|
+
export type RoutedLifecyclePulse = z.infer<typeof RoutedLifecyclePulseSchema>;
|
|
671
|
+
export type LifecyclePulseTarget = z.infer<typeof LifecyclePulseTargetSchema>;
|
|
672
|
+
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
|
@@ -178,6 +178,11 @@ var PulseDeliverySchema = z2.enum(["live", "reliable"]);
|
|
|
178
178
|
var PulseTypeDefinitionSchema = z2.object({
|
|
179
179
|
delivery: PulseDeliverySchema,
|
|
180
180
|
id: z2.string().trim().min(1).max(100),
|
|
181
|
+
lifecycle: z2.object({
|
|
182
|
+
groupId: z2.string().trim().min(1).max(100),
|
|
183
|
+
phase: z2.enum(["start", "observe"]),
|
|
184
|
+
intervalMs: z2.number().int().min(1e3).max(864e5).optional()
|
|
185
|
+
}).strict().optional(),
|
|
181
186
|
payloadSchema: z2.record(z2.string(), z2.unknown()),
|
|
182
187
|
requiresExplicitId: z2.boolean().optional(),
|
|
183
188
|
sampling: z2.object({
|
|
@@ -196,8 +201,11 @@ var PulseTypeDefinitionSchema = z2.object({
|
|
|
196
201
|
if (definition.sampling?.unit === "pulse" && definition.sampling.keyField !== null) {
|
|
197
202
|
context.addIssue({ code: "custom", path: ["sampling", "keyField"], message: "Pulse sampling must not define keyField." });
|
|
198
203
|
}
|
|
204
|
+
if (definition.lifecycle && (definition.samplingAllowed || !definition.requiresExplicitId || definition.delivery !== "live" || definition.lifecycle.phase === "observe" !== Boolean(definition.lifecycle.intervalMs))) {
|
|
205
|
+
context.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle pulses need explicit IDs, live delivery, and an interval only for observations." });
|
|
206
|
+
}
|
|
199
207
|
});
|
|
200
|
-
var SignalReducerSchema = z2.enum(["presence", "distinct-per-window"]);
|
|
208
|
+
var SignalReducerSchema = z2.enum(["presence", "distinct-per-window", "lifecycle"]);
|
|
201
209
|
var SignalTypeDefinitionSchema = z2.object({
|
|
202
210
|
id: z2.string().trim().min(1).max(100),
|
|
203
211
|
inputPulseTypes: z2.array(z2.object({
|
|
@@ -205,6 +213,13 @@ var SignalTypeDefinitionSchema = z2.object({
|
|
|
205
213
|
version: z2.number().int().positive()
|
|
206
214
|
}).strict()).min(1),
|
|
207
215
|
dimensions: z2.array(z2.string().trim().min(1).max(100)).optional(),
|
|
216
|
+
lifecycle: z2.object({
|
|
217
|
+
intervalMs: z2.number().int().min(1e3).max(864e5),
|
|
218
|
+
scopeFields: z2.array(z2.string().trim().min(1).max(100)).min(1),
|
|
219
|
+
targetSampledPerScopeShard: z2.number().int().positive(),
|
|
220
|
+
timeoutMs: z2.number().int().min(1e3).max(864e5),
|
|
221
|
+
windowMs: z2.number().int().min(1e3).max(864e5)
|
|
222
|
+
}).strict().optional(),
|
|
208
223
|
partitionKey: z2.string().trim().min(1).max(200).optional(),
|
|
209
224
|
presence: z2.object({
|
|
210
225
|
immutableDimensions: z2.array(z2.string().trim().min(1).max(100)).optional(),
|
|
@@ -228,6 +243,9 @@ var SignalTypeDefinitionSchema = z2.object({
|
|
|
228
243
|
if (definition.reducer === "distinct-per-window" && !definition.windowMs) {
|
|
229
244
|
context.addIssue({ code: "custom", path: ["windowMs"], message: "Windowed distinct needs a window." });
|
|
230
245
|
}
|
|
246
|
+
if (definition.reducer === "lifecycle" && (!definition.lifecycle || !definition.partitionKey || !definition.dimensions?.length)) {
|
|
247
|
+
context.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle needs a configuration, partition key and dimensions." });
|
|
248
|
+
}
|
|
231
249
|
const schedule = definition.shardSchedule ?? [];
|
|
232
250
|
if (schedule.length > 0 && (!definition.partitionKey || !definition.dimensions?.length)) {
|
|
233
251
|
context.addIssue({ code: "custom", path: ["partitionKey"], message: "Scheduled routing needs partitionKey and dimensions." });
|
|
@@ -235,9 +253,10 @@ var SignalTypeDefinitionSchema = z2.object({
|
|
|
235
253
|
if (schedule.length > 0 && definition.reducer === "presence" && !definition.presence?.startedAtField) {
|
|
236
254
|
context.addIssue({ code: "custom", path: ["presence", "startedAtField"], message: "Scheduled presence needs a stable start time." });
|
|
237
255
|
}
|
|
238
|
-
|
|
256
|
+
const scheduledWindowMs = definition.windowMs ?? definition.lifecycle?.windowMs;
|
|
257
|
+
if (scheduledWindowMs) {
|
|
239
258
|
for (let index = 0; index < schedule.length; index += 1) {
|
|
240
|
-
if (Date.parse(schedule[index].effectiveFrom) %
|
|
259
|
+
if (Date.parse(schedule[index].effectiveFrom) % scheduledWindowMs !== 0) {
|
|
241
260
|
context.addIssue({ code: "custom", path: ["shardSchedule", index], message: "Window generation must start on a window boundary." });
|
|
242
261
|
}
|
|
243
262
|
}
|
|
@@ -282,8 +301,39 @@ var RoutedMetricPulseSchema = z2.object({
|
|
|
282
301
|
pulse: RawPulseSchema,
|
|
283
302
|
target: MetricPulseTargetSchema
|
|
284
303
|
}).strict();
|
|
304
|
+
var LifecyclePulseTargetSchema = z2.object({
|
|
305
|
+
generation: z2.number().int().positive(),
|
|
306
|
+
groupId: z2.string().trim().min(1).max(100),
|
|
307
|
+
intervalMs: z2.number().int().positive(),
|
|
308
|
+
organizationId: z2.string().trim().min(1).max(100),
|
|
309
|
+
ownerId: z2.string().trim().min(1).max(500),
|
|
310
|
+
phase: z2.enum(["start", "observe"]),
|
|
311
|
+
refs: z2.array(MetricSignalRefSchema).min(1),
|
|
312
|
+
scopeId: z2.string().trim().min(1).max(500),
|
|
313
|
+
sessionStartedAt: isoDateTime,
|
|
314
|
+
shardCount: z2.number().int().min(1).max(65536),
|
|
315
|
+
shardIndex: z2.number().int().nonnegative(),
|
|
316
|
+
targetSampledPerScopeShard: z2.number().int().positive(),
|
|
317
|
+
timeoutMs: z2.number().int().positive(),
|
|
318
|
+
windowMs: z2.number().int().positive()
|
|
319
|
+
}).strict().superRefine((target, context) => {
|
|
320
|
+
if (target.shardIndex >= target.shardCount) {
|
|
321
|
+
context.addIssue({ code: "custom", path: ["shardIndex"], message: "Shard index exceeds shard count." });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
var RoutedLifecyclePulseSchema = z2.object({
|
|
325
|
+
pulse: RawPulseSchema,
|
|
326
|
+
target: LifecyclePulseTargetSchema
|
|
327
|
+
}).strict();
|
|
328
|
+
var LifecycleDecisionSchema = z2.object({
|
|
329
|
+
accepted: z2.boolean(),
|
|
330
|
+
nextHeartbeatAtMs: z2.number().int().nonnegative(),
|
|
331
|
+
probability: z2.number().positive().max(1),
|
|
332
|
+
selected: z2.boolean()
|
|
333
|
+
}).strict();
|
|
285
334
|
var MetricSnapshotSchema = z2.object({
|
|
286
335
|
estimatedCount: z2.number().finite().nonnegative(),
|
|
336
|
+
varianceEstimate: z2.number().finite().nonnegative().optional(),
|
|
287
337
|
generation: z2.number().int().positive(),
|
|
288
338
|
kind: z2.enum(["presence", "distinct-per-window"]),
|
|
289
339
|
observedAt: isoDateTime,
|
|
@@ -435,6 +485,47 @@ var CloudflareSdkConfigurationCache = class {
|
|
|
435
485
|
}
|
|
436
486
|
};
|
|
437
487
|
|
|
488
|
+
// src/sessionDecisionCache.ts
|
|
489
|
+
var MemorySessionDecisionCache = class {
|
|
490
|
+
constructor(maxEntries = 1e4) {
|
|
491
|
+
this.maxEntries = maxEntries;
|
|
492
|
+
}
|
|
493
|
+
maxEntries;
|
|
494
|
+
values = /* @__PURE__ */ new Map();
|
|
495
|
+
async get(key) {
|
|
496
|
+
return this.values.get(key);
|
|
497
|
+
}
|
|
498
|
+
async set(key, decision) {
|
|
499
|
+
this.values.delete(key);
|
|
500
|
+
this.values.set(key, decision);
|
|
501
|
+
if (this.values.size > this.maxEntries) this.values.delete(this.values.keys().next().value);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
var CloudflareSessionDecisionCache = class {
|
|
505
|
+
constructor(cache, namespace = "https://session-decision-cache.ultd.invalid") {
|
|
506
|
+
this.cache = cache;
|
|
507
|
+
this.namespace = namespace;
|
|
508
|
+
}
|
|
509
|
+
cache;
|
|
510
|
+
namespace;
|
|
511
|
+
request(key) {
|
|
512
|
+
return new Request(`${this.namespace}/${key}`);
|
|
513
|
+
}
|
|
514
|
+
async get(key) {
|
|
515
|
+
const response = await this.cache.match(this.request(key));
|
|
516
|
+
if (!response) return void 0;
|
|
517
|
+
const value = await response.json();
|
|
518
|
+
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;
|
|
519
|
+
return value;
|
|
520
|
+
}
|
|
521
|
+
async set(key, decision) {
|
|
522
|
+
const seconds = Math.max(1, Math.floor((decision.expiresAtMs - Date.now()) / 1e3));
|
|
523
|
+
await this.cache.put(this.request(key), new Response(JSON.stringify(decision), {
|
|
524
|
+
headers: { "cache-control": `public, max-age=${seconds}`, "content-type": "application/json" }
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
438
529
|
// src/cache.ts
|
|
439
530
|
var MemorySamplingPolicyCache = class {
|
|
440
531
|
policies = /* @__PURE__ */ new Map();
|
|
@@ -499,6 +590,8 @@ var UnlimitedClient = class {
|
|
|
499
590
|
baseUrl;
|
|
500
591
|
source;
|
|
501
592
|
cache;
|
|
593
|
+
sessionCache;
|
|
594
|
+
sessionMemory = new MemorySessionDecisionCache();
|
|
502
595
|
fetcher;
|
|
503
596
|
now;
|
|
504
597
|
randomUUID;
|
|
@@ -516,6 +609,7 @@ var UnlimitedClient = class {
|
|
|
516
609
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
517
610
|
this.source = options.source;
|
|
518
611
|
this.cache = options.cache ?? new MemorySdkConfigurationCache();
|
|
612
|
+
this.sessionCache = options.sessionCache ?? this.sessionMemory;
|
|
519
613
|
this.fetcher = options.fetch ?? fetch;
|
|
520
614
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
521
615
|
this.randomUUID = options.randomUUID ?? (() => crypto.randomUUID());
|
|
@@ -584,6 +678,28 @@ var UnlimitedClient = class {
|
|
|
584
678
|
});
|
|
585
679
|
return this.refreshing;
|
|
586
680
|
}
|
|
681
|
+
async decisionKey(groupId, organizationId, sessionId) {
|
|
682
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(
|
|
683
|
+
`${await this.cacheKey}\0${groupId}\0${organizationId}\0${sessionId}`
|
|
684
|
+
));
|
|
685
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
686
|
+
}
|
|
687
|
+
async getDecision(key) {
|
|
688
|
+
const memory = await this.sessionMemory.get(key);
|
|
689
|
+
if (memory && memory.expiresAtMs > this.now().getTime()) return memory;
|
|
690
|
+
const cached = await this.sessionCache.get(key).catch(() => void 0);
|
|
691
|
+
if (cached && cached.expiresAtMs > this.now().getTime()) {
|
|
692
|
+
await this.sessionMemory.set(key, cached);
|
|
693
|
+
return cached;
|
|
694
|
+
}
|
|
695
|
+
return void 0;
|
|
696
|
+
}
|
|
697
|
+
async putDecision(key, decision) {
|
|
698
|
+
await this.sessionMemory.set(key, decision);
|
|
699
|
+
if (this.sessionCache !== this.sessionMemory) {
|
|
700
|
+
await this.sessionCache.set(key, decision).catch(() => void 0);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
587
703
|
async emit(input) {
|
|
588
704
|
const loaded = await this.configuration();
|
|
589
705
|
const type = loaded.types.get(typeKey(input.pulse.type, input.pulse.version));
|
|
@@ -602,9 +718,36 @@ var UnlimitedClient = class {
|
|
|
602
718
|
if (!type.entry.enabled) {
|
|
603
719
|
return { accepted: false, disabled: true, pulseId: id, sampledOut: false };
|
|
604
720
|
}
|
|
721
|
+
const lifecycle = type.entry.definition.lifecycle;
|
|
722
|
+
const decisionKey = lifecycle ? await this.decisionKey(lifecycle.groupId, input.organizationId, id) : void 0;
|
|
723
|
+
if (lifecycle?.phase === "observe" && decisionKey) {
|
|
724
|
+
const decision = await this.getDecision(decisionKey);
|
|
725
|
+
if (!decision) {
|
|
726
|
+
const start = [...loaded.types.values()].find((candidate) => candidate.entry.definition.lifecycle?.groupId === lifecycle.groupId && candidate.entry.definition.lifecycle.phase === "start");
|
|
727
|
+
if (!start) throw new Error(`Lifecycle group ${lifecycle.groupId} has no start PulseType.`);
|
|
728
|
+
return this.emit({
|
|
729
|
+
...input,
|
|
730
|
+
pulse: { ...input.pulse, type: start.entry.definition.id, version: start.entry.definition.version }
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
if (decision && !decision.selected) {
|
|
734
|
+
return { accepted: false, pulseId: id, sampledOut: true, selected: false, probability: decision.probability };
|
|
735
|
+
}
|
|
736
|
+
if (decision && this.now().getTime() < decision.nextHeartbeatAtMs && decision.lastVideoId === payload.videoId) {
|
|
737
|
+
return {
|
|
738
|
+
accepted: false,
|
|
739
|
+
pulseId: id,
|
|
740
|
+
sampledOut: false,
|
|
741
|
+
throttled: true,
|
|
742
|
+
selected: true,
|
|
743
|
+
probability: decision.probability,
|
|
744
|
+
nextHeartbeatAtMs: decision.nextHeartbeatAtMs
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
605
748
|
const policy = type.entry.samplingPolicy;
|
|
606
749
|
let sampling = NoSamplingContext;
|
|
607
|
-
if (policy) {
|
|
750
|
+
if (policy && !lifecycle) {
|
|
608
751
|
sampling = {
|
|
609
752
|
applied: policy.keepProbability < 1,
|
|
610
753
|
appliedKeepProbability: policy.keepProbability,
|
|
@@ -639,6 +782,26 @@ var UnlimitedClient = class {
|
|
|
639
782
|
});
|
|
640
783
|
if (!response.ok) throw new Error(`Pulse request failed with ${response.status}.`);
|
|
641
784
|
const body = await response.json();
|
|
785
|
+
if (lifecycle && decisionKey) {
|
|
786
|
+
if (typeof body.selected !== "boolean" || typeof body.probability !== "number" || body.probability <= 0 || body.probability > 1 || typeof body.nextHeartbeatAtMs !== "number") {
|
|
787
|
+
throw new Error("Lifecycle response did not contain a valid session decision.");
|
|
788
|
+
}
|
|
789
|
+
await this.putDecision(decisionKey, {
|
|
790
|
+
selected: body.selected,
|
|
791
|
+
probability: body.probability,
|
|
792
|
+
nextHeartbeatAtMs: body.nextHeartbeatAtMs,
|
|
793
|
+
lastVideoId: body.selected && typeof payload.videoId === "string" ? payload.videoId : void 0,
|
|
794
|
+
expiresAtMs: this.now().getTime() + 24 * 60 * 60 * 1e3
|
|
795
|
+
});
|
|
796
|
+
return {
|
|
797
|
+
accepted: body.accepted !== false,
|
|
798
|
+
pulseId: id,
|
|
799
|
+
sampledOut: lifecycle.phase === "observe" && !body.selected,
|
|
800
|
+
selected: body.selected,
|
|
801
|
+
probability: body.probability,
|
|
802
|
+
nextHeartbeatAtMs: body.nextHeartbeatAtMs
|
|
803
|
+
};
|
|
804
|
+
}
|
|
642
805
|
return {
|
|
643
806
|
accepted: body.accepted !== false,
|
|
644
807
|
...body.disabled === true ? { disabled: true } : {},
|
|
@@ -650,7 +813,9 @@ var UnlimitedClient = class {
|
|
|
650
813
|
export {
|
|
651
814
|
CloudflareSamplingPolicyCache,
|
|
652
815
|
CloudflareSdkConfigurationCache,
|
|
816
|
+
CloudflareSessionDecisionCache,
|
|
653
817
|
MemorySamplingPolicyCache,
|
|
654
818
|
MemorySdkConfigurationCache,
|
|
819
|
+
MemorySessionDecisionCache,
|
|
655
820
|
UnlimitedClient
|
|
656
821
|
};
|
|
@@ -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
|
+
}
|