@clocknext/sdk 0.8.0 → 0.10.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 +77 -15
- package/dist/index.cjs +166 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +387 -61
- package/dist/index.d.ts +387 -61
- package/dist/index.js +166 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,16 @@ await cnk.flush();
|
|
|
41
41
|
## Recording signals
|
|
42
42
|
|
|
43
43
|
There are three signal types, matching ClockNext's billable primitives. Each has
|
|
44
|
-
a typed helper that enforces the right required fields at compile time
|
|
44
|
+
a typed helper that enforces the right required fields at compile time, and each
|
|
45
|
+
posts to **its own route** — `/api/v1/signal/credit`, `/signal/wallet`,
|
|
46
|
+
`/signal/outcome`. The signal's `type` is what picks that route; from 0.10 it is
|
|
47
|
+
no longer sent in the body.
|
|
48
|
+
|
|
49
|
+
> **Migrating from 0.9 or earlier?** Your calling code does not change — same
|
|
50
|
+
> methods, same arguments. What changed is the wire: the SDK now targets the
|
|
51
|
+
> `/api/v1/signal/*` family instead of the deprecated `POST /api/v1/usage`, and
|
|
52
|
+
> the `metric` field is now `composite` (see [Composites](#composites)). Bump the
|
|
53
|
+
> package and you are migrated.
|
|
45
54
|
|
|
46
55
|
`model` is always the **catalog model id** enabled in your org (matched
|
|
47
56
|
case-insensitively against `OrgModel.modelId`). Credits and outcomes are
|
|
@@ -76,23 +85,54 @@ await cnk.signals.outcome({ customerId, model, agentKey: "resolved_ticket.verify
|
|
|
76
85
|
await cnk.signals.outcome({ customerId, model, agentKey: "resolved_ticket.resolve", runId, complete: true, tokens: { input: 500, output: 200 } });
|
|
77
86
|
```
|
|
78
87
|
|
|
79
|
-
###
|
|
88
|
+
### Composites
|
|
80
89
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
A **composite** bundles several credits / outcomes / units and is priced and
|
|
91
|
+
billed as one thing. Tag a signal with `composite` to group it into one
|
|
92
|
+
*occurrence* — `ref` is the composite's ref ID and `value` is your own
|
|
93
|
+
correlation id for that occurrence (a session, a call, a job):
|
|
84
94
|
|
|
85
95
|
```ts
|
|
86
96
|
await cnk.signals.credit({
|
|
87
97
|
customerId, model: "llama-3.3-70b-versatile", agentKey: "api_credit",
|
|
88
98
|
tokens: { input: 1200, output: 340 },
|
|
89
|
-
|
|
99
|
+
composite: { ref: "voice_ai", value: "call_5f3a" },
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
An occurrence opens on the first tagged signal and costs nothing. **Closing it
|
|
104
|
+
is what bills it** — one charge for the bundle, however many signals it
|
|
105
|
+
contained:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
await cnk.signals.completeComposite({
|
|
109
|
+
customerId,
|
|
110
|
+
composite: { ref: "voice_ai", value: "call_5f3a" },
|
|
90
111
|
});
|
|
91
112
|
```
|
|
92
113
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
114
|
+
Only your code knows when the work ended, so closing is a declaration, not
|
|
115
|
+
something ClockNext infers from which signals have arrived: tagged signals may
|
|
116
|
+
arrive in any order and the occurrence stays open and unbilled until you say
|
|
117
|
+
otherwise. Put the call on the line before your own `return`.
|
|
118
|
+
|
|
119
|
+
An **outcome never needs it** — its own `complete: true` closes its run and the
|
|
120
|
+
matching occurrence together. Credits and units have no "done" signal of their
|
|
121
|
+
own, so this is theirs.
|
|
122
|
+
|
|
123
|
+
Closing twice is safe (`ALREADY_COMPLETED`, and it cannot bill again), and so is
|
|
124
|
+
closing *early* — before every entitlement in the bundle has reported, or while
|
|
125
|
+
a prepaid slot pool is full: the server parks the request and applies it when
|
|
126
|
+
the blocker clears, rather than failing it.
|
|
127
|
+
|
|
128
|
+
A tag that names no live composite is ignored and never fails the signal — the
|
|
129
|
+
signal carries money, the tag only decides what it is grouped under. Tags on
|
|
130
|
+
wallet signals are ignored.
|
|
131
|
+
|
|
132
|
+
> **Renamed in 0.10.** This field was called `metric` and was documented as
|
|
133
|
+
> "reporting only, never changes what's billed". That was wrong: where the plan
|
|
134
|
+
> sells the composite, the occurrence is the billable unit. Rename `metric:` to
|
|
135
|
+
> `composite:` — the shape (`{ ref, value }`) is unchanged.
|
|
96
136
|
|
|
97
137
|
### Async vs sync
|
|
98
138
|
|
|
@@ -102,9 +142,9 @@ buffer on a size/interval trigger. This keeps billing telemetry off your LLM hot
|
|
|
102
142
|
path.
|
|
103
143
|
|
|
104
144
|
> **"Batching" here means timing, not a bulk request.** Each signal is still its
|
|
105
|
-
> own
|
|
106
|
-
> how many run at once (`batch.maxConcurrency`). There is
|
|
107
|
-
> call, so size your request budget per signal.
|
|
145
|
+
> own POST to its own `/api/v1/signal/*` route; the flusher just groups *when*
|
|
146
|
+
> sends happen and caps how many run at once (`batch.maxConcurrency`). There is
|
|
147
|
+
> no single bulk-ingest call, so size your request budget per signal.
|
|
108
148
|
|
|
109
149
|
To await the request itself (rather than buffering it), either:
|
|
110
150
|
|
|
@@ -160,7 +200,7 @@ const cnk = new ClockNext({ apiKey, mode: "sync" });
|
|
|
160
200
|
const c = await cnk.customers.create({ name: "Acme", email: "ops@acme.com" });
|
|
161
201
|
const one = await cnk.customers.get(c.id);
|
|
162
202
|
const page = await cnk.customers.list({ limit: 50, q: "ac" });
|
|
163
|
-
await cnk.customers.update(c.id, {
|
|
203
|
+
await cnk.customers.update(c.id, { legalName: "Acme Inc." });
|
|
164
204
|
await cnk.customers.delete(c.id);
|
|
165
205
|
|
|
166
206
|
// iterate every customer (auto-follows the cursor)
|
|
@@ -176,7 +216,7 @@ await cnk.customers.revenue(c.id, { range: "30d" });
|
|
|
176
216
|
### Members
|
|
177
217
|
|
|
178
218
|
```ts
|
|
179
|
-
await cnk.customers.addMember(c.id, { name: "Alice",
|
|
219
|
+
await cnk.customers.addMember(c.id, { name: "Alice", email: "alice@acme.com" });
|
|
180
220
|
const members = await cnk.customers.listMembers(c.id);
|
|
181
221
|
await cnk.customers.updateMember(c.id, members[0].id, { name: "Alice R." });
|
|
182
222
|
await cnk.customers.removeMember(c.id, members[0].id);
|
|
@@ -256,13 +296,35 @@ await cnk.units.create({ name: "Seats", agentKey: "seats", pricingType: "FLAT",
|
|
|
256
296
|
|
|
257
297
|
### Unit metering
|
|
258
298
|
|
|
259
|
-
Record a unit event (no tokens) and read a customer's unit balances:
|
|
299
|
+
Record a unit event (no tokens, no model) and read a customer's unit balances:
|
|
260
300
|
|
|
261
301
|
```ts
|
|
262
302
|
await cnk.signals.unit({ customerId: c.id, agentKey: "seats" });
|
|
303
|
+
|
|
304
|
+
// several at once, instead of N separate calls
|
|
305
|
+
await cnk.signals.unit({ customerId: c.id, agentKey: "seats", quantity: 3 });
|
|
306
|
+
|
|
307
|
+
// part of a composite, and safe to retry
|
|
308
|
+
await cnk.signals.unit({
|
|
309
|
+
customerId: c.id, agentKey: "seats",
|
|
310
|
+
composite: { ref: "voice_ai", value: "call_5f3a" },
|
|
311
|
+
idempotencyKey: `seats:${requestId}`,
|
|
312
|
+
});
|
|
313
|
+
|
|
263
314
|
await cnk.signals.unitUsage({ customerId: c.id });
|
|
264
315
|
```
|
|
265
316
|
|
|
317
|
+
`quantity` defaults to 1. On a prepaid (ADVANCE) plan the draw is
|
|
318
|
+
all-or-nothing: asking for more than remains is refused in full rather than
|
|
319
|
+
partially filled.
|
|
320
|
+
|
|
321
|
+
> **Changed in 0.10.** `signals.unit()` moved to `POST /api/v1/signal/unit`,
|
|
322
|
+
> which is **durable** (the report is written down before it is priced, so a
|
|
323
|
+
> failure parks for retry instead of losing a billable event) and **idempotent**
|
|
324
|
+
> (on the old route a retried report drew the balance down twice). Because it can
|
|
325
|
+
> now answer `202`, this resolves `{ queued, unitUsage, rawId }` rather than a
|
|
326
|
+
> bare `{ unitUsage }` — branch on `queued`, don't assume `unitUsage`.
|
|
327
|
+
|
|
266
328
|
## Webhooks
|
|
267
329
|
|
|
268
330
|
Threshold alerts — at most one per kind (`CREDIT` / `OUTCOME` / `WALLET`):
|
package/dist/index.cjs
CHANGED
|
@@ -121,7 +121,8 @@ var Flusher = class {
|
|
|
121
121
|
try {
|
|
122
122
|
await this.transport.request({
|
|
123
123
|
method: "POST",
|
|
124
|
-
|
|
124
|
+
// Per-item, not per-Flusher: one route per meter. See `QueuedSignal`.
|
|
125
|
+
path: item.path,
|
|
125
126
|
body: item.body,
|
|
126
127
|
// Best-effort metering opts into retries on transient failures.
|
|
127
128
|
retry: true
|
|
@@ -229,7 +230,7 @@ function sleep(ms) {
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
// src/http.ts
|
|
232
|
-
var SDK_VERSION = "0.
|
|
233
|
+
var SDK_VERSION = "0.10.0" ;
|
|
233
234
|
var Transport = class {
|
|
234
235
|
constructor(cfg) {
|
|
235
236
|
this.cfg = cfg;
|
|
@@ -324,6 +325,36 @@ var Transport = class {
|
|
|
324
325
|
}
|
|
325
326
|
};
|
|
326
327
|
|
|
328
|
+
// src/resources/composites.ts
|
|
329
|
+
var Composites = class {
|
|
330
|
+
constructor(transport) {
|
|
331
|
+
this.transport = transport;
|
|
332
|
+
}
|
|
333
|
+
transport;
|
|
334
|
+
/** List composites. Pass `{ active: true }` for only the ones a signal can
|
|
335
|
+
* still be tagged with — the default list INCLUDES archived ones, which
|
|
336
|
+
* keep every row they already own. */
|
|
337
|
+
async list(params = {}) {
|
|
338
|
+
const res = await this.transport.request({
|
|
339
|
+
method: "GET",
|
|
340
|
+
path: "/api/v1/composites",
|
|
341
|
+
query: {
|
|
342
|
+
active: params.active === void 0 ? void 0 : String(params.active)
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
return res.composites;
|
|
346
|
+
}
|
|
347
|
+
/** Create a composite. `POST /api/v1/composites`. */
|
|
348
|
+
async create(input) {
|
|
349
|
+
const res = await this.transport.request({
|
|
350
|
+
method: "POST",
|
|
351
|
+
path: "/api/v1/composites",
|
|
352
|
+
body: input
|
|
353
|
+
});
|
|
354
|
+
return res.composite;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
327
358
|
// src/resources/credits.ts
|
|
328
359
|
var Credits = class {
|
|
329
360
|
constructor(transport) {
|
|
@@ -506,7 +537,7 @@ var Customers = class {
|
|
|
506
537
|
});
|
|
507
538
|
return res.members;
|
|
508
539
|
}
|
|
509
|
-
/** Add a member to a customer. Duplicate `
|
|
540
|
+
/** Add a member to a customer. Duplicate `email` → ConflictError. */
|
|
510
541
|
async addMember(id, input) {
|
|
511
542
|
const res = await this.transport.request({
|
|
512
543
|
method: "POST",
|
|
@@ -515,7 +546,7 @@ var Customers = class {
|
|
|
515
546
|
});
|
|
516
547
|
return res.member;
|
|
517
548
|
}
|
|
518
|
-
/** Update a member's profile
|
|
549
|
+
/** Update a member's profile. Duplicate `email` → ConflictError. `PATCH …/:id/members/:memberId`. */
|
|
519
550
|
async updateMember(id, memberId, input) {
|
|
520
551
|
const res = await this.transport.request({
|
|
521
552
|
method: "PATCH",
|
|
@@ -524,12 +555,13 @@ var Customers = class {
|
|
|
524
555
|
});
|
|
525
556
|
return res.member;
|
|
526
557
|
}
|
|
527
|
-
/** Remove a member from a customer. `DELETE …/:id/members/:memberId`. */
|
|
558
|
+
/** Remove a member from a customer, returning who was removed. `DELETE …/:id/members/:memberId`. */
|
|
528
559
|
async removeMember(id, memberId) {
|
|
529
|
-
await this.transport.request({
|
|
560
|
+
const res = await this.transport.request({
|
|
530
561
|
method: "DELETE",
|
|
531
562
|
path: `/api/v1/customers/${encodeURIComponent(id)}/members/${encodeURIComponent(memberId)}`
|
|
532
563
|
});
|
|
564
|
+
return res.member;
|
|
533
565
|
}
|
|
534
566
|
// --- Wallet & billing config --------------------------------------------
|
|
535
567
|
/** The customer's wallet balance + transactions. `GET …/:id/wallet`. */
|
|
@@ -855,18 +887,28 @@ function newKey() {
|
|
|
855
887
|
}
|
|
856
888
|
|
|
857
889
|
// src/resources/signals.ts
|
|
890
|
+
var SIGNAL_PATHS = {
|
|
891
|
+
credit: "/api/v1/signal/credit",
|
|
892
|
+
outcome: "/api/v1/signal/outcome",
|
|
893
|
+
wallet: "/api/v1/signal/wallet"
|
|
894
|
+
};
|
|
895
|
+
var UNIT_PATH = "/api/v1/signal/unit";
|
|
896
|
+
var COMPOSITE_COMPLETE_PATH = "/api/v1/signal/composite/complete";
|
|
858
897
|
function signalToWire(signal) {
|
|
898
|
+
const t = signal.tokens;
|
|
859
899
|
const body = {
|
|
860
900
|
customerId: signal.customerId,
|
|
861
|
-
|
|
862
|
-
|
|
901
|
+
// `usage` is required on all three model-priced kinds. input/output are
|
|
902
|
+
// always sent; `cacheTokens` only when provided (the server defaults 0).
|
|
903
|
+
usage: {
|
|
904
|
+
model: signal.model,
|
|
905
|
+
inputTokens: t.input,
|
|
906
|
+
outputTokens: t.output,
|
|
907
|
+
...t.cache != null ? { cacheTokens: t.cache } : {}
|
|
908
|
+
}
|
|
863
909
|
};
|
|
864
910
|
if (signal.member) body.member = signal.member;
|
|
865
911
|
if (signal.custom) body.custom = signal.custom;
|
|
866
|
-
const t = signal.tokens;
|
|
867
|
-
body.inputTokens = t.input;
|
|
868
|
-
body.outputTokens = t.output;
|
|
869
|
-
if (t.cache != null) body.cacheTokens = t.cache;
|
|
870
912
|
if (signal.type === "credit" || signal.type === "outcome") {
|
|
871
913
|
body.agentKey = signal.agentKey;
|
|
872
914
|
}
|
|
@@ -874,12 +916,18 @@ function signalToWire(signal) {
|
|
|
874
916
|
body.runId = signal.runId;
|
|
875
917
|
if (signal.complete != null) body.complete = signal.complete;
|
|
876
918
|
}
|
|
877
|
-
if (signal.type !== "wallet" && signal.
|
|
878
|
-
body[signal.
|
|
919
|
+
if (signal.type !== "wallet" && signal.composite) {
|
|
920
|
+
body.composite = { [signal.composite.ref]: signal.composite.value };
|
|
879
921
|
}
|
|
880
922
|
if (signal.idempotencyKey) body.idempotencyKey = signal.idempotencyKey;
|
|
881
923
|
return body;
|
|
882
924
|
}
|
|
925
|
+
function handles(res) {
|
|
926
|
+
return {
|
|
927
|
+
...res.messageId != null ? { messageId: res.messageId } : {},
|
|
928
|
+
...res.rawId != null ? { rawId: res.rawId } : {}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
883
931
|
var Signals = class {
|
|
884
932
|
constructor(transport, flusher, cfg) {
|
|
885
933
|
this.transport = transport;
|
|
@@ -890,7 +938,8 @@ var Signals = class {
|
|
|
890
938
|
flusher;
|
|
891
939
|
cfg;
|
|
892
940
|
/**
|
|
893
|
-
* Record one usage signal
|
|
941
|
+
* Record one usage signal, to the route its `type` selects:
|
|
942
|
+
* `POST /api/v1/signal/credit` | `/signal/outcome` | `/signal/wallet`.
|
|
894
943
|
*
|
|
895
944
|
* Returns as soon as the signal is buffered (async mode) or the request has
|
|
896
945
|
* been answered (`{ wait: true }` / sync mode). See {@link TrackResult}: a
|
|
@@ -899,14 +948,15 @@ var Signals = class {
|
|
|
899
948
|
async track(signal, opts = {}) {
|
|
900
949
|
const body = signalToWire(signal);
|
|
901
950
|
if (body.idempotencyKey == null) body.idempotencyKey = newKey();
|
|
951
|
+
const path = SIGNAL_PATHS[signal.type];
|
|
902
952
|
const sendNow = opts.wait || this.cfg.mode === "sync";
|
|
903
953
|
if (!sendNow) {
|
|
904
|
-
this.flusher.enqueue({ body, signal });
|
|
954
|
+
this.flusher.enqueue({ path, body, signal });
|
|
905
955
|
return { queued: true };
|
|
906
956
|
}
|
|
907
957
|
const res = await this.transport.request({
|
|
908
958
|
method: "POST",
|
|
909
|
-
path
|
|
959
|
+
path,
|
|
910
960
|
body,
|
|
911
961
|
// Best-effort metering opts into retries on transient failures.
|
|
912
962
|
retry: true
|
|
@@ -914,39 +964,52 @@ var Signals = class {
|
|
|
914
964
|
return {
|
|
915
965
|
queued: res.queued === true,
|
|
916
966
|
usageLog: res.usageLog ?? null,
|
|
917
|
-
...res
|
|
967
|
+
...handles(res)
|
|
918
968
|
};
|
|
919
969
|
}
|
|
920
970
|
/**
|
|
921
971
|
* Validate + price a signal WITHOUT recording it (server `dryRun`). Returns
|
|
922
972
|
* the projected usage log so an integration can preflight before sending real
|
|
923
973
|
* traffic — no inbox row, wallet / outcome writes, or webhooks fire.
|
|
974
|
+
*
|
|
975
|
+
* Only the model-priced kinds support this. A dry run is not offered for unit
|
|
976
|
+
* reports or composite completions, and the server REJECTS the flag on those
|
|
977
|
+
* routes rather than accepting and ignoring it — which is why there is no
|
|
978
|
+
* dry-run path through `unit()` or `completeComposite()`.
|
|
924
979
|
*/
|
|
925
980
|
async verify(signal) {
|
|
926
981
|
const body = signalToWire(signal);
|
|
927
982
|
body.dryRun = true;
|
|
928
983
|
const res = await this.transport.request({
|
|
929
984
|
method: "POST",
|
|
930
|
-
path:
|
|
985
|
+
path: SIGNAL_PATHS[signal.type],
|
|
931
986
|
body,
|
|
932
987
|
// A dry run writes nothing, so it is always safe to retry.
|
|
933
988
|
retry: true
|
|
934
989
|
});
|
|
935
990
|
return res.usageLog ?? null;
|
|
936
991
|
}
|
|
937
|
-
/** Meter a named credit. */
|
|
992
|
+
/** Meter a named credit. `POST /api/v1/signal/credit`. */
|
|
938
993
|
credit(input, opts) {
|
|
939
994
|
return this.track({ ...input, type: "credit" }, opts);
|
|
940
995
|
}
|
|
941
|
-
/** Debit the customer's wallet at model cost. */
|
|
996
|
+
/** Debit the customer's wallet at model cost. `POST /api/v1/signal/wallet`. */
|
|
942
997
|
wallet(input, opts) {
|
|
943
998
|
return this.track({ ...input, type: "wallet" }, opts);
|
|
944
999
|
}
|
|
945
|
-
/** Advance one step of an outcome workflow. */
|
|
1000
|
+
/** Advance one step of an outcome workflow. `POST /api/v1/signal/outcome`. */
|
|
946
1001
|
outcome(input, opts) {
|
|
947
1002
|
return this.track({ ...input, type: "outcome" }, opts);
|
|
948
1003
|
}
|
|
949
|
-
/**
|
|
1004
|
+
/**
|
|
1005
|
+
* Recent usage logs + totals for a customer. `GET /api/v1/usage`.
|
|
1006
|
+
*
|
|
1007
|
+
* Still the legacy path, deliberately. The `/api/v1/signal/*` family is
|
|
1008
|
+
* POST-only — it replaced how usage is INGESTED, not how it is read — so
|
|
1009
|
+
* `GET /api/v1/usage` has no successor and is not deprecated. It is also the
|
|
1010
|
+
* only way to prove a signal landed, which matters more now that ingestion
|
|
1011
|
+
* answers `202` for everything.
|
|
1012
|
+
*/
|
|
950
1013
|
async list(params) {
|
|
951
1014
|
const res = await this.transport.request({
|
|
952
1015
|
method: "GET",
|
|
@@ -958,22 +1021,94 @@ var Signals = class {
|
|
|
958
1021
|
/**
|
|
959
1022
|
* Record one unit-metered event (a fixed-price meter with no tokens),
|
|
960
1023
|
* addressed to a catalog unit by its `agentKey`. Sent immediately — unit
|
|
961
|
-
* events are not buffered. `POST /api/v1/
|
|
1024
|
+
* events are not buffered. `POST /api/v1/signal/unit`.
|
|
1025
|
+
*
|
|
1026
|
+
* Two things changed with the move off `POST /api/v1/units`, and both show up
|
|
1027
|
+
* in the return type. The route is now durable (the report lands in an inbox
|
|
1028
|
+
* before it is priced, so a failure parks as Pending and can be retried
|
|
1029
|
+
* instead of losing a billable event) and it answers `202` when it queues —
|
|
1030
|
+
* so this resolves {@link UnitTrackResult}, not a bare `{ unitUsage }`.
|
|
1031
|
+
* Branch on `queued`.
|
|
962
1032
|
*/
|
|
963
1033
|
async unit(input) {
|
|
964
|
-
|
|
1034
|
+
const res = await this.transport.request({
|
|
965
1035
|
method: "POST",
|
|
966
|
-
path:
|
|
1036
|
+
path: UNIT_PATH,
|
|
967
1037
|
body: {
|
|
968
1038
|
customerId: input.customerId,
|
|
969
1039
|
agentKey: input.agentKey,
|
|
970
|
-
...input.
|
|
1040
|
+
...input.quantity != null ? { quantity: input.quantity } : {},
|
|
1041
|
+
...input.member ? { member: input.member } : {},
|
|
1042
|
+
...input.composite ? { composite: { [input.composite.ref]: input.composite.value } } : {},
|
|
1043
|
+
// Assigned HERE, before the send, so the SAME key rides every
|
|
1044
|
+
// transport-level retry below. Without it a single 500 followed by a
|
|
1045
|
+
// success is TWO un-keyed reports, and the server dedups on the key —
|
|
1046
|
+
// so it bills the allowance down twice, up to `retry.maxAttempts`
|
|
1047
|
+
// times. The route gained idempotency precisely to stop that; not
|
|
1048
|
+
// using it by default would leave the double-charge reachable from the
|
|
1049
|
+
// client instead of the server.
|
|
1050
|
+
idempotencyKey: input.idempotencyKey ?? newKey()
|
|
971
1051
|
},
|
|
972
1052
|
// Best-effort metering opts into retries on transient failures.
|
|
973
1053
|
retry: true
|
|
974
1054
|
});
|
|
1055
|
+
return {
|
|
1056
|
+
queued: res.queued === true,
|
|
1057
|
+
unitUsage: res.unitUsage ?? null,
|
|
1058
|
+
...handles(res)
|
|
1059
|
+
};
|
|
975
1060
|
}
|
|
976
|
-
/**
|
|
1061
|
+
/**
|
|
1062
|
+
* Close ONE occurrence of a composite — a distinct tag value.
|
|
1063
|
+
* `POST /api/v1/signal/composite/complete`.
|
|
1064
|
+
*
|
|
1065
|
+
* A composite bundles several credits / outcomes / units and is billed as one
|
|
1066
|
+
* thing. The occurrence opens on the first tagged signal and costs nothing;
|
|
1067
|
+
* this call is the moment it is billed. Only your code knows when the
|
|
1068
|
+
* workflow ended, so this is a DECLARATION, not a hint: tagged signals may
|
|
1069
|
+
* arrive in any order and the occurrence stays open and unbilled until you
|
|
1070
|
+
* say otherwise. Put it on the line before your own `return`.
|
|
1071
|
+
*
|
|
1072
|
+
* An OUTCOME never needs this — its own `complete: true` closes its run and
|
|
1073
|
+
* the matching occurrence together. Credits and units have no "done" signal
|
|
1074
|
+
* of their own, so this is theirs.
|
|
1075
|
+
*
|
|
1076
|
+
* Closing twice is safe: the second call answers `ALREADY_COMPLETED` and
|
|
1077
|
+
* cannot bill again. Calling it EARLY is also safe — before every entitlement
|
|
1078
|
+
* the composite is restricted to has reported, or while an ADVANCE pool is
|
|
1079
|
+
* full — the server parks the ping and applies it when the blocker clears,
|
|
1080
|
+
* rather than failing it.
|
|
1081
|
+
*/
|
|
1082
|
+
async completeComposite(input) {
|
|
1083
|
+
const res = await this.transport.request({
|
|
1084
|
+
method: "POST",
|
|
1085
|
+
path: COMPOSITE_COMPLETE_PATH,
|
|
1086
|
+
body: {
|
|
1087
|
+
customerId: input.customerId,
|
|
1088
|
+
composite: { [input.composite.ref]: input.composite.value },
|
|
1089
|
+
// Auto-assigned like the others. A retry here could not double-BILL
|
|
1090
|
+
// even without it — closing a closed occurrence is a server-side
|
|
1091
|
+
// no-op — but that safety comes from occurrence-level dedup, not from
|
|
1092
|
+
// idempotency, and an un-keyed replay still writes a redundant inbox
|
|
1093
|
+
// row. Keying it collapses the retry into the one attempt it is.
|
|
1094
|
+
idempotencyKey: input.idempotencyKey ?? newKey()
|
|
1095
|
+
},
|
|
1096
|
+
retry: true
|
|
1097
|
+
});
|
|
1098
|
+
return {
|
|
1099
|
+
queued: res.queued === true,
|
|
1100
|
+
...res.status != null ? { status: res.status } : {},
|
|
1101
|
+
...res.composite !== void 0 ? { composite: res.composite } : {},
|
|
1102
|
+
...res.value !== void 0 ? { value: res.value } : {},
|
|
1103
|
+
...handles(res)
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* A customer's unit balances + recent unit events. `GET /api/v1/units`.
|
|
1108
|
+
*
|
|
1109
|
+
* Legacy path for the same reason as {@link list}: the signal family is
|
|
1110
|
+
* POST-only, so the read side has no successor and is not deprecated.
|
|
1111
|
+
*/
|
|
977
1112
|
unitUsage(params) {
|
|
978
1113
|
return this.transport.request({
|
|
979
1114
|
method: "GET",
|
|
@@ -1117,6 +1252,8 @@ var ClockNext = class {
|
|
|
1117
1252
|
payments;
|
|
1118
1253
|
/** Manage the credit-type catalog. */
|
|
1119
1254
|
credits;
|
|
1255
|
+
/** Manage the composite catalog — bundles billed as one thing. */
|
|
1256
|
+
composites;
|
|
1120
1257
|
/** Manage the outcome-type catalog. */
|
|
1121
1258
|
outcomes;
|
|
1122
1259
|
/** Manage the unit-type catalog. */
|
|
@@ -1145,6 +1282,7 @@ var ClockNext = class {
|
|
|
1145
1282
|
this.invoices = new Invoices(transport);
|
|
1146
1283
|
this.payments = new Payments(transport);
|
|
1147
1284
|
this.credits = new Credits(transport);
|
|
1285
|
+
this.composites = new Composites(transport);
|
|
1148
1286
|
this.outcomes = new Outcomes(transport);
|
|
1149
1287
|
this.units = new Units(transport);
|
|
1150
1288
|
this.webhooks = new Webhooks(transport);
|