@aethervtt/shard-net 0.0.0-stage → 0.4.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.
@@ -0,0 +1,60 @@
1
+ import { type ComponentDef, type Entity } from '@aethervtt/shard-core';
2
+ import { App, type Plugin } from '@aethervtt/shard-runtime';
3
+ import { type ConditionsOptions, type NetConditions } from './conditions.js';
4
+ import { type NetOptions } from './plugin.js';
5
+ export interface NetHarnessOptions {
6
+ /** Plugins every app gets: a function, so each app has its own instances. */
7
+ plugins?: () => Plugin[];
8
+ /** Extra plugins for the server only, and for each client. */
9
+ server?: () => Plugin[];
10
+ client?: (index: number) => Plugin[];
11
+ /** Net options both sides share (replicate, commands, events, tickRate…). */
12
+ net?: Omit<NetOptions, 'role' | 'transport' | 'address' | 'session' | 'code' | 'now'>;
13
+ /** Per-side overrides (interest and budget for the server, a token for clients…). */
14
+ serverNet?: Partial<NetOptions>;
15
+ clientNet?: (index: number) => Partial<NetOptions>;
16
+ clients?: number;
17
+ /** Conditions on every client's link (one wrapper per client, both directions). */
18
+ conditions?: Omit<ConditionsOptions, 'now'>;
19
+ fixedHz?: number;
20
+ seed?: number;
21
+ maxMessage?: number;
22
+ /**
23
+ * Makes each app (default `new App`): a project build, say (0092). The harness adds the net
24
+ * plugin and initializes it; `plugins`, `server` and `client` are added too.
25
+ */
26
+ createApp?: (side: 'server' | 'client', index: number) => App;
27
+ /** Runs after each app's init, before it connects (an asset scan). */
28
+ afterInit?: (app: App) => Promise<void>;
29
+ }
30
+ export interface NetHarness {
31
+ readonly server: App;
32
+ readonly clients: App[];
33
+ /** The shared fake clock, in ms. */
34
+ now(): number;
35
+ /** Advances every app by `ticks` fixed steps on the shared clock: server first, then clients. */
36
+ step(ticks: number): void;
37
+ /**
38
+ * Steps until every client has finished its handshake (at most `maxTicks`), yielding between
39
+ * ticks so async `accept` and player hooks settle.
40
+ */
41
+ connect(maxTicks?: number): Promise<void>;
42
+ /** Like `step`, but yields to pending promises between ticks. */
43
+ run(ticks: number): Promise<void>;
44
+ setConditions(client: number, conditions: Partial<NetConditions>): void;
45
+ /** Connects one more client mid-game; resolves once its connection is open. */
46
+ addClient(): Promise<App>;
47
+ /** Disposes one client (it says goodbye to the server) and removes it from `clients`. */
48
+ removeClient(index: number): Promise<void>;
49
+ /** Per client, what differs from the server for these components (empty when converged). */
50
+ differences(...defs: ComponentDef[]): string[];
51
+ converged(...defs: ComponentDef[]): boolean;
52
+ /** The client entity with the server entity's net id, if it has one. */
53
+ clientEntity(client: number, serverEntity: Entity): Entity | undefined;
54
+ dispose(): Promise<void>;
55
+ }
56
+ /**
57
+ * A server and N clients in one process, over a conditioned loopback, stepped on one fake clock
58
+ * (0086). Deterministic for a given seed.
59
+ */
60
+ export declare function netHarness(options?: NetHarnessOptions): Promise<NetHarness>;
@@ -0,0 +1,200 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import { App } from "@aethervtt/shard-runtime";
3
+ import { NetId, NetRole } from "./components.js";
4
+ import {
5
+ simulateConditions
6
+ } from "./conditions.js";
7
+ import { loopback } from "./loopback.js";
8
+ import { netPlugin, netSession } from "./plugin.js";
9
+ async function netHarness(options = {}) {
10
+ const fixedHz = options.fixedHz ?? 60;
11
+ const seed = options.seed ?? 1;
12
+ let clock = 0;
13
+ const now = () => clock;
14
+ const network = loopback({ maxMessage: options.maxMessage ?? 1 << 20 });
15
+ const address = "harness";
16
+ const make = (side, index, appSeed) => options.createApp?.(side, index) ?? new App({ fixedHz, seed: appSeed, now });
17
+ const server = make("server", 0, seed);
18
+ for (const p of options.plugins?.() ?? []) server.addPlugin(p);
19
+ for (const p of options.server?.() ?? []) server.addPlugin(p);
20
+ server.addPlugin(
21
+ netPlugin({
22
+ ...options.net,
23
+ ...options.serverNet,
24
+ role: "server",
25
+ transport: network,
26
+ address,
27
+ now
28
+ })
29
+ );
30
+ await server.init();
31
+ await options.afterInit?.(server);
32
+ await netSession(server.world).ready;
33
+ const clients = [];
34
+ const links = [];
35
+ async function addClient() {
36
+ const index = clients.length;
37
+ const link = simulateConditions(network, {
38
+ ...options.conditions,
39
+ seed: (options.conditions?.seed ?? seed) + index * 7919,
40
+ now
41
+ });
42
+ const app = make("client", index, seed + index + 1);
43
+ for (const p of options.plugins?.() ?? []) app.addPlugin(p);
44
+ for (const p of options.client?.(index) ?? []) app.addPlugin(p);
45
+ app.addPlugin(
46
+ netPlugin({
47
+ ...options.net,
48
+ ...options.clientNet?.(index),
49
+ role: "client",
50
+ transport: link,
51
+ address,
52
+ now
53
+ })
54
+ );
55
+ await app.init();
56
+ await options.afterInit?.(app);
57
+ await netSession(app.world).ready;
58
+ clients.push(app);
59
+ links.push(link);
60
+ return app;
61
+ }
62
+ for (let i = 0; i < (options.clients ?? 1); i++) await addClient();
63
+ const step = (ticks) => {
64
+ for (let i = 0; i < ticks; i++) {
65
+ clock += 1e3 / fixedHz;
66
+ server.update(1 / fixedHz);
67
+ for (const c of clients) c.update(1 / fixedHz);
68
+ }
69
+ };
70
+ function clientEntity(client, serverEntity) {
71
+ const world = server.world;
72
+ if (!world.isAlive(serverEntity) || !world.has(serverEntity, NetId)) return void 0;
73
+ const id = world.get(serverEntity, NetId).id;
74
+ return netSession(clients[client].world).client.byNetId.get(id);
75
+ }
76
+ function differences(...defs) {
77
+ const out = [];
78
+ const session = netSession(server.world);
79
+ const entries = session.manifest.components;
80
+ clients.forEach((app, index) => {
81
+ const client = netSession(app.world).client;
82
+ const peerId = app.world.resource(NetRole).peer;
83
+ const peer = session.server.peers.get(peerId);
84
+ if (!peer) {
85
+ out.push(`client ${index}: not connected`);
86
+ return;
87
+ }
88
+ for (const e of peer.relevant.entities) {
89
+ const id = server.world.get(e, NetId).id;
90
+ const local = client.byNetId.get(id);
91
+ if (local === void 0) {
92
+ out.push(`client ${index}: missing net id ${id}`);
93
+ continue;
94
+ }
95
+ for (const def of defs) {
96
+ const entry = entries.find((c) => c.def === def);
97
+ const visible = server.world.has(e, def) && (!entry?.visibleTo || entry.visibleTo(server.world, peerId, e));
98
+ const has = app.world.has(local, def);
99
+ if (visible !== has) {
100
+ out.push(
101
+ `client ${index}: net id ${id} ${def.name} ${visible ? "missing" : "should be absent"}`
102
+ );
103
+ continue;
104
+ }
105
+ if (!visible) continue;
106
+ const a = def.serialize(server.world.get(e, def));
107
+ const b = def.serialize(app.world.get(local, def));
108
+ for (const c of def.layout) {
109
+ const step2 = entry?.quantize[c.name]?.step ?? 0;
110
+ const diff = c.field.kind === "quat" ? compareQuat(a[c.name], b[c.name], `/${c.name}`) : compare(a[c.name], b[c.name], `/${c.name}`, step2);
111
+ if (diff) {
112
+ out.push(`client ${index}: net id ${id} ${def.name}${diff}`);
113
+ break;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ for (const [id] of client.byNetId) {
119
+ const e = session.server.byNetId.get(id);
120
+ if (e === void 0 || !peer.relevant.has(e))
121
+ out.push(`client ${index}: extra net id ${id}`);
122
+ }
123
+ });
124
+ return out;
125
+ }
126
+ return {
127
+ server,
128
+ clients,
129
+ now,
130
+ step,
131
+ async connect(maxTicks = 600) {
132
+ for (let i = 0; i < maxTicks; i++) {
133
+ if (clients.every((c) => c.world.resource(NetRole).connected)) return;
134
+ step(1);
135
+ await yieldToPromises();
136
+ }
137
+ const failed = clients.find((c) => !c.world.resource(NetRole).connected);
138
+ throw netSession(failed.world).client.lastError ?? new ShardError("net/connect-failed", `A client didn't connect within ${maxTicks} ticks`);
139
+ },
140
+ async run(ticks) {
141
+ for (let i = 0; i < ticks; i++) {
142
+ step(1);
143
+ await yieldToPromises();
144
+ }
145
+ },
146
+ setConditions(client, conditions) {
147
+ links[client].setConditions(conditions);
148
+ },
149
+ addClient,
150
+ async removeClient(index) {
151
+ const [app] = clients.splice(index, 1);
152
+ links.splice(index, 1);
153
+ await app?.dispose();
154
+ },
155
+ differences,
156
+ converged: (...defs) => differences(...defs).length === 0,
157
+ clientEntity,
158
+ async dispose() {
159
+ for (const c of clients) await c.dispose();
160
+ await server.dispose();
161
+ }
162
+ };
163
+ }
164
+ function yieldToPromises() {
165
+ return new Promise((resolve) => setTimeout(resolve, 0));
166
+ }
167
+ function compareQuat(a, b, path) {
168
+ const dot = Math.abs(a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]);
169
+ return dot >= 1 - 1e-5 ? "" : ` ${path}: ${JSON.stringify(a)} vs ${JSON.stringify(b)}`;
170
+ }
171
+ function compare(a, b, path, step) {
172
+ if (typeof a === "number" && typeof b === "number") {
173
+ const tolerance = Math.max(step / 2 + 1e-6, 1e-3);
174
+ return Math.abs(a - b) <= tolerance ? "" : ` ${path}: ${a} vs ${b}`;
175
+ }
176
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
177
+ for (let i = 0; i < a.length; i++) {
178
+ const d = compare(a[i], b[i], `${path}/${i}`, step);
179
+ if (d) return d;
180
+ }
181
+ return "";
182
+ }
183
+ if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a)) {
184
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
185
+ for (const k of keys) {
186
+ const d = compare(
187
+ a[k],
188
+ b[k],
189
+ `${path}/${k}`,
190
+ step
191
+ );
192
+ if (d) return d;
193
+ }
194
+ return "";
195
+ }
196
+ return JSON.stringify(a) === JSON.stringify(b) ? "" : ` ${path}: ${JSON.stringify(a)} vs ${JSON.stringify(b)}`;
197
+ }
198
+ export {
199
+ netHarness
200
+ };
@@ -0,0 +1,79 @@
1
+ import { type JsonObject, ShardError } from '@aethervtt/shard-core';
2
+ /** `reliable` arrives once and in order; `unreliable` may be lost, duplicated or reordered. */
3
+ export type Channel = 'reliable' | 'unreliable';
4
+ /** Assigned by a listener; unique among its connections. 0 is the server itself. */
5
+ export type PeerId = number;
6
+ export interface TransportCaps {
7
+ /** False when every message is reliable (WebSocket): unreliable sends go reliable. */
8
+ readonly unreliable: boolean;
9
+ /** The largest message `send` accepts, in bytes. The net core splits bigger reliable messages. */
10
+ readonly maxMessage: number;
11
+ }
12
+ export interface ConnectionStats {
13
+ /** Round trip in ms, if the transport measures one (the net core measures its own). */
14
+ rtt: number;
15
+ sentBytes: number;
16
+ receivedBytes: number;
17
+ /** Unreliable messages known lost (sent but dropped), when the transport can tell. */
18
+ lost: number;
19
+ }
20
+ export interface ConnectOptions {
21
+ /** Passed to the transport (a relay or lobby may check it). The handshake sends its own. */
22
+ token?: string;
23
+ }
24
+ export interface ListenOptions {
25
+ address?: string;
26
+ maxPeers?: number;
27
+ /** What a lobby lists for the room (a name, a mode). Transports ignore it. */
28
+ meta?: JsonObject;
29
+ }
30
+ /**
31
+ * Moves bytes between peers (0086): loopback, WebSocket, and later WebRTC or Steam. A transport
32
+ * never touches a world. Messages it receives wait in the connection until the net core drains them.
33
+ */
34
+ export interface NetTransport {
35
+ readonly name: string;
36
+ readonly caps: TransportCaps;
37
+ connect(address: string, options?: ConnectOptions): Promise<NetConnection>;
38
+ listen?(options: ListenOptions): Promise<NetListener>;
39
+ }
40
+ export interface NetListener {
41
+ onConnection(fn: (connection: NetConnection) => void): () => void;
42
+ close(): Promise<void>;
43
+ }
44
+ export interface NetConnection {
45
+ readonly peer: PeerId;
46
+ /** The transport copies `bytes` before returning: the caller reuses its buffer. */
47
+ send(channel: Channel, bytes: Uint8Array): void;
48
+ onMessage(fn: (channel: Channel, bytes: Uint8Array) => void): () => void;
49
+ onClose(fn: (reason: ShardError | undefined) => void): () => void;
50
+ close(reason?: string): void;
51
+ stats(): ConnectionStats;
52
+ /**
53
+ * Delivers messages that are due. The net core calls it once per frame before draining; a
54
+ * transport that delivers on its own (a socket) leaves it out.
55
+ */
56
+ pump?(): void;
57
+ }
58
+ /** Room codes, lobbies, relays and signaling: how peers find each other (0086). */
59
+ export interface NetSessionProvider {
60
+ readonly name: string;
61
+ /** Limits of the connections it makes, when they differ from a plain transport's. */
62
+ readonly caps?: TransportCaps;
63
+ host(options: ListenOptions): Promise<{
64
+ code: string;
65
+ listener: NetListener;
66
+ }>;
67
+ join(code: string, options?: ConnectOptions): Promise<NetConnection>;
68
+ list?(filter?: JsonObject): Promise<readonly SessionInfo[]>;
69
+ }
70
+ export interface SessionInfo {
71
+ code: string;
72
+ name?: string;
73
+ peers?: number;
74
+ maxPeers?: number;
75
+ meta?: JsonObject;
76
+ }
77
+ /** A session whose code is the transport address: no lobby, peers share a URL. */
78
+ export declare function directSession(transport: NetTransport, address?: string): NetSessionProvider;
79
+ export declare function notListening(transport: NetTransport): ShardError;
@@ -0,0 +1,22 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ function directSession(transport, address = "") {
3
+ return {
4
+ name: `direct:${transport.name}`,
5
+ caps: transport.caps,
6
+ async host(options) {
7
+ if (!transport.listen) throw notListening(transport);
8
+ const listener = await transport.listen({ ...options, address: options.address ?? address });
9
+ return { code: options.address ?? address, listener };
10
+ },
11
+ join: (code, options) => transport.connect(code, options)
12
+ };
13
+ }
14
+ function notListening(transport) {
15
+ return new ShardError("net/cannot-listen", `The ${transport.name} transport can't listen`, {
16
+ hint: "Servers need a transport with listen(), such as webSocketServer() or loopback()."
17
+ });
18
+ }
19
+ export {
20
+ directSession,
21
+ notListening
22
+ };
package/dist/vec.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A growable list of numbers in a Float64Array. `clear` keeps the memory: an array's
3
+ * `length = 0` lets V8 drop its backing store, so refilling it every tick would allocate.
4
+ */
5
+ export declare class NumberList {
6
+ data: Float64Array;
7
+ length: number;
8
+ constructor(capacity?: number);
9
+ push(v: number): void;
10
+ at(i: number): number;
11
+ clear(): void;
12
+ toArray(): number[];
13
+ }
package/dist/vec.js ADDED
@@ -0,0 +1,27 @@
1
+ class NumberList {
2
+ data;
3
+ length = 0;
4
+ constructor(capacity = 64) {
5
+ this.data = new Float64Array(capacity);
6
+ }
7
+ push(v) {
8
+ if (this.length === this.data.length) {
9
+ const next = new Float64Array(this.data.length * 2);
10
+ next.set(this.data);
11
+ this.data = next;
12
+ }
13
+ this.data[this.length++] = v;
14
+ }
15
+ at(i) {
16
+ return this.data[i];
17
+ }
18
+ clear() {
19
+ this.length = 0;
20
+ }
21
+ toArray() {
22
+ return Array.from(this.data.subarray(0, this.length));
23
+ }
24
+ }
25
+ export {
26
+ NumberList
27
+ };
package/dist/wire.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { ByteReader, ByteWriter, ShardError } from '@aethervtt/shard-core';
2
+ import type { NetCommandDef, NetEventDef, ReplicationEntry } from './registry.js';
3
+ import type { Channel, NetConnection } from './transport.js';
4
+ /** Bumped when the message layout changes. */
5
+ export declare const PROTOCOL_VERSION = 1;
6
+ /** The first byte of every message. Each is followed by a u32 tick. */
7
+ export declare const Msg: {
8
+ readonly hello: 1;
9
+ readonly welcome: 2;
10
+ readonly refused: 3;
11
+ readonly ping: 4;
12
+ readonly pong: 5;
13
+ readonly disconnect: 6;
14
+ readonly fragment: 7;
15
+ /** Server → client, unreliable: component updates, acked by sequence. */
16
+ readonly state: 10;
17
+ /** Server → client, reliable: spawns, reliable components, removals, despawns, answers, events. */
18
+ readonly reliable: 11;
19
+ /** Client → server, unreliable: which state messages arrived. */
20
+ readonly ack: 12;
21
+ /** Client → server, reliable. */
22
+ readonly command: 13;
23
+ };
24
+ /** Sections of a `reliable` message, in the order they're written. */
25
+ export declare const Section: {
26
+ readonly spawn: 1;
27
+ readonly set: 2;
28
+ readonly remove: 3;
29
+ readonly despawn: 4;
30
+ readonly answer: 5;
31
+ readonly event: 6;
32
+ readonly end: 0;
33
+ };
34
+ /** What both sides number by sorted name, and hash to refuse mismatched builds (0086). */
35
+ export interface Manifest {
36
+ readonly components: readonly ReplicationEntry[];
37
+ readonly commands: readonly NetCommandDef[];
38
+ readonly events: readonly NetEventDef[];
39
+ readonly hash: number;
40
+ }
41
+ export declare function buildManifest(components: readonly ReplicationEntry[], commands: readonly NetCommandDef[], events: readonly NetEventDef[]): Manifest;
42
+ /** Names the first type two manifests disagree on, for `net/schema-mismatch`. */
43
+ export declare function describeManifest(m: Manifest): string[];
44
+ export declare function header(w: ByteWriter, kind: number, tick: number): void;
45
+ /**
46
+ * Sends a reliable message whole, or split into fragments when it's over the transport's limit.
47
+ * Fragments travel in order on the reliable channel, so reassembly only needs a count.
48
+ */
49
+ export declare class Fragmenter {
50
+ private nextId;
51
+ private readonly scratch;
52
+ private parts;
53
+ private expected;
54
+ private partId;
55
+ send(connection: NetConnection, channel: Channel, bytes: Uint8Array, max: number): void;
56
+ /** Feeds one fragment's body; returns the whole message once the last part arrives. */
57
+ receive(r: ByteReader): Uint8Array | undefined;
58
+ }
59
+ export declare function badMessage(detail: string): ShardError;
60
+ export { ByteReader, ByteWriter };
package/dist/wire.js ADDED
@@ -0,0 +1,141 @@
1
+ import { ByteReader, ByteWriter, ShardError } from "@aethervtt/shard-core";
2
+ const PROTOCOL_VERSION = 1;
3
+ const Msg = {
4
+ hello: 1,
5
+ welcome: 2,
6
+ refused: 3,
7
+ ping: 4,
8
+ pong: 5,
9
+ disconnect: 6,
10
+ fragment: 7,
11
+ /** Server → client, unreliable: component updates, acked by sequence. */
12
+ state: 10,
13
+ /** Server → client, reliable: spawns, reliable components, removals, despawns, answers, events. */
14
+ reliable: 11,
15
+ /** Client → server, unreliable: which state messages arrived. */
16
+ ack: 12,
17
+ /** Client → server, reliable. */
18
+ command: 13
19
+ };
20
+ const Section = {
21
+ spawn: 1,
22
+ set: 2,
23
+ remove: 3,
24
+ despawn: 4,
25
+ answer: 5,
26
+ event: 6,
27
+ end: 0
28
+ };
29
+ const FRAGMENT_OVERHEAD = 32;
30
+ function buildManifest(components, commands, events) {
31
+ const byName = (list, what) => {
32
+ const sorted = [...list].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
33
+ for (let i = 1; i < sorted.length; i++) {
34
+ if (sorted[i].name === sorted[i - 1].name) {
35
+ throw new ShardError("net/duplicate-type", `${what} "${sorted[i].name}" is listed twice`);
36
+ }
37
+ }
38
+ return sorted;
39
+ };
40
+ const comps = byName(
41
+ components.map((c) => ({ ...c, name: c.def.name })),
42
+ "Replicated component"
43
+ );
44
+ const cmds = byName(commands, "Command");
45
+ const evts = byName(events, "Event");
46
+ let text = `p${PROTOCOL_VERSION}`;
47
+ for (const c of comps)
48
+ text += `|c:${describeLayout(c.def)}:${c.mode}:${c.channel}:${JSON.stringify(c.quantize)}`;
49
+ for (const c of cmds) text += `|q:${describeLayout(c.schema)}`;
50
+ for (const e of evts) text += `|e:${describeLayout(e.schema)}`;
51
+ return { components: comps, commands: cmds, events: evts, hash: fnv1a(text) };
52
+ }
53
+ function describeLayout(def) {
54
+ let s = `${def.name}@${def.version}`;
55
+ for (const c of def.layout) s += `,${c.name}:${c.field.kind}:${c.storage}:${c.stride}`;
56
+ return s;
57
+ }
58
+ function describeManifest(m) {
59
+ return [
60
+ ...m.components.map((c) => `component ${describeLayout(c.def)}`),
61
+ ...m.commands.map((c) => `command ${describeLayout(c.schema)}`),
62
+ ...m.events.map((e) => `event ${describeLayout(e.schema)}`)
63
+ ];
64
+ }
65
+ function fnv1a(text) {
66
+ let h = 2166136261;
67
+ for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
68
+ return h >>> 0;
69
+ }
70
+ function header(w, kind, tick) {
71
+ w.reset();
72
+ w.u8(kind);
73
+ w.u32(tick >>> 0);
74
+ }
75
+ class Fragmenter {
76
+ nextId = 1;
77
+ scratch = new ByteWriter(256);
78
+ parts = [];
79
+ expected = 0;
80
+ partId = 0;
81
+ send(connection, channel, bytes, max) {
82
+ if (bytes.length <= max || channel === "unreliable") {
83
+ connection.send(channel, bytes);
84
+ return;
85
+ }
86
+ const chunk = max - FRAGMENT_OVERHEAD;
87
+ if (chunk <= 0) throw new ShardError("net/bad-message", `maxMessage ${max} is too small`);
88
+ const count = Math.ceil(bytes.length / chunk);
89
+ const id = this.nextId++;
90
+ for (let i = 0; i < count; i++) {
91
+ header(this.scratch, Msg.fragment, 0);
92
+ this.scratch.varuint(id);
93
+ this.scratch.varuint(i);
94
+ this.scratch.varuint(count);
95
+ this.scratch.bytes(bytes.subarray(i * chunk, Math.min(bytes.length, (i + 1) * chunk)));
96
+ connection.send("reliable", this.scratch.finish());
97
+ }
98
+ }
99
+ /** Feeds one fragment's body; returns the whole message once the last part arrives. */
100
+ receive(r) {
101
+ const id = r.varuint();
102
+ const index = r.varuint();
103
+ const count = r.varuint();
104
+ if (index === 0) {
105
+ this.parts = [];
106
+ this.expected = count;
107
+ this.partId = id;
108
+ } else if (id !== this.partId || index !== this.parts.length) {
109
+ throw new ShardError("net/bad-message", "A fragment arrived out of order");
110
+ }
111
+ this.parts.push(r.bytes(r.remaining).slice());
112
+ if (this.parts.length < this.expected) return void 0;
113
+ let size = 0;
114
+ for (const p of this.parts) size += p.length;
115
+ const out = new Uint8Array(size);
116
+ let at = 0;
117
+ for (const p of this.parts) {
118
+ out.set(p, at);
119
+ at += p.length;
120
+ }
121
+ this.parts = [];
122
+ return out;
123
+ }
124
+ }
125
+ function badMessage(detail) {
126
+ return new ShardError("net/bad-message", `Malformed network message: ${detail}`, {
127
+ hint: "Both sides must run the same build; the connection is closed."
128
+ });
129
+ }
130
+ export {
131
+ ByteReader,
132
+ ByteWriter,
133
+ Fragmenter,
134
+ Msg,
135
+ PROTOCOL_VERSION,
136
+ Section,
137
+ badMessage,
138
+ buildManifest,
139
+ describeManifest,
140
+ header
141
+ };
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-net",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./testing": {
13
+ "types": "./dist/testing.d.ts",
14
+ "default": "./dist/testing.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "dependencies": {
23
+ "@aethervtt/shard-core": "0.4.1",
24
+ "@aethervtt/shard-runtime": "0.4.1",
25
+ "@aethervtt/shard-transform": "0.4.1"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/aethervtt/shard.git",
30
+ "directory": "packages/net"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "provenance": true
35
+ },
36
+ "shard": {
37
+ "buildSha": "0061be4019b2a0af985eada9a60f757a3c6e2ba4"
38
+ }
39
+ }