@clocknext/sdk 0.8.0 → 0.11.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 +80 -18
- package/dist/index.cjs +160 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +386 -71
- package/dist/index.d.ts +386 -71
- package/dist/index.js +160 -44
- 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
|
|
|
@@ -123,9 +163,9 @@ const cnk = new ClockNext({ apiKey, mode: "sync" });
|
|
|
123
163
|
> the server prices inline do you get `{ queued: false, usageLog }`. Branch on
|
|
124
164
|
> `queued`; never assume `usageLog` is populated.
|
|
125
165
|
>
|
|
126
|
-
> To
|
|
127
|
-
>
|
|
128
|
-
>
|
|
166
|
+
> To confirm a signal landed, read it back with `signals.list({ customerId })`.
|
|
167
|
+
> There is no way to price a signal without recording it — send one against a
|
|
168
|
+
> **sandbox** organisation instead.
|
|
129
169
|
|
|
130
170
|
> In `async` mode, server-side rejections (e.g. insufficient allowance, `422`)
|
|
131
171
|
> surface via the `onError` hook or `flush()`, not from the `track()` call.
|
|
@@ -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.11.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,30 @@ 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
|
-
/**
|
|
921
|
-
* Validate + price a signal WITHOUT recording it (server `dryRun`). Returns
|
|
922
|
-
* the projected usage log so an integration can preflight before sending real
|
|
923
|
-
* traffic — no inbox row, wallet / outcome writes, or webhooks fire.
|
|
924
|
-
*/
|
|
925
|
-
async verify(signal) {
|
|
926
|
-
const body = signalToWire(signal);
|
|
927
|
-
body.dryRun = true;
|
|
928
|
-
const res = await this.transport.request({
|
|
929
|
-
method: "POST",
|
|
930
|
-
path: "/api/v1/usage",
|
|
931
|
-
body,
|
|
932
|
-
// A dry run writes nothing, so it is always safe to retry.
|
|
933
|
-
retry: true
|
|
934
|
-
});
|
|
935
|
-
return res.usageLog ?? null;
|
|
936
|
-
}
|
|
937
|
-
/** Meter a named credit. */
|
|
970
|
+
/** Meter a named credit. `POST /api/v1/signal/credit`. */
|
|
938
971
|
credit(input, opts) {
|
|
939
972
|
return this.track({ ...input, type: "credit" }, opts);
|
|
940
973
|
}
|
|
941
|
-
/** Debit the customer's wallet at model cost. */
|
|
974
|
+
/** Debit the customer's wallet at model cost. `POST /api/v1/signal/wallet`. */
|
|
942
975
|
wallet(input, opts) {
|
|
943
976
|
return this.track({ ...input, type: "wallet" }, opts);
|
|
944
977
|
}
|
|
945
|
-
/** Advance one step of an outcome workflow. */
|
|
978
|
+
/** Advance one step of an outcome workflow. `POST /api/v1/signal/outcome`. */
|
|
946
979
|
outcome(input, opts) {
|
|
947
980
|
return this.track({ ...input, type: "outcome" }, opts);
|
|
948
981
|
}
|
|
949
|
-
/**
|
|
982
|
+
/**
|
|
983
|
+
* Recent usage logs + totals for a customer. `GET /api/v1/usage`.
|
|
984
|
+
*
|
|
985
|
+
* Still the legacy path, deliberately. The `/api/v1/signal/*` family is
|
|
986
|
+
* POST-only — it replaced how usage is INGESTED, not how it is read — so
|
|
987
|
+
* `GET /api/v1/usage` has no successor and is not deprecated. It is also the
|
|
988
|
+
* only way to prove a signal landed, which matters more now that ingestion
|
|
989
|
+
* answers `202` for everything.
|
|
990
|
+
*/
|
|
950
991
|
async list(params) {
|
|
951
992
|
const res = await this.transport.request({
|
|
952
993
|
method: "GET",
|
|
@@ -958,22 +999,94 @@ var Signals = class {
|
|
|
958
999
|
/**
|
|
959
1000
|
* Record one unit-metered event (a fixed-price meter with no tokens),
|
|
960
1001
|
* addressed to a catalog unit by its `agentKey`. Sent immediately — unit
|
|
961
|
-
* events are not buffered. `POST /api/v1/
|
|
1002
|
+
* events are not buffered. `POST /api/v1/signal/unit`.
|
|
1003
|
+
*
|
|
1004
|
+
* Two things changed with the move off `POST /api/v1/units`, and both show up
|
|
1005
|
+
* in the return type. The route is now durable (the report lands in an inbox
|
|
1006
|
+
* before it is priced, so a failure parks as Pending and can be retried
|
|
1007
|
+
* instead of losing a billable event) and it answers `202` when it queues —
|
|
1008
|
+
* so this resolves {@link UnitTrackResult}, not a bare `{ unitUsage }`.
|
|
1009
|
+
* Branch on `queued`.
|
|
962
1010
|
*/
|
|
963
1011
|
async unit(input) {
|
|
964
|
-
|
|
1012
|
+
const res = await this.transport.request({
|
|
965
1013
|
method: "POST",
|
|
966
|
-
path:
|
|
1014
|
+
path: UNIT_PATH,
|
|
967
1015
|
body: {
|
|
968
1016
|
customerId: input.customerId,
|
|
969
1017
|
agentKey: input.agentKey,
|
|
970
|
-
...input.
|
|
1018
|
+
...input.quantity != null ? { quantity: input.quantity } : {},
|
|
1019
|
+
...input.member ? { member: input.member } : {},
|
|
1020
|
+
...input.composite ? { composite: { [input.composite.ref]: input.composite.value } } : {},
|
|
1021
|
+
// Assigned HERE, before the send, so the SAME key rides every
|
|
1022
|
+
// transport-level retry below. Without it a single 500 followed by a
|
|
1023
|
+
// success is TWO un-keyed reports, and the server dedups on the key —
|
|
1024
|
+
// so it bills the allowance down twice, up to `retry.maxAttempts`
|
|
1025
|
+
// times. The route gained idempotency precisely to stop that; not
|
|
1026
|
+
// using it by default would leave the double-charge reachable from the
|
|
1027
|
+
// client instead of the server.
|
|
1028
|
+
idempotencyKey: input.idempotencyKey ?? newKey()
|
|
971
1029
|
},
|
|
972
1030
|
// Best-effort metering opts into retries on transient failures.
|
|
973
1031
|
retry: true
|
|
974
1032
|
});
|
|
1033
|
+
return {
|
|
1034
|
+
queued: res.queued === true,
|
|
1035
|
+
unitUsage: res.unitUsage ?? null,
|
|
1036
|
+
...handles(res)
|
|
1037
|
+
};
|
|
975
1038
|
}
|
|
976
|
-
/**
|
|
1039
|
+
/**
|
|
1040
|
+
* Close ONE occurrence of a composite — a distinct tag value.
|
|
1041
|
+
* `POST /api/v1/signal/composite/complete`.
|
|
1042
|
+
*
|
|
1043
|
+
* A composite bundles several credits / outcomes / units and is billed as one
|
|
1044
|
+
* thing. The occurrence opens on the first tagged signal and costs nothing;
|
|
1045
|
+
* this call is the moment it is billed. Only your code knows when the
|
|
1046
|
+
* workflow ended, so this is a DECLARATION, not a hint: tagged signals may
|
|
1047
|
+
* arrive in any order and the occurrence stays open and unbilled until you
|
|
1048
|
+
* say otherwise. Put it on the line before your own `return`.
|
|
1049
|
+
*
|
|
1050
|
+
* An OUTCOME never needs this — its own `complete: true` closes its run and
|
|
1051
|
+
* the matching occurrence together. Credits and units have no "done" signal
|
|
1052
|
+
* of their own, so this is theirs.
|
|
1053
|
+
*
|
|
1054
|
+
* Closing twice is safe: the second call answers `ALREADY_COMPLETED` and
|
|
1055
|
+
* cannot bill again. Calling it EARLY is also safe — before every entitlement
|
|
1056
|
+
* the composite is restricted to has reported, or while an ADVANCE pool is
|
|
1057
|
+
* full — the server parks the ping and applies it when the blocker clears,
|
|
1058
|
+
* rather than failing it.
|
|
1059
|
+
*/
|
|
1060
|
+
async completeComposite(input) {
|
|
1061
|
+
const res = await this.transport.request({
|
|
1062
|
+
method: "POST",
|
|
1063
|
+
path: COMPOSITE_COMPLETE_PATH,
|
|
1064
|
+
body: {
|
|
1065
|
+
customerId: input.customerId,
|
|
1066
|
+
composite: { [input.composite.ref]: input.composite.value },
|
|
1067
|
+
// Auto-assigned like the others. A retry here could not double-BILL
|
|
1068
|
+
// even without it — closing a closed occurrence is a server-side
|
|
1069
|
+
// no-op — but that safety comes from occurrence-level dedup, not from
|
|
1070
|
+
// idempotency, and an un-keyed replay still writes a redundant inbox
|
|
1071
|
+
// row. Keying it collapses the retry into the one attempt it is.
|
|
1072
|
+
idempotencyKey: input.idempotencyKey ?? newKey()
|
|
1073
|
+
},
|
|
1074
|
+
retry: true
|
|
1075
|
+
});
|
|
1076
|
+
return {
|
|
1077
|
+
queued: res.queued === true,
|
|
1078
|
+
...res.status != null ? { status: res.status } : {},
|
|
1079
|
+
...res.composite !== void 0 ? { composite: res.composite } : {},
|
|
1080
|
+
...res.value !== void 0 ? { value: res.value } : {},
|
|
1081
|
+
...handles(res)
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* A customer's unit balances + recent unit events. `GET /api/v1/units`.
|
|
1086
|
+
*
|
|
1087
|
+
* Legacy path for the same reason as {@link list}: the signal family is
|
|
1088
|
+
* POST-only, so the read side has no successor and is not deprecated.
|
|
1089
|
+
*/
|
|
977
1090
|
unitUsage(params) {
|
|
978
1091
|
return this.transport.request({
|
|
979
1092
|
method: "GET",
|
|
@@ -1117,6 +1230,8 @@ var ClockNext = class {
|
|
|
1117
1230
|
payments;
|
|
1118
1231
|
/** Manage the credit-type catalog. */
|
|
1119
1232
|
credits;
|
|
1233
|
+
/** Manage the composite catalog — bundles billed as one thing. */
|
|
1234
|
+
composites;
|
|
1120
1235
|
/** Manage the outcome-type catalog. */
|
|
1121
1236
|
outcomes;
|
|
1122
1237
|
/** Manage the unit-type catalog. */
|
|
@@ -1145,6 +1260,7 @@ var ClockNext = class {
|
|
|
1145
1260
|
this.invoices = new Invoices(transport);
|
|
1146
1261
|
this.payments = new Payments(transport);
|
|
1147
1262
|
this.credits = new Credits(transport);
|
|
1263
|
+
this.composites = new Composites(transport);
|
|
1148
1264
|
this.outcomes = new Outcomes(transport);
|
|
1149
1265
|
this.units = new Units(transport);
|
|
1150
1266
|
this.webhooks = new Webhooks(transport);
|