doover-js 0.14.0 → 0.14.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.
@@ -1 +1 @@
1
- {"name":"doover-js","version":"0.14.0","type":"module","sideEffects":false}
1
+ {"name":"doover-js","version":"0.14.1","type":"module","sideEffects":false}
@@ -29,6 +29,13 @@ export class RpcDispatcher {
29
29
  };
30
30
  const channelKey = `${channelRef.agent_id}/${channelRef.name}`;
31
31
  this.acquireChannel(channelKey, channelRef);
32
+ const entry = this.channelRefs.get(channelKey);
33
+ entry.posting += 1;
34
+ const finishPosting = () => {
35
+ entry.posting -= 1;
36
+ if (entry.posting === 0)
37
+ entry.earlyReplies.clear();
38
+ };
32
39
  const startedAt = this.stats?.recordRpcStart() ?? null;
33
40
  this.messages
34
41
  .postMessage(channel.agentId, channel.channelName, {
@@ -66,13 +73,25 @@ export class RpcDispatcher {
66
73
  }, options.timeoutMs);
67
74
  }
68
75
  this.pending.set(message.id, pending);
76
+ // A device can answer before the POST response gives us its ID.
77
+ // Replay the live reply before inspecting the (possibly older) POST
78
+ // snapshot, so an older pending status cannot replace final success.
79
+ const early = entry.earlyReplies.get(message.id);
80
+ entry.earlyReplies.delete(message.id);
81
+ if (early)
82
+ this.route(early);
83
+ if (this.pending.has(message.id) && isRpcMessageData(message.data) &&
84
+ (message.data.status.code === "success" || message.data.status.code === "error")) {
85
+ this.route(message);
86
+ }
69
87
  })
70
88
  .catch((err) => {
71
89
  // postMessage failed before we registered pending — release and reject.
72
90
  this.releaseChannel(channelKey);
73
91
  this.stats?.recordRpcEnd(startedAt, "error");
74
92
  reject(err);
75
- });
93
+ })
94
+ .finally(finishPosting);
76
95
  });
77
96
  }
78
97
  acquireChannel(channelKey, channelRef) {
@@ -85,7 +104,7 @@ export class RpcDispatcher {
85
104
  onMessageUpdate: (msg, requestData) => this.route(msg, requestData),
86
105
  };
87
106
  const unsubscribe = this.gateway.subscribeToChannel(channelRef, handlers);
88
- this.channelRefs.set(channelKey, { unsubscribe, refCount: 1 });
107
+ this.channelRefs.set(channelKey, { unsubscribe, refCount: 1, posting: 0, earlyReplies: new Map() });
89
108
  }
90
109
  releaseChannel(channelKey) {
91
110
  const entry = this.channelRefs.get(channelKey);
@@ -98,11 +117,25 @@ export class RpcDispatcher {
98
117
  }
99
118
  }
