rsbuild-plugin-react-router 0.3.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.
Files changed (56) hide show
  1. package/README.md +55 -24
  2. package/dist/451.js +36 -20
  3. package/dist/build-manifest.d.ts +6 -3
  4. package/dist/concurrency.d.ts +2 -1
  5. package/dist/config-imports.d.ts +7 -0
  6. package/dist/dev-background-resources.d.ts +38 -0
  7. package/dist/dev-generation.d.ts +2 -2
  8. package/dist/dev-runtime-artifacts.d.ts +9 -1
  9. package/dist/dev-runtime-compilation.d.ts +17 -2
  10. package/dist/dev-server.d.ts +5 -0
  11. package/dist/effect-runtime.d.ts +18 -0
  12. package/dist/export-utils.d.ts +2 -2
  13. package/dist/index.cjs +11448 -891
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +7950 -758
  16. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  17. package/dist/lazy-compilation.d.ts +2 -1
  18. package/dist/manifest.d.ts +19 -4
  19. package/dist/parallel-route-transforms.d.ts +17 -2
  20. package/dist/prerender-build.d.ts +4 -0
  21. package/dist/prerender.d.ts +0 -1
  22. package/dist/react-router-config.d.ts +8 -5
  23. package/dist/server-build-resolution.d.ts +3 -0
  24. package/dist/ssr-externals.d.ts +1 -0
  25. package/dist/typegen.d.ts +14 -1
  26. package/dist/types.d.ts +15 -6
  27. package/package.json +11 -9
  28. package/src/build-manifest.ts +110 -73
  29. package/src/concurrency.ts +3 -22
  30. package/src/config-imports.ts +38 -0
  31. package/src/dev-background-resources.ts +255 -0
  32. package/src/dev-generation.ts +43 -53
  33. package/src/dev-runtime-artifacts.ts +50 -18
  34. package/src/dev-runtime-compilation.ts +80 -1
  35. package/src/dev-runtime-controller.ts +111 -31
  36. package/src/dev-runtime-session.ts +18 -11
  37. package/src/dev-server.ts +31 -1
  38. package/src/effect-runtime.ts +130 -0
  39. package/src/export-utils.ts +82 -23
  40. package/src/index.ts +118 -153
  41. package/src/lazy-compilation-prewarm.ts +279 -0
  42. package/src/lazy-compilation.ts +12 -5
  43. package/src/manifest.ts +366 -255
  44. package/src/modify-browser-manifest.ts +2 -1
  45. package/src/parallel-route-transforms.ts +195 -69
  46. package/src/prerender-build.ts +125 -61
  47. package/src/prerender.ts +0 -18
  48. package/src/react-router-config.ts +98 -73
  49. package/src/route-artifacts.ts +2 -2
  50. package/src/route-export-resolution.ts +3 -3
  51. package/src/route-watch.ts +119 -84
  52. package/src/server-build-resolution.ts +131 -0
  53. package/src/server-utils.ts +7 -106
  54. package/src/ssr-externals.ts +1 -1
  55. package/src/typegen.ts +162 -33
  56. package/src/types.ts +16 -6
@@ -1,6 +1,8 @@
1
1
  import type { Rspack } from '@rsbuild/core';
2
2
  import type {
3
+ DevCompileAttemptIdentity,
3
4
  DevCompilationIdentity,
5
+ DevRuntimeStats,
4
6
  DevGraphChanges,
5
7
  DevGraphIdentity,
6
8
  } from './dev-runtime-artifacts.js';
@@ -10,13 +12,16 @@ export type DevCompilerPair = {
10
12
  node: Rspack.Compiler;
11
13
  settledCompilations: WeakSet<Rspack.Compilation>;
12
14
  pendingAttempt?: PendingDevCompilation;
15
+ currentAttemptIdentity?: DevCompileAttemptIdentity;
13
16
  latestCompletedWebIdentity?: DevCompilationIdentity;
17
+ latestCompletedWebStats?: Rspack.Stats;
18
+ latestCompletedNodeStats?: Rspack.Stats;
14
19
  latestWebStart?: CompilationStart;
15
20
  latestNodeStart?: CompilationStart;
16
21
  };
17
22
 
