@t402/streaming-payments 1.0.0-beta.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 +422 -0
- package/dist/channels/index.d.ts +1560 -0
- package/dist/channels/index.js +1135 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3482 -0
- package/dist/index.js.map +1 -0
- package/dist/settlement/index.d.ts +867 -0
- package/dist/settlement/index.js +1030 -0
- package/dist/settlement/index.js.map +1 -0
- package/dist/streaming/index.d.ts +1004 -0
- package/dist/streaming/index.js +1321 -0
- package/dist/streaming/index.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
// src/streaming/types.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var StreamState = z.enum([
|
|
4
|
+
"idle",
|
|
5
|
+
// Stream not started
|
|
6
|
+
"active",
|
|
7
|
+
// Streaming in progress
|
|
8
|
+
"paused",
|
|
9
|
+
// Temporarily paused
|
|
10
|
+
"completed",
|
|
11
|
+
// Stream completed (exhausted)
|
|
12
|
+
"cancelled"
|
|
13
|
+
// Stream cancelled
|
|
14
|
+
]);
|
|
15
|
+
var RateType = z.enum([
|
|
16
|
+
"fixed",
|
|
17
|
+
// Fixed rate per second
|
|
18
|
+
"variable",
|
|
19
|
+
// Rate can change
|
|
20
|
+
"tiered",
|
|
21
|
+
// Different rates based on usage
|
|
22
|
+
"dynamic"
|
|
23
|
+
// Demand-based pricing
|
|
24
|
+
]);
|
|
25
|
+
var StreamRate = z.object({
|
|
26
|
+
type: RateType,
|
|
27
|
+
baseRate: z.string(),
|
|
28
|
+
// Base rate per second
|
|
29
|
+
minRate: z.string().optional(),
|
|
30
|
+
// Minimum rate
|
|
31
|
+
maxRate: z.string().optional(),
|
|
32
|
+
// Maximum rate
|
|
33
|
+
// For tiered rates
|
|
34
|
+
tiers: z.array(z.object({
|
|
35
|
+
threshold: z.string(),
|
|
36
|
+
// Usage threshold
|
|
37
|
+
rate: z.string()
|
|
38
|
+
// Rate after threshold
|
|
39
|
+
})).optional(),
|
|
40
|
+
// For dynamic rates
|
|
41
|
+
adjustmentInterval: z.number().optional(),
|
|
42
|
+
// How often to adjust (seconds)
|
|
43
|
+
adjustmentFactor: z.number().optional()
|
|
44
|
+
// Max adjustment per interval
|
|
45
|
+
});
|
|
46
|
+
var UsageMetrics = z.object({
|
|
47
|
+
totalSeconds: z.number(),
|
|
48
|
+
totalAmount: z.string(),
|
|
49
|
+
averageRate: z.string(),
|
|
50
|
+
peakRate: z.string(),
|
|
51
|
+
startTime: z.number(),
|
|
52
|
+
endTime: z.number().optional(),
|
|
53
|
+
// Breakdown by period
|
|
54
|
+
hourly: z.array(z.object({
|
|
55
|
+
hour: z.number(),
|
|
56
|
+
amount: z.string(),
|
|
57
|
+
seconds: z.number()
|
|
58
|
+
})).optional()
|
|
59
|
+
});
|
|
60
|
+
var MeteringRecord = z.object({
|
|
61
|
+
timestamp: z.number(),
|
|
62
|
+
duration: z.number(),
|
|
63
|
+
// Seconds since last record
|
|
64
|
+
amount: z.string(),
|
|
65
|
+
// Amount for this period
|
|
66
|
+
rate: z.string(),
|
|
67
|
+
// Rate applied
|
|
68
|
+
cumulative: z.string(),
|
|
69
|
+
// Cumulative total
|
|
70
|
+
metadata: z.record(z.unknown()).optional()
|
|
71
|
+
});
|
|
72
|
+
var BillingPeriod = z.enum([
|
|
73
|
+
"realtime",
|
|
74
|
+
// Continuous real-time billing
|
|
75
|
+
"second",
|
|
76
|
+
// Per-second
|
|
77
|
+
"minute",
|
|
78
|
+
// Per-minute
|
|
79
|
+
"hour",
|
|
80
|
+
// Per-hour
|
|
81
|
+
"day"
|
|
82
|
+
// Per-day
|
|
83
|
+
]);
|
|
84
|
+
var BillingConfig = z.object({
|
|
85
|
+
period: BillingPeriod,
|
|
86
|
+
minimumCharge: z.string().default("0"),
|
|
87
|
+
roundingMode: z.enum(["floor", "ceil", "round"]).default("floor"),
|
|
88
|
+
gracePeriod: z.number().default(0),
|
|
89
|
+
// Seconds of free usage
|
|
90
|
+
invoiceInterval: z.number().optional()
|
|
91
|
+
// Generate invoice every N seconds
|
|
92
|
+
});
|
|
93
|
+
var InvoiceItem = z.object({
|
|
94
|
+
description: z.string(),
|
|
95
|
+
quantity: z.number(),
|
|
96
|
+
// Duration in billing periods
|
|
97
|
+
rate: z.string(),
|
|
98
|
+
amount: z.string(),
|
|
99
|
+
startTime: z.number(),
|
|
100
|
+
endTime: z.number()
|
|
101
|
+
});
|
|
102
|
+
var Invoice = z.object({
|
|
103
|
+
id: z.string(),
|
|
104
|
+
channelId: z.string(),
|
|
105
|
+
payer: z.string(),
|
|
106
|
+
payee: z.string(),
|
|
107
|
+
items: z.array(InvoiceItem),
|
|
108
|
+
subtotal: z.string(),
|
|
109
|
+
fees: z.string(),
|
|
110
|
+
total: z.string(),
|
|
111
|
+
currency: z.string(),
|
|
112
|
+
status: z.enum(["pending", "paid", "settled", "disputed"]),
|
|
113
|
+
createdAt: z.number(),
|
|
114
|
+
dueAt: z.number().optional(),
|
|
115
|
+
paidAt: z.number().optional()
|
|
116
|
+
});
|
|
117
|
+
var StreamSession = z.object({
|
|
118
|
+
id: z.string(),
|
|
119
|
+
channelId: z.string(),
|
|
120
|
+
state: StreamState,
|
|
121
|
+
rate: StreamRate,
|
|
122
|
+
startedAt: z.number().optional(),
|
|
123
|
+
pausedAt: z.number().optional(),
|
|
124
|
+
endedAt: z.number().optional(),
|
|
125
|
+
totalDuration: z.number(),
|
|
126
|
+
// Total streaming seconds
|
|
127
|
+
totalAmount: z.string(),
|
|
128
|
+
// Total amount streamed
|
|
129
|
+
meteringRecords: z.array(MeteringRecord),
|
|
130
|
+
billingConfig: BillingConfig,
|
|
131
|
+
invoices: z.array(z.string())
|
|
132
|
+
// Invoice IDs
|
|
133
|
+
});
|
|
134
|
+
var RateAdjustmentRequest = z.object({
|
|
135
|
+
sessionId: z.string(),
|
|
136
|
+
newRate: z.string(),
|
|
137
|
+
reason: z.string(),
|
|
138
|
+
effectiveFrom: z.number().optional(),
|
|
139
|
+
// Timestamp, default now
|
|
140
|
+
signature: z.string().optional()
|
|
141
|
+
// For mutual rate changes
|
|
142
|
+
});
|
|
143
|
+
var StreamEvent = z.discriminatedUnion("type", [
|
|
144
|
+
z.object({
|
|
145
|
+
type: z.literal("started"),
|
|
146
|
+
sessionId: z.string(),
|
|
147
|
+
timestamp: z.number(),
|
|
148
|
+
rate: z.string()
|
|
149
|
+
}),
|
|
150
|
+
z.object({
|
|
151
|
+
type: z.literal("paused"),
|
|
152
|
+
sessionId: z.string(),
|
|
153
|
+
timestamp: z.number(),
|
|
154
|
+
totalStreamed: z.string()
|
|
155
|
+
}),
|
|
156
|
+
z.object({
|
|
157
|
+
type: z.literal("resumed"),
|
|
158
|
+
sessionId: z.string(),
|
|
159
|
+
timestamp: z.number()
|
|
160
|
+
}),
|
|
161
|
+
z.object({
|
|
162
|
+
type: z.literal("rate_changed"),
|
|
163
|
+
sessionId: z.string(),
|
|
164
|
+
timestamp: z.number(),
|
|
165
|
+
oldRate: z.string(),
|
|
166
|
+
newRate: z.string()
|
|
167
|
+
}),
|
|
168
|
+
z.object({
|
|
169
|
+
type: z.literal("checkpoint"),
|
|
170
|
+
sessionId: z.string(),
|
|
171
|
+
timestamp: z.number(),
|
|
172
|
+
amount: z.string(),
|
|
173
|
+
checkpointId: z.string()
|
|
174
|
+
}),
|
|
175
|
+
z.object({
|
|
176
|
+
type: z.literal("completed"),
|
|
177
|
+
sessionId: z.string(),
|
|
178
|
+
timestamp: z.number(),
|
|
179
|
+
totalAmount: z.string(),
|
|
180
|
+
totalDuration: z.number()
|
|
181
|
+
}),
|
|
182
|
+
z.object({
|
|
183
|
+
type: z.literal("cancelled"),
|
|
184
|
+
sessionId: z.string(),
|
|
185
|
+
timestamp: z.number(),
|
|
186
|
+
reason: z.string()
|
|
187
|
+
})
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
// src/streaming/flow.ts
|
|
191
|
+
var FlowController = class {
|
|
192
|
+
session;
|
|
193
|
+
config;
|
|
194
|
+
updateTimer;
|
|
195
|
+
checkpointTimer;
|
|
196
|
+
eventListeners = /* @__PURE__ */ new Set();
|
|
197
|
+
lastUpdateTime = 0;
|
|
198
|
+
constructor(channelId, rate, billingConfig, config = {}) {
|
|
199
|
+
this.config = {
|
|
200
|
+
updateInterval: config.updateInterval ?? 1e3,
|
|
201
|
+
bufferTime: config.bufferTime ?? 60,
|
|
202
|
+
autoCheckpoint: config.autoCheckpoint ?? true,
|
|
203
|
+
checkpointInterval: config.checkpointInterval ?? 3600
|
|
204
|
+
};
|
|
205
|
+
this.session = {
|
|
206
|
+
id: this.generateSessionId(),
|
|
207
|
+
channelId,
|
|
208
|
+
state: "idle",
|
|
209
|
+
rate,
|
|
210
|
+
totalDuration: 0,
|
|
211
|
+
totalAmount: "0",
|
|
212
|
+
meteringRecords: [],
|
|
213
|
+
billingConfig,
|
|
214
|
+
invoices: []
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get current session state
|
|
219
|
+
*/
|
|
220
|
+
getSession() {
|
|
221
|
+
return { ...this.session };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get current state
|
|
225
|
+
*/
|
|
226
|
+
getState() {
|
|
227
|
+
return this.session.state;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Start streaming
|
|
231
|
+
*/
|
|
232
|
+
start() {
|
|
233
|
+
if (this.session.state !== "idle" && this.session.state !== "paused") {
|
|
234
|
+
return { success: false, error: "Stream must be idle or paused to start" };
|
|
235
|
+
}
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
if (this.session.state === "idle") {
|
|
238
|
+
this.session.startedAt = now;
|
|
239
|
+
} else {
|
|
240
|
+
const pauseDuration = now - (this.session.pausedAt ?? now);
|
|
241
|
+
this.session.pausedAt = void 0;
|
|
242
|
+
this.addMeteringRecord({
|
|
243
|
+
timestamp: now,
|
|
244
|
+
duration: 0,
|
|
245
|
+
amount: "0",
|
|
246
|
+
rate: "0",
|
|
247
|
+
cumulative: this.session.totalAmount,
|
|
248
|
+
metadata: { event: "resume", pauseDuration }
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
this.session.state = "active";
|
|
252
|
+
this.lastUpdateTime = now;
|
|
253
|
+
this.startUpdateTimer();
|
|
254
|
+
if (this.config.autoCheckpoint) {
|
|
255
|
+
this.startCheckpointTimer();
|
|
256
|
+
}
|
|
257
|
+
this.emitEvent({
|
|
258
|
+
type: "started",
|
|
259
|
+
sessionId: this.session.id,
|
|
260
|
+
timestamp: now,
|
|
261
|
+
rate: this.session.rate.baseRate
|
|
262
|
+
});
|
|
263
|
+
return { success: true };
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Pause streaming
|
|
267
|
+
*/
|
|
268
|
+
pause() {
|
|
269
|
+
if (this.session.state !== "active") {
|
|
270
|
+
return { success: false, error: "Stream must be active to pause" };
|
|
271
|
+
}
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
this.updateTotals(now);
|
|
274
|
+
this.session.state = "paused";
|
|
275
|
+
this.session.pausedAt = now;
|
|
276
|
+
this.stopTimers();
|
|
277
|
+
this.emitEvent({
|
|
278
|
+
type: "paused",
|
|
279
|
+
sessionId: this.session.id,
|
|
280
|
+
timestamp: now,
|
|
281
|
+
totalStreamed: this.session.totalAmount
|
|
282
|
+
});
|
|
283
|
+
return { success: true };
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Resume streaming (alias for start when paused)
|
|
287
|
+
*/
|
|
288
|
+
resume() {
|
|
289
|
+
if (this.session.state !== "paused") {
|
|
290
|
+
return { success: false, error: "Stream must be paused to resume" };
|
|
291
|
+
}
|
|
292
|
+
const result = this.start();
|
|
293
|
+
if (result.success) {
|
|
294
|
+
this.emitEvent({
|
|
295
|
+
type: "resumed",
|
|
296
|
+
sessionId: this.session.id,
|
|
297
|
+
timestamp: Date.now()
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Stop streaming (complete)
|
|
304
|
+
*/
|
|
305
|
+
stop() {
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
if (this.session.state === "active") {
|
|
308
|
+
this.updateTotals(now);
|
|
309
|
+
}
|
|
310
|
+
this.stopTimers();
|
|
311
|
+
this.session.state = "completed";
|
|
312
|
+
this.session.endedAt = now;
|
|
313
|
+
this.emitEvent({
|
|
314
|
+
type: "completed",
|
|
315
|
+
sessionId: this.session.id,
|
|
316
|
+
timestamp: now,
|
|
317
|
+
totalAmount: this.session.totalAmount,
|
|
318
|
+
totalDuration: this.session.totalDuration
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
finalAmount: this.session.totalAmount
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Cancel streaming
|
|
327
|
+
*/
|
|
328
|
+
cancel(reason) {
|
|
329
|
+
const now = Date.now();
|
|
330
|
+
if (this.session.state === "active") {
|
|
331
|
+
this.updateTotals(now);
|
|
332
|
+
}
|
|
333
|
+
this.stopTimers();
|
|
334
|
+
this.session.state = "cancelled";
|
|
335
|
+
this.session.endedAt = now;
|
|
336
|
+
this.emitEvent({
|
|
337
|
+
type: "cancelled",
|
|
338
|
+
sessionId: this.session.id,
|
|
339
|
+
timestamp: now,
|
|
340
|
+
reason
|
|
341
|
+
});
|
|
342
|
+
return { success: true };
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Get current streamed amount
|
|
346
|
+
*/
|
|
347
|
+
getCurrentAmount() {
|
|
348
|
+
if (this.session.state !== "active") {
|
|
349
|
+
return this.session.totalAmount;
|
|
350
|
+
}
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
const elapsed = Math.floor((now - this.lastUpdateTime) / 1e3);
|
|
353
|
+
const additionalAmount = this.calculateAmount(elapsed, this.session.rate);
|
|
354
|
+
return (BigInt(this.session.totalAmount) + BigInt(additionalAmount)).toString();
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Get current rate
|
|
358
|
+
*/
|
|
359
|
+
getCurrentRate() {
|
|
360
|
+
return this.getEffectiveRate(this.session.rate, this.session.totalAmount);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Get time until exhaustion (returns -1 if infinite)
|
|
364
|
+
*/
|
|
365
|
+
getTimeUntilExhaustion(channelCapacity) {
|
|
366
|
+
if (this.session.state !== "active") {
|
|
367
|
+
return -1;
|
|
368
|
+
}
|
|
369
|
+
const remaining = BigInt(channelCapacity) - BigInt(this.getCurrentAmount());
|
|
370
|
+
if (remaining <= 0n) {
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
const rate = BigInt(this.getCurrentRate());
|
|
374
|
+
if (rate <= 0n) {
|
|
375
|
+
return -1;
|
|
376
|
+
}
|
|
377
|
+
return Number(remaining / rate);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Check if stream is near exhaustion
|
|
381
|
+
*/
|
|
382
|
+
isNearExhaustion(channelCapacity) {
|
|
383
|
+
const remaining = this.getTimeUntilExhaustion(channelCapacity);
|
|
384
|
+
return remaining >= 0 && remaining <= this.config.bufferTime;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Create manual checkpoint
|
|
388
|
+
*/
|
|
389
|
+
createCheckpoint() {
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
const amount = this.getCurrentAmount();
|
|
392
|
+
const checkpointId = `cp_${this.session.id}_${now}`;
|
|
393
|
+
this.emitEvent({
|
|
394
|
+
type: "checkpoint",
|
|
395
|
+
sessionId: this.session.id,
|
|
396
|
+
timestamp: now,
|
|
397
|
+
amount,
|
|
398
|
+
checkpointId
|
|
399
|
+
});
|
|
400
|
+
return { id: checkpointId, amount, timestamp: now };
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Subscribe to stream events
|
|
404
|
+
*/
|
|
405
|
+
onEvent(callback) {
|
|
406
|
+
this.eventListeners.add(callback);
|
|
407
|
+
return () => this.eventListeners.delete(callback);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Clean up resources
|
|
411
|
+
*/
|
|
412
|
+
destroy() {
|
|
413
|
+
this.stopTimers();
|
|
414
|
+
this.eventListeners.clear();
|
|
415
|
+
}
|
|
416
|
+
startUpdateTimer() {
|
|
417
|
+
this.updateTimer = setInterval(() => {
|
|
418
|
+
if (this.session.state === "active") {
|
|
419
|
+
this.updateTotals(Date.now());
|
|
420
|
+
}
|
|
421
|
+
}, this.config.updateInterval);
|
|
422
|
+
}
|
|
423
|
+
startCheckpointTimer() {
|
|
424
|
+
this.checkpointTimer = setInterval(() => {
|
|
425
|
+
if (this.session.state === "active") {
|
|
426
|
+
this.createCheckpoint();
|
|
427
|
+
}
|
|
428
|
+
}, this.config.checkpointInterval * 1e3);
|
|
429
|
+
}
|
|
430
|
+
stopTimers() {
|
|
431
|
+
if (this.updateTimer) {
|
|
432
|
+
clearInterval(this.updateTimer);
|
|
433
|
+
this.updateTimer = void 0;
|
|
434
|
+
}
|
|
435
|
+
if (this.checkpointTimer) {
|
|
436
|
+
clearInterval(this.checkpointTimer);
|
|
437
|
+
this.checkpointTimer = void 0;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
updateTotals(now) {
|
|
441
|
+
const elapsed = Math.floor((now - this.lastUpdateTime) / 1e3);
|
|
442
|
+
if (elapsed <= 0) return;
|
|
443
|
+
const amount = this.calculateAmount(elapsed, this.session.rate);
|
|
444
|
+
const newTotal = BigInt(this.session.totalAmount) + BigInt(amount);
|
|
445
|
+
this.session.totalDuration += elapsed;
|
|
446
|
+
this.session.totalAmount = newTotal.toString();
|
|
447
|
+
this.lastUpdateTime = now;
|
|
448
|
+
this.addMeteringRecord({
|
|
449
|
+
timestamp: now,
|
|
450
|
+
duration: elapsed,
|
|
451
|
+
amount,
|
|
452
|
+
rate: this.getCurrentRate(),
|
|
453
|
+
cumulative: this.session.totalAmount
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
calculateAmount(seconds, rate) {
|
|
457
|
+
const effectiveRate = this.getEffectiveRate(rate, this.session.totalAmount);
|
|
458
|
+
return (BigInt(effectiveRate) * BigInt(seconds)).toString();
|
|
459
|
+
}
|
|
460
|
+
getEffectiveRate(rate, totalAmount) {
|
|
461
|
+
if (rate.type === "fixed") {
|
|
462
|
+
return rate.baseRate;
|
|
463
|
+
}
|
|
464
|
+
if (rate.type === "tiered" && rate.tiers) {
|
|
465
|
+
const amount = BigInt(totalAmount);
|
|
466
|
+
let applicableRate = rate.baseRate;
|
|
467
|
+
for (const tier of rate.tiers) {
|
|
468
|
+
if (amount >= BigInt(tier.threshold)) {
|
|
469
|
+
applicableRate = tier.rate;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return applicableRate;
|
|
473
|
+
}
|
|
474
|
+
return rate.baseRate;
|
|
475
|
+
}
|
|
476
|
+
addMeteringRecord(record) {
|
|
477
|
+
this.session.meteringRecords.push(record);
|
|
478
|
+
if (this.session.meteringRecords.length > 1e3) {
|
|
479
|
+
this.session.meteringRecords = this.session.meteringRecords.slice(-1e3);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
emitEvent(event) {
|
|
483
|
+
this.eventListeners.forEach((callback) => {
|
|
484
|
+
try {
|
|
485
|
+
callback(event);
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
generateSessionId() {
|
|
491
|
+
return `ss_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
function createFixedRateFlow(channelId, ratePerSecond, config) {
|
|
495
|
+
const rate = {
|
|
496
|
+
type: "fixed",
|
|
497
|
+
baseRate: ratePerSecond
|
|
498
|
+
};
|
|
499
|
+
const billingConfig = {
|
|
500
|
+
period: "realtime",
|
|
501
|
+
minimumCharge: "0",
|
|
502
|
+
roundingMode: "floor",
|
|
503
|
+
gracePeriod: 0
|
|
504
|
+
};
|
|
505
|
+
return new FlowController(channelId, rate, billingConfig, config);
|
|
506
|
+
}
|
|
507
|
+
function createTieredRateFlow(channelId, baseRate, tiers, config) {
|
|
508
|
+
const rate = {
|
|
509
|
+
type: "tiered",
|
|
510
|
+
baseRate,
|
|
511
|
+
tiers
|
|
512
|
+
};
|
|
513
|
+
const billingConfig = {
|
|
514
|
+
period: "realtime",
|
|
515
|
+
minimumCharge: "0",
|
|
516
|
+
roundingMode: "floor",
|
|
517
|
+
gracePeriod: 0
|
|
518
|
+
};
|
|
519
|
+
return new FlowController(channelId, rate, billingConfig, config);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/streaming/rate.ts
|
|
523
|
+
var RateController = class {
|
|
524
|
+
currentRate;
|
|
525
|
+
config;
|
|
526
|
+
history = [];
|
|
527
|
+
lastChangeTime = 0;
|
|
528
|
+
constructor(initialRate, config = {}) {
|
|
529
|
+
this.currentRate = { ...initialRate };
|
|
530
|
+
this.config = {
|
|
531
|
+
maxChangePercent: config.maxChangePercent ?? 50,
|
|
532
|
+
minChangeInterval: config.minChangeInterval ?? 60,
|
|
533
|
+
smoothingFactor: config.smoothingFactor ?? 0.3
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Get current rate configuration
|
|
538
|
+
*/
|
|
539
|
+
getRate() {
|
|
540
|
+
return { ...this.currentRate };
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Get current effective rate
|
|
544
|
+
*/
|
|
545
|
+
getEffectiveRate(totalUsage) {
|
|
546
|
+
if (this.currentRate.type === "fixed") {
|
|
547
|
+
return this.currentRate.baseRate;
|
|
548
|
+
}
|
|
549
|
+
if (this.currentRate.type === "tiered" && this.currentRate.tiers && totalUsage) {
|
|
550
|
+
return this.calculateTieredRate(totalUsage);
|
|
551
|
+
}
|
|
552
|
+
return this.currentRate.baseRate;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Request rate adjustment
|
|
556
|
+
*/
|
|
557
|
+
adjustRate(request) {
|
|
558
|
+
const now = Date.now();
|
|
559
|
+
const timeSinceLastChange = (now - this.lastChangeTime) / 1e3;
|
|
560
|
+
if (timeSinceLastChange < this.config.minChangeInterval) {
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
error: `Rate can only be changed every ${this.config.minChangeInterval} seconds`
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const newRateBigInt = BigInt(request.newRate);
|
|
567
|
+
if (newRateBigInt <= 0n) {
|
|
568
|
+
return { success: false, error: "Rate must be positive" };
|
|
569
|
+
}
|
|
570
|
+
if (this.currentRate.minRate && newRateBigInt < BigInt(this.currentRate.minRate)) {
|
|
571
|
+
return {
|
|
572
|
+
success: false,
|
|
573
|
+
error: `Rate cannot be below minimum: ${this.currentRate.minRate}`
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
if (this.currentRate.maxRate && newRateBigInt > BigInt(this.currentRate.maxRate)) {
|
|
577
|
+
return {
|
|
578
|
+
success: false,
|
|
579
|
+
error: `Rate cannot exceed maximum: ${this.currentRate.maxRate}`
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const currentRateBigInt = BigInt(this.currentRate.baseRate);
|
|
583
|
+
const changePercent = this.calculateChangePercent(currentRateBigInt, newRateBigInt);
|
|
584
|
+
let adjustedRate = request.newRate;
|
|
585
|
+
if (changePercent > this.config.maxChangePercent) {
|
|
586
|
+
adjustedRate = this.applyMaxChange(
|
|
587
|
+
currentRateBigInt,
|
|
588
|
+
newRateBigInt,
|
|
589
|
+
this.config.maxChangePercent
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
this.history.push({
|
|
593
|
+
timestamp: now,
|
|
594
|
+
rate: adjustedRate,
|
|
595
|
+
reason: request.reason,
|
|
596
|
+
previousRate: this.currentRate.baseRate
|
|
597
|
+
});
|
|
598
|
+
this.currentRate.baseRate = adjustedRate;
|
|
599
|
+
this.lastChangeTime = now;
|
|
600
|
+
return {
|
|
601
|
+
success: true,
|
|
602
|
+
newRate: adjustedRate,
|
|
603
|
+
adjustedAmount: adjustedRate !== request.newRate ? adjustedRate : void 0
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Set rate bounds
|
|
608
|
+
*/
|
|
609
|
+
setBounds(minRate, maxRate) {
|
|
610
|
+
if (minRate !== void 0) {
|
|
611
|
+
this.currentRate.minRate = minRate;
|
|
612
|
+
}
|
|
613
|
+
if (maxRate !== void 0) {
|
|
614
|
+
this.currentRate.maxRate = maxRate;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Add or update a tier
|
|
619
|
+
*/
|
|
620
|
+
addTier(threshold, rate) {
|
|
621
|
+
if (this.currentRate.type !== "tiered") {
|
|
622
|
+
this.currentRate.type = "tiered";
|
|
623
|
+
this.currentRate.tiers = [];
|
|
624
|
+
}
|
|
625
|
+
const tiers = this.currentRate.tiers ?? [];
|
|
626
|
+
const existingIndex = tiers.findIndex((t) => t.threshold === threshold);
|
|
627
|
+
if (existingIndex >= 0) {
|
|
628
|
+
tiers[existingIndex].rate = rate;
|
|
629
|
+
} else {
|
|
630
|
+
tiers.push({ threshold, rate });
|
|
631
|
+
tiers.sort((a, b) => {
|
|
632
|
+
return BigInt(a.threshold) < BigInt(b.threshold) ? -1 : 1;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
this.currentRate.tiers = tiers;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Remove a tier
|
|
639
|
+
*/
|
|
640
|
+
removeTier(threshold) {
|
|
641
|
+
if (!this.currentRate.tiers) return false;
|
|
642
|
+
const initialLength = this.currentRate.tiers.length;
|
|
643
|
+
this.currentRate.tiers = this.currentRate.tiers.filter(
|
|
644
|
+
(t) => t.threshold !== threshold
|
|
645
|
+
);
|
|
646
|
+
return this.currentRate.tiers.length < initialLength;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Get rate history
|
|
650
|
+
*/
|
|
651
|
+
getHistory() {
|
|
652
|
+
return [...this.history];
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Calculate average rate over time period
|
|
656
|
+
*/
|
|
657
|
+
getAverageRate(startTime, endTime) {
|
|
658
|
+
const relevantHistory = this.history.filter(
|
|
659
|
+
(h) => h.timestamp >= startTime && h.timestamp <= endTime
|
|
660
|
+
);
|
|
661
|
+
if (relevantHistory.length === 0) {
|
|
662
|
+
return this.currentRate.baseRate;
|
|
663
|
+
}
|
|
664
|
+
let totalWeight = 0n;
|
|
665
|
+
let weightedSum = 0n;
|
|
666
|
+
let prevTime = startTime;
|
|
667
|
+
for (const entry of relevantHistory) {
|
|
668
|
+
const duration = BigInt(entry.timestamp - prevTime);
|
|
669
|
+
weightedSum += BigInt(entry.previousRate) * duration;
|
|
670
|
+
totalWeight += duration;
|
|
671
|
+
prevTime = entry.timestamp;
|
|
672
|
+
}
|
|
673
|
+
const finalDuration = BigInt(endTime - prevTime);
|
|
674
|
+
weightedSum += BigInt(this.currentRate.baseRate) * finalDuration;
|
|
675
|
+
totalWeight += finalDuration;
|
|
676
|
+
if (totalWeight === 0n) {
|
|
677
|
+
return this.currentRate.baseRate;
|
|
678
|
+
}
|
|
679
|
+
return (weightedSum / totalWeight).toString();
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Calculate rate for dynamic pricing based on demand
|
|
683
|
+
*/
|
|
684
|
+
calculateDynamicRate(demand, _baseRate) {
|
|
685
|
+
const base = BigInt(this.currentRate.baseRate);
|
|
686
|
+
const min = this.currentRate.minRate ? BigInt(this.currentRate.minRate) : base / 2n;
|
|
687
|
+
const max = this.currentRate.maxRate ? BigInt(this.currentRate.maxRate) : base * 2n;
|
|
688
|
+
const range = max - min;
|
|
689
|
+
const adjustment = BigInt(Math.floor(Number(range) * demand));
|
|
690
|
+
return (min + adjustment).toString();
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Apply exponential smoothing for rate changes
|
|
694
|
+
*/
|
|
695
|
+
smoothRate(targetRate) {
|
|
696
|
+
const current = BigInt(this.currentRate.baseRate);
|
|
697
|
+
const target = BigInt(targetRate);
|
|
698
|
+
const alpha = this.config.smoothingFactor;
|
|
699
|
+
const smoothed = BigInt(Math.floor(
|
|
700
|
+
alpha * Number(target) + (1 - alpha) * Number(current)
|
|
701
|
+
));
|
|
702
|
+
return smoothed.toString();
|
|
703
|
+
}
|
|
704
|
+
calculateTieredRate(totalUsage) {
|
|
705
|
+
const usage = BigInt(totalUsage);
|
|
706
|
+
const tiers = this.currentRate.tiers ?? [];
|
|
707
|
+
let applicableRate = this.currentRate.baseRate;
|
|
708
|
+
for (const tier of tiers) {
|
|
709
|
+
if (usage >= BigInt(tier.threshold)) {
|
|
710
|
+
applicableRate = tier.rate;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return applicableRate;
|
|
714
|
+
}
|
|
715
|
+
calculateChangePercent(from, to) {
|
|
716
|
+
if (from === 0n) return 100;
|
|
717
|
+
const diff = to > from ? to - from : from - to;
|
|
718
|
+
return Number(diff * 100n / from);
|
|
719
|
+
}
|
|
720
|
+
applyMaxChange(from, to, maxPercent) {
|
|
721
|
+
const maxChange = from * BigInt(maxPercent) / 100n;
|
|
722
|
+
if (to > from) {
|
|
723
|
+
return (from + maxChange).toString();
|
|
724
|
+
} else {
|
|
725
|
+
return (from - maxChange).toString();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
var RateLimiter = class {
|
|
730
|
+
requests = /* @__PURE__ */ new Map();
|
|
731
|
+
maxRequests;
|
|
732
|
+
windowMs;
|
|
733
|
+
constructor(maxRequests = 10, windowMs = 6e4) {
|
|
734
|
+
this.maxRequests = maxRequests;
|
|
735
|
+
this.windowMs = windowMs;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Check if request is allowed
|
|
739
|
+
*/
|
|
740
|
+
isAllowed(key) {
|
|
741
|
+
const now = Date.now();
|
|
742
|
+
const windowStart = now - this.windowMs;
|
|
743
|
+
let requests = this.requests.get(key) ?? [];
|
|
744
|
+
requests = requests.filter((t) => t > windowStart);
|
|
745
|
+
if (requests.length >= this.maxRequests) {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
requests.push(now);
|
|
749
|
+
this.requests.set(key, requests);
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Get remaining requests in window
|
|
754
|
+
*/
|
|
755
|
+
getRemainingRequests(key) {
|
|
756
|
+
const now = Date.now();
|
|
757
|
+
const windowStart = now - this.windowMs;
|
|
758
|
+
const requests = (this.requests.get(key) ?? []).filter((t) => t > windowStart);
|
|
759
|
+
return Math.max(0, this.maxRequests - requests.length);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Reset limits for a key
|
|
763
|
+
*/
|
|
764
|
+
reset(key) {
|
|
765
|
+
this.requests.delete(key);
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Clear all limits
|
|
769
|
+
*/
|
|
770
|
+
clear() {
|
|
771
|
+
this.requests.clear();
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
function calculateOptimalRate(channelCapacity, desiredDurationSeconds, bufferPercent = 10) {
|
|
775
|
+
const capacity = BigInt(channelCapacity);
|
|
776
|
+
const duration = BigInt(desiredDurationSeconds);
|
|
777
|
+
if (duration === 0n) {
|
|
778
|
+
return "0";
|
|
779
|
+
}
|
|
780
|
+
const effectiveCapacity = capacity * BigInt(100 - bufferPercent) / 100n;
|
|
781
|
+
return (effectiveCapacity / duration).toString();
|
|
782
|
+
}
|
|
783
|
+
function convertRate(rate, fromUnit, toUnit) {
|
|
784
|
+
const unitToSeconds = {
|
|
785
|
+
second: 1,
|
|
786
|
+
minute: 60,
|
|
787
|
+
hour: 3600,
|
|
788
|
+
day: 86400
|
|
789
|
+
};
|
|
790
|
+
const fromSeconds = unitToSeconds[fromUnit];
|
|
791
|
+
const toSeconds = unitToSeconds[toUnit];
|
|
792
|
+
const rateBigInt = BigInt(rate);
|
|
793
|
+
const perSecond = rateBigInt / BigInt(fromSeconds);
|
|
794
|
+
return (perSecond * BigInt(toSeconds)).toString();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/streaming/metering.ts
|
|
798
|
+
var MeteringManager = class {
|
|
799
|
+
records = [];
|
|
800
|
+
config;
|
|
801
|
+
sessionId;
|
|
802
|
+
startTime;
|
|
803
|
+
constructor(sessionId, config = {}) {
|
|
804
|
+
this.sessionId = sessionId;
|
|
805
|
+
this.startTime = Date.now();
|
|
806
|
+
this.config = {
|
|
807
|
+
recordInterval: config.recordInterval ?? 1,
|
|
808
|
+
aggregationInterval: config.aggregationInterval ?? 3600,
|
|
809
|
+
maxRecords: config.maxRecords ?? 1e4,
|
|
810
|
+
precision: config.precision ?? 18
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Record usage
|
|
815
|
+
*/
|
|
816
|
+
record(duration, amount, rate, metadata) {
|
|
817
|
+
const cumulative = this.calculateCumulative(amount);
|
|
818
|
+
const record = {
|
|
819
|
+
timestamp: Date.now(),
|
|
820
|
+
duration,
|
|
821
|
+
amount,
|
|
822
|
+
rate,
|
|
823
|
+
cumulative,
|
|
824
|
+
metadata
|
|
825
|
+
};
|
|
826
|
+
this.records.push(record);
|
|
827
|
+
if (this.records.length > this.config.maxRecords) {
|
|
828
|
+
this.pruneRecords();
|
|
829
|
+
}
|
|
830
|
+
return record;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Get all records
|
|
834
|
+
*/
|
|
835
|
+
getRecords() {
|
|
836
|
+
return [...this.records];
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Get records in time range
|
|
840
|
+
*/
|
|
841
|
+
getRecordsInRange(startTime, endTime) {
|
|
842
|
+
return this.records.filter(
|
|
843
|
+
(r) => r.timestamp >= startTime && r.timestamp <= endTime
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Calculate usage metrics
|
|
848
|
+
*/
|
|
849
|
+
getMetrics() {
|
|
850
|
+
if (this.records.length === 0) {
|
|
851
|
+
return {
|
|
852
|
+
totalSeconds: 0,
|
|
853
|
+
totalAmount: "0",
|
|
854
|
+
averageRate: "0",
|
|
855
|
+
peakRate: "0",
|
|
856
|
+
startTime: this.startTime
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
const totalSeconds = this.records.reduce((sum, r) => sum + r.duration, 0);
|
|
860
|
+
const totalAmount = this.records[this.records.length - 1].cumulative;
|
|
861
|
+
const rates = this.records.map((r) => BigInt(r.rate));
|
|
862
|
+
const peakRate = rates.length > 0 ? rates.reduce((max, r) => r > max ? r : max, 0n) : 0n;
|
|
863
|
+
const averageRate = totalSeconds > 0 ? (BigInt(totalAmount) / BigInt(totalSeconds)).toString() : "0";
|
|
864
|
+
return {
|
|
865
|
+
totalSeconds,
|
|
866
|
+
totalAmount,
|
|
867
|
+
averageRate,
|
|
868
|
+
peakRate: peakRate.toString(),
|
|
869
|
+
startTime: this.startTime,
|
|
870
|
+
endTime: this.records[this.records.length - 1].timestamp,
|
|
871
|
+
hourly: this.getHourlyBreakdown()
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Get aggregated usage by period
|
|
876
|
+
*/
|
|
877
|
+
aggregate(intervalSeconds = 3600) {
|
|
878
|
+
if (this.records.length === 0) return [];
|
|
879
|
+
const aggregated = [];
|
|
880
|
+
const intervalMs = intervalSeconds * 1e3;
|
|
881
|
+
const groups = /* @__PURE__ */ new Map();
|
|
882
|
+
for (const record of this.records) {
|
|
883
|
+
const periodStart = Math.floor(record.timestamp / intervalMs) * intervalMs;
|
|
884
|
+
const existing = groups.get(periodStart) ?? [];
|
|
885
|
+
existing.push(record);
|
|
886
|
+
groups.set(periodStart, existing);
|
|
887
|
+
}
|
|
888
|
+
for (const [periodStart, records] of groups) {
|
|
889
|
+
const totalAmount = records.reduce(
|
|
890
|
+
(sum, r) => sum + BigInt(r.amount),
|
|
891
|
+
0n
|
|
892
|
+
);
|
|
893
|
+
const totalDuration = records.reduce((sum, r) => sum + r.duration, 0);
|
|
894
|
+
const averageRate = totalDuration > 0 ? (totalAmount / BigInt(totalDuration)).toString() : "0";
|
|
895
|
+
aggregated.push({
|
|
896
|
+
period: new Date(periodStart).toISOString(),
|
|
897
|
+
totalAmount: totalAmount.toString(),
|
|
898
|
+
totalDuration,
|
|
899
|
+
averageRate,
|
|
900
|
+
recordCount: records.length
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
return aggregated.sort((a, b) => a.period.localeCompare(b.period));
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Get cumulative amount at a point in time
|
|
907
|
+
*/
|
|
908
|
+
getCumulativeAt(timestamp) {
|
|
909
|
+
for (let i = this.records.length - 1; i >= 0; i--) {
|
|
910
|
+
if (this.records[i].timestamp <= timestamp) {
|
|
911
|
+
return this.records[i].cumulative;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return "0";
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Calculate usage for billing period
|
|
918
|
+
*/
|
|
919
|
+
getUsageForBillingPeriod(startTime, endTime) {
|
|
920
|
+
const periodRecords = this.getRecordsInRange(startTime, endTime);
|
|
921
|
+
const amount = periodRecords.reduce(
|
|
922
|
+
(sum, r) => sum + BigInt(r.amount),
|
|
923
|
+
0n
|
|
924
|
+
);
|
|
925
|
+
const duration = periodRecords.reduce((sum, r) => sum + r.duration, 0);
|
|
926
|
+
return {
|
|
927
|
+
amount: amount.toString(),
|
|
928
|
+
duration,
|
|
929
|
+
records: periodRecords.length
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Export records for backup/audit
|
|
934
|
+
*/
|
|
935
|
+
export() {
|
|
936
|
+
return JSON.stringify({
|
|
937
|
+
sessionId: this.sessionId,
|
|
938
|
+
startTime: this.startTime,
|
|
939
|
+
records: this.records,
|
|
940
|
+
exportedAt: Date.now()
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Import records from backup
|
|
945
|
+
*/
|
|
946
|
+
import(data) {
|
|
947
|
+
try {
|
|
948
|
+
const parsed = JSON.parse(data);
|
|
949
|
+
if (parsed.sessionId !== this.sessionId) {
|
|
950
|
+
return { success: false, recordsImported: 0 };
|
|
951
|
+
}
|
|
952
|
+
const importedRecords = parsed.records;
|
|
953
|
+
let importedCount = 0;
|
|
954
|
+
for (const record of importedRecords) {
|
|
955
|
+
const exists = this.records.some((r) => r.timestamp === record.timestamp);
|
|
956
|
+
if (!exists) {
|
|
957
|
+
this.records.push(record);
|
|
958
|
+
importedCount++;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
this.records.sort((a, b) => a.timestamp - b.timestamp);
|
|
962
|
+
return { success: true, recordsImported: importedCount };
|
|
963
|
+
} catch {
|
|
964
|
+
return { success: false, recordsImported: 0 };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Clear all records
|
|
969
|
+
*/
|
|
970
|
+
clear() {
|
|
971
|
+
this.records = [];
|
|
972
|
+
}
|
|
973
|
+
calculateCumulative(newAmount) {
|
|
974
|
+
if (this.records.length === 0) {
|
|
975
|
+
return newAmount;
|
|
976
|
+
}
|
|
977
|
+
const lastCumulative = BigInt(this.records[this.records.length - 1].cumulative);
|
|
978
|
+
return (lastCumulative + BigInt(newAmount)).toString();
|
|
979
|
+
}
|
|
980
|
+
pruneRecords() {
|
|
981
|
+
const keepCount = this.config.maxRecords;
|
|
982
|
+
if (this.records.length <= keepCount) return;
|
|
983
|
+
const first = this.records[0];
|
|
984
|
+
const recent = this.records.slice(-(keepCount - 1));
|
|
985
|
+
this.records = [first, ...recent];
|
|
986
|
+
}
|
|
987
|
+
getHourlyBreakdown() {
|
|
988
|
+
const hourlyMap = /* @__PURE__ */ new Map();
|
|
989
|
+
for (const record of this.records) {
|
|
990
|
+
const hour = new Date(record.timestamp).getUTCHours();
|
|
991
|
+
const existing = hourlyMap.get(hour) ?? { amount: 0n, seconds: 0 };
|
|
992
|
+
existing.amount += BigInt(record.amount);
|
|
993
|
+
existing.seconds += record.duration;
|
|
994
|
+
hourlyMap.set(hour, existing);
|
|
995
|
+
}
|
|
996
|
+
return Array.from(hourlyMap.entries()).map(([hour, data]) => ({
|
|
997
|
+
hour,
|
|
998
|
+
amount: data.amount.toString(),
|
|
999
|
+
seconds: data.seconds
|
|
1000
|
+
}));
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
function calculateProRatedUsage(fullPeriodAmount, fullPeriodSeconds, actualSeconds) {
|
|
1004
|
+
if (fullPeriodSeconds === 0) return "0";
|
|
1005
|
+
const amount = BigInt(fullPeriodAmount);
|
|
1006
|
+
return (amount * BigInt(actualSeconds) / BigInt(fullPeriodSeconds)).toString();
|
|
1007
|
+
}
|
|
1008
|
+
function estimateUsage(metrics, futureSeconds) {
|
|
1009
|
+
if (metrics.totalSeconds === 0) return "0";
|
|
1010
|
+
const avgRate = BigInt(metrics.averageRate);
|
|
1011
|
+
return (avgRate * BigInt(futureSeconds)).toString();
|
|
1012
|
+
}
|
|
1013
|
+
function compareUsage(current, previous) {
|
|
1014
|
+
const currentAmount = BigInt(current.totalAmount);
|
|
1015
|
+
const previousAmount = BigInt(previous.totalAmount);
|
|
1016
|
+
const amountChange = currentAmount - previousAmount;
|
|
1017
|
+
const amountChangePercent = previousAmount > 0n ? Number(amountChange * 100n / previousAmount) : 0;
|
|
1018
|
+
const currentRate = BigInt(current.averageRate);
|
|
1019
|
+
const previousRate = BigInt(previous.averageRate);
|
|
1020
|
+
const rateChange = currentRate - previousRate;
|
|
1021
|
+
const rateChangePercent = previousRate > 0n ? Number(rateChange * 100n / previousRate) : 0;
|
|
1022
|
+
return {
|
|
1023
|
+
amountChange: amountChange.toString(),
|
|
1024
|
+
amountChangePercent,
|
|
1025
|
+
rateChange: rateChange.toString(),
|
|
1026
|
+
rateChangePercent
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/streaming/billing.ts
|
|
1031
|
+
var BillingManager = class {
|
|
1032
|
+
config;
|
|
1033
|
+
billingConfig;
|
|
1034
|
+
invoices = [];
|
|
1035
|
+
channelId;
|
|
1036
|
+
payer;
|
|
1037
|
+
payee;
|
|
1038
|
+
lastInvoiceTime;
|
|
1039
|
+
constructor(channelId, payer, payee, billingConfig, config = {}) {
|
|
1040
|
+
this.channelId = channelId;
|
|
1041
|
+
this.payer = payer;
|
|
1042
|
+
this.payee = payee;
|
|
1043
|
+
this.billingConfig = billingConfig;
|
|
1044
|
+
this.lastInvoiceTime = Date.now();
|
|
1045
|
+
this.config = {
|
|
1046
|
+
autoInvoice: config.autoInvoice ?? false,
|
|
1047
|
+
invoiceInterval: config.invoiceInterval ?? 86400,
|
|
1048
|
+
currency: config.currency ?? "USDT",
|
|
1049
|
+
taxRate: config.taxRate ?? 0
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Generate invoice from metering records
|
|
1054
|
+
*/
|
|
1055
|
+
generateInvoice(records, startTime, endTime) {
|
|
1056
|
+
const items = this.createInvoiceItems(records, startTime, endTime);
|
|
1057
|
+
const subtotal = this.calculateSubtotal(items);
|
|
1058
|
+
const fees = this.calculateFees(subtotal);
|
|
1059
|
+
const total = (BigInt(subtotal) + BigInt(fees)).toString();
|
|
1060
|
+
const invoice = {
|
|
1061
|
+
id: this.generateInvoiceId(),
|
|
1062
|
+
channelId: this.channelId,
|
|
1063
|
+
payer: this.payer,
|
|
1064
|
+
payee: this.payee,
|
|
1065
|
+
items,
|
|
1066
|
+
subtotal,
|
|
1067
|
+
fees,
|
|
1068
|
+
total,
|
|
1069
|
+
currency: this.config.currency,
|
|
1070
|
+
status: "pending",
|
|
1071
|
+
createdAt: Date.now(),
|
|
1072
|
+
dueAt: Date.now() + 864e5
|
|
1073
|
+
// Due in 24 hours
|
|
1074
|
+
};
|
|
1075
|
+
this.invoices.push(invoice);
|
|
1076
|
+
this.lastInvoiceTime = endTime;
|
|
1077
|
+
return invoice;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Get all invoices
|
|
1081
|
+
*/
|
|
1082
|
+
getInvoices() {
|
|
1083
|
+
return [...this.invoices];
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Get invoice by ID
|
|
1087
|
+
*/
|
|
1088
|
+
getInvoice(id) {
|
|
1089
|
+
return this.invoices.find((inv) => inv.id === id);
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Mark invoice as paid
|
|
1093
|
+
*/
|
|
1094
|
+
markPaid(invoiceId) {
|
|
1095
|
+
const invoice = this.invoices.find((inv) => inv.id === invoiceId);
|
|
1096
|
+
if (!invoice || invoice.status !== "pending") {
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
invoice.status = "paid";
|
|
1100
|
+
invoice.paidAt = Date.now();
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Mark invoice as settled
|
|
1105
|
+
*/
|
|
1106
|
+
markSettled(invoiceId) {
|
|
1107
|
+
const invoice = this.invoices.find((inv) => inv.id === invoiceId);
|
|
1108
|
+
if (!invoice) return false;
|
|
1109
|
+
invoice.status = "settled";
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Get pending amount
|
|
1114
|
+
*/
|
|
1115
|
+
getPendingAmount() {
|
|
1116
|
+
return this.invoices.filter((inv) => inv.status === "pending").reduce((sum, inv) => sum + BigInt(inv.total), 0n).toString();
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Get total billed amount
|
|
1120
|
+
*/
|
|
1121
|
+
getTotalBilled() {
|
|
1122
|
+
return this.invoices.reduce((sum, inv) => sum + BigInt(inv.total), 0n).toString();
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Check if new invoice is due
|
|
1126
|
+
*/
|
|
1127
|
+
isInvoiceDue(currentTime) {
|
|
1128
|
+
const elapsed = currentTime - this.lastInvoiceTime;
|
|
1129
|
+
return elapsed >= this.config.invoiceInterval * 1e3;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Calculate amount for billing period
|
|
1133
|
+
*/
|
|
1134
|
+
calculatePeriodAmount(rate, durationSeconds) {
|
|
1135
|
+
const periodSeconds = this.getPeriodSeconds(this.billingConfig.period);
|
|
1136
|
+
const periods = durationSeconds / periodSeconds;
|
|
1137
|
+
const amount = BigInt(rate) * BigInt(Math.floor(periods * periodSeconds));
|
|
1138
|
+
return this.applyRounding(amount.toString());
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Apply minimum charge
|
|
1142
|
+
*/
|
|
1143
|
+
applyMinimumCharge(amount) {
|
|
1144
|
+
const minCharge = BigInt(this.billingConfig.minimumCharge);
|
|
1145
|
+
const actualAmount = BigInt(amount);
|
|
1146
|
+
return (actualAmount < minCharge ? minCharge : actualAmount).toString();
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Calculate grace period savings
|
|
1150
|
+
*/
|
|
1151
|
+
calculateGracePeriodSavings(rate, totalDuration) {
|
|
1152
|
+
const gracePeriod = this.billingConfig.gracePeriod;
|
|
1153
|
+
if (gracePeriod <= 0 || totalDuration <= gracePeriod) {
|
|
1154
|
+
return "0";
|
|
1155
|
+
}
|
|
1156
|
+
return (BigInt(rate) * BigInt(Math.min(gracePeriod, totalDuration))).toString();
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Get billing summary
|
|
1160
|
+
*/
|
|
1161
|
+
getSummary() {
|
|
1162
|
+
const totalBilled = this.getTotalBilled();
|
|
1163
|
+
const totalPaid = this.invoices.filter((inv) => inv.status === "paid" || inv.status === "settled").reduce((sum, inv) => sum + BigInt(inv.total), 0n).toString();
|
|
1164
|
+
const totalPending = this.getPendingAmount();
|
|
1165
|
+
const averageInvoice = this.invoices.length > 0 ? (BigInt(totalBilled) / BigInt(this.invoices.length)).toString() : "0";
|
|
1166
|
+
return {
|
|
1167
|
+
totalInvoices: this.invoices.length,
|
|
1168
|
+
totalBilled,
|
|
1169
|
+
totalPaid,
|
|
1170
|
+
totalPending,
|
|
1171
|
+
averageInvoice
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Export billing data
|
|
1176
|
+
*/
|
|
1177
|
+
export() {
|
|
1178
|
+
return JSON.stringify({
|
|
1179
|
+
channelId: this.channelId,
|
|
1180
|
+
invoices: this.invoices,
|
|
1181
|
+
exportedAt: Date.now()
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
createInvoiceItems(records, startTime, endTime) {
|
|
1185
|
+
if (records.length === 0) {
|
|
1186
|
+
return [];
|
|
1187
|
+
}
|
|
1188
|
+
const rateGroups = /* @__PURE__ */ new Map();
|
|
1189
|
+
for (const record of records) {
|
|
1190
|
+
const existing = rateGroups.get(record.rate) ?? [];
|
|
1191
|
+
existing.push(record);
|
|
1192
|
+
rateGroups.set(record.rate, existing);
|
|
1193
|
+
}
|
|
1194
|
+
const items = [];
|
|
1195
|
+
for (const [rate, groupRecords] of rateGroups) {
|
|
1196
|
+
const totalDuration = groupRecords.reduce((sum, r) => sum + r.duration, 0);
|
|
1197
|
+
const totalAmount = groupRecords.reduce(
|
|
1198
|
+
(sum, r) => sum + BigInt(r.amount),
|
|
1199
|
+
0n
|
|
1200
|
+
);
|
|
1201
|
+
const periodName = this.getPeriodName(this.billingConfig.period);
|
|
1202
|
+
const quantity = this.calculateQuantity(totalDuration);
|
|
1203
|
+
items.push({
|
|
1204
|
+
description: `Streaming usage at ${rate} per ${periodName}`,
|
|
1205
|
+
quantity,
|
|
1206
|
+
rate,
|
|
1207
|
+
amount: totalAmount.toString(),
|
|
1208
|
+
startTime,
|
|
1209
|
+
endTime
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
return items;
|
|
1213
|
+
}
|
|
1214
|
+
calculateSubtotal(items) {
|
|
1215
|
+
return items.reduce((sum, item) => sum + BigInt(item.amount), 0n).toString();
|
|
1216
|
+
}
|
|
1217
|
+
calculateFees(subtotal) {
|
|
1218
|
+
const amount = BigInt(subtotal);
|
|
1219
|
+
const taxRate = this.config.taxRate;
|
|
1220
|
+
if (taxRate <= 0) return "0";
|
|
1221
|
+
return BigInt(Math.floor(Number(amount) * taxRate)).toString();
|
|
1222
|
+
}
|
|
1223
|
+
applyRounding(amount) {
|
|
1224
|
+
const value = BigInt(amount);
|
|
1225
|
+
const mode = this.billingConfig.roundingMode;
|
|
1226
|
+
switch (mode) {
|
|
1227
|
+
case "floor":
|
|
1228
|
+
return value.toString();
|
|
1229
|
+
case "ceil":
|
|
1230
|
+
return value.toString();
|
|
1231
|
+
case "round":
|
|
1232
|
+
return value.toString();
|
|
1233
|
+
default:
|
|
1234
|
+
return value.toString();
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
getPeriodSeconds(period) {
|
|
1238
|
+
switch (period) {
|
|
1239
|
+
case "realtime":
|
|
1240
|
+
case "second":
|
|
1241
|
+
return 1;
|
|
1242
|
+
case "minute":
|
|
1243
|
+
return 60;
|
|
1244
|
+
case "hour":
|
|
1245
|
+
return 3600;
|
|
1246
|
+
case "day":
|
|
1247
|
+
return 86400;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
getPeriodName(period) {
|
|
1251
|
+
switch (period) {
|
|
1252
|
+
case "realtime":
|
|
1253
|
+
case "second":
|
|
1254
|
+
return "second";
|
|
1255
|
+
case "minute":
|
|
1256
|
+
return "minute";
|
|
1257
|
+
case "hour":
|
|
1258
|
+
return "hour";
|
|
1259
|
+
case "day":
|
|
1260
|
+
return "day";
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
calculateQuantity(totalSeconds) {
|
|
1264
|
+
const periodSeconds = this.getPeriodSeconds(this.billingConfig.period);
|
|
1265
|
+
return totalSeconds / periodSeconds;
|
|
1266
|
+
}
|
|
1267
|
+
generateInvoiceId() {
|
|
1268
|
+
return `inv_${this.channelId.slice(-8)}_${Date.now().toString(36)}`;
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
function formatCurrencyAmount(amount, decimals = 6, symbol = "USDT") {
|
|
1272
|
+
const value = BigInt(amount);
|
|
1273
|
+
const divisor = BigInt(10 ** decimals);
|
|
1274
|
+
const wholePart = value / divisor;
|
|
1275
|
+
const fractionalPart = value % divisor;
|
|
1276
|
+
const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
|
|
1277
|
+
const trimmedFractional = fractionalStr.replace(/0+$/, "") || "0";
|
|
1278
|
+
return `${wholePart}.${trimmedFractional} ${symbol}`;
|
|
1279
|
+
}
|
|
1280
|
+
function parseCurrencyAmount(display, decimals = 6) {
|
|
1281
|
+
const cleaned = display.replace(/[^\d.]/g, "");
|
|
1282
|
+
const [whole, fractional = ""] = cleaned.split(".");
|
|
1283
|
+
const paddedFractional = fractional.slice(0, decimals).padEnd(decimals, "0");
|
|
1284
|
+
const combined = whole + paddedFractional;
|
|
1285
|
+
return BigInt(combined).toString();
|
|
1286
|
+
}
|
|
1287
|
+
function estimateFutureBill(currentRate, durationSeconds, minimumCharge = "0") {
|
|
1288
|
+
const estimated = BigInt(currentRate) * BigInt(durationSeconds);
|
|
1289
|
+
const minimum = BigInt(minimumCharge);
|
|
1290
|
+
return (estimated < minimum ? minimum : estimated).toString();
|
|
1291
|
+
}
|
|
1292
|
+
export {
|
|
1293
|
+
BillingConfig,
|
|
1294
|
+
BillingManager,
|
|
1295
|
+
BillingPeriod,
|
|
1296
|
+
FlowController,
|
|
1297
|
+
Invoice,
|
|
1298
|
+
InvoiceItem,
|
|
1299
|
+
MeteringManager,
|
|
1300
|
+
MeteringRecord,
|
|
1301
|
+
RateAdjustmentRequest,
|
|
1302
|
+
RateController,
|
|
1303
|
+
RateLimiter,
|
|
1304
|
+
RateType,
|
|
1305
|
+
StreamEvent,
|
|
1306
|
+
StreamRate,
|
|
1307
|
+
StreamSession,
|
|
1308
|
+
StreamState,
|
|
1309
|
+
UsageMetrics,
|
|
1310
|
+
calculateOptimalRate,
|
|
1311
|
+
calculateProRatedUsage,
|
|
1312
|
+
compareUsage,
|
|
1313
|
+
convertRate,
|
|
1314
|
+
createFixedRateFlow,
|
|
1315
|
+
createTieredRateFlow,
|
|
1316
|
+
estimateFutureBill,
|
|
1317
|
+
estimateUsage,
|
|
1318
|
+
formatCurrencyAmount,
|
|
1319
|
+
parseCurrencyAmount
|
|
1320
|
+
};
|
|
1321
|
+
//# sourceMappingURL=index.js.map
|