@cero-base/core 1.2.0 → 1.4.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/package.json +8 -4
- package/src/network/index.js +40 -54
- package/src/network/transports/ble.js +779 -0
- package/src/network/transports/dht.js +110 -0
- package/src/network/transports/gatt.js +57 -0
- package/types/network/index.d.ts +7 -3
- package/types/network/{bluetooth.d.ts → transports/ble.d.ts} +59 -19
- package/types/network/transports/dht.d.ts +75 -0
- package/types/network/transports/gatt.d.ts +25 -0
- package/src/network/bluetooth.js +0 -324
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import Hyperswarm from 'hyperswarm'
|
|
2
|
+
import b4a from 'b4a'
|
|
3
|
+
import { hash } from 'hypercore-crypto'
|
|
4
|
+
import safetyCatch from 'safety-catch'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} DHTTransportOpts
|
|
8
|
+
* @property {import('../../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
|
|
9
|
+
* @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
|
|
10
|
+
* @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
|
|
11
|
+
* @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
|
|
12
|
+
* @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The internet transport: a Hyperswarm that finds peers over the DHT. Owns the
|
|
17
|
+
* swarm's whole lifecycle — construction (identity keyPair, bootstrap, firewall,
|
|
18
|
+
* relay), the channel-topic join/leave wrapping, flush, suspend/resume, and
|
|
19
|
+
* teardown. {@link Network} subscribes to `this.swarm`'s connection/peer events
|
|
20
|
+
* and drives topic joins; the swarm-specific wiring lives here.
|
|
21
|
+
*/
|
|
22
|
+
export class DHTTransport {
|
|
23
|
+
/** @param {DHTTransportOpts} [opts] */
|
|
24
|
+
constructor({ identity, bootstrap, firewall, relayThrough, channel } = {}) {
|
|
25
|
+
const opts = {}
|
|
26
|
+
if (identity) opts.keyPair = { publicKey: identity.publicKey, secretKey: identity.secretKey }
|
|
27
|
+
if (bootstrap) opts.bootstrap = bootstrap
|
|
28
|
+
if (firewall) opts.firewall = firewall
|
|
29
|
+
if (relayThrough) opts.relayThrough = relayThrough
|
|
30
|
+
|
|
31
|
+
this.swarm = new Hyperswarm(opts)
|
|
32
|
+
|
|
33
|
+
if (channel) {
|
|
34
|
+
const join = this.swarm.join.bind(this.swarm)
|
|
35
|
+
const leave = this.swarm.leave.bind(this.swarm)
|
|
36
|
+
this.swarm.join = (topic, opts) => join(channelTopic(topic, channel), opts)
|
|
37
|
+
this.swarm.leave = (topic) => leave(channelTopic(topic, channel))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @returns {boolean} */
|
|
42
|
+
get suspended() {
|
|
43
|
+
return this.swarm?.suspended === true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wait for pending DHT announces and lookups to settle, bounded by timeout.
|
|
48
|
+
*
|
|
49
|
+
* @param {{ timeout?: number }} [opts]
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
async flush({ timeout = 500 } = {}) {
|
|
53
|
+
if (!this.swarm) return
|
|
54
|
+
await Promise.race([this.swarm.flush(), new Promise((r) => setTimeout(r, timeout))])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Pause the swarm — keeps state, drops sockets. Idempotent.
|
|
59
|
+
*
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
async suspend() {
|
|
63
|
+
if (!this.swarm || this.swarm.suspended) return
|
|
64
|
+
try {
|
|
65
|
+
await this.swarm.suspend()
|
|
66
|
+
} catch (err) {
|
|
67
|
+
safetyCatch(err)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resume a suspended swarm. Idempotent.
|
|
73
|
+
*
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
async resume() {
|
|
77
|
+
if (!this.swarm || !this.swarm.suspended) return
|
|
78
|
+
try {
|
|
79
|
+
await this.swarm.resume()
|
|
80
|
+
} catch (err) {
|
|
81
|
+
safetyCatch(err)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Flush pending discovery, then tear down the swarm. Idempotent.
|
|
87
|
+
*
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
90
|
+
async destroy() {
|
|
91
|
+
if (!this.swarm) return
|
|
92
|
+
try {
|
|
93
|
+
await this.flush()
|
|
94
|
+
} catch (err) {
|
|
95
|
+
safetyCatch(err)
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await this.swarm.destroy()
|
|
99
|
+
} catch (err) {
|
|
100
|
+
safetyCatch(err)
|
|
101
|
+
}
|
|
102
|
+
this.swarm = null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A channel re-namespaces every swarm topic so only same-channel peers meet.
|
|
107
|
+
// No channel → identity (unchanged, back-compat).
|
|
108
|
+
export function channelTopic(topic, channel) {
|
|
109
|
+
return channel ? hash([topic, b4a.from(channel)]) : topic
|
|
110
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Duplex } from 'streamx'
|
|
2
|
+
|
|
3
|
+
// A single GATT write/notify caps at ATT_MTU − 3 ≈ 182 bytes; 150 stays under
|
|
4
|
+
// that without negotiating an MTU.
|
|
5
|
+
const PAYLOAD = 150
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A dumb byte-carrying duplex for the GATT transport. Framing and session logic
|
|
9
|
+
* live in BLETransport; this only fragments outbound writes to fit a GATT
|
|
10
|
+
* write and pushes inbound payload bytes.
|
|
11
|
+
*
|
|
12
|
+
* @extends Duplex
|
|
13
|
+
*/
|
|
14
|
+
export class GattStream extends Duplex {
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} opts
|
|
17
|
+
* @param {(buffer: Uint8Array) => Promise<void>} opts.send Transmit one payload piece (transport frames it).
|
|
18
|
+
* @param {() => void} [opts.onclose] Called once on teardown (send a close frame, disconnect).
|
|
19
|
+
*/
|
|
20
|
+
constructor({ send, onclose } = {}) {
|
|
21
|
+
super()
|
|
22
|
+
this._send = send
|
|
23
|
+
this._onclose = onclose || null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async _write(chunk, cb) {
|
|
27
|
+
try {
|
|
28
|
+
for (let offset = 0; offset < chunk.byteLength; offset += PAYLOAD) {
|
|
29
|
+
await this._send(chunk.subarray(offset, offset + PAYLOAD))
|
|
30
|
+
}
|
|
31
|
+
cb(null)
|
|
32
|
+
} catch (err) {
|
|
33
|
+
cb(err)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
receive(buffer) {
|
|
38
|
+
this.push(buffer)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
remoteEnd() {
|
|
42
|
+
this.push(null)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_destroy(cb) {
|
|
46
|
+
const onclose = this._onclose
|
|
47
|
+
this._onclose = null
|
|
48
|
+
if (onclose) {
|
|
49
|
+
try {
|
|
50
|
+
onclose()
|
|
51
|
+
} catch {
|
|
52
|
+
// teardown is best-effort
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
cb(null)
|
|
56
|
+
}
|
|
57
|
+
}
|
package/types/network/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { channelTopic };
|
|
2
2
|
/**
|
|
3
3
|
* @typedef {object} NetworkOpts
|
|
4
4
|
* @property {import('../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
|
|
@@ -24,12 +24,14 @@ export class Network extends ReadyResource {
|
|
|
24
24
|
firewall: (remotePublicKey: Uint8Array, payload: any) => boolean;
|
|
25
25
|
relayThrough: Uint8Array<ArrayBufferLike>[];
|
|
26
26
|
channel: string;
|
|
27
|
-
|
|
27
|
+
_dht: DHTTransport;
|
|
28
28
|
wakeup: any;
|
|
29
29
|
_replicateables: Set<any>;
|
|
30
30
|
_discoveries: Set<any>;
|
|
31
31
|
_injected: Set<any>;
|
|
32
32
|
_blind: any;
|
|
33
|
+
/** @returns {any} The underlying hyperswarm, or null before ready / after close. */
|
|
34
|
+
get swarm(): any;
|
|
33
35
|
/**
|
|
34
36
|
* Feed an externally-established connection — a Bluetooth L2CAP channel, a
|
|
35
37
|
* serial link, an in-process pair, any duplex — into the network. A raw
|
|
@@ -75,7 +77,7 @@ export class Network extends ReadyResource {
|
|
|
75
77
|
* @param {{ timeout?: number }} [opts]
|
|
76
78
|
* @returns {Promise<void>}
|
|
77
79
|
*/
|
|
78
|
-
flush(
|
|
80
|
+
flush(opts?: {
|
|
79
81
|
timeout?: number;
|
|
80
82
|
}): Promise<void>;
|
|
81
83
|
/**
|
|
@@ -153,5 +155,7 @@ export type NetworkOpts = {
|
|
|
153
155
|
export type Replicable = {
|
|
154
156
|
replicate: (stream: any) => any;
|
|
155
157
|
};
|
|
158
|
+
import { channelTopic } from './transports/dht.js';
|
|
156
159
|
import ReadyResource from 'ready-resource';
|
|
160
|
+
import { DHTTransport } from './transports/dht.js';
|
|
157
161
|
import { Discovery } from './discovery.js';
|
|
@@ -8,35 +8,34 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export function toServiceUUID(topic: Uint8Array, tag?: string): string;
|
|
10
10
|
/**
|
|
11
|
-
* Dual-role BLE transport: advertises + scans one service UUID, opens
|
|
12
|
-
*
|
|
11
|
+
* Dual-role BLE transport: advertises + scans one service UUID, opens a GATT
|
|
12
|
+
* byte-stream to each discovered peer, and feeds it into `network.inject`. From
|
|
13
13
|
* there replication and pairing are transport-agnostic (see Network.inject).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* ponytail: capability-handshake DoS link-scoring is deferred — it needs a
|
|
21
|
-
* replication-progress signal (design §4b). v1 caps links + times out dials.
|
|
15
|
+
* The server adds one data characteristic (write + notify) and advertises. The
|
|
16
|
+
* central connects, discovers the characteristic, subscribes, then framed bytes
|
|
17
|
+
* flow both ways — central→server as GATT writes, server→central as
|
|
18
|
+
* notifications — each tagged with an 8-byte session id. `backend` is
|
|
19
|
+
* bare-bluetooth in production and a mock in tests.
|
|
22
20
|
*
|
|
23
21
|
* @extends ReadyResource
|
|
24
22
|
*/
|
|
25
|
-
export class
|
|
23
|
+
export class BLETransport extends ReadyResource {
|
|
26
24
|
/**
|
|
27
25
|
* @param {object} opts
|
|
28
26
|
* @param {any} opts.backend bare-bluetooth-shaped module (Central, Server, Service, Characteristic).
|
|
29
|
-
* @param {import('
|
|
27
|
+
* @param {import('../index.js').Network} opts.network
|
|
30
28
|
* @param {Uint8Array} opts.uuid The 32-byte topic the service UUID derives from.
|
|
31
29
|
* @param {Uint8Array} opts.nodeId Stable local id (identity/device key) for the initiate tie-break.
|
|
32
30
|
* @param {string} [opts.tag] UUID namespace (channel mesh vs invite mesh).
|
|
33
31
|
* @param {number} [opts.cap] Max concurrent links; gossip covers the rest.
|
|
34
32
|
* @param {{ scanMode?: any }} [opts.scanOptions] Platform scan options (e.g. Android low-power).
|
|
35
33
|
* @param {boolean} [opts.keepLinks] On close, stop the radio but leave established links alive (invite rendezvous: the link outlives the QR and carries the initial replication).
|
|
34
|
+
* @param {string} [opts.name] Local app-user display name, sent to peers over a hello frame.
|
|
36
35
|
*/
|
|
37
|
-
constructor({ backend, network, uuid, nodeId, tag, cap, scanOptions, keepLinks }: {
|
|
36
|
+
constructor({ backend, network, uuid, nodeId, tag, cap, scanOptions, keepLinks, name }: {
|
|
38
37
|
backend: any;
|
|
39
|
-
network: import("
|
|
38
|
+
network: import("../index.js").Network;
|
|
40
39
|
uuid: Uint8Array;
|
|
41
40
|
nodeId: Uint8Array;
|
|
42
41
|
tag?: string;
|
|
@@ -45,9 +44,11 @@ export class BluetoothTransport extends ReadyResource {
|
|
|
45
44
|
scanMode?: any;
|
|
46
45
|
};
|
|
47
46
|
keepLinks?: boolean;
|
|
47
|
+
name?: string;
|
|
48
48
|
});
|
|
49
49
|
backend: any;
|
|
50
|
-
network: import("
|
|
50
|
+
network: import("../index.js").Network;
|
|
51
|
+
name: string;
|
|
51
52
|
nodeId: Uint8Array<ArrayBufferLike>;
|
|
52
53
|
nodeHex: any;
|
|
53
54
|
serviceUUID: string;
|
|
@@ -59,12 +60,20 @@ export class BluetoothTransport extends ReadyResource {
|
|
|
59
60
|
state: string;
|
|
60
61
|
central: any;
|
|
61
62
|
server: any;
|
|
62
|
-
|
|
63
|
+
_dataChar: any;
|
|
64
|
+
/** sessionId hex → { stream, sid } for server-side (peripheral) sessions */
|
|
65
|
+
_sessions: Map<any, any>;
|
|
66
|
+
/** serialized server notify queue: { frame, resolve, reject } */
|
|
67
|
+
_notifyQueue: any[];
|
|
63
68
|
_scanning: boolean;
|
|
64
69
|
_advertising: boolean;
|
|
65
70
|
_serviceAdded: boolean;
|
|
66
|
-
/** peripheral id
|
|
67
|
-
|
|
71
|
+
/** peripheral id → per-peer dial state { timer, linked, coolUntil, failures, peerKey, peripheral } */
|
|
72
|
+
_devices: Map<any, any>;
|
|
73
|
+
/** last central.connect timestamp — global inter-dial rate limit */
|
|
74
|
+
_lastDial: number;
|
|
75
|
+
_scanTimer: any;
|
|
76
|
+
_suspended: boolean;
|
|
68
77
|
/** live injected links keyed by remote node id hex */
|
|
69
78
|
peers: Map<any, any>;
|
|
70
79
|
/**
|
|
@@ -77,17 +86,48 @@ export class BluetoothTransport extends ReadyResource {
|
|
|
77
86
|
*/
|
|
78
87
|
shouldInitiate(peerNodeId: Uint8Array): boolean;
|
|
79
88
|
get linkCount(): number;
|
|
89
|
+
_device(id: any): any;
|
|
90
|
+
_prune(id: any): void;
|
|
80
91
|
_startServer(Service: any, Characteristic: any): void;
|
|
81
92
|
_maybeAdvertise(): void;
|
|
93
|
+
_onWriteRequests(requests: any): void;
|
|
94
|
+
_onServerFrame(data: any): void;
|
|
95
|
+
_closeServerSession(sidHex: any, sid: any): void;
|
|
96
|
+
_helloPayload(): any;
|
|
97
|
+
_parseHello(payload: any): string;
|
|
98
|
+
_applyPeerName(session: any, payload: any): void;
|
|
99
|
+
_enqueueNotify(f: any): Promise<any>;
|
|
100
|
+
_drainNotify(): void;
|
|
82
101
|
_startScan(): void;
|
|
102
|
+
_armScanRestart(): void;
|
|
103
|
+
_stopScan(): void;
|
|
83
104
|
_onState(raw: any): void;
|
|
84
105
|
_onDiscover(peripheral: any): void;
|
|
85
106
|
_onConnect(peripheral: any): void;
|
|
107
|
+
_startCentralSession(peripheral: any, char: any): void;
|
|
108
|
+
_onCentralNotify(peripheral: any, data: any): void;
|
|
109
|
+
_centralSend(peripheral: any, char: any, f: any): any;
|
|
110
|
+
_writeOnce(peripheral: any, char: any, f: any): Promise<any>;
|
|
86
111
|
_abortDial(peripheral: any, _reason: any): void;
|
|
87
112
|
_clearDial(id: any): void;
|
|
113
|
+
_isDialing(): boolean;
|
|
88
114
|
_onCentralError(err: any): void;
|
|
89
|
-
_onChannel(
|
|
90
|
-
_track(conn: any, peripheralId: any): void;
|
|
115
|
+
_onChannel(stream: any, isInitiator: any, peripheralId: any): any;
|
|
116
|
+
_track(conn: any, peripheralId: any, isInitiator: any): void;
|
|
91
117
|
_untrack(conn: any): void;
|
|
118
|
+
_sayGoodbye(): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Pause radio activity but KEEP the Server/Central instances and the
|
|
121
|
+
* registered GATT service alive — the toggle-friendly counterpart to _close.
|
|
122
|
+
* CoreBluetooth managers can't be destroy()ed (native double-free), so one
|
|
123
|
+
* transport is reused across toggles rather than recreated. Idempotent.
|
|
124
|
+
*/
|
|
125
|
+
suspend(): Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Restart advertising + scanning on the SAME Server/Central. `_serviceAdded`
|
|
128
|
+
* is still true (the service was never removed) so advertising resumes
|
|
129
|
+
* immediately. Safe to call repeatedly; no-op once closing/closed.
|
|
130
|
+
*/
|
|
131
|
+
resume(): void;
|
|
92
132
|
}
|
|
93
133
|
import ReadyResource from 'ready-resource';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function channelTopic(topic: any, channel: any): any;
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {object} DHTTransportOpts
|
|
4
|
+
* @property {import('../../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
|
|
5
|
+
* @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
|
|
6
|
+
* @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
|
|
7
|
+
* @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
|
|
8
|
+
* @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The internet transport: a Hyperswarm that finds peers over the DHT. Owns the
|
|
12
|
+
* swarm's whole lifecycle — construction (identity keyPair, bootstrap, firewall,
|
|
13
|
+
* relay), the channel-topic join/leave wrapping, flush, suspend/resume, and
|
|
14
|
+
* teardown. {@link Network} subscribes to `this.swarm`'s connection/peer events
|
|
15
|
+
* and drives topic joins; the swarm-specific wiring lives here.
|
|
16
|
+
*/
|
|
17
|
+
export class DHTTransport {
|
|
18
|
+
/** @param {DHTTransportOpts} [opts] */
|
|
19
|
+
constructor({ identity, bootstrap, firewall, relayThrough, channel }?: DHTTransportOpts);
|
|
20
|
+
swarm: any;
|
|
21
|
+
/** @returns {boolean} */
|
|
22
|
+
get suspended(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Wait for pending DHT announces and lookups to settle, bounded by timeout.
|
|
25
|
+
*
|
|
26
|
+
* @param {{ timeout?: number }} [opts]
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
flush({ timeout }?: {
|
|
30
|
+
timeout?: number;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Pause the swarm — keeps state, drops sockets. Idempotent.
|
|
34
|
+
*
|
|
35
|
+
* @returns {Promise<void>}
|
|
36
|
+
*/
|
|
37
|
+
suspend(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Resume a suspended swarm. Idempotent.
|
|
40
|
+
*
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
resume(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Flush pending discovery, then tear down the swarm. Idempotent.
|
|
46
|
+
*
|
|
47
|
+
* @returns {Promise<void>}
|
|
48
|
+
*/
|
|
49
|
+
destroy(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export type DHTTransportOpts = {
|
|
52
|
+
/**
|
|
53
|
+
* Long-lived keypair used as the swarm identity.
|
|
54
|
+
*/
|
|
55
|
+
identity?: import("../../identity/index.js").Identity;
|
|
56
|
+
/**
|
|
57
|
+
* Custom DHT bootstrap nodes.
|
|
58
|
+
*/
|
|
59
|
+
bootstrap?: Array<{
|
|
60
|
+
host: string;
|
|
61
|
+
port: number;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Incoming-connection filter.
|
|
65
|
+
*/
|
|
66
|
+
firewall?: (remotePublicKey: Uint8Array, payload: any) => boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Relay public keys to tunnel through.
|
|
69
|
+
*/
|
|
70
|
+
relayThrough?: Uint8Array[];
|
|
71
|
+
/**
|
|
72
|
+
* Optional network-isolation label; only same-channel peers meet.
|
|
73
|
+
*/
|
|
74
|
+
channel?: string;
|
|
75
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dumb byte-carrying duplex for the GATT transport. Framing and session logic
|
|
3
|
+
* live in BLETransport; this only fragments outbound writes to fit a GATT
|
|
4
|
+
* write and pushes inbound payload bytes.
|
|
5
|
+
*
|
|
6
|
+
* @extends Duplex
|
|
7
|
+
*/
|
|
8
|
+
export class GattStream extends Duplex<import("streamx").DuplexEvents> {
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} opts
|
|
11
|
+
* @param {(buffer: Uint8Array) => Promise<void>} opts.send Transmit one payload piece (transport frames it).
|
|
12
|
+
* @param {() => void} [opts.onclose] Called once on teardown (send a close frame, disconnect).
|
|
13
|
+
*/
|
|
14
|
+
constructor({ send, onclose }?: {
|
|
15
|
+
send: (buffer: Uint8Array) => Promise<void>;
|
|
16
|
+
onclose?: () => void;
|
|
17
|
+
});
|
|
18
|
+
_send: (buffer: Uint8Array) => Promise<void>;
|
|
19
|
+
_onclose: () => void;
|
|
20
|
+
_write(chunk: any, cb: any): Promise<void>;
|
|
21
|
+
receive(buffer: any): void;
|
|
22
|
+
remoteEnd(): void;
|
|
23
|
+
_destroy(cb: any): void;
|
|
24
|
+
}
|
|
25
|
+
import { Duplex } from 'streamx';
|