100
119
  route(msg, _requestData) {
101
- const pending = this.pending.get(msg.id);
102
- if (!pending)
103
- return;
104
120
  if (!isRpcMessageData(msg.data))
105
121
  return;
122
+ const pending = this.pending.get(msg.id);
123
+ if (!pending) {
124
+ const entry = this.channelRefs.get(`${msg.channel.agent_id}/${msg.channel.name}`);
125
+ if (!entry?.posting)
126
+ return;
127
+ const previous = entry.earlyReplies.get(msg.id);
128
+ if (previous && isRpcMessageData(previous.data) &&
129
+ (previous.data.status.code === "success" || previous.data.status.code === "error"))
130
+ return;
131
+ // Only retain replies during POST registration, bounded even on busy
132
+ // shared channels. Entries are cleared when all outstanding POSTs finish.
133
+ if (!entry.earlyReplies.has(msg.id) && entry.earlyReplies.size >= 256) {
134
+ entry.earlyReplies.delete(entry.earlyReplies.keys().next().value);
135
+ }
136
+ entry.earlyReplies.set(msg.id, msg);
137
+ return;
138
+ }
106
139
  const status = msg.data.status;
107
140
  pending.onStatus?.(status);
108
141
  if (status.code === "success") {
@@ -32,6 +32,13 @@ class RpcDispatcher {
32
32
  };
33
33
  const channelKey = `${channelRef.agent_id}/${channelRef.name}`;
34
34
  this.acquireChannel(channelKey, channelRef);
35
+ const entry = this.channelRefs.get(channelKey);
36
+ entry.posting += 1;
37
+ const finishPosting = () => {
38
+ entry.posting -= 1;
39
+ if (entry.posting === 0)
40
+ entry.earlyReplies.clear();
41
+ };
35
42
  const startedAt = this.stats?.recordRpcStart() ?? null;
36
43
  this.messages
37
44
  .postMessage(channel.agentId, channel.channelName, {
@@ -69,13 +76,25 @@ class RpcDispatcher {
69
76
  }, options.timeoutMs);
70
77
  }
71
78
  this.pending.set(message.id, pending);
79
+ // A device can answer before the POST response gives us its ID.
80
+ // Replay the live reply before inspecting the (possibly older) POST
81
+ // snapshot, so an older pending status cannot replace final success.
82
+ const early = entry.earlyReplies.get(message.id);
83
+ entry.earlyReplies.delete(message.id);
84
+ if (early)
85
+ this.route(early);
86
+ if (this.pending.has(message.id) && isRpcMessageData(message.data) &&
87
+ (message.data.status.code === "success" || message.data.status.code === "error")) {
88
+ this.route(message);
89
+ }
72
90
  })
73
91
  .catch((err) => {
74
92
  // postMessage failed before we registered pending — release and reject.
75
93
  this.releaseChannel(channelKey);
76
94
  this.stats?.recordRpcEnd(startedAt, "error");
77
95
  reject(err);
78
- });
96
+ })
97
+ .finally(finishPosting);
79
98
  });
80
99
  }
81
100
  acquireChannel(channelKey, channelRef) {
@@ -88,7 +107,7 @@ class RpcDispatcher {
88
107
  onMessageUpdate: (msg, requestData) => this.route(msg, requestData),
89
108
  };
90
109
  const unsubscribe = this.gateway.subscribeToChannel(channelRef, handlers);
91
- this.channelRefs.set(channelKey, { unsubscribe, refCount: 1 });
110
+ this.channelRefs.set(channelKey, { unsubscribe, refCount: 1, posting: 0, earlyReplies: new Map() });
92
111
  }
93
112
  releaseChannel(channelKey) {
94
113
  const entry = this.channelRefs.get(channelKey);
@@ -101,11 +120,25 @@ class RpcDispatcher {
101
120
  }
102
121
  }
103
122
  route(msg, _requestData) {
104
- const pending = this.pending.get(msg.id);
105
- if (!pending)
106
- return;
107
123
  if (!isRpcMessageData(msg.data))
108
124
  return;
125
+ const pending = this.pending.get(msg.id);
126
+ if (!pending) {
127
+ const entry = this.channelRefs.get(`${msg.channel.agent_id}/${msg.channel.name}`);
128
+ if (!entry?.posting)
129
+ return;
130
+ const previous = entry.earlyReplies.get(msg.id);
131
+ if (previous && isRpcMessageData(previous.data) &&
132
+ (previous.data.status.code === "success" || previous.data.status.code === "error"))
133
+ return;
134
+ // Only retain replies during POST registration, bounded even on busy
135
+ // shared channels. Entries are cleared when all outstanding POSTs finish.
136
+ if (!entry.earlyReplies.has(msg.id) && entry.earlyReplies.size >= 256) {
137
+ entry.earlyReplies.delete(entry.earlyReplies.keys().next().value);
138
+ }
139
+ entry.earlyReplies.set(msg.id, msg);
140
+ return;
141
+ }
109
142
  const status = msg.data.status;
110
143
  pending.onStatus?.(status);
111
144
  if (status.code === "success") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doover-js",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "TypeScript client for Doover.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",