@visulima/pail 4.0.0-alpha.5 → 4.0.0-alpha.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE.md +6 -6
  3. package/README.md +323 -0
  4. package/dist/error.d.ts +104 -0
  5. package/dist/error.js +76 -0
  6. package/dist/index.browser.d.ts +2 -0
  7. package/dist/index.browser.js +2 -1
  8. package/dist/index.server.d.ts +2 -0
  9. package/dist/index.server.js +6 -4
  10. package/dist/interactive/index.js +1 -1
  11. package/dist/middleware/elysia.d.ts +71 -0
  12. package/dist/middleware/elysia.js +70 -0
  13. package/dist/middleware/express.d.ts +86 -0
  14. package/dist/middleware/express.js +29 -0
  15. package/dist/middleware/fastify.d.ts +81 -0
  16. package/dist/middleware/fastify.js +46 -0
  17. package/dist/middleware/hono.d.ts +85 -0
  18. package/dist/middleware/hono.js +33 -0
  19. package/dist/middleware/next/handler.d.ts +36 -0
  20. package/dist/middleware/next/handler.js +53 -0
  21. package/dist/middleware/next/middleware.d.ts +59 -0
  22. package/dist/middleware/next/storage.d.ts +14 -0
  23. package/dist/middleware/shared/create-middleware-logger.d.ts +82 -0
  24. package/dist/middleware/shared/headers.d.ts +14 -0
  25. package/dist/middleware/shared/routes.d.ts +30 -0
  26. package/dist/middleware/shared/storage.d.ts +29 -0
  27. package/dist/middleware/sveltekit.d.ts +123 -0
  28. package/dist/middleware/sveltekit.js +43 -0
  29. package/dist/packem_shared/{AbstractJsonReporter-DWRpTtGw.js → AbstractJsonReporter-CGKHS8_M.js} +103 -21
  30. package/dist/packem_shared/{AbstractJsonReporter-BaZ33PlE.js → AbstractJsonReporter-DDjDkciI.js} +103 -21
  31. package/dist/packem_shared/{InteractiveManager-CbE7d1kY.js → InteractiveManager-Cd6A14ZK.js} +1 -1
  32. package/dist/packem_shared/{JsonReporter-BV5lMnJX.js → JsonReporter-B3XX8GHN.js} +1 -1
  33. package/dist/packem_shared/{JsonReporter-BRw4skd5.js → JsonReporter-p_BXg6Sj.js} +1 -1
  34. package/dist/packem_shared/{PrettyReporter-DLQtmATi.js → PrettyReporter-CvBn-hxP.js} +5 -4
  35. package/dist/packem_shared/{Spinner-B9JUdsbY.js → Spinner-DIdVcfWq.js} +34 -1
  36. package/dist/packem_shared/{abstract-pretty-reporter-DckLMlGF.js → abstract-pretty-reporter-jU8WL_6c.js} +121 -15
  37. package/dist/packem_shared/createPailError-B11aRfrT.js +76 -0
  38. package/dist/packem_shared/headers-Cp4uLtr4.js +123 -0
  39. package/dist/packem_shared/{index-EZ_WSQZS.js → index-frijFf5m.js} +120 -14
  40. package/dist/packem_shared/pailMiddleware-Ci88geIF.js +24 -0
  41. package/dist/packem_shared/storage-D0vqz8OX.js +36 -0
  42. package/dist/packem_shared/useLogger-D0rU3lcX.js +33 -0
  43. package/dist/processor/environment-processor.d.ts +124 -0
  44. package/dist/processor/environment-processor.js +78 -0
  45. package/dist/processor/message-formatter-processor.d.ts +1 -2
  46. package/dist/processor/sampling-processor.d.ts +111 -0
  47. package/dist/processor/sampling-processor.js +59 -0
  48. package/dist/reporter/file/json-file-reporter.js +1 -1
  49. package/dist/reporter/http/abstract-http-reporter.js +1 -1
  50. package/dist/reporter/http/http-reporter.edge-light.js +103 -21
  51. package/dist/reporter/json/index.browser.js +2 -2
  52. package/dist/reporter/json/index.js +2 -2
  53. package/dist/reporter/pretty/index.js +1 -1
  54. package/dist/reporter/simple/simple-reporter.server.js +4 -3
  55. package/dist/spinner.js +34 -1
  56. package/dist/wide-event.d.ts +300 -0
  57. package/dist/wide-event.js +281 -0
  58. package/package.json +68 -4
