@heojeongbo/wirestage 0.1.0 → 0.2.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.
Files changed (65) hide show
  1. package/dist/entities/plugin/model/http.d.ts +12 -3
  2. package/dist/entities/plugin/model/http.d.ts.map +1 -1
  3. package/dist/entities/plugin/model/socket.d.ts +12 -7
  4. package/dist/entities/plugin/model/socket.d.ts.map +1 -1
  5. package/dist/entities/plugin/model/stream.d.ts +7 -4
  6. package/dist/entities/plugin/model/stream.d.ts.map +1 -1
  7. package/dist/features/sse/lib/create-sse.d.ts.map +1 -1
  8. package/dist/features/sse/lib/create-sse.js +2 -1
  9. package/dist/features/sse/lib/create-sse.js.map +1 -1
  10. package/dist/features/sse/model/types.d.ts +8 -2
  11. package/dist/features/sse/model/types.d.ts.map +1 -1
  12. package/dist/features/websocket/lib/proxy.d.ts.map +1 -1
  13. package/dist/features/websocket/lib/proxy.js +16 -3
  14. package/dist/features/websocket/lib/proxy.js.map +1 -1
  15. package/dist/features/websocket/model/types.d.ts +5 -1
  16. package/dist/features/websocket/model/types.d.ts.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/shared/api/playwright/ports.d.ts +10 -1
  21. package/dist/shared/api/playwright/ports.d.ts.map +1 -1
  22. package/dist/shared/api/stream-server/stream-server.d.ts +3 -1
  23. package/dist/shared/api/stream-server/stream-server.d.ts.map +1 -1
  24. package/dist/shared/api/stream-server/stream-server.js +10 -3
  25. package/dist/shared/api/stream-server/stream-server.js.map +1 -1
  26. package/dist/widgets/stage/index.d.ts +1 -1
  27. package/dist/widgets/stage/index.d.ts.map +1 -1
  28. package/dist/widgets/stage/lib/create-stage.d.ts.map +1 -1
  29. package/dist/widgets/stage/lib/create-stage.js +4 -0
  30. package/dist/widgets/stage/lib/create-stage.js.map +1 -1
  31. package/dist/widgets/stage/lib/http-router.d.ts +11 -2
  32. package/dist/widgets/stage/lib/http-router.d.ts.map +1 -1
  33. package/dist/widgets/stage/lib/http-router.js +317 -76
  34. package/dist/widgets/stage/lib/http-router.js.map +1 -1
  35. package/dist/widgets/stage/lib/reads.d.ts +4 -1
  36. package/dist/widgets/stage/lib/reads.d.ts.map +1 -1
  37. package/dist/widgets/stage/lib/reads.js +32 -1
  38. package/dist/widgets/stage/lib/reads.js.map +1 -1
  39. package/dist/widgets/stage/lib/response-stream.d.ts +17 -3
  40. package/dist/widgets/stage/lib/response-stream.d.ts.map +1 -1
  41. package/dist/widgets/stage/lib/response-stream.js +88 -36
  42. package/dist/widgets/stage/lib/response-stream.js.map +1 -1
  43. package/dist/widgets/stage/lib/socket-router.d.ts.map +1 -1
  44. package/dist/widgets/stage/lib/socket-router.js +44 -15
  45. package/dist/widgets/stage/lib/socket-router.js.map +1 -1
  46. package/dist/widgets/stage/model/stage.d.ts +40 -7
  47. package/dist/widgets/stage/model/stage.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/entities/plugin/model/http.ts +12 -3
  50. package/src/entities/plugin/model/socket.ts +12 -7
  51. package/src/entities/plugin/model/stream.ts +7 -4
  52. package/src/features/sse/lib/create-sse.ts +2 -1
  53. package/src/features/sse/model/types.ts +8 -2
  54. package/src/features/websocket/lib/proxy.ts +15 -3
  55. package/src/features/websocket/model/types.ts +5 -1
  56. package/src/index.ts +2 -0
  57. package/src/shared/api/playwright/ports.ts +7 -2
  58. package/src/shared/api/stream-server/stream-server.ts +11 -3
  59. package/src/widgets/stage/index.ts +2 -0
  60. package/src/widgets/stage/lib/create-stage.ts +4 -0
  61. package/src/widgets/stage/lib/http-router.ts +425 -91
  62. package/src/widgets/stage/lib/reads.ts +44 -2
  63. package/src/widgets/stage/lib/response-stream.ts +110 -37
  64. package/src/widgets/stage/lib/socket-router.ts +45 -16
  65. package/src/widgets/stage/model/stage.ts +42 -7
