@ultdai/pulse 0.3.0 → 0.4.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 +29 -27
- package/dist/contracts/index.d.ts +3 -0
- package/dist/index.js +22 -3
- package/dist/sessionDecisionCache.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,49 +7,51 @@ 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
|
-
...(session.channelId ? { channelId: session.channelId } : {}),
|
|
28
|
+
|
|
29
|
+
context.waitUntil((async () => {
|
|
30
|
+
await client.emit({
|
|
31
|
+
organizationId: env.ULTD_ORGANIZATION_ID,
|
|
32
|
+
occurredAtMs: Date.now(),
|
|
33
|
+
pulse: {
|
|
34
|
+
type: 'your.configured.pulse-type',
|
|
35
|
+
version: 1,
|
|
36
|
+
id: crypto.randomUUID(),
|
|
37
|
+
payload: { value: 1 },
|
|
41
38
|
},
|
|
42
|
-
}
|
|
43
|
-
}).catch((error) => console.error('ULTD pulse failed', error)))
|
|
39
|
+
})
|
|
40
|
+
})().catch((error) => console.error('ULTD pulse failed', error)))
|
|
41
|
+
|
|
44
42
|
return new Response('ok')
|
|
45
43
|
},
|
|
46
44
|
}
|
|
47
45
|
```
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
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í.
|
|
48
|
+
|
|
49
|
+
`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.
|
|
50
|
+
|
|
51
|
+
Konstruktor uloží nastavení bez síťového volání. První `emit()` načte povolené PulseTypes, jejich schémata a pravidla měření z autorizovaného ULTD API nebo z dosud platné sdílené cache, a teprve potom puls lokálně validuje. Neznámý typ nebo nevalidní `pulse.payload` SDK odmítne před POSTem. Vypnutý nebo nevzorkovaný puls neodešle. Další volání používají platnou konfiguraci; po expiraci se obnoví. Pokud obnova selže, puls se neodešle. Cloudflare cache je volitelná; bez ní SDK používá paměť instance.
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
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
54
|
|
|
53
|
-
|
|
55
|
+
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
56
|
|
|
55
|
-
|
|
57
|
+
Dnešní POC má na backendu jeden technický API klíč a jeden katalog definic. Budoucí výběr katalogu podle organizace bude vycházet z identity API klíče na serveru; volající nebude předávat další token ani měnit veřejné rozhraní SDK.
|
|
@@ -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>;
|
|
@@ -552,6 +553,7 @@ export declare const SdkPulseTypeSchema: z.ZodObject<{
|
|
|
552
553
|
start: "start";
|
|
553
554
|
}>;
|
|
554
555
|
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
556
|
+
reportOnChangeFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
555
557
|
}, z.core.$strict>>;
|
|
556
558
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
557
559
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -597,6 +599,7 @@ export declare const SdkConfigurationSchema: z.ZodObject<{
|
|
|
597
599
|
start: "start";
|
|
598
600
|
}>;
|
|
599
601
|
intervalMs: z.ZodOptional<z.ZodNumber>;
|
|
602
|
+
reportOnChangeFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
600
603
|
}, z.core.$strict>>;
|
|
601
604
|
payloadSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
602
605
|
requiresExplicitId: z.ZodOptional<z.ZodBoolean>;
|
package/dist/index.js
CHANGED
|
@@ -214,7 +214,8 @@ var PulseTypeDefinitionSchema = z2.object({
|
|
|
214
214
|
lifecycle: z2.object({
|
|
215
215
|
groupId: z2.string().trim().min(1).max(100),
|
|
216
216
|
phase: z2.enum(["start", "observe"]),
|
|
217
|
-
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()
|
|
218
219
|
}).strict().optional(),
|
|
219
220
|
payloadSchema: z2.record(z2.string(), z2.unknown()),
|
|
220
221
|
requiresExplicitId: z2.boolean().optional(),
|
|
@@ -237,6 +238,16 @@ var PulseTypeDefinitionSchema = z2.object({
|
|
|
237
238
|
if (definition.lifecycle && (definition.samplingAllowed || !definition.requiresExplicitId || definition.delivery !== "live" || definition.lifecycle.phase === "observe" !== Boolean(definition.lifecycle.intervalMs))) {
|
|
238
239
|
context.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle pulses need explicit IDs, live delivery, and an interval only for observations." });
|
|
239
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
|
+
}
|
|
240
251
|
});
|
|
241
252
|
var SignalReducerSchema = z2.enum(["presence", "distinct-per-window", "lifecycle", "additive-window"]);
|
|
242
253
|
var MetricNameSchema = z2.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/).max(64);
|
|
@@ -708,6 +719,12 @@ var configKey = async (baseUrl, apiToken) => {
|
|
|
708
719
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${baseUrl}\0${apiToken}`));
|
|
709
720
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
710
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
|
+
};
|
|
711
728
|
var UnlimitedClient = class {
|
|
712
729
|
apiToken;
|
|
713
730
|
baseUrl;
|
|
@@ -842,6 +859,8 @@ var UnlimitedClient = class {
|
|
|
842
859
|
return { accepted: false, disabled: true, pulseId: id, sampledOut: false };
|
|
843
860
|
}
|
|
844
861
|
const lifecycle = type.entry.definition.lifecycle;
|
|
862
|
+
const observation = lifecycle?.phase === "start" ? [...loaded.types.values()].find((candidate) => candidate.entry.definition.lifecycle?.groupId === lifecycle.groupId && candidate.entry.definition.lifecycle.phase === "observe") : type;
|
|
863
|
+
const changeKey = lifecycle ? await reportedChangeKey(observation?.entry.definition.lifecycle?.reportOnChangeFields, payload) : void 0;
|
|
845
864
|
const decisionKey = lifecycle ? await this.decisionKey(lifecycle.groupId, input.organizationId, id) : void 0;
|
|
846
865
|
if (lifecycle?.phase === "observe" && decisionKey) {
|
|
847
866
|
const decision = await this.getDecision(decisionKey);
|
|
@@ -856,7 +875,7 @@ var UnlimitedClient = class {
|
|
|
856
875
|
if (decision && !decision.selected) {
|
|
857
876
|
return { accepted: false, pulseId: id, sampledOut: true, selected: false, probability: decision.probability };
|
|
858
877
|
}
|
|
859
|
-
if (decision && this.now().getTime() < decision.nextHeartbeatAtMs && decision.
|
|
878
|
+
if (decision && this.now().getTime() < decision.nextHeartbeatAtMs && decision.lastReportedChangeKey === changeKey) {
|
|
860
879
|
return {
|
|
861
880
|
accepted: false,
|
|
862
881
|
pulseId: id,
|
|
@@ -913,7 +932,7 @@ var UnlimitedClient = class {
|
|
|
913
932
|
selected: body.selected,
|
|
914
933
|
probability: body.probability,
|
|
915
934
|
nextHeartbeatAtMs: body.nextHeartbeatAtMs,
|
|
916
|
-
|
|
935
|
+
...body.selected && changeKey ? { lastReportedChangeKey: changeKey } : {},
|
|
917
936
|
expiresAtMs: this.now().getTime() + 24 * 60 * 60 * 1e3
|
|
918
937
|
});
|
|
919
938
|
return {
|