broapp 0.2.0 → 0.4.0

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.
@@ -80,6 +80,60 @@ export class PublicError extends Error {
80
80
  }
81
81
  }
82
82
 
83
+ /**
84
+ * True when `error` is a {@link PublicError}, including one from another copy
85
+ * of this module.
86
+ *
87
+ * `instanceof` is not enough, and the reason is architectural rather than
88
+ * pedantic. An Autoapp release bundles its own copy of `broapp`, while the
89
+ * child runtime that supervises it has the copy compiled into the launcher —
90
+ * so a `PublicError` thrown by the gate in one and caught by `runOperation` in
91
+ * the other is a different class object with the same shape. Reducing it to
92
+ * "internal error" would silently turn every deliberate refusal, the preview
93
+ * policy's included, into a mystery.
94
+ *
95
+ * The check is deliberately narrow: the name, and a `code` from the known set.
96
+ * Nothing an untrusted value could set by accident.
97
+ */
98
+ export function isPublicError(error: unknown): error is PublicError {
99
+ // No `instanceof PublicError` fast path. It would be true for one of the two
100
+ // copies and false for the other, and a check whose answer depends on which
101
+ // bundle asked is the bug this function exists to fix — including in the
102
+ // reader's mind, the next time somebody copies this shape.
103
+ if (!(error instanceof Error) || error.name !== 'PublicError') return false;
104
+ const code: unknown = (error as { code?: unknown }).code;
105
+ return typeof code === 'string' && (PUBLIC_CODES as readonly string[]).includes(code);
106
+ }
107
+
108
+ /**
109
+ * True when `error` is a {@link BroappError}, including one from another copy
110
+ * of this module.
111
+ *
112
+ * Same reason as {@link isPublicError}. `fromTransportError` is shared code:
113
+ * the browser calls it inside one bundle, and the Autoapp child runtime calls
114
+ * it on an error that came out of a release's own bundle.
115
+ */
116
+ function isBroappError(error: unknown): error is BroappError {
117
+ if (!(error instanceof Error) || error.name !== 'BroappError') return false;
118
+ const code: unknown = (error as { code?: unknown }).code;
119
+ return typeof code === 'string' && (PUBLIC_CODES as readonly string[]).includes(code);
120
+ }
121
+
122
+ /**
123
+ * True when `error` is Brobridge's cancellation, from any copy of Brobridge.
124
+ *
125
+ * A release bundles its own, so the class identity is not shared. `ErrorCode`
126
+ * is a set of string constants rather than a symbol, which is what makes the
127
+ * value comparison meaningful across copies.
128
+ */
129
+ function isCancelled(error: unknown): boolean {
130
+ return (
131
+ error instanceof Error &&
132
+ error.name === 'BridgeError' &&
133
+ (error as { code?: unknown }).code === ErrorCode.CANCELLED
134
+ );
135
+ }
136
+
83
137
  /** The message every unhandled host failure becomes, on both sides. */
84
138
  export const INTERNAL_ERROR_MESSAGE = 'The application could not complete that operation.';
85
139
 
@@ -105,7 +159,7 @@ export class BroappError extends Error {
105
159
  * for a user to read.
106
160
  */
107
161
  export function fromTransportError(error: unknown): BroappError {
108
- if (error instanceof BroappError) return error;
162
+ if (isBroappError(error)) return error;
109
163
  const message = error instanceof Error ? error.message : '';
110
164
  if (message.startsWith(MARKER)) {
111
165
  const space = message.indexOf(' ');
@@ -118,7 +172,7 @@ export function fromTransportError(error: unknown): BroappError {
118
172
  );
119
173
  }
120
174
  }
121
- if (error instanceof BridgeError && error.code === ErrorCode.CANCELLED) {
175
+ if (isCancelled(error)) {
122
176
  return new BroappError('rejected', 'The operation was cancelled.', error);
123
177
  }
124
178
  return new BroappError('internal', INTERNAL_ERROR_MESSAGE, error);
@@ -8,6 +8,7 @@
8
8
  export {
9
9
  assertNoReservedRoutes,
10
10
  defineContract,
11
+ effectOf,
11
12
  mergeContracts,
12
13
  RESERVED_GROUPS,
13
14
  splitRoute,
@@ -16,6 +17,7 @@ export type {
16
17
  AnyContract,
17
18
  Contract,
18
19
  ContractShape,
20
+ Effect,
19
21
  ShapeOf,
20
22
  OperationInput,
21
23
  OperationName,
@@ -27,7 +29,7 @@ export type {
27
29
  StreamSpec,
28
30
  } from './contract.ts';
29
31
 
30
- export { s, ValidationError } from './schema.ts';
32
+ export { isValidationError, s, ValidationError } from './schema.ts';
31
33
  export type { Infer, InferObject, Issue, JsonSchema, Result, Schema } from './schema.ts';
32
34
 
33
35
  export {
@@ -35,8 +37,11 @@ export {
35
37
  INTERNAL_ERROR_MESSAGE,
36
38
  PublicError,
37
39
  fromTransportError,
40
+ isPublicError,
38
41
  publicError,
39
42
  } from './errors.ts';
40
43
  export type { PublicErrorCode } from './errors.ts';
41
44
 
42
45
  export { encodeEvent, MAX_EVENT_BYTES, NdjsonDecoder } from './ndjson.ts';
46
+
47
+ export { countdown, isUrgent, remainingMs, URGENT_MS } from './countdown.ts';
@@ -35,6 +35,22 @@ export class ValidationError extends Error {
35
35
  }
36
36
  }
37
37
 
38
+ /**
39
+ * True when `error` is a {@link ValidationError}, including one from another
40
+ * copy of this module.
41
+ *
42
+ * `instanceof` is not enough for the same reason it is not enough for
43
+ * `PublicError`: an Autoapp release bundles its own copy of `broapp`, so a
44
+ * schema built in one bundle and parsed by code in another throws a different
45
+ * class object with the same shape. Reducing that to "invalid input" with no
46
+ * message would tell a caller nothing about what was actually wrong.
47
+ */
48
+ export function isValidationError(error: unknown): error is ValidationError {
49
+ return (
50
+ error instanceof Error && error.name === 'ValidationError' && Array.isArray((error as { issues?: unknown }).issues)
51
+ );
52
+ }
53
+
38
54
  function formatPath(path: IssuePath): string {
39
55
  let out = '';
40
56
  for (const segment of path) {