@ultdai/pulse 0.3.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 +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +29 -3
- package/dist/sessionDecisionCache.d.ts +1 -1
- package/package.json +1 -1
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>;
|
|
@@ -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.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
|
@@ -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;
|
|
@@ -801,6 +818,13 @@ var UnlimitedClient = class {
|
|
|
801
818
|
});
|
|
802
819
|
return this.refreshing;
|
|
803
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
|
+
}
|
|
804
828
|
async decisionKey(groupId, organizationId, sessionId) {
|
|
805
829
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(
|
|
806
830
|
`${await this.cacheKey}\0${groupId}\0${organizationId}\0${sessionId}`
|
|
@@ -842,6 +866,8 @@ var UnlimitedClient = class {
|
|
|
842
866
|
return { accepted: false, disabled: true, pulseId: id, sampledOut: false };
|
|
843
867
|
}
|
|
844
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;
|
|
845
871
|
const decisionKey = lifecycle ? await this.decisionKey(lifecycle.groupId, input.organizationId, id) : void 0;
|
|
846
872
|
if (lifecycle?.phase === "observe" && decisionKey) {
|
|
847
873
|
const decision = await this.getDecision(decisionKey);
|
|
@@ -856,7 +882,7 @@ var UnlimitedClient = class {
|
|
|
856
882
|
if (decision && !decision.selected) {
|
|
857
883
|
return { accepted: false, pulseId: id, sampledOut: true, selected: false, probability: decision.probability };
|
|
858
884
|
}
|
|
859
|
-
if (decision && this.now().getTime() < decision.nextHeartbeatAtMs && decision.
|
|
885
|
+
if (decision && this.now().getTime() < decision.nextHeartbeatAtMs && decision.lastReportedChangeKey === changeKey) {
|
|
860
886
|
return {
|
|
861
887
|
accepted: false,
|
|
862
888
|
pulseId: id,
|
|
@@ -913,7 +939,7 @@ var UnlimitedClient = class {
|
|
|
913
939
|
selected: body.selected,
|
|
914
940
|
probability: body.probability,
|
|
915
941
|
nextHeartbeatAtMs: body.nextHeartbeatAtMs,
|
|
916
|
-
|
|
942
|
+
...body.selected && changeKey ? { lastReportedChangeKey: changeKey } : {},
|
|
917
943
|
expiresAtMs: this.now().getTime() + 24 * 60 * 60 * 1e3
|
|
918
944
|
});
|
|
919
945
|
return {
|