@@ -0,0 +1,111 @@
1
+ import type { Meta, Processor } from "../types.d.ts";
2
+ /**
3
+ * Head sampling configuration.
4
+ *
5
+ * Controls random sampling rates per log level. Each key is a log level name
6
+ * and the value is the percentage (0-100) of logs at that level to keep.
7
+ * Levels not listed default to 100 (keep all).
8
+ * @example
9
+ * ```typescript
10
+ * const headSampling: HeadSamplingConfig = {
11
+ * debug: 0, // Drop all debug logs
12
+ * informational: 10, // Keep 10% of info logs
13
+ * warning: 50, // Keep 50% of warning logs
14
+ * error: 100, // Keep all error logs
15
+ * };
16
+ * ```
17
+ */
18
+ type HeadSamplingConfig = Partial<Record<string, number>>;
19
+ /**
20
+ * Tail sampling condition function.
21
+ *
22
+ * A function that receives the log metadata and returns true if the log
23
+ * should be force-kept regardless of head sampling. This allows keeping
24
+ * important logs based on their content (e.g., errors, slow operations).
25
+ * @template L - The log level type
26
+ */
27
+ type TailSamplingCondition<L extends string = string> = (meta: Readonly<Meta<L>>) => boolean;
28
+ /**
29
+ * Sampling processor configuration options.
30
+ */
31
+ interface SamplingProcessorOptions<L extends string = string> {
32
+ /**
33
+ * Head sampling rates per log level.
34
+ *
35
+ * A map of log level to sampling percentage (0-100).
36
+ * Levels not specified default to 100 (keep all).
37
+ * Set to 0 to drop all logs at that level.
38
+ */
39
+ head?: HeadSamplingConfig;
40
+ /**
41
+ * Tail sampling conditions.
42
+ *
43
+ * An array of condition functions that can force-keep a log entry
44
+ * even if it was dropped by head sampling. If any condition returns true,
45
+ * the log is kept.
46
+ */
47
+ tail?: TailSamplingCondition<L>[];
48
+ }
49
+ /**
50
+ * Sampling Processor.
51
+ *
52
+ * Inspired by evlog's production sampling strategy, this processor implements
53
+ * both head sampling (random per-level) and tail sampling (force-keep based
54
+ * on conditions) to control log volume in production environments.
55
+ *
56
+ * **Head sampling** randomly drops a percentage of logs per level. This is
57
+ * evaluated first and provides broad volume control.
58
+ *
59
+ * **Tail sampling** can override head sampling to force-keep important logs
60
+ * based on their content. For example, you might drop 90% of info logs but
61
+ * force-keep any that contain error information or relate to slow operations.
62
+ *
63
+ * When a log is dropped by sampling, the processor sets a `__dropped: true`
64
+ * boolean flag on the meta object. Reporters should check for this flag and
65
+ * skip entries where `__dropped` is `true`.
66
+ * @template L - The log level type
67
+ * @example
68
+ * ```typescript
69
+ * import { createPail } from "@visulima/pail";
70
+ * import SamplingProcessor from "@visulima/pail/processor/sampling";
71
+ *
72
+ * const logger = createPail({
73
+ * processors: [
74
+ * new SamplingProcessor({
75
+ * head: {
76
+ * debug: 0, // Drop all debug logs
77
+ * informational: 10, // Keep 10% of info logs
78
+ * warning: 50, // Keep 50% of warnings
79
+ * error: 100, // Keep all errors
80
+ * },
81
+ * tail: [
82
+ * // Force-keep logs with errors regardless of head sampling
83
+ * (meta) => meta.error !== undefined,
84
+ * // Force-keep logs from critical scopes
85
+ * (meta) => meta.scope?.includes("payment") ?? false,
86
+ * ],
87
+ * }),
88
+ * ],
89
+ * });
90
+ * ```
91
+ */
92
+ declare class SamplingProcessor<L extends string = string> implements Processor<L> {
93
+ #private;
94
+ /**
95
+ * Creates a new SamplingProcessor instance.
96
+ * @param options Sampling configuration options
97
+ */
98
+ constructor(options?: SamplingProcessorOptions<L>);
99
+ /**
100
+ * Processes log metadata to apply sampling rules.
101
+ *
102
+ * First evaluates head sampling (random per-level), then checks tail
103
+ * sampling conditions. If a log is dropped, the meta is marked with
104
+ * `__dropped: true` so reporters can skip it.
105
+ * @param meta The log metadata to process
106
+ * @returns The processed metadata, potentially marked as dropped
107
+ */
108
+ process(meta: Meta<L>): Meta<L>;
109
+ }
110
+ export default SamplingProcessor;
111
+ export type { HeadSamplingConfig, SamplingProcessorOptions, TailSamplingCondition };
@@ -0,0 +1,59 @@
1
+ class SamplingProcessor {
2
+ #headRates;
3
+ #tailConditions;
4
+ /**
5
+ * Creates a new SamplingProcessor instance.
6
+ * @param options Sampling configuration options
7
+ */
8
+ constructor(options = {}) {
9
+ this.#headRates = options.head ?? {};
10
+ this.#tailConditions = options.tail ?? [];
11
+ }
12
+ /**
13
+ * Processes log metadata to apply sampling rules.
14
+ *
15
+ * First evaluates head sampling (random per-level), then checks tail
16
+ * sampling conditions. If a log is dropped, the meta is marked with
17
+ * `__dropped: true` so reporters can skip it.
18
+ * @param meta The log metadata to process
19
+ * @returns The processed metadata, potentially marked as dropped
20
+ */
21
+ process(meta) {
22
+ const level = meta.type.level;
23
+ if (this.#shouldDrop(level) && !this.#shouldForceKeep(meta)) {
24
+ return { ...meta, dropped: true };
25
+ }
26
+ return meta;
27
+ }
28
+ /**
29
+ * Evaluates head sampling for the given log level.
30
+ * @returns true if the log should be dropped
31
+ */
32
+ #shouldDrop(level) {
33
+ const rate = this.#headRates[level];
34
+ if (rate === void 0) {
35
+ return false;
36
+ }
37
+ if (rate <= 0) {
38
+ return true;
39
+ }
40
+ if (rate >= 100) {
41
+ return false;
42
+ }
43
+ return Math.random() * 100 >= rate;
44
+ }
45
+ /**
46
+ * Evaluates tail sampling conditions.
47
+ * @returns true if any condition forces keeping the log
48
+ */
49
+ #shouldForceKeep(meta) {
50
+ for (const condition of this.#tailConditions) {
51
+ if (condition(meta)) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+ }
58
+
59
+ export { SamplingProcessor as default };
@@ -1,4 +1,4 @@
1
- import { AbstractJsonReporter } from '../../packem_shared/AbstractJsonReporter-DWRpTtGw.js';
1
+ import { AbstractJsonReporter } from '../../packem_shared/AbstractJsonReporter-CGKHS8_M.js';
2
2
 
