@forgeax/engine-net 0.1.2

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.
Files changed (45) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +145 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/endpoint/endpoint.d.ts +36 -0
  5. package/dist/endpoint/endpoint.d.ts.map +1 -0
  6. package/dist/endpoint/errors.d.ts +82 -0
  7. package/dist/endpoint/errors.d.ts.map +1 -0
  8. package/dist/endpoint/memory.d.ts +13 -0
  9. package/dist/endpoint/memory.d.ts.map +1 -0
  10. package/dist/index.d.ts +17 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.mjs +903 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/replication/authority.d.ts +17 -0
  15. package/dist/replication/authority.d.ts.map +1 -0
  16. package/dist/replication/codec.d.ts +26 -0
  17. package/dist/replication/codec.d.ts.map +1 -0
  18. package/dist/replication/constants.d.ts +2 -0
  19. package/dist/replication/constants.d.ts.map +1 -0
  20. package/dist/replication/errors.d.ts +66 -0
  21. package/dist/replication/errors.d.ts.map +1 -0
  22. package/dist/replication/handshake.d.ts +5 -0
  23. package/dist/replication/handshake.d.ts.map +1 -0
  24. package/dist/replication/profile.d.ts +31 -0
  25. package/dist/replication/profile.d.ts.map +1 -0
  26. package/dist/replication/replica.d.ts +27 -0
  27. package/dist/replication/replica.d.ts.map +1 -0
  28. package/dist/session/net-session.d.ts +32 -0
  29. package/dist/session/net-session.d.ts.map +1 -0
  30. package/dist/session/session-plugin.d.ts +8 -0
  31. package/dist/session/session-plugin.d.ts.map +1 -0
  32. package/package.json +58 -0
  33. package/src/endpoint/endpoint.ts +50 -0
  34. package/src/endpoint/errors.ts +164 -0
  35. package/src/endpoint/memory.ts +172 -0
  36. package/src/index.ts +46 -0
  37. package/src/replication/authority.ts +145 -0
  38. package/src/replication/codec.ts +257 -0
  39. package/src/replication/constants.ts +1 -0
  40. package/src/replication/errors.ts +60 -0
  41. package/src/replication/handshake.ts +18 -0
  42. package/src/replication/profile.ts +111 -0
  43. package/src/replication/replica.ts +240 -0
  44. package/src/session/net-session.ts +118 -0
  45. package/src/session/session-plugin.ts +52 -0
