@alepha/react 0.14.4 → 0.15.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.
@@ -2,54 +2,53 @@ import { ChannelPrimitive, TWSObject } from "alepha/websocket";
2
2
  import { Static } from "alepha";
3
3
 
4
4
  //#region ../../src/websocket/hooks/useRoom.d.ts
5
-
6
5
  /**
7
6
  * UseRoom options
8
7
  */
9
8
  interface UseRoomOptions<TClient extends TWSObject, TServer extends TWSObject> {
10
9
  /**
11
- * Room ID to connect to
12
- */
10
+ * Room ID to connect to
11
+ */
13
12
  roomId: string;
14
13
  /**
15
- * Channel primitive defining the schemas
16
- */
14
+ * Channel primitive defining the schemas
15
+ */
17
16
  channel: ChannelPrimitive<TClient, TServer>;
18
17
  /**
19
- * Handler for incoming messages from the server
20
- */
18
+ * Handler for incoming messages from the server
19
+ */
21
20
  handler: (message: Static<TClient>) => void;
22
21
  /**
23
- * Optional WebSocket URL override
24
- * Defaults to auto-detected URL based on window.location
25
- */
22
+ * Optional WebSocket URL override
23
+ * Defaults to auto-detected URL based on window.location
24
+ */
26
25
  url?: string;
27
26
  /**
28
- * Enable automatic reconnection on disconnect
29
- * @default true
30
- */
27
+ * Enable automatic reconnection on disconnect
28
+ * @default true
29
+ */
31
30
  autoReconnect?: boolean;
32
31
  /**
33
- * Reconnection interval in milliseconds
34
- * @default 3000
35
- */
32
+ * Reconnection interval in milliseconds
33
+ * @default 3000
34
+ */
36
35
  reconnectInterval?: number;
37
36
  /**
38
- * Maximum reconnection attempts (-1 for infinite)
39
- * @default 10
40
- */
37
+ * Maximum reconnection attempts (-1 for infinite)
38
+ * @default 10
39
+ */
41
40
  maxReconnectAttempts?: number;
42
41
  /**
43
- * Called when connection is established
44
- */
42
+ * Called when connection is established
43
+ */
45
44
  onConnect?: () => void;
46
45
  /**
47
- * Called when connection is closed
48
- */
46
+ * Called when connection is closed
47
+ */
49
48
  onDisconnect?: () => void;
50
49
  /**
51
- * Called on connection error
52
- */
50
+ * Called on connection error
51
+ */
53
52
  onError?: (error: Error) => void;
54
53
  }
55
54
  /**
@@ -57,32 +56,32 @@ interface UseRoomOptions<TClient extends TWSObject, TServer extends TWSObject> {
57
56
  */
58
57
  interface UseRoomReturn<TServer extends TWSObject> {
59
58
  /**
60
- * Send a message to the server
61
- */
59
+ * Send a message to the server
60
+ */
62
61
  send: (message: Static<TServer>) => Promise<void>;
63
62
  /**
64
- * Whether the connection is established
65
- */
63
+ * Whether the connection is established
64
+ */
66
65
  isConnected: boolean;
67
66
  /**
68
- * Whether the connection is in progress
69
- */
67
+ * Whether the connection is in progress
68
+ */
70
69
  isConnecting: boolean;
71
70
  /**
72
- * Whether there was an error
73
- */
71
+ * Whether there was an error
72
+ */
74
73
  isError: boolean;
75
74
  /**
76
- * The error object if any
77
- */
75
+ * The error object if any
76
+ */
78
77
  error?: Error;
79
78
  /**
80
- * Manually reconnect
81
- */
79
+ * Manually reconnect
80
+ */
82
81
  reconnect: () => void;
83
82
  /**
84
- * Manually disconnect
85
- */
83
+ * Manually disconnect
84
+ */
86
85
  disconnect: () => void;
87
86
  }