@@ -31,9 +31,15 @@ export interface SseConnection {
31
31
  readonly sent: number;
32
32
  /** Write an event. A string is sent as `data`. Resolves once written. */
33
33
  send(event: SseEvent | string): Promise<void>;
34
- /** Close cleanly; EventSource will reconnect after its retry delay. */
34
+ /**
35
+ * Close cleanly; EventSource will reconnect after its retry delay.
36
+ * Resolves once the end is on the timeline.
37
+ */
35
38
  end(): Promise<void>;
36
- /** Reset the connection, as a network drop would. */
39
+ /**
40
+ * Reset the connection, as a network drop would. Resolves once the cut is
41
+ * on the timeline.
42
+ */
37
43
  cut(): Promise<void>;
38
44
  readonly closed: Promise<SseEnd>;
39
45
  }
@@ -49,6 +49,9 @@ export async function createProxy(
49
49
  label: `message on ${socket.url.pathname}`,
50
50
  });
51
51
  const closed = createDeferred<WsCloseInfo>();
52
+ // Who closed, when neither end did: the test, or the stub — the
53
+ // stage closes the socket of a rule that throws.
54
+ let closedBy: "test" | "stub" | undefined;
52
55
  log("lifecycle", "none", "page", { event: "open", url: socket.url.href });