3
3
  class SafeStreamHandler {
4
4
  #ready = true;
@@ -20,7 +20,7 @@ const __cjs_getBuiltinModule = (module) => {
20
20
  const {
21
21
  Buffer
22
22
  } = __cjs_getBuiltinModule("node:buffer");
23
- import { AbstractJsonReporter } from '../../packem_shared/AbstractJsonReporter-DWRpTtGw.js';
23
+ import { AbstractJsonReporter } from '../../packem_shared/AbstractJsonReporter-CGKHS8_M.js';
24
24
  const {
25
25
  gzipSync
26
26
  } = __cjs_getBuiltinModule("node:zlib");
@@ -36,7 +36,7 @@ const ErrorProto = Object.create(
36
36
  {},
37
37
  {
38
38
  cause: {
39
- enumerable: true,
39
+ enumerable: false,
40
40
  value: void 0,
41
41
  writable: true
42
42
  },
@@ -46,32 +46,55 @@ const ErrorProto = Object.create(
46
46
  writable: true
47
47
  },
48
48
  errors: {
49
- enumerable: true,
49
+ enumerable: false,
50
50
  value: void 0,
51
51
  writable: true
52
52
  },
53
53
  message: {
54
- enumerable: true,
54
+ enumerable: false,
55
55
  value: void 0,
56
56
  writable: true
57
57
  },
58
58
  name: {
59
- enumerable: true,
59
+ enumerable: false,
60
60
  value: void 0,
61
61
  writable: true
62
62
  },
63
63
  stack: {
64
- enumerable: true,
64
+ enumerable: false,
65
65
  value: void 0,
66
66
  writable: true
67
67
  }
68
68
  }
69
69
  );
70
70
  const toJsonWasCalled = /* @__PURE__ */ new WeakSet();
71
+ const makePropertiesEnumerable = (object) => {
72
+ if (!object || typeof object !== "object") {
73
+ return;
74
+ }
75
+ const props = Object.getOwnPropertyNames(object);
76
+ for (const prop of props) {
77
+ const descriptor = Object.getOwnPropertyDescriptor(object, prop);
78
+ if (descriptor) {
79
+ if (!descriptor.enumerable) {
80
+ Object.defineProperty(object, prop, {
81
+ ...descriptor,
82
+ enumerable: true
83
+ });
84
+ }
85
+ if (descriptor.value && typeof descriptor.value === "object" && !Array.isArray(descriptor.value) && (Object.getPrototypeOf(descriptor.value) === Object.prototype || Object.getPrototypeOf(descriptor.value) === null)) {
86
+ makePropertiesEnumerable(descriptor.value);
87
+ }
88
+ }
89
+ }
90
+ };
71
91
  const toJSON = (from) => {
72
92
  toJsonWasCalled.add(from);
73
93
  const json = from.toJSON();
74
94
  toJsonWasCalled.delete(from);
95
+ if (json && typeof json === "object" && Object.isExtensible(json)) {
96
+ makePropertiesEnumerable(json);
97
+ }
75
98
  return json;
76
99
  };
77
100
  const serializeValue = (value, seen, depth, options) => {
@@ -82,7 +105,7 @@ const serializeValue = (value, seen, depth, options) => {
82
105
  return "[object Stream]";
83
106
  }
84
107
  if (value instanceof Error) {
85
- if (seen.includes(value)) {
108
+ if (seen.has(value)) {
86
109
  return "[Circular]";
87
110
  }
88
111
  depth += 1;
@@ -97,11 +120,14 @@ const serializeValue = (value, seen, depth, options) => {
97
120
  if (typeof value === "function") {
98
121
  return `[Function: ${value.name || "anonymous"}]`;
99
122
  }
123
+ if (typeof value === "bigint") {
124
+ return `${value}n`;
125
+ }
100
126
  if (isPlainObject(value)) {
101
- depth += 1;
102
- if (options.maxDepth && depth >= options.maxDepth) {
127
+ if (options.maxDepth !== void 0 && options.maxDepth !== Number.POSITIVE_INFINITY && depth + 1 >= options.maxDepth) {
103
128
  return {};
104
129
  }
130
+ depth += 1;
105
131
  const plainObject = {};
106
132
  for (const key in value) {
107
133
  plainObject[key] = serializeValue(value[key], seen, depth, options);
@@ -115,7 +141,7 @@ const serializeValue = (value, seen, depth, options) => {
115
141
  }
116
142
  };
117
143
  const _serialize = (error, options, seen, depth) => {
118
- seen.push(error);
144
+ seen.add(error);
119
145
  if (options.maxDepth === 0) {
120
146
  return {};
121
147
  }
@@ -123,31 +149,87 @@ const _serialize = (error, options, seen, depth) => {
123
149
  return toJSON(error);
124
150
  }
125
151
  const protoError = Object.create(ErrorProto);
126
- protoError.name = Object.prototype.toString.call(error.constructor) === "[object Function]" ? error.constructor.name : error.name;
127
- protoError.message = error.message;
128
- protoError.stack = error.stack;
152
+ Object.defineProperty(protoError, "name", {
153
+ configurable: true,
154
+ enumerable: true,
155
+ value: Object.prototype.toString.call(error.constructor) === "[object Function]" ? error.constructor.name : error.name,
156
+ writable: true
157
+ });
158
+ Object.defineProperty(protoError, "message", {
159
+ configurable: true,
160
+ enumerable: true,
161
+ value: error.message,
162
+ writable: true
163
+ });
164
+ Object.defineProperty(protoError, "stack", {
165
+ configurable: true,
166
+ enumerable: true,
167
+ value: error.stack,
168
+ writable: true
169
+ });
129
170
  if (Array.isArray(error.errors)) {
130
171
  const aggregateErrors = [];
131
172
  for (const aggregateError of error.errors) {
132
173
  if (!(aggregateError instanceof Error)) {
133
174
  throw new TypeError("All errors in the 'errors' property must be instances of Error");
134
175
  }
135
- if (seen.includes(aggregateError)) {
136
- protoError.errors = [];
176
+ if (seen.has(aggregateError)) {
177
+ Object.defineProperty(protoError, "errors", {
178
+ configurable: true,
179
+ enumerable: true,
180
+ value: [],
181
+ writable: true
182
+ });
137
183
  return protoError;
138
184
  }
139
185
  aggregateErrors.push(_serialize(aggregateError, options, seen, depth));
140
186
  }
141
- protoError.errors = aggregateErrors;
187
+ Object.defineProperty(protoError, "errors", {
188
+ configurable: true,
189
+ enumerable: true,
190
+ value: aggregateErrors,
191
+ writable: true
192
+ });
142
193
  }
143
- if (error.cause instanceof Error && !seen.includes(error.cause)) {
144
- protoError.cause = _serialize(error.cause, options, seen, depth);
194
+ if (error.cause !== void 0 && error.cause !== null) {
195
+ if (error.cause instanceof Error) {
196
+ if (seen.has(error.cause)) {
197
+ Object.defineProperty(protoError, "cause", {
198
+ configurable: true,
199
+ enumerable: true,
200
+ value: "[Circular]",
201
+ writable: true
202
+ });
203
+ } else {
204
+ Object.defineProperty(protoError, "cause", {
205
+ configurable: true,
206
+ enumerable: true,
207
+ value: _serialize(error.cause, options, seen, depth),
208
+ writable: true
209
+ });
210
+ }
211
+ } else {
212
+ const serializedCause = serializeValue(error.cause, seen, depth, options);
213
+ Object.defineProperty(protoError, "cause", {
214
+ configurable: true,
215
+ enumerable: true,
216
+ value: serializedCause,
217
+ writable: true
218
+ });
219
+ }
145
220
  }
146
221
  for (const key in error) {
147
- if (protoError[key] === void 0) {
148
- const value = error[key];
149
- protoError[key] = serializeValue(value, seen, depth, options);
222
+ if (key === "name" || key === "message" || key === "stack" || key === "cause" || key === "errors") {
223
+ continue;
150
224
  }
225
+ const value = error[key];
226
+ const serializedValue = serializeValue(value, seen, depth, options);
227
+ Object.defineProperty(protoError, key, {
228
+ configurable: true,
229
+ enumerable: true,
230
+ value: serializedValue,
231
+ writable: true
232
+ });
151
233
  }
152
234
  if (Array.isArray(options.exclude) && options.exclude.length > 0) {
153
235
  for (const key of options.exclude) {
@@ -166,7 +248,7 @@ const serialize = (error, options = {}) => _serialize(
166
248
  maxDepth: options.maxDepth ?? Number.POSITIVE_INFINITY,
167
249
  useToJSON: options.useToJSON ?? false
168
250
  },
169
- [],
251
+ /* @__PURE__ */ new Set(),
170
252
  0
171
253
  );
172
254
 
@@ -1,2 +1,2 @@
1
- export { AbstractJsonReporter } from '../../packem_shared/AbstractJsonReporter-BaZ33PlE.js';
2
- export { default as JsonReporter } from '../../packem_shared/JsonReporter-BRw4skd5.js';
1
+ export { AbstractJsonReporter } from '../../packem_shared/AbstractJsonReporter-DDjDkciI.js';
2
+ export { default as JsonReporter } from '../../packem_shared/JsonReporter-p_BXg6Sj.js';
@@ -1,2 +1,2 @@
1
- export { AbstractJsonReporter } from '../../packem_shared/AbstractJsonReporter-DWRpTtGw.js';
2
- export { default as JsonReporter } from '../../packem_shared/JsonReporter-BV5lMnJX.js';
1
+ export { AbstractJsonReporter } from '../../packem_shared/AbstractJsonReporter-CGKHS8_M.js';
2
+ export { default as JsonReporter } from '../../packem_shared/JsonReporter-B3XX8GHN.js';
@@ -1 +1 @@
1
- export { PrettyReporter } from '../../packem_shared/PrettyReporter-DLQtmATi.js';
1
+ export { PrettyReporter } from '../../packem_shared/PrettyReporter-CvBn-hxP.js';
@@ -8,10 +8,11 @@ const {
8
8
  stdout,
9
9
  stderr
10
10
  } = __cjs_getProcess;
11
- import colorize, { red, greenBright, cyan, green, grey, bold, white, bgGrey, underline } from '@visulima/colorize';
12
- import { A as AbstractPrettyReporter, d as defaultInspectorConfig, b as writeStream, t as terminalSize, c as getLongestBadge, g as getLongestLabel, e as getStringWidth, f as formatLabel, a as EMPTY_SYMBOL, i as inspect, w as wordWrap, r as renderError, W as WrapMode } from '../../packem_shared/abstract-pretty-reporter-DckLMlGF.js';
11
+ import colorize, { red, greenBright, cyan, green, grey, bold, bgGrey, underline, white } from '@visulima/colorize';
12
+ import { A as AbstractPrettyReporter, d as defaultInspectorConfig, b as writeStream, t as terminalSize, c as getLongestBadge, g as getLongestLabel, e as getStringWidth, f as formatLabel, a as EMPTY_SYMBOL, i as inspect, w as wordWrap, r as renderError, W as WrapMode } from '../../packem_shared/abstract-pretty-reporter-jU8WL_6c.js';
13
13
 
14
- const pailFileFilter = (line) => !/[\\/]pail[\\/]dist/.test(line);
14
+ const PAIL_DIST_REGEX = /[\\/]pail[\\/]dist/;
15
+ const pailFileFilter = (line) => !PAIL_DIST_REGEX.test(line);
15
16
  class SimpleReporter extends AbstractPrettyReporter {
16
17
  #stdout;
17
18
  #stderr;
package/dist/spinner.js CHANGED
@@ -1663,6 +1663,38 @@ var dwarfFortress = {
1663
1663
  " ██████£££ "
1664
1664
  ]
1665
1665
  };
1666
+ var fish = {
1667
+ interval: 80,
1668
+ frames: [
1669
+ "~~~~~~~~~~~~~~~~~~~~",
1670
+ "> ~~~~~~~~~~~~~~~~~~",
1671
+ "º> ~~~~~~~~~~~~~~~~~",
1672
+ "(º> ~~~~~~~~~~~~~~~~",
1673
+ "((º> ~~~~~~~~~~~~~~~",
1674
+ "<((º> ~~~~~~~~~~~~~~",
1675
+ "><((º> ~~~~~~~~~~~~~",
1676
+ " ><((º> ~~~~~~~~~~~~",
1677
+ "~ ><((º> ~~~~~~~~~~~",
1678
+ "~~ <>((º> ~~~~~~~~~~",
1679
+ "~~~ ><((º> ~~~~~~~~~",
1680
+ "~~~~ <>((º> ~~~~~~~~",
1681
+ "~~~~~ ><((º> ~~~~~~~",
1682
+ "~~~~~~ <>((º> ~~~~~~",
1683
+ "~~~~~~~ ><((º> ~~~~~",
1684
+ "~~~~~~~~ <>((º> ~~~~",
1685
+ "~~~~~~~~~ ><((º> ~~~",
1686
+ "~~~~~~~~~~ <>((º> ~~",
1687
+ "~~~~~~~~~~~ ><((º> ~",
1688
+ "~~~~~~~~~~~~ <>((º> ",
1689
+ "~~~~~~~~~~~~~ ><((º>",
1690
+ "~~~~~~~~~~~~~~ <>((º",
1691
+ "~~~~~~~~~~~~~~~ ><((",
1692
+ "~~~~~~~~~~~~~~~~ <>(",
1693
+ "~~~~~~~~~~~~~~~~~ ><",
1694
+ "~~~~~~~~~~~~~~~~~~ <",
1695
+ "~~~~~~~~~~~~~~~~~~~~"
1696
+ ]
1697
+ };
1666
1698
  const spinners = {
1667
1699
  dots: dots,
1668
1700
  dots2: dots2,
@@ -1752,7 +1784,8 @@ const spinners = {
1752
1784
  orangeBluePulse: orangeBluePulse,
1753
1785
  timeTravel: timeTravel,
1754
1786
  aesthetic: aesthetic,
1755
- dwarfFortress: dwarfFortress
1787
+ dwarfFortress: dwarfFortress,
1788
+ fish: fish
1756
1789
  };
1757
1790
 
1758
1791
  const DEFAULT_ICONS = {