@ultdai/pulse 0.2.0 → 0.3.1
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 +32 -27
- package/dist/contracts/index.d.ts +123 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +170 -21
- package/dist/sessionDecisionCache.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,49 +7,54 @@ npm install @ultdai/pulse
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
|
-
import {
|
|
11
|
-
CloudflareSdkConfigurationCache,
|
|
12
|
-
CloudflareSessionDecisionCache,
|
|
13
|
-
UnlimitedClient,
|
|
14
|
-
} from '@ultdai/pulse'
|
|
10
|
+
import { CloudflareSdkConfigurationCache, UnlimitedClient } from '@ultdai/pulse'
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
interface Env {
|
|
13
|
+
ULTD_API_TOKEN: string
|
|
14
|
+
ULTD_API_URL: string
|
|
15
|
+
ULTD_ORGANIZATION_ID: string
|
|
16
|
+
}
|
|
17
17
|
|
|
18
18
|
let client: UnlimitedClient | undefined
|
|
19
19
|
|
|
20
20
|
export default {
|
|
21
21
|
async fetch(request: Request, env: Env, context: ExecutionContext) {
|
|
22
|
-
const session = getPlaybackSession(request) // stabilní ID a čas vzniku v Unixových milisekundách
|
|
23
22
|
client ??= new UnlimitedClient({
|
|
24
23
|
apiToken: env.ULTD_API_TOKEN,
|
|
25
|
-
baseUrl:
|
|
24
|
+
baseUrl: env.ULTD_API_URL,
|
|
26
25
|
cache: new CloudflareSdkConfigurationCache(caches.default),
|
|
27
|
-
|
|
28
|
-
source: 'stream-worker',
|
|
26
|
+
source: 'my-server',
|
|
29
27
|
})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
|
|
29
|
+
// Inicializace načte aktuální povolené typy a jejich pravidla. Další
|
|
30
|
+
// volání používají platnou cache; po expiraci se katalog obnoví.
|
|
31
|
+
context.waitUntil((async () => {
|
|
32
|
+
await client.initialize('') // výběr organizace je rezervovaný pro budoucí katalog
|
|
33
|
+
await client.emit({
|
|
34
|
+
organizationId: env.ULTD_ORGANIZATION_ID,
|
|
35
|
+
occurredAtMs: Date.now(),
|
|
36
|
+
pulse: {
|
|
37
|
+
type: 'your.configured.pulse-type',
|
|
38
|
+
version: 1,
|
|
39
|
+
id: crypto.randomUUID(),
|
|
40
|
+
payload: { value: 1 },
|
|
41
41
|
},
|
|
42
|
-
}
|
|
43
|
-
}).catch((error) => console.error('ULTD pulse failed', error)))
|
|
42
|
+
})
|
|
43
|
+
})().catch((error) => console.error('ULTD pulse failed', error)))
|
|
44
|
+
|
|
44
45
|
return new Response('ok')
|
|
45
46
|
},
|
|
46
47
|
}
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
Ukázkový PulseType výše je jen zástupný název. Skutečný název, verzi, identitu a pole payloadu určuje vaše konfigurace v ULTD. Opakované pulsy, které patří ke stejné sledované entitě, musí používat stabilní `pulse.id`; jednorázová událost potřebuje ID stabilní při opakovaném odeslání.
|
|
51
|
+
|
|
52
|
+
`apiToken` 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.
|
|
53
|
+
|
|
54
|
+
`await client.initialize(token)` načte povolené PulseTypes, jejich schémata a pravidla měření z autorizovaného ULTD API nebo z dosud platné sdílené cache. Argument `token` je rezervovaný pro budoucí výběr organizace a jejího katalogu; v tomto POC se nepoužívá a smí být `''`. Není to API klíč ani náhrada za `apiToken`. Samotný konstruktor nemůže čekat na síť. Pokud inicializaci vynecháte, `emit()` konfiguraci načte před prvním pulsem. Neznámý typ nebo nevalidní `pulse.payload` SDK odmítne lokálně. Vypnutý nebo nevzorkovaný puls neodešle. 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.
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
Pro opakované pulsy může konfigurace definovat lifecycle s úvodním a průběžným PulseTypem. SDK při prvním volání odešle úvodní puls, uloží rozhodnutí o dalším měření a dodržuje interval z konfigurace. `CloudflareSessionDecisionCache(caches.default)` lze předat jako `sessionCache`, aby se toto rozhodnutí sdílelo mezi instancemi Workeru; bez něj se používá paměť instance. Cache je pouze optimalizace a její ztráta nezakládá novou logickou entitu.
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
SDK obsahuje obecné 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á. SDK neobsahuje zákaznické PulseTypes. Samotné zveřejnění SDK neposkytuje oprávnění k API.
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
Dnešní POC má na backendu jeden technický API klíč a jeden katalog definic. Rozdělení katalogu podle organizace je další serverový krok; rozhraní SDK už s jejím tokenem při inicializaci počítá.
|
|
@@ -163,6 +163,7 @@ export declare const PulseTypeDefinitionSchema: z.ZodObject<{
|
|
|
163
163
|
start: "start";
|
|
164
164
|
}>;
|
|
165
165
|
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
166
|
+
reportOnChangeFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
166
167
|
}, z.core.$strict>>;
|
|
167
168
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
168
169
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -177,11 +178,37 @@ export declare const PulseTypeDefinitionSchema: z.ZodObject<{
|
|
|
177
178
|
version: z.ZodNumber;
|
|
178
179
|
}, z.core.$strict>;
|
|
179
180
|
export declare const SignalReducerSchema: z.ZodEnum<{
|
|
181
|
+
"additive-window": "additive-window";
|
|
180
182
|
"distinct-per-window": "distinct-per-window";
|
|
181
183
|
lifecycle: "lifecycle";
|
|
182
184
|
presence: "presence";
|
|
183
185
|
}>;
|
|
184
186
|
export declare const SignalTypeDefinitionSchema: z.ZodObject<{
|
|
187
|
+
additive: z.ZodOptional<z.ZodObject<{
|
|
188
|
+
baseScopeFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
189
|
+
cohortAtField: z.ZodString;
|
|
190
|
+
dedupeFields: z.ZodArray<z.ZodString>;
|
|
191
|
+
dimensionSets: z.ZodArray<z.ZodObject<{
|
|
192
|
+
kind: z.ZodString;
|
|
193
|
+
fields: z.ZodArray<z.ZodString>;
|
|
194
|
+
}, z.core.$strict>>;
|
|
195
|
+
latenessMs: z.ZodNumber;
|
|
196
|
+
metricRules: z.ZodArray<z.ZodObject<{
|
|
197
|
+
name: z.ZodString;
|
|
198
|
+
op: z.ZodEnum<{
|
|
199
|
+
count: "count";
|
|
200
|
+
sum: "sum";
|
|
201
|
+
}>;
|
|
202
|
+
unitField: z.ZodOptional<z.ZodString>;
|
|
203
|
+
valueField: z.ZodOptional<z.ZodString>;
|
|
204
|
+
when: z.ZodOptional<z.ZodObject<{
|
|
205
|
+
field: z.ZodString;
|
|
206
|
+
equals: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
207
|
+
}, z.core.$strict>>;
|
|
208
|
+
}, z.core.$strict>>;
|
|
209
|
+
primaryMetric: z.ZodString;
|
|
210
|
+
stableScopeField: z.ZodOptional<z.ZodString>;
|
|
211
|
+
}, z.core.$strict>>;
|
|
185
212
|
id: z.ZodString;
|
|
186
213
|
inputPulseTypes: z.ZodArray<z.ZodObject<{
|
|
187
214
|
id: z.ZodString;
|
|
@@ -203,6 +230,7 @@ export declare const SignalTypeDefinitionSchema: z.ZodObject<{
|
|
|
203
230
|
timeoutMs: z.ZodNumber;
|
|
204
231
|
}, z.core.$strict>>;
|
|
205
232
|
reducer: z.ZodEnum<{
|
|
233
|
+
"additive-window": "additive-window";
|
|
206
234
|
"distinct-per-window": "distinct-per-window";
|
|
207
235
|
lifecycle: "lifecycle";
|
|
208
236
|
presence: "presence";
|
|
@@ -229,6 +257,7 @@ export declare const MetricPulseTargetSchema: z.ZodObject<{
|
|
|
229
257
|
refs: z.ZodArray<z.ZodObject<{
|
|
230
258
|
signalId: z.ZodString;
|
|
231
259
|
dimension: z.ZodString;
|
|
260
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
232
261
|
}, z.core.$strict>>;
|
|
233
262
|
sessionStartedAt: z.ZodString;
|
|
234
263
|
shardCount: z.ZodNumber;
|
|
@@ -289,6 +318,7 @@ export declare const RoutedMetricPulseSchema: z.ZodObject<{
|
|
|
289
318
|
refs: z.ZodArray<z.ZodObject<{
|
|
290
319
|
signalId: z.ZodString;
|
|
291
320
|
dimension: z.ZodString;
|
|
321
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
292
322
|
}, z.core.$strict>>;
|
|
293
323
|
sessionStartedAt: z.ZodString;
|
|
294
324
|
shardCount: z.ZodNumber;
|
|
@@ -299,6 +329,86 @@ export declare const RoutedMetricPulseSchema: z.ZodObject<{
|
|
|
299
329
|
windowStartAt: z.ZodOptional<z.ZodString>;
|
|
300
330
|
}, z.core.$strict>;
|
|
301
331
|
}, z.core.$strict>;
|
|
332
|
+
/** A pulse carries a bounded vector of exact, non-sampled metric additions. */
|
|
333
|
+
export declare const AdditivePulseTargetSchema: z.ZodObject<{
|
|
334
|
+
dedupeKey: z.ZodString;
|
|
335
|
+
generation: z.ZodNumber;
|
|
336
|
+
latenessMs: z.ZodNumber;
|
|
337
|
+
metrics: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
338
|
+
organizationId: z.ZodString;
|
|
339
|
+
ownerId: z.ZodString;
|
|
340
|
+
primaryMetric: z.ZodString;
|
|
341
|
+
refs: z.ZodArray<z.ZodObject<{
|
|
342
|
+
signalId: z.ZodString;
|
|
343
|
+
dimension: z.ZodString;
|
|
344
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
345
|
+
}, z.core.$strict>>;
|
|
346
|
+
shardCount: z.ZodNumber;
|
|
347
|
+
shardIndex: z.ZodNumber;
|
|
348
|
+
signalTypeId: z.ZodString;
|
|
349
|
+
stableScopeKey: z.ZodOptional<z.ZodString>;
|
|
350
|
+
windowEndAt: z.ZodString;
|
|
351
|
+
windowStartAt: z.ZodString;
|
|
352
|
+
}, z.core.$strict>;
|
|
353
|
+
export declare const RoutedAdditivePulseSchema: z.ZodObject<{
|
|
354
|
+
pulse: z.ZodObject<{
|
|
355
|
+
data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
356
|
+
eventId: z.ZodOptional<z.ZodString>;
|
|
357
|
+
id: z.ZodString;
|
|
358
|
+
organizationId: z.ZodString;
|
|
359
|
+
sampling: z.ZodDefault<z.ZodObject<{
|
|
360
|
+
applied: z.ZodBoolean;
|
|
361
|
+
appliedKeepProbability: z.ZodNumber;
|
|
362
|
+
effectiveKeepProbability: z.ZodNumber;
|
|
363
|
+
ingressApplied: z.ZodBoolean;
|
|
364
|
+
ingressKeepProbability: z.ZodNumber;
|
|
365
|
+
ingressMethod: z.ZodEnum<{
|
|
366
|
+
none: "none";
|
|
367
|
+
"pulse-hash-v1": "pulse-hash-v1";
|
|
368
|
+
}>;
|
|
369
|
+
knownKeepProbability: z.ZodNullable<z.ZodNumber>;
|
|
370
|
+
method: z.ZodEnum<{
|
|
371
|
+
none: "none";
|
|
372
|
+
"pulse-hash-v1": "pulse-hash-v1";
|
|
373
|
+
}>;
|
|
374
|
+
policyId: z.ZodNullable<z.ZodString>;
|
|
375
|
+
policyRevision: z.ZodNullable<z.ZodNumber>;
|
|
376
|
+
unit: z.ZodEnum<{
|
|
377
|
+
member: "member";
|
|
378
|
+
pulse: "pulse";
|
|
379
|
+
}>;
|
|
380
|
+
}, z.core.$strict>>;
|
|
381
|
+
source: z.ZodString;
|
|
382
|
+
subject: z.ZodOptional<z.ZodObject<{
|
|
383
|
+
id: z.ZodString;
|
|
384
|
+
type: z.ZodString;
|
|
385
|
+
}, z.core.$strip>>;
|
|
386
|
+
type: z.ZodString;
|
|
387
|
+
typeVersion: z.ZodNumber;
|
|
388
|
+
occurredAt: z.ZodString;
|
|
389
|
+
receivedAt: z.ZodString;
|
|
390
|
+
}, z.core.$strict>;
|
|
391
|
+
target: z.ZodObject<{
|
|
392
|
+
dedupeKey: z.ZodString;
|
|
393
|
+
generation: z.ZodNumber;
|
|
394
|
+
latenessMs: z.ZodNumber;
|
|
395
|
+
metrics: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
396
|
+
organizationId: z.ZodString;
|
|
397
|
+
ownerId: z.ZodString;
|
|
398
|
+
primaryMetric: z.ZodString;
|
|
399
|
+
refs: z.ZodArray<z.ZodObject<{
|
|
400
|
+
signalId: z.ZodString;
|
|
401
|
+
dimension: z.ZodString;
|
|
402
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
403
|
+
}, z.core.$strict>>;
|
|
404
|
+
shardCount: z.ZodNumber;
|
|
405
|
+
shardIndex: z.ZodNumber;
|
|
406
|
+
signalTypeId: z.ZodString;
|
|
407
|
+
stableScopeKey: z.ZodOptional<z.ZodString>;
|
|
408
|
+
windowEndAt: z.ZodString;
|
|
409
|
+
windowStartAt: z.ZodString;
|
|
410
|
+
}, z.core.$strict>;
|
|
411
|
+
}, z.core.$strict>;
|
|
302
412
|
export declare const LifecyclePulseTargetSchema: z.ZodObject<{
|
|
303
413
|
generation: z.ZodNumber;
|
|
304
414
|
groupId: z.ZodString;
|
|
@@ -312,6 +422,7 @@ export declare const LifecyclePulseTargetSchema: z.ZodObject<{
|
|
|
312
422
|
refs: z.ZodArray<z.ZodObject<{
|
|
313
423
|
signalId: z.ZodString;
|
|
314
424
|
dimension: z.ZodString;
|
|
425
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
315
426
|
}, z.core.$strict>>;
|
|
316
427
|
scopeId: z.ZodString;
|
|
317
428
|
sessionStartedAt: z.ZodString;
|
|
@@ -372,6 +483,7 @@ export declare const RoutedLifecyclePulseSchema: z.ZodObject<{
|
|
|
372
483
|
refs: z.ZodArray<z.ZodObject<{
|
|
373
484
|
signalId: z.ZodString;
|
|
374
485
|
dimension: z.ZodString;
|
|
486
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
375
487
|
}, z.core.$strict>>;
|
|
376
488
|
scopeId: z.ZodString;
|
|
377
489
|
sessionStartedAt: z.ZodString;
|
|
@@ -393,15 +505,20 @@ export declare const MetricSnapshotSchema: z.ZodObject<{
|
|
|
393
505
|
varianceEstimate: z.ZodOptional<z.ZodNumber>;
|
|
394
506
|
generation: z.ZodNumber;
|
|
395
507
|
kind: z.ZodEnum<{
|
|
508
|
+
"additive-window": "additive-window";
|
|
396
509
|
"distinct-per-window": "distinct-per-window";
|
|
397
510
|
presence: "presence";
|
|
398
511
|
}>;
|
|
512
|
+
latenessMs: z.ZodOptional<z.ZodNumber>;
|
|
513
|
+
metrics: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
399
514
|
observedAt: z.ZodString;
|
|
400
515
|
organizationId: z.ZodString;
|
|
401
516
|
originId: z.ZodString;
|
|
402
517
|
recovering: z.ZodBoolean;
|
|
403
518
|
revision: z.ZodNumber;
|
|
404
519
|
sampledCount: z.ZodNumber;
|
|
520
|
+
scope: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
521
|
+
scopeKind: z.ZodOptional<z.ZodString>;
|
|
405
522
|
shardCount: z.ZodNumber;
|
|
406
523
|
signalId: z.ZodString;
|
|
407
524
|
signalTypeId: z.ZodString;
|
|
@@ -436,6 +553,7 @@ export declare const SdkPulseTypeSchema: z.ZodObject<{
|
|
|
436
553
|
start: "start";
|
|
437
554
|
}>;
|
|
438
555
|
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
556
|
+
reportOnChangeFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
439
557
|
}, z.core.$strict>>;
|
|
440
558
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
441
559
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -481,6 +599,7 @@ export declare const SdkConfigurationSchema: z.ZodObject<{
|
|
|
481
599
|
start: "start";
|
|
482
600
|
}>;
|
|
483
601
|
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
602
|
+
reportOnChangeFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
484
603
|
}, z.core.$strict>>;
|
|
485
604
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
486
605
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -517,6 +636,7 @@ export declare const LivePulseTargetSchema: z.ZodObject<{
|
|
|
517
636
|
memberId: z.ZodString;
|
|
518
637
|
organizationId: z.ZodString;
|
|
519
638
|
reducer: z.ZodEnum<{
|
|
639
|
+
"additive-window": "additive-window";
|
|
520
640
|
"distinct-per-window": "distinct-per-window";
|
|
521
641
|
lifecycle: "lifecycle";
|
|
522
642
|
presence: "presence";
|
|
@@ -570,6 +690,7 @@ export declare const RoutedLivePulseSchema: z.ZodObject<{
|
|
|
570
690
|
memberId: z.ZodString;
|
|
571
691
|
organizationId: z.ZodString;
|
|
572
692
|
reducer: z.ZodEnum<{
|
|
693
|
+
"additive-window": "additive-window";
|
|
573
694
|
"distinct-per-window": "distinct-per-window";
|
|
574
695
|
lifecycle: "lifecycle";
|
|
575
696
|
presence: "presence";
|
|
@@ -667,6 +788,8 @@ export type LiveShardSnapshot = z.infer<typeof LiveShardSnapshotSchema>;
|
|
|
667
788
|
export type MetricPulseTarget = z.infer<typeof MetricPulseTargetSchema>;
|
|
668
789
|
export type MetricSnapshot = z.infer<typeof MetricSnapshotSchema>;
|
|
669
790
|
export type RoutedMetricPulse = z.infer<typeof RoutedMetricPulseSchema>;
|
|
791
|
+
export type RoutedAdditivePulse = z.infer<typeof RoutedAdditivePulseSchema>;
|
|
792
|
+
export type AdditivePulseTarget = z.infer<typeof AdditivePulseTargetSchema>;
|
|
670
793
|
export type RoutedLifecyclePulse = z.infer<typeof RoutedLifecyclePulseSchema>;
|
|
671
794
|
export type LifecyclePulseTarget = z.infer<typeof LifecyclePulseTargetSchema>;
|
|
672
795
|
export type LifecycleDecision = z.infer<typeof LifecycleDecisionSchema>;
|
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,11 @@ export declare class UnlimitedClient {
|
|
|
49
49
|
private install;
|
|
50
50
|
private refresh;
|
|
51
51
|
private configuration;
|
|
52
|
+
/** Load the authorized, versioned PulseType catalog before producing pulses.
|
|
53
|
+
* The organization token is reserved for future catalog scoping and currently
|
|
54
|
+
* has no effect, including when it is an empty string. API authentication
|
|
55
|
+
* continues to use the apiToken supplied in the constructor. */
|
|
56
|
+
initialize(_organizationToken: string): Promise<void>;
|
|
52
57
|
private decisionKey;
|
|
53
58
|
private getDecision;
|
|
54
59
|
private putDecision;
|
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
|
|
@@ -181,7 +214,8 @@ var PulseTypeDefinitionSchema = z2.object({
|
|
|
181
214
|
lifecycle: z2.object({
|
|
182
215
|
groupId: z2.string().trim().min(1).max(100),
|
|
183
216
|
phase: z2.enum(["start", "observe"]),
|
|
184
|
-
intervalMs: z2.number().int().min(1e3).max(864e5).optional()
|
|
217
|
+
intervalMs: z2.number().int().min(1e3).max(864e5).optional(),
|
|
218
|
+
reportOnChangeFields: z2.array(z2.string().trim().min(1).max(200)).max(8).optional()
|
|
185
219
|
}).strict().optional(),
|
|
186
220
|
payloadSchema: z2.record(z2.string(), z2.unknown()),
|
|
187
221
|
requiresExplicitId: z2.boolean().optional(),
|
|
@@ -204,9 +238,47 @@ var PulseTypeDefinitionSchema = z2.object({
|
|
|
204
238
|
if (definition.lifecycle && (definition.samplingAllowed || !definition.requiresExplicitId || definition.delivery !== "live" || definition.lifecycle.phase === "observe" !== Boolean(definition.lifecycle.intervalMs))) {
|
|
205
239
|
context.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle pulses need explicit IDs, live delivery, and an interval only for observations." });
|
|
206
240
|
}
|
|
241
|
+
if (definition.lifecycle?.reportOnChangeFields) {
|
|
242
|
+
const fields = definition.lifecycle.reportOnChangeFields;
|
|
243
|
+
if (definition.lifecycle.phase !== "observe" || new Set(fields).size !== fields.length) {
|
|
244
|
+
context.addIssue({
|
|
245
|
+
code: "custom",
|
|
246
|
+
path: ["lifecycle", "reportOnChangeFields"],
|
|
247
|
+
message: "Change-trigger fields must be unique and belong to an observation PulseType."
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
var SignalReducerSchema = z2.enum(["presence", "distinct-per-window", "lifecycle", "additive-window"]);
|
|
253
|
+
var MetricNameSchema = z2.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/).max(64);
|
|
254
|
+
var AdditiveMetricRuleSchema = z2.object({
|
|
255
|
+
name: MetricNameSchema,
|
|
256
|
+
op: z2.enum(["count", "sum"]),
|
|
257
|
+
unitField: z2.string().trim().min(1).max(200).optional(),
|
|
258
|
+
valueField: z2.string().trim().min(1).max(200).optional(),
|
|
259
|
+
when: z2.object({
|
|
260
|
+
field: z2.string().trim().min(1).max(200),
|
|
261
|
+
equals: z2.union([z2.string(), z2.number().finite(), z2.boolean()])
|
|
262
|
+
}).strict().optional()
|
|
263
|
+
}).strict().superRefine((rule, context) => {
|
|
264
|
+
if (rule.op === "sum" !== Boolean(rule.valueField)) {
|
|
265
|
+
context.addIssue({ code: "custom", path: ["valueField"], message: "Sum needs valueField; count must not have one." });
|
|
266
|
+
}
|
|
207
267
|
});
|
|
208
|
-
var SignalReducerSchema = z2.enum(["presence", "distinct-per-window", "lifecycle"]);
|
|
209
268
|
var SignalTypeDefinitionSchema = z2.object({
|
|
269
|
+
additive: z2.object({
|
|
270
|
+
baseScopeFields: z2.array(z2.string().trim().min(1).max(100)).max(4).optional(),
|
|
271
|
+
cohortAtField: z2.string().trim().min(1).max(200),
|
|
272
|
+
dedupeFields: z2.array(z2.string().trim().min(1).max(200)).min(1).max(4),
|
|
273
|
+
dimensionSets: z2.array(z2.object({
|
|
274
|
+
kind: z2.string().trim().min(1).max(100),
|
|
275
|
+
fields: z2.array(z2.string().trim().min(1).max(100)).max(4)
|
|
276
|
+
}).strict()).min(1).max(16),
|
|
277
|
+
latenessMs: z2.number().int().min(0).max(36e5),
|
|
278
|
+
metricRules: z2.array(AdditiveMetricRuleSchema).min(1).max(32),
|
|
279
|
+
primaryMetric: MetricNameSchema,
|
|
280
|
+
stableScopeField: z2.string().trim().min(1).max(200).optional()
|
|
281
|
+
}).strict().optional(),
|
|
210
282
|
id: z2.string().trim().min(1).max(100),
|
|
211
283
|
inputPulseTypes: z2.array(z2.object({
|
|
212
284
|
id: z2.string().trim().min(1).max(100),
|
|
@@ -246,8 +318,26 @@ var SignalTypeDefinitionSchema = z2.object({
|
|
|
246
318
|
if (definition.reducer === "lifecycle" && (!definition.lifecycle || !definition.partitionKey || !definition.dimensions?.length)) {
|
|
247
319
|
context.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle needs a configuration, partition key and dimensions." });
|
|
248
320
|
}
|
|
321
|
+
if (definition.reducer === "additive-window") {
|
|
322
|
+
const additive = definition.additive;
|
|
323
|
+
if (!additive || !definition.windowMs || !definition.partitionKey) {
|
|
324
|
+
context.addIssue({ code: "custom", path: ["additive"], message: "Additive window needs metrics, windowMs and partitionKey." });
|
|
325
|
+
} else {
|
|
326
|
+
const names = additive.metricRules.map((rule) => rule.name);
|
|
327
|
+
if (new Set(names).size !== names.length || !names.includes(additive.primaryMetric)) {
|
|
328
|
+
context.addIssue({ code: "custom", path: ["additive", "metricRules"], message: "Metric names must be unique and include the primary metric." });
|
|
329
|
+
}
|
|
330
|
+
const kinds = additive.dimensionSets.map((set) => set.kind);
|
|
331
|
+
const sets = additive.dimensionSets.map((set) => JSON.stringify(set.fields));
|
|
332
|
+
if (new Set(sets).size !== sets.length || new Set(kinds).size !== kinds.length) {
|
|
333
|
+
context.addIssue({ code: "custom", path: ["additive", "dimensionSets"], message: "Dimension sets and kinds must be unique." });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else if (definition.additive) {
|
|
337
|
+
context.addIssue({ code: "custom", path: ["additive"], message: "Only additive-window signals may define additive metrics." });
|
|
338
|
+
}
|
|
249
339
|
const schedule = definition.shardSchedule ?? [];
|
|
250
|
-
if (schedule.length > 0 && (!definition.partitionKey || !definition.dimensions?.length)) {
|
|
340
|
+
if (schedule.length > 0 && (!definition.partitionKey || !definition.dimensions?.length && !definition.additive?.dimensionSets.length)) {
|
|
251
341
|
context.addIssue({ code: "custom", path: ["partitionKey"], message: "Scheduled routing needs partitionKey and dimensions." });
|
|
252
342
|
}
|
|
253
343
|
if (schedule.length > 0 && definition.reducer === "presence" && !definition.presence?.startedAtField) {
|
|
@@ -269,7 +359,8 @@ var SignalTypeDefinitionSchema = z2.object({
|
|
|
269
359
|
});
|
|
270
360
|
var MetricSignalRefSchema = z2.object({
|
|
271
361
|
signalId: z2.string().trim().min(1).max(500),
|
|
272
|
-
dimension: z2.string().trim().min(1).max(100)
|
|
362
|
+
dimension: z2.string().trim().min(1).max(100),
|
|
363
|
+
scope: z2.record(z2.string().trim().min(1).max(100), z2.string().trim().min(1).max(200)).optional()
|
|
273
364
|
}).strict();
|
|
274
365
|
var MetricPulseTargetSchema = z2.object({
|
|
275
366
|
generation: z2.number().int().positive(),
|
|
@@ -301,6 +392,33 @@ var RoutedMetricPulseSchema = z2.object({
|
|
|
301
392
|
pulse: RawPulseSchema,
|
|
302
393
|
target: MetricPulseTargetSchema
|
|
303
394
|
}).strict();
|
|
395
|
+
var AdditivePulseTargetSchema = z2.object({
|
|
396
|
+
dedupeKey: z2.string().trim().min(1).max(500),
|
|
397
|
+
generation: z2.number().int().positive(),
|
|
398
|
+
latenessMs: z2.number().int().min(0).max(36e5),
|
|
399
|
+
metrics: z2.record(MetricNameSchema, z2.number().int().safe().nonnegative()),
|
|
400
|
+
organizationId: z2.string().trim().min(1).max(100),
|
|
401
|
+
ownerId: z2.string().trim().min(1).max(500),
|
|
402
|
+
primaryMetric: MetricNameSchema,
|
|
403
|
+
refs: z2.array(MetricSignalRefSchema).min(1).max(16),
|
|
404
|
+
shardCount: z2.number().int().min(1).max(65536),
|
|
405
|
+
shardIndex: z2.number().int().nonnegative(),
|
|
406
|
+
signalTypeId: z2.string().trim().min(1).max(100),
|
|
407
|
+
stableScopeKey: z2.string().trim().min(1).max(500).optional(),
|
|
408
|
+
windowEndAt: isoDateTime,
|
|
409
|
+
windowStartAt: isoDateTime
|
|
410
|
+
}).strict().superRefine((target, context) => {
|
|
411
|
+
if (target.shardIndex >= target.shardCount || Date.parse(target.windowStartAt) >= Date.parse(target.windowEndAt)) {
|
|
412
|
+
context.addIssue({ code: "custom", path: ["shardIndex"], message: "Invalid additive shard or window." });
|
|
413
|
+
}
|
|
414
|
+
if (!Object.hasOwn(target.metrics, target.primaryMetric)) {
|
|
415
|
+
context.addIssue({ code: "custom", path: ["metrics"], message: "Additive metrics must include the primary metric." });
|
|
416
|
+
}
|
|
417
|
+
if (new Set(target.refs.map((ref) => ref.signalId)).size !== target.refs.length) {
|
|
418
|
+
context.addIssue({ code: "custom", path: ["refs"], message: "Additive signal references must be unique." });
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
var RoutedAdditivePulseSchema = z2.object({ pulse: RawPulseSchema, target: AdditivePulseTargetSchema }).strict();
|
|
304
422
|
var LifecyclePulseTargetSchema = z2.object({
|
|
305
423
|
generation: z2.number().int().positive(),
|
|
306
424
|
groupId: z2.string().trim().min(1).max(100),
|
|
@@ -335,22 +453,38 @@ var MetricSnapshotSchema = z2.object({
|
|
|
335
453
|
estimatedCount: z2.number().finite().nonnegative(),
|
|
336
454
|
varianceEstimate: z2.number().finite().nonnegative().optional(),
|
|
337
455
|
generation: z2.number().int().positive(),
|
|
338
|
-
kind: z2.enum(["presence", "distinct-per-window"]),
|
|
456
|
+
kind: z2.enum(["presence", "distinct-per-window", "additive-window"]),
|
|
457
|
+
latenessMs: z2.number().int().min(0).max(36e5).optional(),
|
|
458
|
+
metrics: z2.record(MetricNameSchema, z2.number().int().safe().nonnegative()).optional(),
|
|
339
459
|
observedAt: isoDateTime,
|
|
340
460
|
organizationId: z2.string().trim().min(1).max(100),
|
|
341
461
|
originId: z2.string().trim().min(1).max(200),
|
|
342
462
|
recovering: z2.boolean(),
|
|
343
463
|
revision: z2.number().int().nonnegative(),
|
|
344
464
|
sampledCount: z2.number().int().nonnegative(),
|
|
465
|
+
scope: z2.record(z2.string().trim().min(1).max(100), z2.string().trim().min(1).max(200)).optional(),
|
|
466
|
+
scopeKind: z2.string().trim().min(1).max(100).optional(),
|
|
345
467
|
shardCount: z2.number().int().min(1).max(65536),
|
|
346
468
|
signalId: z2.string().trim().min(1).max(500),
|
|
347
469
|
signalTypeId: z2.string().trim().min(1).max(100),
|
|
348
470
|
windowEndAt: isoDateTime.optional(),
|
|
349
471
|
windowStartAt: isoDateTime.optional()
|
|
350
472
|
}).strict().superRefine((snapshot, context) => {
|
|
351
|
-
if (snapshot.kind === "distinct-per-window" && (!snapshot.windowStartAt || !snapshot.windowEndAt)) {
|
|
473
|
+
if ((snapshot.kind === "distinct-per-window" || snapshot.kind === "additive-window") && (!snapshot.windowStartAt || !snapshot.windowEndAt)) {
|
|
352
474
|
context.addIssue({ code: "custom", path: ["windowStartAt"], message: "Distinct snapshot needs a window." });
|
|
353
475
|
}
|
|
476
|
+
if (snapshot.kind === "additive-window" && !snapshot.metrics) {
|
|
477
|
+
context.addIssue({ code: "custom", path: ["metrics"], message: "Additive snapshot needs metrics." });
|
|
478
|
+
}
|
|
479
|
+
if (snapshot.kind === "additive-window" && snapshot.latenessMs === void 0) {
|
|
480
|
+
context.addIssue({ code: "custom", path: ["latenessMs"], message: "Additive snapshot needs its lateness bound." });
|
|
481
|
+
}
|
|
482
|
+
if (snapshot.kind === "additive-window" && (!snapshot.scope || !snapshot.scopeKind)) {
|
|
483
|
+
context.addIssue({ code: "custom", path: ["scope"], message: "Additive snapshot needs a structured scope." });
|
|
484
|
+
}
|
|
485
|
+
if (snapshot.kind !== "additive-window" && snapshot.metrics) {
|
|
486
|
+
context.addIssue({ code: "custom", path: ["metrics"], message: "Only additive snapshots carry metrics." });
|
|
487
|
+
}
|
|
354
488
|
if (snapshot.kind === "presence" && (snapshot.windowStartAt || snapshot.windowEndAt)) {
|
|
355
489
|
context.addIssue({ code: "custom", path: ["windowStartAt"], message: "Presence snapshot must not have a window." });
|
|
356
490
|
}
|
|
@@ -585,6 +719,12 @@ var configKey = async (baseUrl, apiToken) => {
|
|
|
585
719
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${baseUrl}\0${apiToken}`));
|
|
586
720
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
587
721
|
};
|
|
722
|
+
var reportedChangeKey = async (fields, payload) => {
|
|
723
|
+
if (!fields?.length) return void 0;
|
|
724
|
+
const values = fields.map((field) => [field, Object.hasOwn(payload, field), payload[field]]);
|
|
725
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(values)));
|
|
726
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
727
|
+
};
|
|
588
728
|
var UnlimitedClient = class {
|
|
589
729
|
apiToken;
|
|
590
730
|
baseUrl;
|
|
@@ -678,6 +818,13 @@ var UnlimitedClient = class {
|
|
|
678
818
|
});
|
|
679
819
|
return this.refreshing;
|
|
680
820
|
}
|
|
821
|
+
/** Load the authorized, versioned PulseType catalog before producing pulses.
|
|
822
|
+
* The organization token is reserved for future catalog scoping and currently
|
|
823
|
+
* has no effect, including when it is an empty string. API authentication
|
|
824
|
+
* continues to use the apiToken supplied in the constructor. */
|
|
825
|
+
async initialize(_organizationToken) {
|
|
826
|
+
await this.configuration();
|
|
827
|
+
}
|
|
681
828
|
async decisionKey(groupId, organizationId, sessionId) {
|
|
682
829
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(
|
|
683
830
|
`${await this.cacheKey}\0${groupId}\0${organizationId}\0${sessionId}`
|
|
@@ -719,6 +866,8 @@ var UnlimitedClient = class {
|
|
|
719
866
|
return { accepted: false, disabled: true, pulseId: id, sampledOut: false };
|
|
720
867
|
}
|
|
721
868
|
const lifecycle = type.entry.definition.lifecycle;
|
|
869
|
+
const observation = lifecycle?.phase === "start" ? [...loaded.types.values()].find((candidate) => candidate.entry.definition.lifecycle?.groupId === lifecycle.groupId && candidate.entry.definition.lifecycle.phase === "observe") : type;
|
|
870
|
+
const changeKey = lifecycle ? await reportedChangeKey(observation?.entry.definition.lifecycle?.reportOnChangeFields, payload) : void 0;
|
|
722
871
|
const decisionKey = lifecycle ? await this.decisionKey(lifecycle.groupId, input.organizationId, id) : void 0;
|
|
723
872
|
if (lifecycle?.phase === "observe" && decisionKey) {
|
|
724
873
|
const decision = await this.getDecision(decisionKey);
|
|
@@ -733,7 +882,7 @@ var UnlimitedClient = class {
|
|
|
733
882
|
if (decision && !decision.selected) {
|
|
734
883
|
return { accepted: false, pulseId: id, sampledOut: true, selected: false, probability: decision.probability };
|
|
735
884
|
}
|
|
736
|
-
if (decision && this.now().getTime() < decision.nextHeartbeatAtMs && decision.
|
|
885
|
+
if (decision && this.now().getTime() < decision.nextHeartbeatAtMs && decision.lastReportedChangeKey === changeKey) {
|
|
737
886
|
return {
|
|
738
887
|
accepted: false,
|
|
739
888
|
pulseId: id,
|
|
@@ -790,7 +939,7 @@ var UnlimitedClient = class {
|
|
|
790
939
|
selected: body.selected,
|
|
791
940
|
probability: body.probability,
|
|
792
941
|
nextHeartbeatAtMs: body.nextHeartbeatAtMs,
|
|
793
|
-
|
|
942
|
+
...body.selected && changeKey ? { lastReportedChangeKey: changeKey } : {},
|
|
794
943
|
expiresAtMs: this.now().getTime() + 24 * 60 * 60 * 1e3
|
|
795
944
|
});
|
|
796
945
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultdai/pulse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
},
|