88
87
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/websocket/hooks/useRoom.tsx"],"sourcesContent":[],"mappings":";;;;;;;AASA;AACkB,UADD,cACC,CAAA,gBAAA,SAAA,EAAA,gBACA,SADA,CAAA,CAAA;EACA;;;EAUP,MAAA,EAAA,MAAA;EAKiB;;;EAuCH,OAAA,EA5Cd,gBA4Cc,CA5CG,OA4CH,EA5CY,OA4CZ,CAAA;EAMR;;;EAIC,OAAA,EAAA,CAAA,OAAA,EAjDG,MAiDH,CAjDU,OAiDV,CAAA,EAAA,GAAA,IAAA;EAAoB;;;AA4DtC;EAAwC,GAAA,CAAA,EAAA,MAAA;EAA2B;;;;EAGlD,aAAA,CAAA,EAAA,OAAA;EAAd;;;;;;;;;;;;;;;;;;;;;oBAzEiB;;;;;UAMH,8BAA8B;;;;kBAI7B,OAAO,aAAa;;;;;;;;;;;;;;;;UAoB5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwCG,0BAA2B,2BAA2B,oBACxD,eAAe,SAAS,8BAEhC,cAAc"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/websocket/hooks/useRoom.tsx"],"mappings":";;;;;AASA;;UAAiB,cAAA,iBACC,SAAA,kBACA,SAAA;EAAA;;;EAAA,MAAA;EAAA;;;EAAA,OAAA,EAUP,gBAAA,CAAiB,OAAA,EAAS,OAAA;EAAA;;;EAAA,OAAA,GAAA,OAAA,EAKhB,MAAA,CAAO,OAAA;EAAA;;;;EAAA,GAAA;EAAA;;;;EAAA,aAAA;EAAA;;;;EAAA,iBAAA;EAAA;;;;EAAA,oBAAA;EAAA;;;EAAA,SAAA;EAAA;;;EAAA,YAAA;EAAA;;;EAAA,OAAA,IAAA,KAAA,EAuCR,KAAA;AAAA;AAAA;;AAMpB;AANoB,UAMH,aAAA,iBAA8B,SAAA;EAAA;;;EAAA,IAAA,GAAA,OAAA,EAI7B,MAAA,CAAO,OAAA,MAAa,OAAA;EAAA;;;EAAA,WAAA;EAAA;;;EAAA,YAAA;EAAA;;;EAAA,OAAA;EAAA;;;EAAA,KAAA,GAoB5B,KAAA;EAAA;;AAwCV;EAxCU,SAAA;EAAA;;AAwCV;EAxCU,UAAA;AAAA;AAAA;;AAwCV;;;;;;;;;;;;;;;;;;;;;;;;;AAxCU,cAwCG,OAAA,mBAA2B,SAAA,kBAA2B,SAAA,EAAA,OAAA,EACxD,cAAA,CAAe,OAAA,EAAS,OAAA,GAAA,IAAA,gBAEhC,aAAA,CAAc,OAAA"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@alepha/react",
3
3
  "description": "React components and hooks for building Alepha applications.",
4
4
  "author": "Nicolas Foures",
5
- "version": "0.14.4",
5
+ "version": "0.15.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22.0.0"
@@ -25,14 +25,14 @@
25
25
  "@testing-library/react": "^16.3.1",
26
26
  "@types/react": "^19",
27
27
  "@types/react-dom": "^19",
28
- "alepha": "0.14.4",
28
+ "alepha": "0.15.0",
29
29
  "jsdom": "^27.4.0",
30
30
  "react": "^19.2.3",
31
31
  "typescript": "^5.9.3",
32
- "vitest": "^4.0.16"
32
+ "vitest": "^4.0.17"
33
33
  },
34
34
  "peerDependencies": {
35
- "alepha": "0.14.4",
35
+ "alepha": "0.15.0",
36
36
  "react": "^19"
37
37
  },
