mppx 0.5.7 → 0.5.9
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/CHANGELOG.md +14 -0
- package/dist/Challenge.d.ts +3 -2
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +27 -9
- package/dist/Challenge.js.map +1 -1
- package/dist/Method.d.ts +32 -14
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js.map +1 -1
- package/dist/Store.d.ts +68 -2
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +41 -4
- package/dist/Store.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js +7 -0
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +133 -70
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +8 -2
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +26 -1
- package/dist/server/Transport.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +13 -2
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +429 -4
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +28 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +89 -0
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +4 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +90 -66
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +3 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +3 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +8 -2
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/index.d.ts +1 -0
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js +1 -0
- package/dist/tempo/server/index.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +16 -6
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +12 -1
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +55 -14
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts +11 -2
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js +66 -25
- package/dist/tempo/session/Sse.js.map +1 -1
- package/dist/tempo/session/Ws.d.ts +87 -0
- package/dist/tempo/session/Ws.d.ts.map +1 -0
- package/dist/tempo/session/Ws.js +428 -0
- package/dist/tempo/session/Ws.js.map +1 -0
- package/dist/tempo/session/index.d.ts +1 -0
- package/dist/tempo/session/index.d.ts.map +1 -1
- package/dist/tempo/session/index.js +1 -0
- package/dist/tempo/session/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Challenge.test.ts +1 -1
- package/src/Challenge.ts +28 -9
- package/src/Method.ts +61 -20
- package/src/Store.test-d.ts +80 -2
- package/src/Store.test.ts +150 -13
- package/src/Store.ts +140 -3
- package/src/mcp-sdk/server/Transport.test.ts +12 -0
- package/src/mcp-sdk/server/Transport.ts +8 -0
- package/src/server/Mppx.test.ts +105 -0
- package/src/server/Mppx.ts +178 -88
- package/src/server/Transport.test.ts +31 -0
- package/src/server/Transport.ts +31 -2
- package/src/tempo/client/SessionManager.ts +510 -7
- package/src/tempo/internal/fee-payer.test.ts +115 -1
- package/src/tempo/internal/fee-payer.ts +138 -1
- package/src/tempo/server/AtomicStore.test-d.ts +34 -0
- package/src/tempo/server/Charge.test.ts +128 -0
- package/src/tempo/server/Charge.ts +118 -93
- package/src/tempo/server/Methods.ts +3 -0
- package/src/tempo/server/Session.test.ts +1044 -47
- package/src/tempo/server/Session.ts +8 -2
- package/src/tempo/server/Sse.test.ts +29 -0
- package/src/tempo/server/index.ts +1 -0
- package/src/tempo/server/internal/html/main.ts +9 -10
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/transport.ts +19 -6
- package/src/tempo/session/ChannelStore.test.ts +20 -1
- package/src/tempo/session/ChannelStore.ts +77 -14
- package/src/tempo/session/Sse.ts +77 -24
- package/src/tempo/session/Ws.test.ts +410 -0
- package/src/tempo/session/Ws.ts +563 -0
- package/src/tempo/session/index.ts +1 -0
|
@@ -38,6 +38,16 @@ export function sse(options: sse.Options & { store: ChannelStore.ChannelStore })
|
|
|
38
38
|
return Transport.from<Request, Response, Transport.ReceiptResponseOf<Sse>, Response>({
|
|
39
39
|
name: 'sse',
|
|
40
40
|
|
|
41
|
+
captureRequest(request) {
|
|
42
|
+
return (
|
|
43
|
+
base.captureRequest?.(request) ?? {
|
|
44
|
+
headers: new Headers(request.headers),
|
|
45
|
+
method: request.method,
|
|
46
|
+
url: new URL(request.url),
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
},
|
|
50
|
+
|
|
41
51
|
getCredential(request) {
|
|
42
52
|
return base.getCredential(request)
|
|
43
53
|
},
|
|
@@ -46,11 +56,13 @@ export function sse(options: sse.Options & { store: ChannelStore.ChannelStore })
|
|
|
46
56
|
return base.respondChallenge(options) as Response
|
|
47
57
|
},
|
|
48
58
|
|
|
49
|
-
respondReceipt({ credential, receipt, response, challengeId, input }) {
|
|
50
|
-
const
|
|
59
|
+
respondReceipt({ credential, envelope, receipt, response, challengeId, input }) {
|
|
60
|
+
const verifiedCredential = envelope?.credential ?? credential
|
|
61
|
+
const verifiedChallengeId = envelope?.challenge.id ?? challengeId
|
|
62
|
+
const payload = verifiedCredential.payload as Partial<SessionCredentialPayload>
|
|
51
63
|
if (!payload.channelId) throw new Error('No SSE context available')
|
|
52
64
|
const channelId = payload.channelId
|
|
53
|
-
const tickCost = BigInt(
|
|
65
|
+
const tickCost = BigInt(verifiedCredential.challenge.request.amount as string)
|
|
54
66
|
|
|
55
67
|
// Auto-detect upstream SSE responses and parse them into an
|
|
56
68
|
// AsyncIterable so they flow through the metered pipeline.
|
|
@@ -71,7 +83,7 @@ export function sse(options: sse.Options & { store: ChannelStore.ChannelStore })
|
|
|
71
83
|
const stream = Sse_core.serve({
|
|
72
84
|
store,
|
|
73
85
|
channelId,
|
|
74
|
-
challengeId,
|
|
86
|
+
challengeId: verifiedChallengeId,
|
|
75
87
|
tickCost,
|
|
76
88
|
pollIntervalMs: pollingInterval,
|
|
77
89
|
generate,
|
|
@@ -81,11 +93,12 @@ export function sse(options: sse.Options & { store: ChannelStore.ChannelStore })
|
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
const baseResponse = base.respondReceipt({
|
|
84
|
-
credential,
|
|
96
|
+
credential: verifiedCredential,
|
|
97
|
+
envelope,
|
|
85
98
|
input,
|
|
86
99
|
receipt,
|
|
87
100
|
response: response as Response,
|
|
88
|
-
challengeId,
|
|
101
|
+
challengeId: verifiedChallengeId,
|
|
89
102
|
})
|
|
90
103
|
|
|
91
104
|
// Non-SSE response (e.g. upstream returned JSON instead of event-stream).
|
|
@@ -37,7 +37,7 @@ function seedChannel(
|
|
|
37
37
|
return store.updateChannel(channelId, () => makeChannel(overrides))
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function stripUpdateMethod(store: Store.Store): Store.Store {
|
|
40
|
+
function stripUpdateMethod(store: Store.Store | Store.AtomicStore): Store.Store {
|
|
41
41
|
return {
|
|
42
42
|
get: store.get.bind(store),
|
|
43
43
|
put: store.put.bind(store),
|
|
@@ -209,6 +209,25 @@ describe('channelStore', () => {
|
|
|
209
209
|
await sleep(10)
|
|
210
210
|
expect(ch1Resolved).toBe(false)
|
|
211
211
|
})
|
|
212
|
+
|
|
213
|
+
test('resolves on successful deductFromChannel with atomic store.update', async () => {
|
|
214
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
215
|
+
await seedChannel(cs, { highestVoucherAmount: 5_000_000n, spent: 0n })
|
|
216
|
+
|
|
217
|
+
let resolved = false
|
|
218
|
+
const waiter = cs.waitForUpdate!(channelId).then(() => {
|
|
219
|
+
resolved = true
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
await sleep(10)
|
|
223
|
+
expect(resolved).toBe(false)
|
|
224
|
+
|
|
225
|
+
const result = await ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)
|
|
226
|
+
expect(result.ok).toBe(true)
|
|
227
|
+
|
|
228
|
+
await waiter
|
|
229
|
+
expect(resolved).toBe(true)
|
|
230
|
+
})
|
|
212
231
|
})
|
|
213
232
|
})
|
|
214
233
|
|
|
@@ -65,6 +65,9 @@ export interface State {
|
|
|
65
65
|
* guarantee that no concurrent mutation occurs between reading `current`
|
|
66
66
|
* and writing the return value.
|
|
67
67
|
*
|
|
68
|
+
* Callbacks should be synchronous and deterministic. When a `ChannelStore`
|
|
69
|
+
* is backed by `Store.update()`, adapters may retry them internally.
|
|
70
|
+
*
|
|
68
71
|
* Backends implement this via their native mechanisms:
|
|
69
72
|
* - **In-memory / JS single-thread**: Synchronous callback execution
|
|
70
73
|
* - **Durable Objects**: Single-threaded execution model
|
|
@@ -90,6 +93,18 @@ export type ChannelStore = {
|
|
|
90
93
|
* When not implemented, callers fall back to polling.
|
|
91
94
|
*/
|
|
92
95
|
waitForUpdate?(channelId: Hex): Promise<void>
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Atomic read-modify-write that returns the callback's `result` directly.
|
|
99
|
+
*
|
|
100
|
+
* Used by {@link deductFromChannel} to atomically compute the deduction
|
|
101
|
+
* outcome. When backed by `Store.update()`, this delegates to the store's
|
|
102
|
+
* native atomic primitive.
|
|
103
|
+
*/
|
|
104
|
+
updateChannelResult?<result>(
|
|
105
|
+
channelId: Hex,
|
|
106
|
+
fn: (current: State | null) => Store.Change<State, result>,
|
|
107
|
+
): Promise<result>
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
export type DeductResult = { ok: true; channel: State } | { ok: false; channel: State }
|
|
@@ -106,19 +121,41 @@ export async function deductFromChannel(
|
|
|
106
121
|
channelId: Hex,
|
|
107
122
|
amount: bigint,
|
|
108
123
|
): Promise<DeductResult> {
|
|
109
|
-
|
|
124
|
+
if (store.updateChannelResult) {
|
|
125
|
+
const result = await store.updateChannelResult<DeductResult | null>(
|
|
126
|
+
channelId,
|
|
127
|
+
(current): Store.Change<State, DeductResult | null> => {
|
|
128
|
+
if (!current) return { op: 'noop', result: null }
|
|
129
|
+
if (current.finalized)
|
|
130
|
+
return { op: 'noop', result: { ok: false, channel: current } as const }
|
|
131
|
+
if (current.highestVoucherAmount - current.spent >= amount) {
|
|
132
|
+
const next = { ...current, spent: current.spent + amount, units: current.units + 1 }
|
|
133
|
+
return { op: 'set', value: next, result: { ok: true, channel: next } as const }
|
|
134
|
+
}
|
|
135
|
+
return { op: 'noop', result: { ok: false, channel: current } as const }
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
if (!result) throw new Error('channel not found')
|
|
139
|
+
return result
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let result: DeductResult | null = null
|
|
110
143
|
const channel = await store.updateChannel(channelId, (current) => {
|
|
111
|
-
deducted = false
|
|
112
144
|
if (!current) return null
|
|
113
|
-
if (current.finalized)
|
|
145
|
+
if (current.finalized) {
|
|
146
|
+
result = { ok: false, channel: current }
|
|
147
|
+
return current
|
|
148
|
+
}
|
|
114
149
|
if (current.highestVoucherAmount - current.spent >= amount) {
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
const next = { ...current, spent: current.spent + amount, units: current.units + 1 }
|
|
151
|
+
result = { ok: true, channel: next }
|
|
152
|
+
return next
|
|
117
153
|
}
|
|
154
|
+
result = { ok: false, channel: current }
|
|
118
155
|
return current
|
|
119
156
|
})
|
|
120
157
|
if (!channel) throw new Error('channel not found')
|
|
121
|
-
return { ok:
|
|
158
|
+
return result ?? { ok: false, channel }
|
|
122
159
|
}
|
|
123
160
|
|
|
124
161
|
/**
|
|
@@ -140,10 +177,12 @@ export async function deductFromChannel(
|
|
|
140
177
|
*/
|
|
141
178
|
const storeCache = new WeakMap<Store.Store, ChannelStore>()
|
|
142
179
|
|
|
143
|
-
export function fromStore(store: Store.Store): ChannelStore {
|
|
180
|
+
export function fromStore(store: Store.Store | Store.AtomicStore): ChannelStore {
|
|
144
181
|
const cached = storeCache.get(store)
|
|
145
182
|
if (cached) return cached
|
|
146
183
|
|
|
184
|
+
const atomicUpdate = 'update' in store ? (store as Store.AtomicStore).update : undefined
|
|
185
|
+
|
|
147
186
|
const waiters = new Map<string, Set<() => void>>()
|
|
148
187
|
const locks = new Map<string, Promise<void>>()
|
|
149
188
|
|
|
@@ -158,6 +197,29 @@ export function fromStore(store: Store.Store): ChannelStore {
|
|
|
158
197
|
channelId: Hex,
|
|
159
198
|
fn: (current: State | null) => State | null,
|
|
160
199
|
): Promise<State | null> {
|
|
200
|
+
return updateResult(channelId, (current) => {
|
|
201
|
+
const next = fn(current)
|
|
202
|
+
if (next) return { op: 'set', value: next, result: next }
|
|
203
|
+
return { op: 'delete', result: null }
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function updateResult<result>(
|
|
208
|
+
channelId: Hex,
|
|
209
|
+
fn: (current: State | null) => Store.Change<State, result>,
|
|
210
|
+
): Promise<result> {
|
|
211
|
+
let change: Store.Change<State, result> | undefined
|
|
212
|
+
|
|
213
|
+
if (atomicUpdate) {
|
|
214
|
+
const result = await atomicUpdate(channelId, (current) => {
|
|
215
|
+
change = fn((current as State | null) ?? null)
|
|
216
|
+
if (change.op !== 'set') return change
|
|
217
|
+
return { ...change, value: change.value as never }
|
|
218
|
+
})
|
|
219
|
+
if (change?.op !== 'noop') notify(channelId)
|
|
220
|
+
return result
|
|
221
|
+
}
|
|
222
|
+
|
|
161
223
|
while (locks.has(channelId)) await locks.get(channelId)
|
|
162
224
|
|
|
163
225
|
let release!: () => void
|
|
@@ -170,10 +232,11 @@ export function fromStore(store: Store.Store): ChannelStore {
|
|
|
170
232
|
|
|
171
233
|
try {
|
|
172
234
|
const current = (await store.get(channelId)) as State | null
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
235
|
+
change = fn(current)
|
|
236
|
+
if (change.op === 'set') await store.put(channelId, change.value as never)
|
|
237
|
+
if (change.op === 'delete') await store.delete(channelId)
|
|
238
|
+
if (change.op !== 'noop') notify(channelId)
|
|
239
|
+
return change.result
|
|
177
240
|
} finally {
|
|
178
241
|
locks.delete(channelId)
|
|
179
242
|
release()
|
|
@@ -185,9 +248,7 @@ export function fromStore(store: Store.Store): ChannelStore {
|
|
|
185
248
|
return (await store.get(channelId)) as State | null
|
|
186
249
|
},
|
|
187
250
|
async updateChannel(channelId, fn) {
|
|
188
|
-
|
|
189
|
-
notify(channelId)
|
|
190
|
-
return result
|
|
251
|
+
return update(channelId, fn)
|
|
191
252
|
},
|
|
192
253
|
waitForUpdate(channelId) {
|
|
193
254
|
return new Promise<void>((resolve) => {
|
|
@@ -201,6 +262,8 @@ export function fromStore(store: Store.Store): ChannelStore {
|
|
|
201
262
|
},
|
|
202
263
|
}
|
|
203
264
|
|
|
265
|
+
cs.updateChannelResult = updateResult
|
|
266
|
+
|
|
204
267
|
storeCache.set(store, cs)
|
|
205
268
|
return cs
|
|
206
269
|
}
|
package/src/tempo/session/Sse.ts
CHANGED
|
@@ -80,6 +80,13 @@ export function parseEvent(raw: string): SseEvent | null {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export type SessionController = {
|
|
83
|
+
/**
|
|
84
|
+
* Reserve voucher coverage for the next emitted chunk.
|
|
85
|
+
*
|
|
86
|
+
* The reservation blocks until sufficient voucher headroom exists, but the
|
|
87
|
+
* charge is only committed once a chunk is actually emitted. If the stream
|
|
88
|
+
* ends or aborts before that emission, the reservation is dropped.
|
|
89
|
+
*/
|
|
83
90
|
charge(): Promise<void>
|
|
84
91
|
}
|
|
85
92
|
|
|
@@ -93,11 +100,13 @@ export type SessionController = {
|
|
|
93
100
|
* generator controls when charges happen by calling `stream.charge()`.
|
|
94
101
|
*
|
|
95
102
|
* For each emitted value the stream:
|
|
96
|
-
* 1.
|
|
103
|
+
* 1. Reserves `tickCost` from the channel's available voucher headroom
|
|
104
|
+
* (auto or manual).
|
|
97
105
|
* 2. If balance is sufficient, emits `event: message` with the value.
|
|
98
106
|
* 3. If balance is exhausted, emits `event: payment-need-voucher`
|
|
99
107
|
* and polls store until the client tops up the channel.
|
|
100
|
-
* 4.
|
|
108
|
+
* 4. Commits the reserved charge immediately before the chunk is emitted.
|
|
109
|
+
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
101
110
|
*
|
|
102
111
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
103
112
|
*/
|
|
@@ -118,15 +127,21 @@ export function serve(options: serve.Options): ReadableStream<Uint8Array> {
|
|
|
118
127
|
async start(controller) {
|
|
119
128
|
const aborted = () => signal?.aborted ?? false
|
|
120
129
|
const emit = (event: string) => controller.enqueue(encoder.encode(event))
|
|
130
|
+
let reservedAmount = 0n
|
|
131
|
+
let reservedUnits = 0
|
|
121
132
|
|
|
122
133
|
const charge = () =>
|
|
123
|
-
|
|
134
|
+
reserveChargeOrWait({
|
|
124
135
|
store,
|
|
125
136
|
channelId,
|
|
126
137
|
amount: tickCost,
|
|
138
|
+
reservedAmount,
|
|
127
139
|
emit,
|
|
128
140
|
pollIntervalMs,
|
|
129
141
|
signal,
|
|
142
|
+
}).then(() => {
|
|
143
|
+
reservedAmount += tickCost
|
|
144
|
+
reservedUnits += 1
|
|
130
145
|
})
|
|
131
146
|
|
|
132
147
|
const iterable: AsyncIterable<string> =
|
|
@@ -137,7 +152,14 @@ export function serve(options: serve.Options): ReadableStream<Uint8Array> {
|
|
|
137
152
|
if (aborted()) break
|
|
138
153
|
|
|
139
154
|
if (typeof generate !== 'function') await charge()
|
|
140
|
-
|
|
155
|
+
await commitReservedCharges({
|
|
156
|
+
store,
|
|
157
|
+
channelId,
|
|
158
|
+
amount: reservedAmount,
|
|
159
|
+
units: reservedUnits,
|
|
160
|
+
})
|
|
161
|
+
reservedAmount = 0n
|
|
162
|
+
reservedUnits = 0
|
|
141
163
|
controller.enqueue(encoder.encode(`event: message\ndata: ${value}\n\n`))
|
|
142
164
|
}
|
|
143
165
|
|
|
@@ -221,45 +243,76 @@ export declare namespace fromRequest {
|
|
|
221
243
|
}
|
|
222
244
|
|
|
223
245
|
/**
|
|
224
|
-
*
|
|
225
|
-
* insufficient. Uses `store.waitForUpdate()` when available for
|
|
246
|
+
* Reserve `amount` of voucher headroom for a future emission, retrying when
|
|
247
|
+
* balance is insufficient. Uses `store.waitForUpdate()` when available for
|
|
226
248
|
* event-driven wakeups, falling back to polling otherwise. Emits
|
|
227
249
|
* `payment-need-voucher` events via `emit` while waiting.
|
|
228
250
|
*/
|
|
229
|
-
async function
|
|
251
|
+
async function reserveChargeOrWait(options: {
|
|
230
252
|
store: ChannelStore.ChannelStore
|
|
231
253
|
channelId: Hex
|
|
232
254
|
amount: bigint
|
|
233
|
-
|
|
255
|
+
reservedAmount: bigint
|
|
256
|
+
emit: (event: string) => void | Promise<void>
|
|
234
257
|
pollIntervalMs: number
|
|
235
258
|
signal?: AbortSignal | undefined
|
|
236
259
|
}): Promise<void> {
|
|
237
|
-
const { store, channelId, amount, emit, pollIntervalMs, signal } = options
|
|
260
|
+
const { store, channelId, amount, emit, pollIntervalMs, reservedAmount, signal } = options
|
|
261
|
+
|
|
262
|
+
let channel = await store.getChannel(channelId)
|
|
263
|
+
if (!channel) throw new Error('channel not found')
|
|
238
264
|
|
|
239
|
-
|
|
265
|
+
const hasHeadroom = (state: ChannelStore.State) =>
|
|
266
|
+
state.highestVoucherAmount - state.spent - reservedAmount >= amount
|
|
240
267
|
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// voucher POSTs from the client.
|
|
268
|
+
if (hasHeadroom(channel)) return
|
|
269
|
+
|
|
270
|
+
// Emit a single need-voucher event, then wait until the accepted voucher
|
|
271
|
+
// headroom covers both already-reserved units and the next requested unit.
|
|
272
|
+
await Promise.resolve(
|
|
247
273
|
emit(
|
|
248
274
|
formatNeedVoucherEvent({
|
|
249
275
|
channelId,
|
|
250
|
-
requiredCumulative: (
|
|
251
|
-
acceptedCumulative:
|
|
252
|
-
deposit:
|
|
276
|
+
requiredCumulative: (channel.spent + reservedAmount + amount).toString(),
|
|
277
|
+
acceptedCumulative: channel.highestVoucherAmount.toString(),
|
|
278
|
+
deposit: channel.deposit.toString(),
|
|
253
279
|
}),
|
|
254
|
-
)
|
|
280
|
+
),
|
|
281
|
+
)
|
|
255
282
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
283
|
+
while (!hasHeadroom(channel)) {
|
|
284
|
+
await waitForUpdate(store, channelId, pollIntervalMs, signal)
|
|
285
|
+
channel = await store.getChannel(channelId)
|
|
286
|
+
if (!channel) throw new Error('channel not found')
|
|
260
287
|
}
|
|
261
288
|
}
|
|
262
289
|
|
|
290
|
+
async function commitReservedCharges(options: {
|
|
291
|
+
store: ChannelStore.ChannelStore
|
|
292
|
+
channelId: Hex
|
|
293
|
+
amount: bigint
|
|
294
|
+
units: number
|
|
295
|
+
}): Promise<void> {
|
|
296
|
+
const { store, channelId, amount, units } = options
|
|
297
|
+
if (amount === 0n || units === 0) return
|
|
298
|
+
|
|
299
|
+
let committed = false
|
|
300
|
+
const channel = await store.updateChannel(channelId, (current) => {
|
|
301
|
+
if (!current) return null
|
|
302
|
+
if (current.finalized) return current
|
|
303
|
+
if (current.highestVoucherAmount - current.spent < amount) return current
|
|
304
|
+
committed = true
|
|
305
|
+
return {
|
|
306
|
+
...current,
|
|
307
|
+
spent: current.spent + amount,
|
|
308
|
+
units: current.units + units,
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
if (!channel) throw new Error('channel not found')
|
|
313
|
+
if (!committed) throw new Error('reserved voucher coverage is no longer available')
|
|
314
|
+
}
|
|
315
|
+
|
|
263
316
|
async function waitForUpdate(
|
|
264
317
|
store: ChannelStore.ChannelStore,
|
|
265
318
|
channelId: Hex,
|