@c9up/transit 0.1.6 → 0.1.7

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.
@@ -31,23 +31,62 @@ interface ConnectionSource {
31
31
  * `"connection" in manager` is FALSE while reading it returns the function —
32
32
  * gating on `in` rejected the real manager. And a module namespace under a
33
33
  * test double RAISES for an export it does not have, rather than answering
34
- * undefined, so a bare read fails on the mock. Reading through a catch is what
35
- * satisfies both.
34
+ * undefined, so a bare read fails on the mock.
35
+ *
36
+ * So the read is caught, but what it raised is kept: on quasar's accessor a
37
+ * throw is not an absence, it is the accessor saying its provider has not
38
+ * booted, and dropping that turned it into a wrong diagnosis.
36
39
  */
37
- function readMember(value: unknown, name: string): unknown {
38
- if (typeof value !== "object" || value === null) return undefined;
40
+ type MemberRead = { value: unknown } | { threw: unknown };
41
+
42
+ function readMember(value: unknown, name: string): MemberRead {
43
+ if (typeof value !== "object" || value === null) return { value: undefined };
39
44
  try {
40
- return Reflect.get(value, name);
41
- } catch {
42
- return undefined;
45
+ return { value: Reflect.get(value, name) };
46
+ } catch (threw) {
47
+ return { threw };
43
48
  }
44
49
  }
45
50
 
51
+ function memberValue(read: MemberRead): unknown {
52
+ return "value" in read ? read.value : undefined;
53
+ }
54
+
46
55
  function isConnectionSource(value: unknown): value is ConnectionSource {
47
- return typeof readMember(value, "connection") === "function";
56
+ return typeof memberValue(readMember(value, "connection")) === "function";
57
+ }
58
+
59
+ /**
60
+ * The commands among `commands` that `value` does not carry as functions.
61
+ *
62
+ * Reads go through the same catch as above, so a command whose read throws
63
+ * counts as missing.
64
+ */
65
+ export function missingCommands(
66
+ value: unknown,
67
+ commands: readonly string[],
68
+ ): string[] {
69
+ return commands.filter(
70
+ (command) => typeof memberValue(readMember(value, command)) !== "function",
71
+ );
48
72
  }
49
73
 
50
- export interface QuasarConnectionRequest {
74
+ /**
75
+ * Whether `value` carries every one of `commands` as a function.
76
+ *
77
+ * The building block of the guard a caller passes as `isConnection`, over the
78
+ * same list it passes as `required` — which is what names the gap when the
79
+ * guard says no.
80
+ */
81
+ export function hasCommands(
82
+ value: unknown,
83
+ commands: readonly string[],
84
+ ): value is object {
85
+ if (typeof value !== "object" || value === null) return false;
86
+ return missingCommands(value, commands).length === 0;
87
+ }
88
+
89
+ export interface QuasarConnectionRequest<T> {
51
90
  /** The package asking. It prefixes every message this can raise. */
52
91
  pkg: string;
53
92
  /**
@@ -65,8 +104,16 @@ export interface QuasarConnectionRequest {
65
104
  ) => Error;
66
105
  /** Connection name, or `undefined` for quasar's default. */
67
106
  name?: string;
68
- /** The commands the caller will actually issue. */
107
+ /** The commands the caller will actually issue, named in the message. */
69
108
  required: readonly string[];
109
+ /**
110
+ * The caller's check that the connection is the client it needs.
111
+ *
112
+ * Written by the caller over the commands it issues (`hasCommands` with
113
+ * `required` does it), because only the caller knows its client type. The
114
+ * connection is handed over only once this has said yes.
115
+ */
116
+ isConnection: (value: unknown) => value is T;
70
117
  /** What the caller does with them, for the message that names a gap. */
71
118
  what: string;
72
119
  /**
@@ -86,9 +133,10 @@ export interface QuasarConnectionRequest {
86
133
  * command, far from the line that asked for it.
87
134
  */
88
135
  export async function quasarConnection<T>(
89
- request: QuasarConnectionRequest,
136
+ request: QuasarConnectionRequest<T>,
90
137
  ): Promise<T> {
91
- const { pkg, name, required, what, raise, alternative } = request;
138
+ const { pkg, name, required, what, raise, alternative, isConnection } =
139
+ request;
92
140
  const specifier = "@c9up/quasar/services/main";
93
141
  let loaded: unknown;
94
142
  try {
@@ -102,9 +150,22 @@ export async function quasarConnection<T>(
102
150
  );
103
151
  }
104
152
 
105
- const manager = isConnectionSource(loaded)
106
- ? loaded
107
- : Reflect.get(Object(loaded), "default");
153
+ // The namespace itself may be the manager (a test double); a throw there is
154
+ // the mock refusing an export it lacks, not an answer.
155
+ const direct = memberValue(readMember(loaded, "connection"));
156
+ const manager =
157
+ typeof direct === "function"
158
+ ? loaded
159
+ : memberValue(readMember(Object(loaded), "default"));
160
+ const read = readMember(manager, "connection");
161
+ if ("threw" in read) {
162
+ throw raise(
163
+ "quasar-missing",
164
+ `[${pkg}] ${what} asks for the quasar connection "${name ?? "default"}", but @c9up/quasar's manager is not available yet: ` +
165
+ "its provider has not booted, or is not registered in this application's providers.",
166
+ read.threw,
167
+ );
168
+ }
108
169
  if (!isConnectionSource(manager)) {
109
170
  throw raise(
110
171
  "quasar-missing",
@@ -113,17 +174,18 @@ export async function quasarConnection<T>(
113
174
  }
114
175
 
115
176
  const connection = manager.connection(name);
116
- const missing = required.filter(
117
- (command) => typeof readMember(connection, command) !== "function",
118
- );
177
+ const missing = missingCommands(connection, required);
119
178
  if (missing.length > 0) {
120
179
  throw raise(
121
180
  "incomplete-connection",
122
181
  `[${pkg}] the quasar connection${name ? ` '${name}'` : ""} is missing ${missing.join(", ")}, which ${what} issues`,
123
182
  );
124
183
  }
125
- // Load-bearing: every member of `required` has just been checked to be a
126
- // function on this object. The caller names the type it needs because only
127
- // the caller knows which commands it asked for.
128
- return connection as T;
184
+ if (!isConnection(connection)) {
185
+ throw raise(
186
+ "incomplete-connection",
187
+ `[${pkg}] the quasar connection${name ? ` '${name}'` : ""} is not the client ${what} needs`,
188
+ );
189
+ }
190
+ return connection;
129
191
  }