@@ -0,0 +1,118 @@
1
+ // @forgeax/engine-net -- NetSession host-neutral World integration.
2
+ // (requirements AC-04, plan-strategy D-1/D-3)
3
+
4
+ import { err, ok, type Result } from '@forgeax/engine-types';
5
+ import type { NetEndpoint, PeerId } from '../endpoint/endpoint';
6
+ import type { EndpointError } from '../endpoint/errors';
7
+ import type { AuthorityCoordinator } from '../replication/authority';
8
+ import type { NetError } from '../replication/errors';
9
+ import type { ReplicationLimits } from '../replication/profile';
10
+ import { decodeAndApplyReplicaBatch, type ReplicaCoordinator } from '../replication/replica';
11
+
12
+ export interface PeerSnapshot {
13
+ readonly peerIds: ReadonlyArray<PeerId>;
14
+ readonly connected: boolean;
15
+ }
16
+
17
+ export interface NetSessionConfig {
18
+ readonly endpoint: NetEndpoint;
19
+ readonly maxRawMessages: number;
20
+ }
21
+
22
+ export interface RawMessage {
23
+ readonly peerId: PeerId;
24
+ readonly data: Uint8Array;
25
+ }
26
+
27
+ export class NetSession {
28
+ readonly #endpoint: NetEndpoint;
29
+ readonly #peerIds = new Set<PeerId>();
30
+ #rawMessages: RawMessage[] = [];
31
+ readonly #maxRawMessages: number;
32
+ #authority: AuthorityCoordinator | undefined;
33
+ readonly #pendingFullPeers = new Set<PeerId>();
34
+ #replica:
35
+ | { readonly coordinator: ReplicaCoordinator; readonly limits: ReplicationLimits }
36
+ | undefined;
37
+
38
+ constructor(config: NetSessionConfig) {
39
+ this.#endpoint = config.endpoint;
40
+ this.#maxRawMessages = config.maxRawMessages;
41
+ }
42
+
43
+ receiveEvents(): readonly NetError[] {
44
+ const errors: NetError[] = [];
45
+ for (const event of this.#endpoint.poll()) {
46
+ if (event.kind === 'peer-connected') {
47
+ this.#peerIds.add(event.peerId);
48
+ // Transport admission must not imply application admission, but every
49
+ // newly connected replica still needs the generic replication baseline.
50
+ this.#pendingFullPeers.add(event.peerId);
51
+ } else if (event.kind === 'peer-disconnected') {
52
+ this.#peerIds.delete(event.peerId);
53
+ this.#pendingFullPeers.delete(event.peerId);
54
+ this.#replica?.coordinator.clear();
55
+ } else {
56
+ if (this.#replica !== undefined) {
57
+ const result = decodeAndApplyReplicaBatch(
58
+ this.#replica.coordinator,
59
+ event.data,
60
+ this.#replica.limits,
61
+ );
62
+ if (!result.ok) errors.push(result.error);
63
+ } else if (this.#rawMessages.length < this.#maxRawMessages) {
64
+ this.#rawMessages.push({ peerId: event.peerId, data: event.data });
65
+ }
66
+ }
67
+ }
68
+ return errors;
69
+ }
70
+
71
+ drainRawMessages(): RawMessage[] {
72
+ return this.#rawMessages.splice(0);
73
+ }
74
+
75
+ getPeerSnapshot(): PeerSnapshot {
76
+ const peerIds = [...this.#peerIds].sort((left, right) => left - right);
77
+ return { peerIds, connected: peerIds.length > 0 };
78
+ }
79
+
80
+ sendRaw(peerId: PeerId, data: Uint8Array): Result<void, EndpointError> {
81
+ const result = this.#endpoint.send(peerId, data);
82
+ return result.ok ? ok(undefined) : err(result.error);
83
+ }
84
+
85
+ attachAuthority(authority: AuthorityCoordinator): void {
86
+ this.#authority = authority;
87
+ }
88
+
89
+ requestFullBaseline(peerId: PeerId): void {
90
+ if (this.#peerIds.has(peerId)) this.#pendingFullPeers.add(peerId);
91
+ }
92
+
93
+ attachReplica(coordinator: ReplicaCoordinator, limits: ReplicationLimits): void {
94
+ this.#replica = { coordinator, limits };
95
+ }
96
+
97
+ publish(): Result<void, NetError | EndpointError> {
98
+ if (this.#authority === undefined) return ok(undefined);
99
+ if (this.#pendingFullPeers.size > 0) {
100
+ const published = this.#authority.publishFull();
101
+ if (!published.ok) return err(published.error);
102
+ for (const peerId of this.#pendingFullPeers) {
103
+ if (this.#peerIds.has(peerId)) {
104
+ const sent = this.#endpoint.send(peerId, published.value.bytes);
105
+ if (!sent.ok) return err(sent.error);
106
+ }
107
+ }
108
+ this.#pendingFullPeers.clear();
109
+ }
110
+ const published = this.#authority.publish();
111
+ if (!published.ok) return err(published.error);
112
+ for (const peerId of this.#peerIds) {
113
+ const sent = this.#endpoint.send(peerId, published.value.bytes);
114
+ if (!sent.ok) return err(sent.error);
115
+ }
116
+ return ok(undefined);
117
+ }
118
+ }
@@ -0,0 +1,52 @@
1
+ // @forgeax/engine-net -- session plugin (host-neutral World integration).
2
+ // (requirements AC-04, plan-strategy D-1/D-3)
3
+
4
+ import { FixedUpdate, Update } from '@forgeax/engine-ecs';
5
+ import type { Plugin } from '@forgeax/engine-plugin';
6
+ import type { NetEndpoint } from '../endpoint/endpoint';
7
+ import { NetSession } from './net-session';
8
+
9
+ export interface NetPluginConfig {
10
+ readonly endpoint: NetEndpoint;
11
+ readonly maxRawMessages?: number;
12
+ }
13
+
14
+ export function netPlugin(config: NetPluginConfig): Plugin {
15
+ return {
16
+ name: 'net-session',
17
+ inject: ['world'],
18
+ apply(ctx) {
19
+ const world = ctx.world;
20
+ const session = new NetSession({
21
+ endpoint: config.endpoint,
22
+ maxRawMessages: config.maxRawMessages ?? 256,
23
+ });
24
+ ctx.effect(() => {
25
+ world.insertResource('net-session', session);
26
+ return () => world.removeResource('net-session');
27
+ }, 'net/session-resource');
28
+ ctx.effect(() => {
29
+ world
30
+ .addSystem(Update, {
31
+ name: 'net-receive',
32
+ queries: [],
33
+ before: [FixedUpdate],
34
+ fn: (world) => world.getResource<NetSession>('net-session').receiveEvents(),
35
+ })
36
+ .unwrap();
37
+ return () => world.removeSystem(Update, 'net-receive');
38
+ }, 'net/receive');
39
+ ctx.effect(() => {
40
+ world
41
+ .addSystem(Update, {
42
+ name: 'net-publish',
43
+ queries: [],
44
+ after: [FixedUpdate],
45
+ fn: (world) => world.getResource<NetSession>('net-session').publish(),
46
+ })
47
+ .unwrap();
48
+ return () => world.removeSystem(Update, 'net-publish');
49
+ }, 'net/publish');
50
+ },
51
+ };
52
+ }