@statewalker/webrun-streams-libp2p 0.1.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/connect-serve.d.ts +96 -0
- package/dist/connect-serve.d.ts.map +1 -0
- package/dist/duplex-over-stream.d.ts +68 -0
- package/dist/duplex-over-stream.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +405 -0
- package/package.json +61 -0
- package/src/connect-serve.ts +309 -0
- package/src/duplex-over-stream.ts +367 -0
- package/src/index.ts +15 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { deserializeError, serializeError } from "@statewalker/webrun-streams";
|
|
2
|
+
//#region src/duplex-over-stream.ts
|
|
3
|
+
const TYPE_DATA = 0;
|
|
4
|
+
const TYPE_ERROR = 2;
|
|
5
|
+
/**
|
|
6
|
+
* Default bound for {@link closeStream}'s wait for a graceful close. Matches
|
|
7
|
+
* 2.x's own default (`DEFAULT_SEND_CLOSE_WRITE_TIMEOUT`,
|
|
8
|
+
* `@libp2p/utils@6.7.2/dist/src/abstract-stream.js:7`) — this restores that
|
|
9
|
+
* bound rather than inventing a new number.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 5e3;
|
|
12
|
+
/**
|
|
13
|
+
* Default bound for {@link waitForDrain}'s wait for the peer to make room in
|
|
14
|
+
* its receive window. Without a bound, a peer that requests something and then
|
|
15
|
+
* simply stops reading — alive, so no `close` event ever fires — parks the
|
|
16
|
+
* serving side's outbound pump forever, holding the stream, the handler and
|
|
17
|
+
* whatever the handler buffered. On the serving side there is no escape hatch:
|
|
18
|
+
* `connect`'s `.return`/abort override is a *caller*-side affordance.
|
|
19
|
+
*
|
|
20
|
+
* The number is deliberately generous, because a bound that is too tight
|
|
21
|
+
* resets a slow-but-alive peer mid-transfer — exactly what backpressure exists
|
|
22
|
+
* to avoid. Five minutes covers a peer draining a full yamux receive window
|
|
23
|
+
* (256 KiB by default) at under 1 KiB/s, i.e. slower than any link on which
|
|
24
|
+
* the transfer would complete anyway; a peer slower than that is
|
|
25
|
+
* indistinguishable from one that has stopped reading altogether. Override via
|
|
26
|
+
* `drainTimeoutMs` when a deployment knows better.
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 3e5;
|
|
29
|
+
const textEncoder = new TextEncoder();
|
|
30
|
+
const textDecoder = new TextDecoder();
|
|
31
|
+
/**
|
|
32
|
+
* Drive one `Duplex` over one libp2p `Stream` using a small in-band framing
|
|
33
|
+
* protocol:
|
|
34
|
+
*
|
|
35
|
+
* [1-byte type][varint length][payload bytes]
|
|
36
|
+
*
|
|
37
|
+
* Types are `DATA` (0x00, body bytes) and `ERROR` (0x02, followed by a
|
|
38
|
+
* JSON-serialised `Error`). Normal end-of-input is signalled by libp2p's
|
|
39
|
+
* `close()`. The frame layer exists so we can preserve `Error` fidelity
|
|
40
|
+
* across the wire (yamux's native stream reset only carries "stream reset").
|
|
41
|
+
*/
|
|
42
|
+
async function* duplexOverStream(stream, input, opts = {}) {
|
|
43
|
+
let peerEndedCalled = false;
|
|
44
|
+
const firePeerInputEnd = (err) => {
|
|
45
|
+
if (peerEndedCalled) return;
|
|
46
|
+
peerEndedCalled = true;
|
|
47
|
+
opts.onPeerInputEnd?.(err);
|
|
48
|
+
};
|
|
49
|
+
const outboundSource = framedOutbound(input);
|
|
50
|
+
const outbound = (async () => {
|
|
51
|
+
try {
|
|
52
|
+
for await (const chunk of outboundSource) if (!stream.send(chunk)) await waitForDrain(stream, opts.drainTimeoutMs ?? 3e5);
|
|
53
|
+
await closeStream(stream);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
56
|
+
try {
|
|
57
|
+
stream.abort(e);
|
|
58
|
+
} catch {}
|
|
59
|
+
} finally {
|
|
60
|
+
try {
|
|
61
|
+
await outboundSource.return?.(void 0);
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
let sourceCompleted = false;
|
|
66
|
+
try {
|
|
67
|
+
for await (const frame of parseFrames(stream)) if (frame.type === TYPE_DATA) yield frame.payload;
|
|
68
|
+
else if (frame.type === TYPE_ERROR) {
|
|
69
|
+
const err = decodeError(frame.payload);
|
|
70
|
+
firePeerInputEnd(err);
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
sourceCompleted = true;
|
|
74
|
+
firePeerInputEnd();
|
|
75
|
+
opts.onSourceCompleted?.();
|
|
76
|
+
} finally {
|
|
77
|
+
firePeerInputEnd();
|
|
78
|
+
if (!sourceCompleted) try {
|
|
79
|
+
await outboundSource.return?.(void 0);
|
|
80
|
+
} catch {}
|
|
81
|
+
await outbound;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Wait for `stream` to signal it can accept more data, per the real
|
|
86
|
+
* `'drain'` event rather than the broken {@link Stream.onDrain}. Rejects if
|
|
87
|
+
* the stream closes first (including a remote reset) so a peer that goes
|
|
88
|
+
* away while we're backpressured unwinds into the caller's existing
|
|
89
|
+
* `catch` instead of hanging forever — and rejects after `timeoutMs` for the
|
|
90
|
+
* peer that neither reads nor closes, which produces no event at all. The
|
|
91
|
+
* expiry is loud (a `console.warn` naming the protocol and the bound) rather
|
|
92
|
+
* than a silent stall, per this project's "silent failures deserve loud
|
|
93
|
+
* guards" rule; the rejection itself reaches the outbound pump's `catch`,
|
|
94
|
+
* which aborts the stream so the peer learns it was dropped.
|
|
95
|
+
*/
|
|
96
|
+
function waitForDrain(stream, timeoutMs) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const cleanup = () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
stream.removeEventListener("drain", onDrain);
|
|
101
|
+
stream.removeEventListener("close", onClose);
|
|
102
|
+
};
|
|
103
|
+
const onDrain = () => {
|
|
104
|
+
cleanup();
|
|
105
|
+
resolve();
|
|
106
|
+
};
|
|
107
|
+
const onClose = (evt) => {
|
|
108
|
+
cleanup();
|
|
109
|
+
reject(evt.error ?? /* @__PURE__ */ new Error("stream closed"));
|
|
110
|
+
};
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
cleanup();
|
|
113
|
+
const message = `[webrun-streams-libp2p] waitForDrain: peer on protocol ${stream.protocol} did not accept more data within ${timeoutMs}ms and never closed the stream; dropping it so this side's pump is not parked forever`;
|
|
114
|
+
console.warn(message);
|
|
115
|
+
reject(new Error(message));
|
|
116
|
+
}, timeoutMs);
|
|
117
|
+
stream.addEventListener("drain", onDrain);
|
|
118
|
+
stream.addEventListener("close", onClose);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Close a `Stream`'s writable end, bounded by `timeoutMs`. Plain
|
|
123
|
+
* `stream.close()` awaits the write queue draining and the peer
|
|
124
|
+
* acknowledging with no bound of its own — a peer that stops reading
|
|
125
|
+
* without resetting (a suspended tab, a paused container, `SIGSTOP`) means
|
|
126
|
+
* it never settles, which would otherwise hang every caller waiting on it
|
|
127
|
+
* (the outbound pump here, and `connect`/`serve`'s teardown in
|
|
128
|
+
* `connect-serve.ts`). On timeout we fall back to a hard `abort()` so the
|
|
129
|
+
* caller is never left hanging.
|
|
130
|
+
*/
|
|
131
|
+
async function closeStream(stream, timeoutMs = DEFAULT_CLOSE_TIMEOUT_MS) {
|
|
132
|
+
try {
|
|
133
|
+
await stream.close({ signal: AbortSignal.timeout(timeoutMs) });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
136
|
+
console.warn(`[webrun-streams-libp2p] closeStream: graceful close of protocol ${stream.protocol} failed (bound ${timeoutMs}ms), aborting instead (peer may see truncated data): ${e.message}`);
|
|
137
|
+
try {
|
|
138
|
+
stream.abort(e);
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function* framedOutbound(input) {
|
|
143
|
+
try {
|
|
144
|
+
for await (const chunk of toAsyncIterable(input)) yield frameData(chunk);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
yield frameError(err instanceof Error ? err : new Error(String(err)));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function* parseFrames(source) {
|
|
150
|
+
let buf = /* @__PURE__ */ new Uint8Array(0);
|
|
151
|
+
for await (const item of source) {
|
|
152
|
+
const incoming = normalizeChunk(item);
|
|
153
|
+
if (incoming.byteLength === 0) continue;
|
|
154
|
+
if (buf.byteLength === 0) buf = new Uint8Array(incoming);
|
|
155
|
+
else {
|
|
156
|
+
const merged = new Uint8Array(buf.byteLength + incoming.byteLength);
|
|
157
|
+
merged.set(buf, 0);
|
|
158
|
+
merged.set(incoming, buf.byteLength);
|
|
159
|
+
buf = merged;
|
|
160
|
+
}
|
|
161
|
+
while (buf.byteLength > 0) {
|
|
162
|
+
if (buf.byteLength < 2) break;
|
|
163
|
+
const type = buf[0];
|
|
164
|
+
let lenInfo;
|
|
165
|
+
try {
|
|
166
|
+
lenInfo = decodeVarint(buf, 1);
|
|
167
|
+
} catch {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
const total = lenInfo.offset + lenInfo.value;
|
|
171
|
+
if (buf.byteLength < total) break;
|
|
172
|
+
yield {
|
|
173
|
+
type,
|
|
174
|
+
payload: new Uint8Array(buf.subarray(lenInfo.offset, total))
|
|
175
|
+
};
|
|
176
|
+
buf = buf.byteLength === total ? /* @__PURE__ */ new Uint8Array(0) : new Uint8Array(buf.subarray(total));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function normalizeChunk(item) {
|
|
181
|
+
if (item instanceof Uint8Array) return item;
|
|
182
|
+
const asList = item;
|
|
183
|
+
if (typeof asList.subarray === "function") return new Uint8Array(asList.subarray());
|
|
184
|
+
return /* @__PURE__ */ new Uint8Array(0);
|
|
185
|
+
}
|
|
186
|
+
function frameData(payload) {
|
|
187
|
+
const lenEnc = encodeVarint(payload.byteLength);
|
|
188
|
+
const out = new Uint8Array(1 + lenEnc.byteLength + payload.byteLength);
|
|
189
|
+
out[0] = TYPE_DATA;
|
|
190
|
+
out.set(lenEnc, 1);
|
|
191
|
+
out.set(payload, 1 + lenEnc.byteLength);
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
function frameError(err) {
|
|
195
|
+
const payload = textEncoder.encode(JSON.stringify(serializeError(err)));
|
|
196
|
+
const lenEnc = encodeVarint(payload.byteLength);
|
|
197
|
+
const out = new Uint8Array(1 + lenEnc.byteLength + payload.byteLength);
|
|
198
|
+
out[0] = TYPE_ERROR;
|
|
199
|
+
out.set(lenEnc, 1);
|
|
200
|
+
out.set(payload, 1 + lenEnc.byteLength);
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
function decodeError(payload) {
|
|
204
|
+
if (payload.byteLength === 0) return /* @__PURE__ */ new Error("unknown stream error");
|
|
205
|
+
try {
|
|
206
|
+
return deserializeError(JSON.parse(textDecoder.decode(payload)));
|
|
207
|
+
} catch {
|
|
208
|
+
return new Error(textDecoder.decode(payload));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function encodeVarint(value) {
|
|
212
|
+
if (!Number.isInteger(value) || value < 0) throw new RangeError(`encodeVarint: ${value} is not a non-negative integer`);
|
|
213
|
+
const out = [];
|
|
214
|
+
let v = value;
|
|
215
|
+
while (v >= 128) {
|
|
216
|
+
out.push(v & 127 | 128);
|
|
217
|
+
v >>>= 7;
|
|
218
|
+
}
|
|
219
|
+
out.push(v & 127);
|
|
220
|
+
return new Uint8Array(out);
|
|
221
|
+
}
|
|
222
|
+
function decodeVarint(buf, start) {
|
|
223
|
+
let value = 0;
|
|
224
|
+
let shift = 0;
|
|
225
|
+
let i = start;
|
|
226
|
+
while (i < buf.length) {
|
|
227
|
+
const b = buf[i++];
|
|
228
|
+
value |= (b & 127) << shift;
|
|
229
|
+
if ((b & 128) === 0) return {
|
|
230
|
+
value: value >>> 0,
|
|
231
|
+
offset: i
|
|
232
|
+
};
|
|
233
|
+
shift += 7;
|
|
234
|
+
if (shift > 28) throw new Error("decodeVarint: too long");
|
|
235
|
+
}
|
|
236
|
+
throw new Error("decodeVarint: truncated");
|
|
237
|
+
}
|
|
238
|
+
function toAsyncIterable(input) {
|
|
239
|
+
if (input[Symbol.asyncIterator]) return input;
|
|
240
|
+
const it = input[Symbol.iterator]();
|
|
241
|
+
return { [Symbol.asyncIterator]() {
|
|
242
|
+
return { next: () => Promise.resolve(it.next()) };
|
|
243
|
+
} };
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/connect-serve.ts
|
|
247
|
+
const DEFAULT_PROTOCOL = "/webrun-streams/1.0.0";
|
|
248
|
+
/**
|
|
249
|
+
* Builds the options object passed to `node.dialProtocol`/`node.handle`,
|
|
250
|
+
* omitting any field the caller didn't supply rather than setting it to
|
|
251
|
+
* `undefined` — so an unset field falls back to libp2p's own default instead
|
|
252
|
+
* of an explicit `undefined` overriding it.
|
|
253
|
+
*/
|
|
254
|
+
function definedOptions(fields) {
|
|
255
|
+
const options = {};
|
|
256
|
+
let any = false;
|
|
257
|
+
for (const key of Object.keys(fields)) {
|
|
258
|
+
const value = fields[key];
|
|
259
|
+
if (value !== void 0) {
|
|
260
|
+
options[key] = value;
|
|
261
|
+
any = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return any ? options : void 0;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Caller-side: each `call(input)` opens a new libp2p `Stream` via
|
|
268
|
+
* `node.dialProtocol(peer, [protocol])` and runs the call over it.
|
|
269
|
+
*/
|
|
270
|
+
const connect = async ({ node, peer, protocol, drainTimeoutMs, maxOutboundStreams, runOnLimitedConnection }) => {
|
|
271
|
+
const proto = protocol ?? "/webrun-streams/1.0.0";
|
|
272
|
+
const dialOptions = definedOptions({
|
|
273
|
+
maxOutboundStreams,
|
|
274
|
+
runOnLimitedConnection
|
|
275
|
+
});
|
|
276
|
+
const open = /* @__PURE__ */ new Set();
|
|
277
|
+
const call = (input) => {
|
|
278
|
+
let streamRef = null;
|
|
279
|
+
const gen = (async function* () {
|
|
280
|
+
const stream = await node.dialProtocol(peer, [proto], dialOptions);
|
|
281
|
+
streamRef = stream;
|
|
282
|
+
open.add(stream);
|
|
283
|
+
let sourceCompleted = false;
|
|
284
|
+
try {
|
|
285
|
+
yield* duplexOverStream(stream, input, {
|
|
286
|
+
drainTimeoutMs,
|
|
287
|
+
onSourceCompleted: () => {
|
|
288
|
+
sourceCompleted = true;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
} finally {
|
|
292
|
+
open.delete(stream);
|
|
293
|
+
if (sourceCompleted) await closeStream(stream);
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
296
|
+
const origReturn = gen.return.bind(gen);
|
|
297
|
+
gen.return = async (value) => {
|
|
298
|
+
if (streamRef) try {
|
|
299
|
+
streamRef.abort(/* @__PURE__ */ new Error("call cancelled"));
|
|
300
|
+
} catch {}
|
|
301
|
+
return origReturn(value);
|
|
302
|
+
};
|
|
303
|
+
return gen;
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
call,
|
|
307
|
+
async close() {
|
|
308
|
+
for (const s of open) try {
|
|
309
|
+
s.abort(/* @__PURE__ */ new Error("connection close"));
|
|
310
|
+
} catch {}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Server-side: registers `node.handle(protocol, ...)`. Each inbound stream is
|
|
316
|
+
* wrapped as a `Duplex` and handed to `handler`. Identity-unaware; use
|
|
317
|
+
* `serveConnections` when the handler needs to know who is calling.
|
|
318
|
+
*/
|
|
319
|
+
const serve = async (params, handler) => serveConnections(params, () => handler);
|
|
320
|
+
/**
|
|
321
|
+
* Like `serve`, but the handler is built per inbound stream and is told which
|
|
322
|
+
* peer libp2p proved on that connection.
|
|
323
|
+
*/
|
|
324
|
+
async function serveConnections({ node, protocol, drainTimeoutMs, maxInboundStreams, maxOutboundStreams, runOnLimitedConnection }, makeHandler) {
|
|
325
|
+
const proto = protocol ?? "/webrun-streams/1.0.0";
|
|
326
|
+
const handleOptions = definedOptions({
|
|
327
|
+
maxInboundStreams,
|
|
328
|
+
maxOutboundStreams,
|
|
329
|
+
runOnLimitedConnection
|
|
330
|
+
});
|
|
331
|
+
const onStream = (stream, connection) => {
|
|
332
|
+
(async () => {
|
|
333
|
+
const handler = makeHandler({ remotePeer: connection.remotePeer });
|
|
334
|
+
const inputQueue = makeInputQueue();
|
|
335
|
+
const output = handler(inputQueue.iter());
|
|
336
|
+
try {
|
|
337
|
+
for await (const chunk of duplexOverStream(stream, output, {
|
|
338
|
+
drainTimeoutMs,
|
|
339
|
+
onPeerInputEnd: (err) => inputQueue.done(err)
|
|
340
|
+
})) inputQueue.push(chunk);
|
|
341
|
+
} finally {
|
|
342
|
+
inputQueue.done();
|
|
343
|
+
await closeStream(stream);
|
|
344
|
+
}
|
|
345
|
+
})().catch((err) => {
|
|
346
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
347
|
+
console.warn(`[webrun-streams-libp2p] serve: inbound stream on protocol ${proto} failed: ${e.message}`, e);
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
await node.handle(proto, onStream, handleOptions);
|
|
351
|
+
let torn = false;
|
|
352
|
+
return async () => {
|
|
353
|
+
if (torn) return;
|
|
354
|
+
torn = true;
|
|
355
|
+
await node.unhandle(proto);
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function makeInputQueue() {
|
|
359
|
+
const slots = [];
|
|
360
|
+
let wake = null;
|
|
361
|
+
let closed = false;
|
|
362
|
+
return {
|
|
363
|
+
iter() {
|
|
364
|
+
return (async function* () {
|
|
365
|
+
try {
|
|
366
|
+
while (true) {
|
|
367
|
+
if (slots.length === 0) {
|
|
368
|
+
await new Promise((r) => {
|
|
369
|
+
wake = r;
|
|
370
|
+
});
|
|
371
|
+
wake = null;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const s = slots.shift();
|
|
375
|
+
if (s.type === "done") {
|
|
376
|
+
if (s.err) throw s.err;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
yield s.value;
|
|
380
|
+
}
|
|
381
|
+
} finally {
|
|
382
|
+
closed = true;
|
|
383
|
+
}
|
|
384
|
+
})();
|
|
385
|
+
},
|
|
386
|
+
push(chunk) {
|
|
387
|
+
if (closed) return;
|
|
388
|
+
slots.push({
|
|
389
|
+
type: "value",
|
|
390
|
+
value: chunk
|
|
391
|
+
});
|
|
392
|
+
wake?.();
|
|
393
|
+
},
|
|
394
|
+
done(err) {
|
|
395
|
+
if (closed) return;
|
|
396
|
+
slots.push({
|
|
397
|
+
type: "done",
|
|
398
|
+
err
|
|
399
|
+
});
|
|
400
|
+
wake?.();
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
export { DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_PROTOCOL, connect, duplexOverStream, serve, serveConnections };
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@statewalker/webrun-streams-libp2p",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "libp2p native multi-stream Connect/Serve adapter in the webrun-streams-* family",
|
|
7
|
+
"homepage": "https://github.com/statewalker/webrun-wire",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Mikhail Kotelnikov",
|
|
10
|
+
"email": "mikhail.kotelnikov@gmail.com"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git@github.com:statewalker/webrun-wire.git"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./src/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@statewalker/webrun-streams": "0.1.1"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@libp2p/interface": "^3.0.0",
|
|
29
|
+
"@multiformats/multiaddr": "^13.0.0",
|
|
30
|
+
"libp2p": "^3.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"libp2p": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@chainsafe/libp2p-noise": "17.0.0",
|
|
39
|
+
"@chainsafe/libp2p-yamux": "8.0.1",
|
|
40
|
+
"@libp2p/interface": "3.2.5",
|
|
41
|
+
"@libp2p/tcp": "11.0.26",
|
|
42
|
+
"@libp2p/utils": "7.3.2",
|
|
43
|
+
"@multiformats/multiaddr": "13.0.3",
|
|
44
|
+
"@types/node": "^26.2.0",
|
|
45
|
+
"libp2p": "3.3.8",
|
|
46
|
+
"rimraf": "^6.1.3",
|
|
47
|
+
"rolldown": "^1.2.4",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"vitest": "^4.1.10",
|
|
50
|
+
"@statewalker/webrun-streams-conformance": "0.1.1"
|
|
51
|
+
},
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"lint": "biome check src tests"
|
|
60
|
+
}
|
|
61
|
+
}
|