@particle-academy/fancy-connector-core 0.2.0 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,69 @@ All notable changes to `fancy-connector-core` are documented here, in
6
6
  **This package is pre-1.0, so breaking changes land in MINOR releases.** The
7
7
  version number is not a promise it can keep yet; the entries below are.
8
8
 
9
+ ## [0.3.1] - 2026-08-20
10
+
11
+ ### Fixed
12
+
13
+ - **`CallOptions.mode` accepts `"auto"`.** It was typed `ConnectorMode`
14
+ (`"fake" | "sandbox" | "live"`) while the value is handed straight to
15
+ `resolveConnection`, which takes `RequestedMode`, and `resolveConnectorMode`
16
+ gates on `requested && requested !== "auto"`. So `"auto"` was fully handled at
17
+ RUNTIME and rejected by the TYPE.
18
+
19
+ Not a corner case: `"auto"` is the `defaultConfig` of every connector node, so
20
+ the most common value a caller holds was the one it could not pass. The reason
21
+ it stayed hidden is that the workaround — omitting `mode` — is exactly
22
+ equivalent, because `null`, `undefined` and `"auto"` all take the same branch.
23
+ Nothing failed; it just could not be written.
24
+
25
+ **What a consumer must DO:** nothing. This is a widening, so every call that
26
+ compiled before still compiles. If you were omitting `mode` to route around
27
+ it, you can now pass `"auto"` explicitly, which says what you meant.
28
+
29
+ A test now pins the three-way equivalence, so if `null`, `undefined` and
30
+ `"auto"` ever stop meaning the same thing that is a failure rather than a
31
+ connector silently choosing a different estate.
32
+
33
+ Reported by Weaver, found by compiling against the published artifact.
34
+
35
+ ## [0.3.0] - 2026-08-19
36
+
37
+ ### Fixed
38
+
39
+ - **`ECONNRESET` and `EPIPE` are `ambiguous`, not `unreachable`. This was a live
40
+ duplicate-send hazard.** Both were listed as codes proving the request never
41
+ left. They do not. A peer sends RST whenever it tears the connection down,
42
+ *including after it has received and acted on the request but before the
43
+ response comes back*; `EPIPE` is the same story from our side. Node surfaces
44
+ the safe case and the unsafe case as the same code, so the code cannot carry
45
+ the claim.
46
+
47
+ Because `shouldRetry` returns true for `unreachable` **without consulting
48
+ `idempotent`**, a reset produced up to three sends of a connector that had
49
+ explicitly declared `idempotent: false`. For a consumer publishing to social
50
+ networks that is a second public post that cannot be withdrawn.
51
+
52
+ This is the same defect as the 5xx / thrown-transport split fixed earlier, and
53
+ it contradicted this module's own stated rule -- *"an unknown failure treated
54
+ as unreachable would be retried, and the one thing worse than a lost send is
55
+ two sends nobody approved."* The two codes were standing exceptions to a rule
56
+ that should have covered them.
57
+
58
+ **What a consumer must DO:** nothing, if your connectors declare `idempotent`
59
+ honestly -- the fix only removes retries that were never safe. If you have a
60
+ connector marked `idempotent: true` that is not actually safe to repeat, this
61
+ release does not save you; that field is still load-bearing. If you were
62
+ relying on resets being retried for a genuinely idempotent connector, they
63
+ still are: `ambiguous` retries when `idempotent: true`.
64
+
65
+ `ECONNREFUSED`, `ENOTFOUND`, `EAI_AGAIN`, `EHOSTUNREACH` and `ENETUNREACH`
66
+ stay `unreachable` -- they do prove nothing was delivered -- and a test now
67
+ pins them there so the fix cannot overcorrect.
68
+
69
+ Found by a consumer's conformance fixture disagreeing with the core on its
70
+ first run, with a reproduction.
71
+
9
72
  ## [Unreleased]
10
73
 
11
74
  ## [0.2.0] — unreleased
package/dist/index.cjs CHANGED
@@ -158,16 +158,33 @@ function satisfiesMinimum(coreVersion, minimum) {
158
158
 
159
159
  // src/delivery.ts
160
160
  var UNREACHABLE_CODES = /* @__PURE__ */ new Set([
161
+ // Every code here must PROVE the request never reached an application:
162
+ // nothing listening, no route, or a name that never resolved. If a code can
163
+ // also occur after the peer received and acted on the request, it belongs in
164
+ // AMBIGUOUS_CODES -- `shouldRetry` retries `unreachable` without consulting
165
+ // `idempotent`, so a wrong entry here is a duplicate send, not a slow one.
161
166
  "ECONNREFUSED",
162
167
  "ENOTFOUND",
163
168
  "EAI_AGAIN",
164
169
  "EHOSTUNREACH",
165
170
  "ENETUNREACH",
166
- "ECONNRESET",
167
- "EPIPE",
168
171
  "ERR_SOCKET_CONNECTION_TIMEOUT"
169
172
  ]);
170
173
  var AMBIGUOUS_CODES = /* @__PURE__ */ new Set([
174
+ // A peer sends RST whenever it tears the connection down -- INCLUDING after
175
+ // it has received and acted on the request but before the response comes
176
+ // back. EPIPE is the same story from our side: we wrote to a socket the peer
177
+ // had already closed, and whether an earlier complete request was processed
178
+ // first is not knowable from here. Node surfaces the safe case and the unsafe
179
+ // case as the same code, so the code cannot carry the claim.
180
+ //
181
+ // These two sat in UNREACHABLE_CODES until 0.3.0, which made them
182
+ // unconditionally retryable and produced three sends of connectors that had
183
+ // declared `idempotent: false`. Same defect as the 5xx / thrown-transport
184
+ // split this module already fixed once, and a stated exception to a rule that
185
+ // should have covered them.
186
+ "ECONNRESET",
187
+ "EPIPE",
171
188
  "ETIMEDOUT",
172
189
  "UND_ERR_HEADERS_TIMEOUT",
173
190
  "UND_ERR_BODY_TIMEOUT",