53
56
  const forward = (
54
57
  direction: "in" | "out",
@@ -57,7 +60,16 @@ export async function createProxy(
57
60
  ) => {
58
61
  const passing = toMessage(direction, raw, context.clock.now());
59
62
  messages.push(passing);
60
- if (options.drop?.(passing)) {
63
+ let dropped: boolean | undefined;
64
+ try {
65
+ dropped = options.drop?.(passing);
66
+ } catch (error) {
67
+ // The stage fails a rule that throws and closes both ends: the
68
+ // closes they report are the stub's, not the page's or server's.
69
+ closedBy ??= "stub";
70
+ throw error;
71
+ }
72
+ if (dropped) {
61
73
  log("fault", direction, "stub", { reason: "dropped" }, raw);
62
74
  return;
63
75
  }
@@ -77,7 +89,6 @@ export async function createProxy(
77
89
  // end to pass the close on, and the end that started it to complete
78
90
  // it — the page's socket hangs in CLOSING until its end is closed.
79
91
  // Once: each of those closes is reported back through a handler.
80
- let closedBy: "test" | undefined;
81
92
  const relay =
82
93
  (from: "page" | "network") =>
83
94
  (code: number | undefined, reason: string | undefined) => {
@@ -97,7 +108,8 @@ export async function createProxy(
97
108
  };
98
109
  page.onClose(relay("page"));
99
110
  server.onClose(relay("network"));
100
- // Over from the moment the test closes it, before either end hears.
111
+ // Over from the moment the test closes it or the rule fails, before
112
+ // either end hears.
101
113
  const over = () => closed.settled || closedBy !== undefined;
102
114
  connections.push({
103
115
  url: socket.url.href,
@@ -90,7 +90,11 @@ export interface WsMock {
90
90
  export interface WsProxyOptions {
91
91
  /** Proxy this many connections, then leave the rest to the next rule. */
92
92
  readonly times?: number;
93
- /** Drop a message instead of forwarding it. Dropped messages are recorded as faults. */
93
+ /**
94
+ * Drop a message instead of forwarding it. Dropped messages are recorded
95
+ * as faults. If it throws, the stage closes both ends, the page's with
96
+ * 1011, and records a `rule-error` fault.
97
+ */
94
98
  readonly drop?: (message: WsMessage) => boolean;
95
99
  }
96
100
 
package/src/index.ts CHANGED
@@ -102,9 +102,11 @@ export type { UrlMatcher } from "./shared/lib/match/index.js";
102
102
  export { compileUrlMatcher } from "./shared/lib/match/index.js";
103
103
  export type {
104
104
  AttachOptions,
105
+ MarkOptions,
105
106
  ReportOptions,
106
107
  Stage,
107
108
  StageCore,
109
+ StageCursor,
108
110
  StageOptions,
109
111
  StageReport,
110
112
  } from "./widgets/stage/index.js";
@@ -30,6 +30,8 @@ export interface ResponsePort {
30
30
  status(): number;
31
31
  headers(): HeaderMap;
32
32
  body(): Promise<Buffer>;
33
+ /** Who sent it; null for a response a route made up. */
34
+ serverAddr(): Promise<{ ipAddress: string; port: number } | null>;
33
35
  }
34
36
 
35
37
  export interface RequestPort {
@@ -44,8 +46,11 @@ export interface RequestPort {
44
46
  allHeaders(): Promise<HeaderMap>;
45
47
  postDataBuffer(): Buffer | null;
46
48
  resourceType(): string;
47
- /** Throws for requests a service worker makes; callers must guard it. */
48
- frame(): { page(): PagePort };
49
+ /**
50
+ * The frame that sent it, the same object for every request of that
51
+ * frame. Throws for requests a service worker makes; callers must guard it.
52
+ */
53
+ frame(): { url(): string; page(): PagePort };
49
54
  response(): Promise<ResponsePort | null>;
50
55
  failure(): { errorText: string } | null;
51
56
  }
@@ -37,7 +37,9 @@ export interface StreamServer {
37
37
  /**
38
38
  * Register a handler and return the URL to redirect one request to.
39
39
  * `secure` is whether the page's request was https: Playwright only lets
40
- * `route.continue` change the URL within the same scheme.
40
+ * `route.continue` change the URL within the same scheme. Every response
41
+ * closes its connection (`connection: close`); a handler leaves that
42
+ * header alone.
41
43
  */
42
44
  open(
43
45
  handler: NodeHandler,
@@ -107,6 +109,11 @@ export function createStreamServer(
107
109
  };
108
110
 
109
111
  const onRequest = (request: IncomingMessage, response: ServerResponse) => {
112
+ // One response per connection. A browser re-sends a request that dies
113
+ // before its first byte on a connection it reused — a stream cut at
114
+ // once — and its session is gone by then: the page would get a 404
115
+ // where the test cut the stream.
116
+ response.setHeader("connection", "close");
110
117
  allowCors(request, response);
111
118
  if (request.method === "OPTIONS") {
112
119
  response.writeHead(204);
@@ -143,8 +150,9 @@ export function createStreamServer(
143
150
  return;
144
151
  }
145
152
  options.log?.error("stream handler failed", { session: id }, error);
146
- if (!response.headersSent) response.writeHead(500);
147
- response.end();
153
+ // After the head, a clean end would pass for the whole response.
154
+ if (response.headersSent) response.destroy();
155
+ else response.writeHead(500).end();
148
156
  });
149
157
  };
150
158
 
@@ -1,9 +1,11 @@
1
1
  export { createStage } from "./lib/create-stage.js";
2
2
  export type {
3
3
  AttachOptions,
4
+ MarkOptions,
4
5
  ReportOptions,
5
6
  Stage,
6
7
  StageCore,
8
+ StageCursor,
7
9
  StageOptions,
8
10
  StageReport,
9
11
  } from "./model/stage.js";
@@ -41,6 +41,7 @@ const RESERVED = new Set<string>([
41
41
  "frames",
42
42
  "drain",
43
43
  "mark",
44
+ "cursor",
44
45
  "since",
45
46
  "between",
46
47
  "waitFor",
@@ -93,6 +94,8 @@ export function createStage<const Plugins extends readonly AnyWirePlugin[]>(
93
94
  streams: streamServer,
94
95
  pageId,
95
96
  correlation: createSequence("x"),
97
+ clock,
98
+ timeoutMs: options.pageTimeoutMs ?? 2_000,
96
99
  });
97
100
  const sockets = createSocketRouter({
98
101
  timeline,
@@ -121,6 +124,7 @@ export function createStage<const Plugins extends readonly AnyWirePlugin[]>(
121
124
  timeline,
122
125
  clock,
123
126
  sync,
127
+ pageId,
124
128
  wait: {
125
129
  timeout: options.wait?.timeout ?? 5_000,
126
130
  interval: options.wait?.interval ?? 25,