sen-ether-client 0.1.3 → 0.1.5

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 (3) hide show
  1. package/lib/client.js +41 -17
  2. package/lib/sen.js +251 -55
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -103,6 +103,25 @@ function resolveInterfaceAddress(value) {
103
103
  return ipv4.address;
104
104
  }
105
105
 
106
+ function multicastInterfaceCandidates(interfaceAddress) {
107
+ if (interfaceAddress) {
108
+ return [interfaceAddress];
109
+ }
110
+ try {
111
+ const addresses = [];
112
+ for (const candidates of Object.values(os.networkInterfaces())) {
113
+ for (const item of candidates ?? []) {
114
+ if ((item.family === 'IPv4' || item.family === 4) && !item.internal && item.address) {
115
+ addresses.push(item.address);
116
+ }
117
+ }
118
+ }
119
+ return [...new Set(addresses)];
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+
106
125
  function normalizeMulticastRange(value) {
107
126
  const ranges = Array.isArray(value) && value.length === 4 ? value : DEFAULT_MULTICAST_RANGE;
108
127
  return ranges.map((range, index) => {
@@ -435,6 +454,15 @@ export class EtherClient extends EventEmitter {
435
454
  return { busName: busState.busName, busId: busState.busId, id, query };
436
455
  }
437
456
 
457
+ #restartInterestForRemote(busState) {
458
+ for (const interest of busState.interests.values()) {
459
+ this.#sendBusControl(busState, busState.participantId, {
460
+ type: 'InterestStarted',
461
+ value: { query: interest.query, id: interest.id }
462
+ });
463
+ }
464
+ }
465
+
438
466
  /**
439
467
  * Stop a previously started interest.
440
468
  *
@@ -711,25 +739,13 @@ export class EtherClient extends EventEmitter {
711
739
 
712
740
  #onMulticastBusDatagram(busState, message, remote) {
713
741
  try {
714
- if (message.length < 8) {
742
+ if (message.length < 9) {
715
743
  throw new RangeError(`SEN multicast bus datagram too small: ${message.length}`);
716
744
  }
717
- const processId = message.readUInt32LE(0);
718
- const payloadSize = message.readUInt32LE(4);
719
- if (processId === this.processInfo.processId) {
745
+ const frame = decodeConfirmedBusFrame(message);
746
+ if (frame.to !== busState.participantId) {
720
747
  return;
721
748
  }
722
- if (payloadSize !== message.length - 8) {
723
- throw new RangeError(
724
- `SEN multicast bus payload size mismatch: expected ${payloadSize}, got ${message.length - 8}`
725
- );
726
- }
727
-
728
- const frame = {
729
- to: busState.participantId,
730
- busId: busState.busId,
731
- message: message.subarray(8)
732
- };
733
749
  const busMessage = decodeBusMessage(frame.message);
734
750
  this.emit('busFrame', { ...frame, busMessage, remote, multicast: true });
735
751
  if (busMessage.categoryName === 'controlMessage') {
@@ -766,7 +782,14 @@ export class EtherClient extends EventEmitter {
766
782
  const onListening = () => {
767
783
  socket.off('error', onError);
768
784
  try {
769
- socket.addMembership(group, this.interfaceAddress);
785
+ const interfaces = multicastInterfaceCandidates(this.interfaceAddress);
786
+ if (interfaces.length) {
787
+ for (const interfaceAddress of interfaces) {
788
+ socket.addMembership(group, interfaceAddress);
789
+ }
790
+ } else {
791
+ socket.addMembership(group);
792
+ }
770
793
  socket.setMulticastLoopback(true);
771
794
  if (this.interfaceAddress) {
772
795
  socket.setMulticastInterface(this.interfaceAddress);
@@ -789,13 +812,14 @@ export class EtherClient extends EventEmitter {
789
812
  return;
790
813
  }
791
814
 
792
- const remoteParticipantId = frame.to;
815
+ const remoteParticipantId = frame.to >>> 0;
793
816
  if (!busState.readyRemoteParticipants.has(remoteParticipantId)) {
794
817
  busState.readyRemoteParticipants.add(remoteParticipantId);
795
818
  this.#sendBusControl(busState, busState.participantId, {
796
819
  type: 'RemoteParticipantReady',
797
820
  value: { id: remoteParticipantId }
798
821
  });
822
+ this.#restartInterestForRemote(busState);
799
823
  this.emit('busParticipantReady', {
800
824
  busName: busState.busName,
801
825
  busId: busState.busId,
package/lib/sen.js CHANGED
@@ -9,6 +9,9 @@ function wait(ms) {
9
9
  return new Promise(resolve => setTimeout(resolve, ms));
10
10
  }
11
11
 
12
+ const STATE_RESYNC_DELAYS_MS = [250, 1000, 3000, 8000];
13
+ const STATE_RESYNC_INTERVAL_MS = 1000;
14
+
12
15
  async function waitForEvent(emitter, event, timeoutMs) {
13
16
  let timeoutId;
14
17
  const timeout = new Promise((_, reject) => {
@@ -156,17 +159,14 @@ function stateRequestKey(interestId, objectId) {
156
159
  }
157
160
 
158
161
  async function waitForSessionBuses(session, timeoutMs) {
159
- let buses = session.listBuses();
160
- if (buses.length || timeoutMs <= 0) {
161
- return buses;
162
+ if (timeoutMs <= 0) {
163
+ return session.listBuses();
162
164
  }
163
-
164
165
  const deadline = Date.now() + timeoutMs;
165
- while (!buses.length && Date.now() < deadline) {
166
+ while (Date.now() < deadline) {
166
167
  await wait(Math.min(50, Math.max(1, deadline - Date.now())));
167
- buses = session.listBuses();
168
168
  }
169
- return buses;
169
+ return session.listBuses();
170
170
  }
171
171
 
172
172
  class ChangeBatcher {
@@ -782,6 +782,13 @@ export class Sen extends EventEmitter {
782
782
  return target;
783
783
  }
784
784
 
785
+ async #reconnectTarget(options) {
786
+ if (options.tcpHub || !options.target) {
787
+ return await this.#discoverTarget(options);
788
+ }
789
+ return options.target;
790
+ }
791
+
785
792
  #sessionNameForBus(busName, options = {}) {
786
793
  const explicit = String(options.session || '').trim();
787
794
  if (explicit) {
@@ -899,7 +906,7 @@ export class Sen extends EventEmitter {
899
906
  }
900
907
 
901
908
  const config = this.connectOptions;
902
- const target = config.target ?? await this.#discoverTarget(config);
909
+ const target = await this.#reconnectTarget(config);
903
910
  if (!target) {
904
911
  throw new Error('no SEN ether process matches the requested filters');
905
912
  }
@@ -1062,6 +1069,8 @@ export class SenBus extends EventEmitter {
1062
1069
  this.typeRegistry = new Map();
1063
1070
  this.requestedTypeHashes = new Set();
1064
1071
  this.stateRequestedObjectIds = new Set();
1072
+ this.stateResyncTimers = new Set();
1073
+ this.stateResyncInterval = undefined;
1065
1074
  this.interests = new Map();
1066
1075
  this.pendingCalls = new Map();
1067
1076
  this.nextTicketId = 1;
@@ -1076,10 +1085,13 @@ export class SenBus extends EventEmitter {
1076
1085
 
1077
1086
  stopInterest(id) {
1078
1087
  const interestId = typeof id === 'object' ? id.id : id;
1079
- this.sen.client.stopInterest(this.name, interestId);
1088
+ this.sen.client?.stopInterest(this.name, interestId);
1080
1089
  const interest = this.interests.get(interestId);
1081
1090
  this.#detachInterestObjects(interestId, interest);
1082
1091
  this.interests.delete(interestId);
1092
+ if (!this.interests.size) {
1093
+ this.#clearStateResyncTimers();
1094
+ }
1083
1095
  interest?.closeLocal();
1084
1096
  interest?.emit('close');
1085
1097
  }
@@ -1097,7 +1109,7 @@ export class SenBus extends EventEmitter {
1097
1109
  }
1098
1110
  }
1099
1111
  try {
1100
- this.sen.client.leaveBus(this.name);
1112
+ this.sen.client?.leaveBus(this.name);
1101
1113
  } catch (error) {
1102
1114
  this.sen.emit('warning', error);
1103
1115
  }
@@ -1117,6 +1129,7 @@ export class SenBus extends EventEmitter {
1117
1129
  this.typeRegistry.clear();
1118
1130
  this.requestedTypeHashes.clear();
1119
1131
  this.stateRequestedObjectIds.clear();
1132
+ this.#clearStateResyncTimers();
1120
1133
  for (const interest of this.interests.values()) {
1121
1134
  interest.closeLocal();
1122
1135
  interest.objectsById.clear();
@@ -1158,8 +1171,25 @@ export class SenBus extends EventEmitter {
1158
1171
  handleObjectsPublished(event) {
1159
1172
  const newTypeHashes = new Set();
1160
1173
  for (const discovery of event.discoveries ?? []) {
1174
+ const interest = this.interests.get(discovery.interestId);
1175
+ if (interest && event.ownerId !== undefined) {
1176
+ if (interest.ownerId !== undefined && interest.ownerId !== event.ownerId) {
1177
+ this.#resetInterestForOwner(interest, event.ownerId);
1178
+ } else {
1179
+ interest.ownerId = event.ownerId;
1180
+ }
1181
+ }
1182
+
1161
1183
  for (const info of discovery.objects ?? []) {
1162
1184
  let object = this.objectsById.get(info.id);
1185
+ if (object && event.ownerId !== undefined && object.ownerId !== event.ownerId) {
1186
+ this.#removeObjectFromAllInterests(object, {
1187
+ reason: 'ownerChanged',
1188
+ ownerId: event.ownerId,
1189
+ previousOwnerId: object.ownerId
1190
+ });
1191
+ object = undefined;
1192
+ }
1163
1193
  const isNewObject = !object;
1164
1194
  if (!object) {
1165
1195
  object = new SenRemoteObject(this, {
@@ -1175,26 +1205,22 @@ export class SenBus extends EventEmitter {
1175
1205
  ownerId: event.ownerId
1176
1206
  });
1177
1207
  }
1178
- const interest = this.interests.get(discovery.interestId);
1179
1208
  interest?.objectsById.set(object.id, object);
1180
1209
  if (info.state?.length) {
1181
- object.applyState(info.state, 'state', info.time, { interestId: discovery.interestId });
1210
+ object.applyState(info.state, 'published', info.time, { interestId: discovery.interestId });
1182
1211
  }
1183
1212
  if (!this.requestedTypeHashes.has(info.typeHash)) {
1184
1213
  this.requestedTypeHashes.add(info.typeHash);
1185
1214
  newTypeHashes.add(info.typeHash);
1186
1215
  }
1187
- interest?.emit('object', object);
1188
- if (isNewObject) {
1189
- this.emit('object', object);
1190
- this.sen.emit('object', object);
1191
- }
1216
+ this.#emitObjectWhenReady(interest, object, isNewObject);
1192
1217
  }
1193
1218
  }
1194
1219
  if (newTypeHashes.size) {
1195
1220
  this.sen.client.requestTypes(this.name, newTypeHashes);
1196
1221
  }
1197
1222
  this.#requestReadyObjectStates();
1223
+ this.#scheduleStateResyncs();
1198
1224
  }
1199
1225
 
1200
1226
  handleObjectsRemoved(event) {
@@ -1202,18 +1228,8 @@ export class SenBus extends EventEmitter {
1202
1228
  for (const id of removal.ids ?? []) {
1203
1229
  const object = this.objectsById.get(id);
1204
1230
  const interest = this.interests.get(removal.interestId);
1205
- interest?.objectsById.delete(id);
1206
- this.stateRequestedObjectIds.delete(stateRequestKey(removal.interestId, id));
1207
1231
  if (object) {
1208
- object.detachInterest(removal.interestId);
1209
- object.emit('remove', { interestId: removal.interestId });
1210
- interest?.emit('remove', object);
1211
- if (object.interestIds.size === 0) {
1212
- this.objectsById.delete(id);
1213
- this.requestedTypeHashes.delete(object.typeHash);
1214
- this.emit('remove', object);
1215
- this.sen.emit('remove', object);
1216
- }
1232
+ this.#removeObjectFromInterest(object, removal.interestId, interest);
1217
1233
  }
1218
1234
  }
1219
1235
  }
@@ -1233,13 +1249,74 @@ export class SenBus extends EventEmitter {
1233
1249
  }
1234
1250
 
1235
1251
  for (const object of interest.objectsById.values()) {
1236
- object.detachInterest(normalizedInterestId);
1237
- if (object.interestIds.size === 0) {
1238
- this.objectsById.delete(object.id);
1239
- this.requestedTypeHashes.delete(object.typeHash);
1240
- }
1252
+ this.#removeObjectFromInterest(object, normalizedInterestId, interest);
1253
+ }
1254
+ interest.objectsById.clear();
1255
+ }
1256
+
1257
+ #resetInterestForOwner(interest, ownerId) {
1258
+ const previousOwnerId = interest.ownerId;
1259
+ const detail = { reason: 'ownerChanged', ownerId, previousOwnerId };
1260
+ for (const object of [...interest.objectsById.values()]) {
1261
+ this.#removeObjectFromAllInterests(object, detail);
1241
1262
  }
1242
1263
  interest.objectsById.clear();
1264
+ interest.ownerId = ownerId;
1265
+ interest.resetLocal();
1266
+ interest.emit('stale', detail);
1267
+ this.emit('stale', { interest, ...detail });
1268
+ this.sen.emit('stale', { bus: this, interest, ...detail });
1269
+ }
1270
+
1271
+ #removeObjectFromAllInterests(object, detail = {}) {
1272
+ for (const interestId of [...object.interestIds]) {
1273
+ this.#removeObjectFromInterest(object, interestId, this.interests.get(interestId), detail);
1274
+ }
1275
+ }
1276
+
1277
+ #removeObjectFromInterest(object, interestId, interest, detail = {}) {
1278
+ const normalizedInterestId = interestId >>> 0;
1279
+ this.stateRequestedObjectIds.delete(stateRequestKey(normalizedInterestId, object.id));
1280
+ interest?.objectsById.delete(object.id);
1281
+ object.detachInterest(normalizedInterestId);
1282
+ object.emit('remove', { interestId: normalizedInterestId, ...detail });
1283
+ interest?.emit('remove', object);
1284
+ if (object.interestIds.size === 0) {
1285
+ this.objectsById.delete(object.id);
1286
+ this.requestedTypeHashes.delete(object.typeHash);
1287
+ this.emit('remove', object);
1288
+ this.sen.emit('remove', object);
1289
+ }
1290
+ }
1291
+
1292
+ #emitObjectWhenReady(interest, object, emitGlobal) {
1293
+ if (!interest) {
1294
+ return;
1295
+ }
1296
+ const publish = () => {
1297
+ if (!object.isReadyForInterest(interest.id)) {
1298
+ return false;
1299
+ }
1300
+ if (object.markInterestObjectEmitted(interest.id)) {
1301
+ interest.emit('object', object);
1302
+ }
1303
+ if (emitGlobal && object.markGlobalObjectEmitted()) {
1304
+ this.emit('object', object);
1305
+ this.sen.emit('object', object);
1306
+ }
1307
+ return true;
1308
+ };
1309
+
1310
+ if (publish()) {
1311
+ return;
1312
+ }
1313
+
1314
+ const onReady = () => {
1315
+ if (publish()) {
1316
+ object.off('ready', onReady);
1317
+ }
1318
+ };
1319
+ object.on('ready', onReady);
1243
1320
  }
1244
1321
 
1245
1322
  handleTypesInfoResponse(event) {
@@ -1267,13 +1344,14 @@ export class SenBus extends EventEmitter {
1267
1344
  }
1268
1345
  this.#retryPendingStates();
1269
1346
  this.#requestReadyObjectStates();
1347
+ this.#scheduleStateResyncs();
1270
1348
  }
1271
1349
 
1272
1350
  handleObjectsStateResponse(event) {
1273
1351
  for (const response of event.responses ?? []) {
1274
1352
  for (const state of response.objectStates ?? []) {
1275
1353
  const object = this.objectsById.get(state.id);
1276
- if (!object) {
1354
+ if (!object || (event.ownerId !== undefined && object.ownerId !== event.ownerId)) {
1277
1355
  continue;
1278
1356
  }
1279
1357
  object.applyState(state.state, 'state', state.timestamp, { interestId: response.interestId });
@@ -1354,7 +1432,8 @@ export class SenBus extends EventEmitter {
1354
1432
  });
1355
1433
  }
1356
1434
 
1357
- #requestReadyObjectStates() {
1435
+ #requestReadyObjectStates(options = {}) {
1436
+ const force = options.force === true;
1358
1437
  const requestsByInterest = new Map();
1359
1438
  for (const interest of this.interests.values()) {
1360
1439
  for (const object of interest.objectsById.values()) {
@@ -1362,7 +1441,7 @@ export class SenBus extends EventEmitter {
1362
1441
  continue;
1363
1442
  }
1364
1443
  const key = stateRequestKey(interest.id, object.id);
1365
- if (this.stateRequestedObjectIds.has(key)) {
1444
+ if (!force && this.stateRequestedObjectIds.has(key)) {
1366
1445
  continue;
1367
1446
  }
1368
1447
  this.stateRequestedObjectIds.add(key);
@@ -1372,22 +1451,64 @@ export class SenBus extends EventEmitter {
1372
1451
  }
1373
1452
  }
1374
1453
 
1375
- if (requestsByInterest.size) {
1376
- this.sen.client.requestObjectStates(this.name, [...requestsByInterest].map(([interestId, objectIds]) => ({
1377
- interestId,
1378
- objectIds
1379
- })));
1454
+ if (requestsByInterest.size && this.sen.client) {
1455
+ try {
1456
+ this.sen.client.requestObjectStates(this.name, [...requestsByInterest].map(([interestId, objectIds]) => ({
1457
+ interestId,
1458
+ objectIds
1459
+ })));
1460
+ } catch (error) {
1461
+ this.sen.emit('warning', error);
1462
+ }
1463
+ }
1464
+ }
1465
+
1466
+ #scheduleStateResyncs() {
1467
+ if (!this.interests.size || this.stateResyncTimers.size) {
1468
+ return;
1469
+ }
1470
+
1471
+ for (const delayMs of STATE_RESYNC_DELAYS_MS) {
1472
+ const timer = setTimeout(() => {
1473
+ this.stateResyncTimers.delete(timer);
1474
+ try {
1475
+ this.#requestReadyObjectStates({ force: true });
1476
+ } catch (error) {
1477
+ this.sen.emit('warning', error);
1478
+ }
1479
+ }, delayMs);
1480
+ timer.unref?.();
1481
+ this.stateResyncTimers.add(timer);
1482
+ }
1483
+ const interval = setInterval(() => {
1484
+ try {
1485
+ this.#requestReadyObjectStates({ force: true });
1486
+ } catch (error) {
1487
+ this.sen.emit('warning', error);
1488
+ }
1489
+ }, STATE_RESYNC_INTERVAL_MS);
1490
+ interval.unref?.();
1491
+ this.stateResyncInterval = interval;
1492
+ this.stateResyncTimers.add(interval);
1493
+ }
1494
+
1495
+ #clearStateResyncTimers() {
1496
+ for (const timer of this.stateResyncTimers) {
1497
+ clearTimeout(timer);
1380
1498
  }
1499
+ this.stateResyncTimers.clear();
1500
+ this.stateResyncInterval = undefined;
1381
1501
  }
1382
1502
 
1383
1503
  #retryPendingStates() {
1384
1504
  for (const object of this.objectsById.values()) {
1385
- if (object.pendingState) {
1505
+ const pendingStates = object.pendingStates.splice(0);
1506
+ for (const pendingState of pendingStates) {
1386
1507
  object.applyState(
1387
- object.pendingState.buffer,
1388
- object.pendingState.source,
1389
- object.pendingState.timestampNs,
1390
- { interestId: object.pendingState.interestId }
1508
+ pendingState.buffer,
1509
+ pendingState.source,
1510
+ pendingState.timestampNs,
1511
+ { interestId: pendingState.interestId }
1391
1512
  );
1392
1513
  }
1393
1514
  }
@@ -1400,6 +1521,7 @@ export class SenInterest extends EventEmitter {
1400
1521
  this.bus = bus;
1401
1522
  this.id = id;
1402
1523
  this.query = query;
1524
+ this.ownerId = undefined;
1403
1525
  this.options = { ...options };
1404
1526
  this.propertyNames = normalizePropertyNames(options.properties ?? options.propertyNames);
1405
1527
  this.changeMode = options.changeMode ?? (options.batch ? 'batch' : 'individual');
@@ -1491,7 +1613,12 @@ export class SenRemoteObject extends EventEmitter {
1491
1613
  }
1492
1614
  this.snapshot = {};
1493
1615
  this.spec = undefined;
1616
+ this.typePromise = undefined;
1494
1617
  this.pendingState = undefined;
1618
+ this.pendingStates = [];
1619
+ this.readyInterestIds = new Set();
1620
+ this.emittedInterestObjectIds = new Set();
1621
+ this.emittedGlobalObject = false;
1495
1622
  this.timestamp = undefined;
1496
1623
  this.timestampNs = undefined;
1497
1624
  this.lastStateTimestamp = undefined;
@@ -1520,7 +1647,10 @@ export class SenRemoteObject extends EventEmitter {
1520
1647
 
1521
1648
  detachInterest(interestId) {
1522
1649
  if (interestId !== undefined) {
1523
- this.interestIds.delete(interestId);
1650
+ const normalizedInterestId = interestId >>> 0;
1651
+ this.interestIds.delete(normalizedInterestId);
1652
+ this.readyInterestIds.delete(normalizedInterestId);
1653
+ this.emittedInterestObjectIds.delete(normalizedInterestId);
1524
1654
  if (this.interestId === interestId) {
1525
1655
  this.interestId = this.interestIds.values().next().value;
1526
1656
  }
@@ -1552,18 +1682,38 @@ export class SenRemoteObject extends EventEmitter {
1552
1682
  }
1553
1683
 
1554
1684
  const timeoutMs = options.timeout ?? 3000;
1555
- return await new Promise((resolve, reject) => {
1556
- const timeout = setTimeout(() => {
1557
- this.off('type', onType);
1558
- reject(new Error(`timeout waiting for SEN type ${this.className}`));
1559
- }, timeoutMs);
1685
+ let timeout;
1686
+ try {
1687
+ return await Promise.race([
1688
+ this.#waitForTypeReady(),
1689
+ new Promise((_, reject) => {
1690
+ timeout = setTimeout(() => {
1691
+ reject(new Error(`timeout waiting for SEN type ${this.className}`));
1692
+ }, timeoutMs);
1693
+ timeout.unref?.();
1694
+ })
1695
+ ]);
1696
+ } finally {
1697
+ clearTimeout(timeout);
1698
+ }
1699
+ }
1700
+
1701
+ #waitForTypeReady() {
1702
+ if (this.spec) {
1703
+ return Promise.resolve(this.spec);
1704
+ }
1705
+ if (this.typePromise) {
1706
+ return this.typePromise;
1707
+ }
1708
+ this.typePromise = new Promise(resolve => {
1560
1709
  const onType = spec => {
1561
- clearTimeout(timeout);
1562
1710
  this.off('type', onType);
1711
+ this.typePromise = undefined;
1563
1712
  resolve(spec);
1564
1713
  };
1565
1714
  this.on('type', onType);
1566
1715
  });
1716
+ return this.typePromise;
1567
1717
  }
1568
1718
 
1569
1719
  async get(name) {
@@ -1574,6 +1724,27 @@ export class SenRemoteObject extends EventEmitter {
1574
1724
  return this.propertyTimestamps.get(name);
1575
1725
  }
1576
1726
 
1727
+ isReadyForInterest(interestId) {
1728
+ return this.readyInterestIds.has(interestId >>> 0);
1729
+ }
1730
+
1731
+ markInterestObjectEmitted(interestId) {
1732
+ const normalizedInterestId = interestId >>> 0;
1733
+ if (this.emittedInterestObjectIds.has(normalizedInterestId)) {
1734
+ return false;
1735
+ }
1736
+ this.emittedInterestObjectIds.add(normalizedInterestId);
1737
+ return true;
1738
+ }
1739
+
1740
+ markGlobalObjectEmitted() {
1741
+ if (this.emittedGlobalObject) {
1742
+ return false;
1743
+ }
1744
+ this.emittedGlobalObject = true;
1745
+ return true;
1746
+ }
1747
+
1577
1748
  async set(name, value, options = {}) {
1578
1749
  await this.waitForType(options);
1579
1750
  const property = this.property(name);
@@ -1612,7 +1783,7 @@ export class SenRemoteObject extends EventEmitter {
1612
1783
  this.#rememberObjectTimestamp(source, timestampNs);
1613
1784
 
1614
1785
  if (!this.spec) {
1615
- this.pendingState = { buffer, source, timestampNs, interestId: options.interestId };
1786
+ this.#queuePendingState({ buffer, source, timestampNs, interestId: options.interestId });
1616
1787
  return;
1617
1788
  }
1618
1789
 
@@ -1652,7 +1823,32 @@ export class SenRemoteObject extends EventEmitter {
1652
1823
  }
1653
1824
  }
1654
1825
 
1655
- this.pendingState = complete ? undefined : { buffer, source, timestampNs, interestId: options.interestId };
1826
+ if (complete) {
1827
+ if (source === 'state') {
1828
+ this.#markReady(options.interestId);
1829
+ }
1830
+ if (!this.pendingStates.length) this.pendingState = undefined;
1831
+ } else {
1832
+ this.#queuePendingState({ buffer, source, timestampNs, interestId: options.interestId });
1833
+ }
1834
+ }
1835
+
1836
+ #markReady(interestId) {
1837
+ if (interestId !== undefined) {
1838
+ const normalizedInterestId = interestId >>> 0;
1839
+ this.readyInterestIds.add(normalizedInterestId);
1840
+ this.emit('ready', { interestId: normalizedInterestId });
1841
+ return;
1842
+ }
1843
+ for (const id of this.interestIds) {
1844
+ this.readyInterestIds.add(id >>> 0);
1845
+ this.emit('ready', { interestId: id >>> 0 });
1846
+ }
1847
+ }
1848
+
1849
+ #queuePendingState(state) {
1850
+ this.pendingStates.push(state);
1851
+ this.pendingState = state;
1656
1852
  }
1657
1853
 
1658
1854
  #targetInterests(interestId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sen-ether-client",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Pure JavaScript SEN client for existing kernels over ether",
5
5
  "senCompatibility": {
6
6
  "kernelProtocolVersion": 9,