18
23
  export type PendingDevCompilation = {
19
- stats: Rspack.Stats | Rspack.MultiStats;
24
+ stats: DevRuntimeStats;
20
25
  changes: DevGraphChanges;
21
26
  identity: DevGraphIdentity;
22
27
  webCompilation: Rspack.Compilation;
@@ -27,6 +32,30 @@ export type CompilationStart =
27
32
  | { status: 'pending' }
28
33
  | { status: 'started'; identity: DevCompilationIdentity };
29
34
 
35
+ type CompilerPairStartSide = 'latestWebStart' | 'latestNodeStart';
36
+
37
+ export const createDevCompilerPair = ({
38
+ web,
39
+ node,
40
+ }: {
41
+ web: Rspack.Compiler;
42
+ node: Rspack.Compiler;
43
+ }): DevCompilerPair => ({
44
+ web,
45
+ node,
46
+ settledCompilations: new WeakSet(),
47
+ });
48
+
49
+ export const resetDevCompilerPair = (pair: DevCompilerPair): void => {
50
+ pair.pendingAttempt = undefined;
51
+ pair.currentAttemptIdentity = undefined;
52
+ pair.latestCompletedWebIdentity = undefined;
53
+ pair.latestCompletedWebStats = undefined;
54
+ pair.latestCompletedNodeStats = undefined;
55
+ pair.latestWebStart = undefined;
56
+ pair.latestNodeStart = undefined;
57
+ };
58
+
30
59
  export const isLatestStartedCompilation = (
31
60
  identity: DevCompilationIdentity | undefined,
32
61
  start: CompilationStart | undefined
@@ -37,6 +66,32 @@ export const hasPendingCompilation = (pair: DevCompilerPair): boolean =>
37
66
  pair.latestWebStart?.status === 'pending' ||
38
67
  pair.latestNodeStart?.status === 'pending';
39
68
 
69
+ export const beginDevCompilerAttempt = (pair: DevCompilerPair): void => {
70
+ pair.pendingAttempt = undefined;
71
+ pair.currentAttemptIdentity = Symbol();
72
+ };
73
+
74
+ export const markDevCompilerPending = (
75
+ pair: DevCompilerPair,
76
+ side: CompilerPairStartSide
77
+ ): boolean => {
78
+ const attemptAlreadyPending = hasPendingCompilation(pair);
79
+ pair[side] = { status: 'pending' };
80
+ pair.pendingAttempt = undefined;
81
+ if (!attemptAlreadyPending) {
82
+ pair.currentAttemptIdentity = Symbol();
83
+ }
84
+ return !attemptAlreadyPending;
85
+ };
86
+
87
+ export const clearDevCompilerStart = (
88
+ pair: DevCompilerPair,
89
+ side: CompilerPairStartSide
90
+ ): void => {
91
+ pair[side] = undefined;
92
+ pair.pendingAttempt = undefined;
93
+ };
94
+
40
95
  export type CompilationIdentityTracker = {
41
96
  getCompilationIdentity(
42
97
  compilation: Rspack.Compilation
@@ -44,6 +99,13 @@ export type CompilationIdentityTracker = {
44
99
  getWebIdentityForNodeCompilation(
45
100
  compilation: Rspack.Compilation
46
101
  ): DevCompilationIdentity | undefined;
102
+ getAttemptIdentityForCompilation(
103
+ compilation: Rspack.Compilation
104
+ ): DevCompileAttemptIdentity | undefined;
105
+ setAttemptIdentityForCompilation(
106
+ compilation: Rspack.Compilation,
107
+ identity: DevCompileAttemptIdentity
108
+ ): void;
47
109
  setWebIdentityForNodeCompilation(
48
110
  compilation: Rspack.Compilation,
49
111
  identity: DevCompilationIdentity
@@ -60,6 +122,10 @@ export const createCompilationIdentityTracker =
60
122
  Rspack.Compilation,
61
123
  DevCompilationIdentity
62
124
  >();
125
+ const attemptIdentityByCompilation = new WeakMap<
126
+ Rspack.Compilation,
127
+ DevCompileAttemptIdentity
128
+ >();
63
129
 
64
130
  return {
65
131
  getCompilationIdentity(
@@ -82,6 +148,19 @@ export const createCompilationIdentityTracker =
82
148
  return webIdentityByNodeCompilation.get(compilation);
83
149
  },
84
150
 
151
+ getAttemptIdentityForCompilation(
152
+ compilation: Rspack.Compilation
153
+ ): DevCompileAttemptIdentity | undefined {
154
+ return attemptIdentityByCompilation.get(compilation);
155
+ },
156
+
157
+ setAttemptIdentityForCompilation(
158
+ compilation: Rspack.Compilation,
159
+ identity: DevCompileAttemptIdentity
160
+ ): void {
161
+ attemptIdentityByCompilation.set(compilation, identity);
162
+ },
163
+
85
164
  setWebIdentityForNodeCompilation(
86
165
  compilation: Rspack.Compilation,
87
166
  identity: DevCompilationIdentity
@@ -1,10 +1,16 @@
1
1
  import type { RsbuildConfig, RsbuildPluginAPI, Rspack } from '@rsbuild/core';
2
+ import * as Effect from 'effect/Effect';
2
3
  import type { ServerBuild } from 'react-router';
3
4
  import { PLUGIN_NAME } from './constants.js';
4
5
  import {
6
+ beginDevCompilerAttempt,
7
+ clearDevCompilerStart,
5
8
  createCompilationIdentityTracker,
9
+ createDevCompilerPair,
6
10
  hasPendingCompilation,
7
11
  isLatestStartedCompilation,
12
+ markDevCompilerPending,
13
+ resetDevCompilerPair,
8
14
  type DevCompilerPair,
9
15
  } from './dev-runtime-compilation.js';
10
16
  import {
@@ -16,6 +22,8 @@ import {
16
22
  import {
17
23
  getEnvironmentStats,
18
24
  snapshotDevChangedFiles,
25
+ type DevGraphIdentity,
26
+ type DevRuntimeStats,
19
27
  type ReactRouterDevBuildPlan,
20
28
  type ReactRouterDevManifestSet,
21
29
  } from './dev-runtime-artifacts.js';
@@ -23,6 +31,12 @@ import {
23
31
  createDevRuntimeSessionManager,
24
32
  type RuntimeBinding,
25
33
  } from './dev-runtime-session.js';
34
+ import {
35
+ normalizeEffectError,
36
+ runPluginEffect,
37
+ tryPluginPromise,
38
+ tryPluginSync,
39
+ } from './effect-runtime.js';
26
40
 
27
41
  type ServerSetup = Exclude<
28
42
  NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
@@ -107,10 +121,7 @@ export const createReactRouterDevRuntimeController = ({
107
121
  reloadAfterCssAssetOwnershipRemoval = false;
108
122
  const pair = binding.compilers;
109
123
  if (pair) {
110
- pair.pendingAttempt = undefined;
111
- pair.latestCompletedWebIdentity = undefined;
112
- pair.latestWebStart = undefined;
113
- pair.latestNodeStart = undefined;
124
+ resetDevCompilerPair(pair);
114
125
  }
115
126
  binding.compilers = undefined;
116
127
  binding.runtime.close(error);
@@ -121,6 +132,46 @@ export const createReactRouterDevRuntimeController = ({
121
132
  const compilationIdentities = createCompilationIdentityTracker();
122
133
  const { getCompilationIdentity } = compilationIdentities;
123
134
 
135
+ const finishRuntimeAttemptEffect = (
136
+ binding: RuntimeBinding,
137
+ pair: DevCompilerPair,
138
+ stats: DevRuntimeStats,
139
+ changes: Parameters<RuntimeBinding['runtime']['finishAttempt']>[1],
140
+ identity: Parameters<RuntimeBinding['runtime']['finishAttempt']>[2]
141
+ ): Effect.Effect<void, Error, never> =>
142
+ tryPluginPromise(() =>
143
+ binding.runtime.finishAttempt(stats, changes, identity)
144
+ ).pipe(
145
+ Effect.flatMap(result =>
146
+ tryPluginSync(() => {
147
+ if (
148
+ result === 'retry-node' &&
149
+ sessions.getActiveBinding()?.id === binding.id
150
+ ) {
151
+ pair.node.watching?.invalidate();
152
+ }
153
+ })
154
+ ),
155
+ Effect.catchAll(cause =>
156
+ tryPluginSync(() => {
157
+ if (sessions.getActiveBinding()?.id === binding.id) {
158
+ binding.runtime.failAttempt(normalizeEffectError(cause));
159
+ }
160
+ })
161
+ )
162
+ );
163
+
164
+ const finishRuntimeAttempt = (
165
+ binding: RuntimeBinding,
166
+ pair: DevCompilerPair,
167
+ stats: DevRuntimeStats,
168
+ changes: Parameters<RuntimeBinding['runtime']['finishAttempt']>[1],
169
+ identity: Parameters<RuntimeBinding['runtime']['finishAttempt']>[2]
170
+ ): Promise<void> =>
171
+ runPluginEffect(
172
+ finishRuntimeAttemptEffect(binding, pair, stats, changes, identity)
173
+ );
174
+
124
175
  const flushSettledAttempt = (
125
176
  binding: RuntimeBinding,
126
177
  pair: DevCompilerPair
@@ -141,15 +192,13 @@ export const createReactRouterDevRuntimeController = ({
141
192
  ) {
142
193
  return;
143
194
  }
144
- void binding.runtime
145
- .finishAttempt(pending.stats, pending.changes, pending.identity)
146
- .catch(cause => {
147
- if (sessions.getActiveBinding()?.id === binding.id) {
148
- binding.runtime.failAttempt(
149
- cause instanceof Error ? cause : new Error(String(cause))
150
- );
151
- }
152
- });
195
+ void finishRuntimeAttempt(
196
+ binding,
197
+ pair,
198
+ pending.stats,
199
+ pending.changes,
200
+ pending.identity
201
+ );
153
202
  };
154
203
 
155
204
  const rejectUnsupportedCompiler = (reason: string): void => {
@@ -247,7 +296,7 @@ export const createReactRouterDevRuntimeController = ({
247
296
  if (!binding || !pair || hasPendingCompilation(pair)) {
248
297
  return;
249
298
  }
250
- pair.pendingAttempt = undefined;
299
+ beginDevCompilerAttempt(pair);
251
300
  binding.runtime.beginAttempt();
252
301
  },
253
302
  });
@@ -267,22 +316,17 @@ export const createReactRouterDevRuntimeController = ({
267
316
  if (!binding) {
268
317
  return;
269
318
  }
270
- const pair: DevCompilerPair = {
271
- web,
272
- node,
273
- settledCompilations: new WeakSet(),
274
- };
319
+ const pair: DevCompilerPair = createDevCompilerPair({ web, node });
275
320
  binding.compilers = pair;
276
321
  const sessionId = binding.id;
277
322
  const runtime = binding.runtime;
278
323
  const failCurrentAttempt = (side: 'web' | 'node', error: Error): void => {
279
324
  if (sessions.getActiveBinding()?.id === sessionId) {
280
325
  if (side === 'web') {
281
- pair.latestWebStart = undefined;
326
+ clearDevCompilerStart(pair, 'latestWebStart');
282
327
  } else {
283
- pair.latestNodeStart = undefined;
328
+ clearDevCompilerStart(pair, 'latestNodeStart');
284
329
  }
285
- pair.pendingAttempt = undefined;
286
330
  runtime.failAttempt(error);
287
331
  }
288
332
  };
@@ -293,12 +337,9 @@ export const createReactRouterDevRuntimeController = ({
293
337
  sessions.getActiveBinding()?.id === sessionId &&
294
338
  pair[side]?.status !== 'pending'
295
339
  ) {
296
- const attemptAlreadyPending = hasPendingCompilation(pair);
297
340
  // Invalidation can arrive before the aggregate before-compile hook.
298
341
  // Supersede any evaluation that could resolve in that gap immediately.
299
- pair[side] = { status: 'pending' };
300
- pair.pendingAttempt = undefined;
301
- if (!attemptAlreadyPending) {
342
+ if (markDevCompilerPending(pair, side)) {
302
343
  runtime.beginAttempt();
303
344
  }
304
345
  if (side === 'latestWebStart' && reloadAfterCssAssetOwnershipRemoval) {
@@ -322,12 +363,27 @@ export const createReactRouterDevRuntimeController = ({
322
363
  pair.latestCompletedWebIdentity = getCompilationIdentity(
323
364
  stats.compilation
324
365
  );
366
+ pair.latestCompletedWebStats = stats;
367
+ }
368
+ );
369
+ node.hooks.done.tap(
370
+ { name: `${PLUGIN_NAME}:dev-node-complete`, stage: -1000 },
371
+ stats => {
372
+ if (sessions.getActiveBinding()?.id === sessionId) {
373
+ pair.latestCompletedNodeStats = stats;
374
+ }
325
375
  }
326
376
  );
327
377
  web.hooks.thisCompilation.tap(
328
378
  `${PLUGIN_NAME}:dev-web-compilation`,
329
379
  compilation => {
330
380
  if (sessions.getActiveBinding()?.id === sessionId) {
381
+ if (pair.currentAttemptIdentity) {
382
+ compilationIdentities.setAttemptIdentityForCompilation(
383
+ compilation,
384
+ pair.currentAttemptIdentity
385
+ );
386
+ }
331
387
  pair.latestWebStart = {
332
388
  status: 'started',
333
389
  identity: getCompilationIdentity(compilation),
@@ -349,6 +405,12 @@ export const createReactRouterDevRuntimeController = ({
349
405
  status: 'started',
350
406
  identity: getCompilationIdentity(compilation),
351
407
  };
408
+ if (pair.currentAttemptIdentity) {
409
+ compilationIdentities.setAttemptIdentityForCompilation(
410
+ compilation,
411
+ pair.currentAttemptIdentity
412
+ );
413
+ }
352
414
  if (pair.latestCompletedWebIdentity) {
353
415
  compilationIdentities.setWebIdentityForNodeCompilation(
354
416
  compilation,
@@ -386,8 +448,10 @@ export const createReactRouterDevRuntimeController = ({
386
448
  if (!binding || !pair) {
387
449
  return;
388
450
  }
389
- const webStats = getEnvironmentStats(stats, 'web');
390
- const nodeStats = getEnvironmentStats(stats, 'node');
451
+ const webStats =
452
+ getEnvironmentStats(stats, 'web') ?? pair.latestCompletedWebStats;
453
+ const nodeStats =
454
+ getEnvironmentStats(stats, 'node') ?? pair.latestCompletedNodeStats;
391
455
  if (
392
456
  (webStats && webStats.compilation.compiler !== pair.web) ||
393
457
  (nodeStats && nodeStats.compilation.compiler !== pair.node)
@@ -410,7 +474,17 @@ export const createReactRouterDevRuntimeController = ({
410
474
  web: snapshotDevChangedFiles(pair.web),
411
475
  node: snapshotDevChangedFiles(pair.node),
412
476
  };
413
- const identity = {
477
+ const webAttempt = webStats
478
+ ? compilationIdentities.getAttemptIdentityForCompilation(
479
+ webStats.compilation
480
+ )
481
+ : undefined;
482
+ const nodeAttempt = nodeStats
483
+ ? compilationIdentities.getAttemptIdentityForCompilation(
484
+ nodeStats.compilation
485
+ )
486
+ : undefined;
487
+ const identity: DevGraphIdentity = {
414
488
  web: webIdentity,
415
489
  node: nodeIdentity,
416
490
  nodeWeb: nodeStats
@@ -418,13 +492,19 @@ export const createReactRouterDevRuntimeController = ({
418
492
  nodeStats.compilation
419
493
  )
420
494
  : undefined,
495
+ attempt:
496
+ webAttempt && nodeAttempt && webAttempt === nodeAttempt
497
+ ? webAttempt
498
+ : undefined,
421
499
  };
500
+ const finishStats: DevRuntimeStats =
501
+ webStats && nodeStats ? { web: webStats, node: nodeStats } : stats;
422
502
  if (!webStats || !nodeStats) {
423
- await binding.runtime.finishAttempt(stats, changes, identity);
503
+ await finishRuntimeAttempt(binding, pair, finishStats, changes, identity);
424
504
  return;
425
505
  }
426
506
  pair.pendingAttempt = {
427
- stats,
507
+ stats: finishStats,
428
508
  changes,
429
509
  identity,
430
510
  webCompilation: webStats.compilation,
@@ -1,7 +1,13 @@
1
1
  import type { RsbuildDevServer } from '@rsbuild/core';
2
+ import * as Effect from 'effect/Effect';
2
3
  import { PLUGIN_NAME } from './constants.js';
3
4
  import type { ReactRouterDevRuntime } from './dev-generation.js';
4
5
  import type { DevCompilerPair } from './dev-runtime-compilation.js';
6
+ import {
7
+ runPluginEffect,
8
+ tryPluginPromise,
9
+ tryPluginSync,
10
+ } from './effect-runtime.js';
5
11
 
6
12
  export type RuntimeBinding = {
7
13
  id: number;
@@ -109,18 +115,19 @@ export const createDevRuntimeSessionManager = (
109
115
  if (observation.promise) {
110
116
  return observation.promise;
111
117
  }
112
- let closePromise: Promise<void>;
113
- try {
114
- closePromise = close();
115
- } catch (cause) {
116
- closePromise = Promise.reject(cause);
117
- }
118
- observation.promise = closePromise;
119
- void closePromise.then(
120
- () => applyCloseOutcome(observation, { ok: true }),
121
- cause => applyCloseOutcome(observation, { ok: false, cause })
118
+ observation.promise = runPluginEffect(
119
+ tryPluginPromise(close).pipe(
120
+ Effect.tap(() =>
121
+ tryPluginSync(() => applyCloseOutcome(observation, { ok: true }))
122
+ ),
123
+ Effect.catchAll(cause =>
124
+ tryPluginSync(() =>
125
+ applyCloseOutcome(observation, { ok: false, cause })
126
+ ).pipe(Effect.zipRight(Effect.fail(cause)))
127
+ )
128
+ )
122
129
  );
123
- return closePromise;
130
+ return observation.promise;
124
131
  };
125
132
  closeObservationByServer.set(server, observation);
126
133
  return observation;
package/src/dev-server.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { RsbuildConfig } from '@rsbuild/core';
2
3
  import type { ServerBuild } from 'react-router';
3
4
 
5
+ export type ServerSetup = Exclude<
6
+ NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
7
+ unknown[]
8
+ >;
9
+
4
10
  export type DevServerMiddleware = (
5
11
  req: IncomingMessage,
6
12
  res: ServerResponse,
@@ -42,11 +48,18 @@ export const createDevServerMiddleware = (
42
48
  dependencies.loadBuild,
43
49
  'development'
44
50
  );
45
- return createRequestListener(requestHandler);
51
+ return createRequestListener(request => requestHandler(request));
46
52
  })();
47
53
  return listenerPromise;
48
54
  };
49
55
 
56
+ // Warm the handler imports now so the first request does not pay them.
57
+ // On failure, reset so the first real request retries and surfaces the
58
+ // error through the middleware's own error path.
59
+ void getListener().catch(() => {
60
+ listenerPromise = undefined;
61
+ });
62
+
50
63
  return async (req, res, next): Promise<void> => {
51
64
  try {
52
65
  const listener = await getListener();
@@ -56,3 +69,20 @@ export const createDevServerMiddleware = (
56
69
  }
57
70
  };
58
71
  };
72
+
73
+ export const createReactRouterDevServerSetup = ({
74
+ loadBuild,
75
+ }: {
76
+ loadBuild: BuildProvider;
77
+ }): ServerSetup =>
78
+ function reactRouterDevServerSetup(context) {
79
+ if (context.action !== 'dev') {
80
+ return;
81
+ }
82
+ // The returned callback runs after Rsbuild registers its built-in
83
+ // middlewares, so static assets are served before the React Router
84
+ // request handler.
85
+ return () => {
86
+ context.server.middlewares.use(createDevServerMiddleware({ loadBuild }));
87
+ };
88
+ };
@@ -0,0 +1,130 @@
1
+ import * as Cause from 'effect/Cause';
2
+ import * as Duration from 'effect/Duration';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as Exit from 'effect/Exit';
5
+ import * as Fiber from 'effect/Fiber';
6
+ import * as Option from 'effect/Option';
7
+
8
+ export const DEV_BACKGROUND_STARTUP_DELAY_MS = 3_000;
9
+
10
+ export const normalizeEffectError = (cause: unknown): Error =>
11
+ cause instanceof Error ? cause : new Error(String(cause));
12
+
13
+ const normalizeEffectCause = <E>(cause: Cause.Cause<E>): Error => {
14
+ const failure = Cause.failureOption(cause);
15
+ return normalizeEffectError(
16
+ Option.isSome(failure) ? failure.value : Cause.squash(cause)
17
+ );
18
+ };
19
+
20
+ export const runPluginEffect = async <A, E>(
21
+ effect: Effect.Effect<A, E, never>
22
+ ): Promise<A> => {
23
+ const exit = await Effect.runPromiseExit(effect);
24
+ if (Exit.isSuccess(exit)) {
25
+ return exit.value;
26
+ }
27
+ throw normalizeEffectCause(exit.cause);
28
+ };
29
+
30
+ export const tryPluginSync = <A>(
31
+ evaluate: () => A
32
+ ): Effect.Effect<A, Error, never> =>
33
+ Effect.try({
34
+ try: evaluate,
35
+ catch: normalizeEffectError,
36
+ });
37
+
38
+ export const tryPluginPromise = <A>(
39
+ evaluate: () => PromiseLike<A> | A
40
+ ): Effect.Effect<A, Error, never> =>
41
+ Effect.tryPromise({
42
+ try: () => Promise.resolve(evaluate()),
43
+ catch: normalizeEffectError,
44
+ });
45
+
46
+ type DelayedPluginTask = {
47
+ schedule(): void;
48
+ reschedule(): void;
49
+ cancelEffect(): Effect.Effect<void, Error, never>;
50
+ cancel(): Promise<void>;
51
+ };
52
+
53
+ export const createDelayedPluginTask = ({
54
+ delayMs,
55
+ run,
56
+ onError,
57
+ }: {
58
+ delayMs: number;
59
+ run: () => Effect.Effect<void, Error, never>;
60
+ onError: (error: Error) => void;
61
+ }): DelayedPluginTask => {
62
+ let activeFiber: ReturnType<typeof Effect.runFork> | undefined;
63
+ let version = 0;
64
+
65
+ const cancelActiveEffect = (): Effect.Effect<void, Error, never> =>
66
+ Effect.sync(() => {
67
+ const fiber = activeFiber;
68
+ activeFiber = undefined;
69
+ return fiber;
70
+ }).pipe(
71
+ Effect.flatMap(fiber =>
72
+ fiber ? Fiber.interrupt(fiber).pipe(Effect.asVoid) : Effect.void
73
+ )
74
+ );
75
+
76
+ const cancelEffect = (): Effect.Effect<void, Error, never> =>
77
+ Effect.sync(() => {
78
+ version += 1;
79
+ }).pipe(Effect.zipRight(cancelActiveEffect()));
80
+
81
+ const start = (taskVersion: number): void => {
82
+ if (activeFiber || version !== taskVersion) {
83
+ return;
84
+ }
85
+
86
+ let fiber: ReturnType<typeof Effect.runFork>;
87
+ fiber = Effect.runFork(
88
+ Effect.sleep(Duration.millis(delayMs)).pipe(
89
+ Effect.zipRight(Effect.suspend(run)),
90
+ Effect.catchAll(error =>
91
+ Effect.sync(() => {
92
+ onError(error);
93
+ })
94
+ ),
95
+ Effect.ensuring(
96
+ Effect.sync(() => {
97
+ if (activeFiber === fiber) {
98
+ activeFiber = undefined;
99
+ }
100
+ })
101
+ )
102
+ )
103
+ );
104
+ activeFiber = fiber;
105
+ };
106
+
107
+ return {
108
+ schedule(): void {
109
+ if (activeFiber) {
110
+ return;
111
+ }
112
+ version += 1;
113
+ start(version);
114
+ },
115
+
116
+ reschedule(): void {
117
+ version += 1;
118
+ const taskVersion = version;
119
+ void runPluginEffect(cancelActiveEffect())
120
+ .then(() => start(taskVersion))
121
+ .catch(onError);
122
+ },
123
+
124
+ cancelEffect,
125
+
126
+ async cancel(): Promise<void> {
127
+ await runPluginEffect(cancelEffect());
128
+ },
129
+ };
130
+ };