38
38
  "scripts": {
@@ -1,10 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { Alepha } from "alepha";
3
3
  import { DateTimeProvider } from "alepha/datetime";
4
- import { $realm } from "alepha/security";
5
- import { HttpClient, ServerProvider } from "alepha/server";
4
+ import { $issuer, AlephaSecurity } from "alepha/security";
5
+ import { AlephaServer, HttpClient, ServerProvider } from "alepha/server";
6
6
  import { $client } from "alepha/server/links";
7
- import { AlephaServerSecurity } from "alepha/server/security";
8
7
  import { describe, test } from "vitest";
9
8
  import {
10
9
  $auth,
@@ -25,7 +24,7 @@ describe("$auth", () => {
25
24
  };
26
25
 
27
26
  class App {
28
- realm = $realm({
27
+ issuer = $issuer({
29
28
  secret: "my-secret-key",
30
29
  roles: [
31
30
  {
@@ -36,7 +35,7 @@ describe("$auth", () => {
36
35
  });
37
36
 
38
37
  auth = $auth({
39
- realm: this.realm,
38
+ issuer: this.issuer,
40
39
  credentials: {
41
40
  account: () => user,
42
41
  },
@@ -94,7 +93,7 @@ describe("$auth", () => {
94
93
  );
95
94
 
96
95
  test("should login with credentials", async ({ expect }) => {
97
- const alepha = Alepha.create().with(App).with(AlephaServerSecurity);
96
+ const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
98
97
  const auth = alepha.inject(ReactAuth);
99
98
  await alepha.start();
100
99
 
@@ -113,7 +112,7 @@ describe("$auth", () => {
113
112
  });
114
113
 
115
114
  test("should get userinfo", async ({ expect }) => {
116
- const alepha = Alepha.create().with(App).with(AlephaServerSecurity);
115
+ const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
117
116
  await alepha.start();
118
117
 
119
118
  const { data: tokens } = await login(alepha);
@@ -134,7 +133,7 @@ describe("$auth", () => {
134
133
  });
135
134
 
136
135
  test("should reject expired token", async ({ expect }) => {
137
- const alepha = Alepha.create().with(App);
136
+ const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
138
137
  await alepha.start();
139
138
 
140
139
  const { data: tokens } = await login(alepha);
@@ -150,7 +149,7 @@ describe("$auth", () => {
150
149
  });
151
150
 
152
151
  test("should refresh expired token", async ({ expect }) => {
153
- const alepha = Alepha.create().with(App);
152
+ const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
154
153
  await alepha.start();
155
154
 
156
155
  const { data: tokens } = await login(alepha);
@@ -174,7 +173,7 @@ describe("$auth", () => {
174
173
  });
175
174
 
176
175
  test("should reject expired refresh token", async ({ expect }) => {
177
- const alepha = Alepha.create().with(App);
176
+ const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
178
177
  await alepha.start();
179
178
 
180
179
  const { data: tokens } = await login(alepha);
@@ -182,7 +181,7 @@ describe("$auth", () => {
182
181
  await alepha.inject(DateTimeProvider).travel(40, "days");
183
182
 
184
183
  await expect(refresh(alepha, tokens)).rejects.toThrowError(
185
- "Failed to refresh access token using the refresh token (realm)",
184
+ "Failed to refresh access token using the refresh token (issuer)",
186
185
  );
187
186
  });
188
187
  });
@@ -67,10 +67,6 @@ export class ReactServerProvider {
67
67
 
68
68
  this.alepha.store.set("alepha.react.server.ssr", ssrEnabled);
69
69
 
70
- if (ssrEnabled) {
71
- this.log.info("SSR streaming enabled");
72
- }
73
-
74
70
  // development mode
75
71
  if (this.alepha.isViteDev()) {
76
72
  await this.configureVite(ssrEnabled);
@@ -338,9 +334,9 @@ export class ReactServerProvider {
338
334
  const result = await this.renderPage(route, state);
339
335
 
340
336
  if (result.redirect) {
341
- // Note: redirect happens after early head is sent, handled by stream
342
- reply.redirect(result.redirect);
343
- return null;
337
+ // Return redirect URL - template provider will inject meta refresh
338
+ // since HTTP headers have already been sent
339
+ return { redirect: result.redirect };
344
340
  }
345
341
 
346
342
  return { state, reactStream: result.reactStream! };
@@ -389,10 +385,7 @@ export class ReactServerProvider {
389
385
  state: ReactRouterState,
390
386
  ): Promise<{ redirect?: string; reactStream?: ReadableStream<Uint8Array> }> {
391
387
  // Resolve page layers (loaders)
392
- this.serverTimingProvider.beginTiming("createLayers");
393
388
  const { redirect } = await this.pageApi.createLayers(route, state);
394
- this.serverTimingProvider.endTiming("createLayers");
395
-
396
389
  if (redirect) {
397
390
  this.log.debug("Resolver resulted in redirection", { redirect });
398
391
  return { redirect };
@@ -409,7 +402,6 @@ export class ReactServerProvider {
409
402
  }
410
403
 
411
404
  // Render React to stream
412
- this.serverTimingProvider.beginTiming("renderToStream");
413
405
 
414
406
  const element = this.pageApi.root(state);
415
407
  this.alepha.store.set("alepha.react.router.state", state);
@@ -426,8 +418,6 @@ export class ReactServerProvider {
426
418
  },
427
419
  });
428
420
 
429
- this.serverTimingProvider.endTiming("renderToStream");
430
-
431
421
  return { reactStream };
432
422
  }
433
423
 
@@ -392,26 +392,31 @@ export class ReactServerTemplateProvider {
392
392
  const { request, context, ...store } =
393
393
  this.alepha.context.als?.getStore() ?? {};
394
394
 
395
- return {
396
- ...store,
397
- "alepha.react.router.state": undefined,
398
- layers: state.layers.map((layer) => ({
399
- ...layer,
400
- error: layer.error
401
- ? {
402
- ...layer.error,
403
- name: layer.error.name,
404
- message: layer.error.message,
405
- stack: !this.alepha.isProduction() ? layer.error.stack : undefined,
406
- }
407
- : undefined,
408
- // Remove non-serializable properties
409
- index: undefined,
410
- path: undefined,
411
- element: undefined,
412
- route: undefined,
413
- })),
414
- };
395
+ const layers = state.layers.map((layer) => ({
396
+ name: layer.name,
397
+ props: layer.props,
398
+ config: layer.config,
399
+ error: layer.error
400
+ ? {
401
+ ...layer.error,
402
+ name: layer.error.name,
403
+ message: layer.error.message,
404
+ stack: !this.alepha.isProduction() ? layer.error.stack : undefined,
405
+ }
406
+ : undefined,
407
+ }));
408
+
409
+ const hydrationData: HydrationData = {
410
+ layers,
411
+ }
412
+
413
+ for (const [key, value] of Object.entries(store)) {
414
+ if (key.charAt(0) !== "_" && key !== "alepha.react.router.state" && key !== "registry") {
415
+ hydrationData[key] = value;
416
+ }
417
+ }
418
+
419
+ return hydrationData;
415
420
  }
416
421
 
417
422
  /**
@@ -613,17 +618,20 @@ export class ReactServerTemplateProvider {
613
618
  */
614
619
  public createEarlyHtmlStream(
615
620
  globalHead: SimpleHead,
616
- asyncWork: () => Promise<{
617
- state: ReactRouterState;
618
- reactStream: ReadableStream<Uint8Array>;
619
- } | null>,
621
+ asyncWork: () => Promise<
622
+ | {
623
+ state: ReactRouterState;
624
+ reactStream: ReadableStream<Uint8Array>;
625
+ }
626
+ | { redirect: string }
627
+ | null
628
+ >,
620
629
  options: {
621
630
  hydration?: boolean;
622
631
  onError?: (error: unknown) => void;
623
- onRedirect?: (url: string) => void;
624
632
  } = {},
625
633
  ): ReadableStream<Uint8Array> {
626
- const { hydration = true, onError, onRedirect } = options;
634
+ const { hydration = true, onError } = options;
627
635
  const slots = this.getSlots();
628
636
  const encoder = this.encoder;
629
637
 
@@ -653,9 +661,19 @@ export class ReactServerTemplateProvider {
653
661
  // === ASYNC WORK (createLayers, etc.) ===
654
662
  const result = await asyncWork();
655
663
 
656
- // Handle redirect - can't undo what we've sent, but caller handles it
657
- if (!result) {
658
- // Redirect happened - close with minimal valid HTML
664
+ // Handle redirect - inject meta refresh since headers already sent
665
+ if (!result || "redirect" in result) {
666
+ if (result && "redirect" in result) {
667
+ this.log.debug(
668
+ "Loader redirect detected after streaming started, using meta refresh",
669
+ { redirect: result.redirect },
670
+ );
671
+ controller.enqueue(
672
+ encoder.encode(
673
+ `<meta http-equiv="refresh" content="0; url=${this.escapeHtml(result.redirect)}">\n`,
674
+ ),
675
+ );
676
+ }
659
677
  controller.enqueue(slots.headClose);
660
678
  controller.enqueue(encoder.encode("<body></body></html>"));
661
679
  controller.close();