@driftengine/network 4.7.4 → 4.8.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.
@@ -106,6 +106,8 @@ export declare class LockstepSession<S> {
106
106
  /** Digests we published, by tick, so a peer's claim has something to be compared against. */
107
107
  private readonly ourDigests;
108
108
  private statusValue;
109
+ /** The tick of this session's first `submit`, or -1 before it. See `open`. */
110
+ private openedAt;
109
111
  private desyncValue;
110
112
  private haltReason;
111
113
  private confirmedValue;
@@ -136,6 +138,19 @@ export declare class LockstepSession<S> {
136
138
  * prediction is supposed to prevent.
137
139
  */
138
140
  submit(tick: number, payload: Uint8Array): void;
141
+ /**
142
+ * The ticks before anybody's first input, settled as neutral for every participant.
143
+ *
144
+ * **Nobody can publish an input for them**: the earliest a peer publishes is its first tick plus
145
+ * the input delay. So nothing will ever arrive to confirm or correct them, and whatever a peer
146
+ * guesses for them is final. A guess repeats the newest input the log holds, and for this peer's
147
+ * own participant that was the input just submitted for a *later* tick, which no other peer can
148
+ * know: a player holding a control on the first tick split the worlds there, and the first
149
+ * fingerprint halted the session. Neutral is the one answer every peer reaches alone, because
150
+ * nobody had pressed anything before the game began. Lockstep peers start on the same tick, so
151
+ * they all settle the same ticks.
152
+ */
153
+ private open;
139
154
  /**
140
155
  * Keep this input, and the few before it, for the next packet to carry.
141
156
  *
package/dist/lockstep.js CHANGED
@@ -58,6 +58,8 @@ export class LockstepSession {
58
58
  /** Digests we published, by tick, so a peer's claim has something to be compared against. */
59
59
  ourDigests = new Map();
60
60
  statusValue = 'running';
61
+ /** The tick of this session's first `submit`, or -1 before it. See `open`. */
62
+ openedAt = -1;
61
63
  desyncValue = null;
62
64
  haltReason = '';
63
65
  confirmedValue = -1;
@@ -111,6 +113,8 @@ export class LockstepSession {
111
113
  if (this.statusValue !== 'running')
112
114
  return;
113
115
  const at = tick + this.inputDelay;
116
+ if (this.openedAt < 0)
117
+ this.open(tick);
114
118
  if (!this.inputs.holds(at)) {
115
119
  this.halt(`an input for tick ${at} is outside the log's window`);
116
120
  return;
@@ -124,6 +128,29 @@ export class LockstepSession {
124
128
  }
125
129
  this.transport.send(BROADCAST, this.outgoing.subarray(0, length));
126
130
  }
131
+ /**
132
+ * The ticks before anybody's first input, settled as neutral for every participant.
133
+ *
134
+ * **Nobody can publish an input for them**: the earliest a peer publishes is its first tick plus
135
+ * the input delay. So nothing will ever arrive to confirm or correct them, and whatever a peer
136
+ * guesses for them is final. A guess repeats the newest input the log holds, and for this peer's
137
+ * own participant that was the input just submitted for a *later* tick, which no other peer can
138
+ * know: a player holding a control on the first tick split the worlds there, and the first
139
+ * fingerprint halted the session. Neutral is the one answer every peer reaches alone, because
140
+ * nobody had pressed anything before the game began. Lockstep peers start on the same tick, so
141
+ * they all settle the same ticks.
142
+ */
143
+ open(tick) {
144
+ this.openedAt = tick;
145
+ const neutral = new Uint8Array(this.inputs.inputBytes);
146
+ for (let at = tick; at < tick + this.inputDelay; at++) {
147
+ if (!this.inputs.holds(at))
148
+ continue;
149
+ for (let participant = 0; participant < this.participants; participant++) {
150
+ this.inputs.set(participant, at, neutral);
151
+ }
152
+ }
153
+ }
127
154
  /**
128
155
  * Keep this input, and the few before it, for the next packet to carry.
129
156
  *
@@ -61,3 +61,10 @@ export interface Transport {
61
61
  drain(into: MessageSink): void;
62
62
  close(): void;
63
63
  }
64
+ /**
65
+ * The message as a view of an `ArrayBuffer`, which is what a socket or a channel will send.
66
+ *
67
+ * Everything this package sends is already one, so this is a check and no copy. A message on
68
+ * shared memory, which a browser refuses to send, is copied off it.
69
+ */
70
+ export declare function sendable(message: Uint8Array): Uint8Array<ArrayBuffer>;
package/dist/transport.js CHANGED
@@ -38,3 +38,14 @@
38
38
  */
39
39
  /** Send to everyone the transport can reach. */
40
40
  export const BROADCAST = -1;
41
+ /**
42
+ * The message as a view of an `ArrayBuffer`, which is what a socket or a channel will send.
43
+ *
44
+ * Everything this package sends is already one, so this is a check and no copy. A message on
45
+ * shared memory, which a browser refuses to send, is copied off it.
46
+ */
47
+ export function sendable(message) {
48
+ return message.buffer instanceof ArrayBuffer
49
+ ? message
50
+ : message.slice();
51
+ }
package/dist/webrtc.d.ts CHANGED
@@ -35,7 +35,8 @@ import { type MessageSink, type PeerId, type Transport, type TransportState } fr
35
35
  export interface DataChannelLike {
36
36
  readonly readyState: string;
37
37
  binaryType?: string;
38
- send(data: ArrayBufferView | ArrayBuffer): void;
38
+ /** An `ArrayBuffer`'s view, as for a socket: see `SocketLike.send`. */
39
+ send(data: ArrayBufferView<ArrayBuffer> | ArrayBuffer): void;
39
40
  close(): void;
40
41
  addEventListener(type: string, listener: (event: unknown) => void): void;
41
42
  }
package/dist/webrtc.js CHANGED
@@ -30,7 +30,7 @@
30
30
  * That is a weaker guarantee than the loopback transport has, and it is why WebSocket is the
31
31
  * default rather than this.
32
32
  */
33
- import { BROADCAST, } from './transport.js';
33
+ import { BROADCAST, sendable, } from './transport.js';
34
34
  /**
35
35
  * A channel configured the way a simulation wants one.
36
36
  *
@@ -83,7 +83,7 @@ export class WebRtcTransport {
83
83
  send(_to, message) {
84
84
  if (this.state !== 'open')
85
85
  return;
86
- this.channel.send(message);
86
+ this.channel.send(sendable(message));
87
87
  }
88
88
  drain(into) {
89
89
  for (const message of this.arrived)
@@ -28,7 +28,12 @@ import { type MessageSink, type PeerId, type Transport, type TransportState } fr
28
28
  export interface SocketLike {
29
29
  readonly readyState: number;
30
30
  binaryType?: string;
31
- send(data: ArrayBufferView | ArrayBuffer): void;
31
+ /**
32
+ * A view of an `ArrayBuffer`, never of shared memory: that is what a browser's `WebSocket.send`
33
+ * takes, and declaring any buffer here made a browser socket not fit its own transport, so
34
+ * `new WebSocketTransport({ socket: new WebSocket(url) })` did not typecheck.
35
+ */
36
+ send(data: ArrayBufferView<ArrayBuffer> | ArrayBuffer): void;
32
37
  close(): void;
33
38
  addEventListener(type: string, listener: (event: unknown) => void): void;
34
39
  }
package/dist/websocket.js CHANGED
@@ -23,7 +23,7 @@
23
23
  * the credentials and the reconnection policy. A transport that opened its own connection would be
24
24
  * making four decisions that belong to whoever is paying for the server.
25
25
  */
26
- import { BROADCAST, } from './transport.js';
26
+ import { BROADCAST, sendable, } from './transport.js';
27
27
  /** The `readyState` values, which are the same three numbers everywhere. */
28
28
  const OPEN = 1;
29
29
  const CLOSED = 3;
@@ -73,7 +73,7 @@ export class WebSocketTransport {
73
73
  send(_to, message) {
74
74
  if (this.state !== 'open')
75
75
  return;
76
- this.socket.send(message);
76
+ this.socket.send(sendable(message));
77
77
  }
78
78
  drain(into) {
79
79
  for (const message of this.arrived)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/network",
3
- "version": "4.7.4",
3
+ "version": "4.8.1",
4
4
  "description": "A transport seam, rewind and replay, lockstep and an authoritative host, on the fixed-step loop",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,10 +27,10 @@
27
27
  ],
28
28
  "sideEffects": false,
29
29
  "peerDependencies": {
30
- "@driftengine/entities": "4.7.4"
30
+ "@driftengine/entities": "4.8.1"
31
31
  },
32
32
  "devDependencies": {
33
- "@driftengine/core": "4.7.4"
33
+ "@driftengine/core": "4.8.1"
34
34
  },
35
35
  "author": "Drift Technologies",
36
36
  "repository": {
package/src/lockstep.ts CHANGED
@@ -122,6 +122,8 @@ export class LockstepSession<S> {
122
122
  private readonly ourDigests = new Map<number, string>();
123
123
 
124
124
  private statusValue: LockstepStatus = 'running';
125
+ /** The tick of this session's first `submit`, or -1 before it. See `open`. */
126
+ private openedAt = -1;
125
127
  private desyncValue: Desync | null = null;
126
128
  private haltReason = '';
127
129
  private confirmedValue = -1;
@@ -180,6 +182,7 @@ export class LockstepSession<S> {
180
182
  submit(tick: number, payload: Uint8Array): void {
181
183
  if (this.statusValue !== 'running') return;
182
184
  const at = tick + this.inputDelay;
185
+ if (this.openedAt < 0) this.open(tick);
183
186
 
184
187
  if (!this.inputs.holds(at)) {
185
188
  this.halt(`an input for tick ${at} is outside the log's window`);
@@ -203,6 +206,29 @@ export class LockstepSession<S> {
203
206
  this.transport.send(BROADCAST, this.outgoing.subarray(0, length));
204
207
  }
205
208
 
209
+ /**
210
+ * The ticks before anybody's first input, settled as neutral for every participant.
211
+ *
212
+ * **Nobody can publish an input for them**: the earliest a peer publishes is its first tick plus
213
+ * the input delay. So nothing will ever arrive to confirm or correct them, and whatever a peer
214
+ * guesses for them is final. A guess repeats the newest input the log holds, and for this peer's
215
+ * own participant that was the input just submitted for a *later* tick, which no other peer can
216
+ * know: a player holding a control on the first tick split the worlds there, and the first
217
+ * fingerprint halted the session. Neutral is the one answer every peer reaches alone, because
218
+ * nobody had pressed anything before the game began. Lockstep peers start on the same tick, so
219
+ * they all settle the same ticks.
220
+ */
221
+ private open(tick: number): void {
222
+ this.openedAt = tick;
223
+ const neutral = new Uint8Array(this.inputs.inputBytes);
224
+ for (let at = tick; at < tick + this.inputDelay; at++) {
225
+ if (!this.inputs.holds(at)) continue;
226
+ for (let participant = 0; participant < this.participants; participant++) {
227
+ this.inputs.set(participant, at, neutral);
228
+ }
229
+ }
230
+ }
231
+
206
232
  /**
207
233
  * Keep this input, and the few before it, for the next packet to carry.
208
234
  *
package/src/transport.ts CHANGED
@@ -67,3 +67,15 @@ export interface Transport {
67
67
  drain(into: MessageSink): void;
68
68
  close(): void;
69
69
  }
70
+
71
+ /**
72
+ * The message as a view of an `ArrayBuffer`, which is what a socket or a channel will send.
73
+ *
74
+ * Everything this package sends is already one, so this is a check and no copy. A message on
75
+ * shared memory, which a browser refuses to send, is copied off it.
76
+ */
77
+ export function sendable(message: Uint8Array): Uint8Array<ArrayBuffer> {
78
+ return message.buffer instanceof ArrayBuffer
79
+ ? (message as Uint8Array<ArrayBuffer>)
80
+ : message.slice();
81
+ }
package/src/webrtc.ts CHANGED
@@ -36,13 +36,15 @@ import {
36
36
  type PeerId,
37
37
  type Transport,
38
38
  type TransportState,
39
+ sendable,
39
40
  } from './transport.ts';
40
41
 
41
42
  /** What an `RTCDataChannel` satisfies. Declared rather than imported; see `websocket.ts`. */
42
43
  export interface DataChannelLike {
43
44
  readonly readyState: string;
44
45
  binaryType?: string;
45
- send(data: ArrayBufferView | ArrayBuffer): void;
46
+ /** An `ArrayBuffer`'s view, as for a socket: see `SocketLike.send`. */
47
+ send(data: ArrayBufferView<ArrayBuffer> | ArrayBuffer): void;
46
48
  close(): void;
47
49
  addEventListener(type: string, listener: (event: unknown) => void): void;
48
50
  }
@@ -116,7 +118,7 @@ export class WebRtcTransport implements Transport {
116
118
 
117
119
  send(_to: PeerId, message: Uint8Array): void {
118
120
  if (this.state !== 'open') return;
119
- this.channel.send(message);
121
+ this.channel.send(sendable(message));
120
122
  }
121
123
 
122
124
  drain(into: MessageSink): void {
package/src/websocket.ts CHANGED
@@ -29,13 +29,19 @@ import {
29
29
  type PeerId,
30
30
  type Transport,
31
31
  type TransportState,
32
+ sendable,
32
33
  } from './transport.ts';
33
34
 
34
35
  /** What a browser `WebSocket` and a Node `ws` both satisfy. Named here so neither is imported. */
35
36
  export interface SocketLike {
36
37
  readonly readyState: number;
37
38
  binaryType?: string;
38
- send(data: ArrayBufferView | ArrayBuffer): void;
39
+ /**
40
+ * A view of an `ArrayBuffer`, never of shared memory: that is what a browser's `WebSocket.send`
41
+ * takes, and declaring any buffer here made a browser socket not fit its own transport, so
42
+ * `new WebSocketTransport({ socket: new WebSocket(url) })` did not typecheck.
43
+ */
44
+ send(data: ArrayBufferView<ArrayBuffer> | ArrayBuffer): void;
39
45
  close(): void;
40
46
  addEventListener(type: string, listener: (event: unknown) => void): void;
41
47
  }
@@ -108,7 +114,7 @@ export class WebSocketTransport implements Transport {
108
114
  */
109
115
  send(_to: PeerId, message: Uint8Array): void {
110
116
  if (this.state !== 'open') return;
111
- this.socket.send(message);
117
+ this.socket.send(sendable(message));
112
118
  }
113
119
 
114
120
  drain(into: MessageSink): void {