@dxos/react-client 0.6.14-main.7bd9c89 → 0.6.14-main.f49f251

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.
@@ -3,13 +3,14 @@
3
3
  //
4
4
 
5
5
  import { type Decorator } from '@storybook/react';
6
- import React, { useRef } from 'react';
7
- import { type FallbackProps, ErrorBoundary } from 'react-error-boundary';
6
+ import React, { createContext, type PropsWithChildren, useContext, useEffect, useRef, useState } from 'react';
7
+ import { type FallbackProps, ErrorBoundary as NativeErrorBoundary } from 'react-error-boundary';
8
8
 
9
9
  import { Trigger } from '@dxos/async';
10
10
  import { type Client } from '@dxos/client';
11
11
  import { type Space } from '@dxos/client/echo';
12
12
  import { performInvitation, TestBuilder } from '@dxos/client/testing';
13
+ import { log } from '@dxos/log';
13
14
  import { type MaybePromise } from '@dxos/util';
14
15
 
15
16
  import { ClientProvider, type ClientProviderProps } from '../client';
@@ -66,7 +67,7 @@ export const withClientProvider = ({
66
67
  };
67
68
 
68
69
  return (
69
- <ErrorBoundary FallbackComponent={ErrorFallback}>
70
+ <ErrorBoundary>
70
71
  <ClientProvider onInitialized={handleInitialized} {...props}>
71
72
  <Story />
72
73
  </ClientProvider>
@@ -80,6 +81,10 @@ export const withClientProvider = ({
80
81
  // TODO(burdon): Delay/jitter for creation of other clients.
81
82
  export type WithMultiClientProviderProps = InitializeProps & ClientProviderProps & { numClients?: number };
82
83
 
84
+ const MultiClientContext = createContext<{ id: number }>({ id: 0 });
85
+
86
+ export const useMultiClient = () => useContext(MultiClientContext);
87
+
83
88
  /**
84
89
  * Decorator that creates a scaffold for multiple clients.
85
90
  * Orchestrates invitations between a randomly selected host and the remaining clients.
@@ -99,16 +104,20 @@ export const withMultiClientProvider = ({
99
104
  const spaceReady = useRef(new Trigger<Space | undefined>());
100
105
 
101
106
  // Handle invitations.
102
- const handleInitialized = async (client: Client) => {
107
+ // NOTE: The zeroth client isn't necessarily the first to be initialized.
108
+ const handleInitialized = async (client: Client, index: number) => {
109
+ log.info('initialized', { index });
103
110
  if (createSpace) {
104
111
  if (!hostRef.current) {
105
112
  hostRef.current = client;
106
113
  const space = await initializeClient(client, { createIdentity, createSpace, onSpaceCreated, onInitialized });
107
114
  spaceReady.current.wake(space);
115
+ log.info('inviting', { index });
108
116
  } else {
109
117
  await initializeClient(client, { createIdentity, onInitialized });
110
118
  const space = await spaceReady.current.wait();
111
119
  if (space) {
120
+ log.info('joining', { index });
112
121
  await Promise.all(performInvitation({ host: space, guest: client.spaces }));
113
122
  }
114
123
  }
@@ -116,28 +125,46 @@ export const withMultiClientProvider = ({
116
125
  };
117
126
 
118
127
  return (
119
- <>
128
+ <ErrorBoundary>
120
129
  {Array.from({ length: numClients }).map((_, index) => (
121
- <ErrorBoundary key={index} FallbackComponent={ErrorFallback}>
130
+ <MultiClientContext.Provider key={index} value={{ id: index }}>
122
131
  <ClientProvider
123
132
  services={builder.current.createLocalClientServices()}
124
- onInitialized={handleInitialized}
133
+ onInitialized={(client) => handleInitialized(client, index)}
125
134
  {...props}
126
135
  >
127
136
  <Story />
128
137
  </ClientProvider>
129
- </ErrorBoundary>
138
+ </MultiClientContext.Provider>
130
139
  ))}
131
- </>
140
+ </ErrorBoundary>
132
141
  );
133
142
  };
134
143
  };
135
144
 
145
+ const ErrorBoundary = ({ children }: PropsWithChildren) => {
146
+ const [error, setError] = useState<Error>();
147
+ useEffect(() => {
148
+ const handleError = (event: PromiseRejectionEvent) => {
149
+ setError(event.reason);
150
+ };
151
+
152
+ window.addEventListener('unhandledrejection', handleError);
153
+ return () => window.removeEventListener('unhandledrejection', handleError);
154
+ }, []);
155
+
156
+ if (error) {
157
+ return <ErrorFallback error={error} resetErrorBoundary={() => setError(undefined)} />;
158
+ }
159
+
160
+ return <NativeErrorBoundary FallbackComponent={ErrorFallback}>{children}</NativeErrorBoundary>;
161
+ };
162
+
136
163
  const ErrorFallback = ({ error }: FallbackProps) => {
137
164
  const { name, message, stack } =
138
165
  error instanceof Error ? error : { name: 'Error', message: String(error), stack: undefined };
139
166
  return (
140
- <div role='alert' className='flex flex-col overflow-hidden p-4 gap-4'>
167
+ <div role='alert' className='flex flex-col p-4 gap-4 overflow-auto'>
141
168
  <h1 className='text-xl text-red-500'>{name}</h1>
142
169
  <div className='text-lg'>{message}</div>
143
170
  {stack && <pre className='whitespace-pre-wrap text-sm text-subdued'>{stack}</pre>}