@client-compose/client 0.6.0 → 0.8.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.
package/dist/proxy.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { type ActionRequest, type ActionResponse, type ClientContext, type ScreenRequest, type ScreenResponse } from "@client-compose/protocol";
1
+ import { type ActionRequest, type ActionResponse, type ClientContext, type IslandEntry, type IslandRequest, type ScreenRequest, type ScreenResponse } from "@client-compose/protocol";
2
2
  /** A screen request from the browser: the server sets the client context. */
3
3
  export type ProxiedScreenRequest = Omit<ScreenRequest, "client">;
4
+ /** An island request from the browser: the server sets the client context. */
5
+ export type ProxiedIslandRequest = Omit<IslandRequest, "client">;
4
6
  /** An action request from the browser: the server sets the client context. */
5
7
  export type ProxiedActionRequest = Omit<ActionRequest, "client">;
6
8
  /** The composer's response, or the status and body it failed with. */
@@ -34,8 +36,14 @@ export interface ComposerProxy {
34
36
  performAction(body: unknown): Promise<ProxyResult<ActionResponse>>;
35
37
  /** Checks a browser's screen request, then loads it with the server's context and headers. */
36
38
  loadScreen(body: unknown): Promise<ProxyResult<ScreenResponse>>;
39
+ /**
40
+ * Checks a browser's island request (a page of an island, spec §4.5), then loads it with the
41
+ * server's context and headers. Resolves with the island's entry.
42
+ */
43
+ loadIsland(body: unknown): Promise<ProxyResult<IslandEntry>>;
37
44
  }
38
45
  export declare function isProxiedScreenRequest(value: unknown): value is ProxiedScreenRequest;
46
+ export declare function isProxiedIslandRequest(value: unknown): value is ProxiedIslandRequest;
39
47
  export declare function isProxiedActionRequest(value: unknown): value is ProxiedActionRequest;
40
48
  export declare function createComposerProxy(options: ComposerProxyOptions): ComposerProxy;
41
49
  //# sourceMappingURL=proxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAeA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,aAAa,EAElB,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,0BAA0B,CAAC;AAIlC,6EAA6E;AAC7E,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AACjE,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AAEjE,sEAAsE;AACtE,MAAM,MAAM,WAAW,CAAC,CAAC,IACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1E,MAAM,WAAW,oBAAoB;IACnC,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,MAAM,EAAE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzE,qEAAqE;IACrE,WAAW,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpD,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CACjB,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,cAAc,KACrB,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC;IACnE,8FAA8F;IAC9F,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC;CACjE;AAkBD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAQpF;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CASpF;AASD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAsChF"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAeA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,0BAA0B,CAAC;AAWlC,6EAA6E;AAC7E,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AACjE,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AACjE,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AAEjE,sEAAsE;AACtE,MAAM,MAAM,WAAW,CAAC,CAAC,IACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1E,MAAM,WAAW,oBAAoB;IACnC,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,MAAM,EAAE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzE,qEAAqE;IACrE,WAAW,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpD,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CACjB,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,cAAc,KACrB,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC;IACnE,8FAA8F;IAC9F,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC;IAChE;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC;CAC9D;AAoBD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAWpF;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAcpF;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAWpF;AASD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAiFhF"}
package/dist/proxy.js CHANGED
@@ -13,7 +13,7 @@
13
13
  // Errors come back as data, since frameworks often strip thrown errors' details on the way to the
14
14
  // browser.
15
15
  import { PROTOCOL_VERSION, } from "@client-compose/protocol";
16
- import { ActionFetchError, fetchAction, fetchScreen, ScreenFetchError } from "./index.js";
16
+ import { ActionFetchError, fetchAction, fetchIsland, fetchScreen, IslandFetchError, ScreenFetchError, } from "./index.js";
17
17
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
18
18
  function isCapabilities(value) {
19
19
  return (isRecord(value) &&
@@ -22,20 +22,45 @@ function isCapabilities(value) {
22
22
  function isStringRecord(value) {
23
23
  return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
24
24
  }
25
+ // Each guard returns at the first thing a request gets wrong, so it checks no more than it must.
25
26
  export function isProxiedScreenRequest(value) {
26
- return (isRecord(value) &&
27
- value.protocol === PROTOCOL_VERSION &&
28
- typeof value.screen === "string" &&
29
- (value.params === undefined || isStringRecord(value.params)) &&
30
- isCapabilities(value.islands));
27
+ if (!isRecord(value) || value.protocol !== PROTOCOL_VERSION) {
28
+ return false;
29
+ }
30
+ if (typeof value.screen !== "string") {
31
+ return false;
32
+ }
33
+ if (value.params !== undefined && !isStringRecord(value.params)) {
34
+ return false;
35
+ }
36
+ return isCapabilities(value.islands);
37
+ }
38
+ export function isProxiedIslandRequest(value) {
39
+ if (!isRecord(value) || value.protocol !== PROTOCOL_VERSION) {
40
+ return false;
41
+ }
42
+ if (typeof value.screen !== "string" || typeof value.key !== "string") {
43
+ return false;
44
+ }
45
+ if (typeof value.version !== "number" || !Number.isInteger(value.version)) {
46
+ return false;
47
+ }
48
+ if (value.params !== undefined && !isStringRecord(value.params)) {
49
+ return false;
50
+ }
51
+ return value.cursor === undefined || typeof value.cursor === "string";
31
52
  }
32
53
  export function isProxiedActionRequest(value) {
33
- return (isRecord(value) &&
34
- value.protocol === PROTOCOL_VERSION &&
35
- typeof value.action === "string" &&
36
- typeof value.version === "number" &&
37
- "input" in value &&
38
- (value.idempotencyKey === undefined || typeof value.idempotencyKey === "string"));
54
+ if (!isRecord(value) || value.protocol !== PROTOCOL_VERSION) {
55
+ return false;
56
+ }
57
+ if (typeof value.action !== "string" || typeof value.version !== "number") {
58
+ return false;
59
+ }
60
+ if (!("input" in value)) {
61
+ return false;
62
+ }
63
+ return value.idempotencyKey === undefined || typeof value.idempotencyKey === "string";
39
64
  }
40
65
  /** What the composer itself answers to a malformed request. */
41
66
  const BAD_REQUEST = {
@@ -45,12 +70,16 @@ const BAD_REQUEST = {
45
70
  };
46
71
  export function createComposerProxy(options) {
47
72
  async function init() {
48
- const headers = (await options.headers?.()) ?? {};
49
- return { ...options.requestInit, headers: { ...options.requestInit?.headers, ...headers } };
73
+ const headers = new Headers(options.requestInit?.headers);
74
+ for (const [name, value] of Object.entries((await options.headers?.()) ?? {})) {
75
+ headers.set(name, value);
76
+ }
77
+ return { ...options.requestInit, headers };
50
78
  }
51
79
  async function performAction(body) {
52
- if (!isProxiedActionRequest(body))
80
+ if (!isProxiedActionRequest(body)) {
53
81
  return BAD_REQUEST;
82
+ }
54
83
  const request = { ...body, client: await options.client() };
55
84
  try {
56
85
  const response = await fetchAction(options.baseUrl, request, await init(), options.fetch);
@@ -60,14 +89,20 @@ export function createComposerProxy(options) {
60
89
  };
61
90
  }
62
91
  catch (error) {
63
- if (error instanceof ActionFetchError)
64
- return { ok: false, status: error.status, body: error.body };
92
+ if (error instanceof ActionFetchError) {
93
+ return {
94
+ ok: false,
95
+ status: error.status,
96
+ body: error.body,
97
+ };
98
+ }
65
99
  throw error;
66
100
  }
67
101
  }
68
102
  async function loadScreen(body) {
69
- if (!isProxiedScreenRequest(body))
103
+ if (!isProxiedScreenRequest(body)) {
70
104
  return BAD_REQUEST;
105
+ }
71
106
  const request = { ...body, client: await options.client() };
72
107
  try {
73
108
  return {
@@ -76,11 +111,42 @@ export function createComposerProxy(options) {
76
111
  };
77
112
  }
78
113
  catch (error) {
79
- if (error instanceof ScreenFetchError)
80
- return { ok: false, status: error.status, body: error.body };
114
+ if (error instanceof ScreenFetchError) {
115
+ return {
116
+ ok: false,
117
+ status: error.status,
118
+ body: error.body,
119
+ };
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+ async function loadIsland(body) {
125
+ if (!isProxiedIslandRequest(body)) {
126
+ return BAD_REQUEST;
127
+ }
128
+ const request = { ...body, client: await options.client() };
129
+ try {
130
+ return {
131
+ ok: true,
132
+ response: await fetchIsland(options.baseUrl, request, await init(), options.fetch),
133
+ };
134
+ }
135
+ catch (error) {
136
+ if (error instanceof IslandFetchError) {
137
+ return {
138
+ ok: false,
139
+ status: error.status,
140
+ body: error.body,
141
+ };
142
+ }
81
143
  throw error;
82
144
  }
83
145
  }
84
- return { performAction, loadScreen };
146
+ return {
147
+ performAction,
148
+ loadScreen,
149
+ loadIsland,
150
+ };
85
151
  }
86
152
  //# sourceMappingURL=proxy.js.map
package/dist/proxy.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,oGAAoG;AACpG,+FAA+F;AAC/F,EAAE;AACF,0CAA0C;AAC1C,+BAA+B;AAC/B,0FAA0F;AAC1F,uFAAuF;AACvF,UAAU;AACV,+FAA+F;AAC/F,EAAE;AACF,mGAAmG;AACnG,kGAAkG;AAClG,WAAW;AAEX,OAAO,EAKL,gBAAgB,GAGjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAuC1F,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CACpE,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAEvE,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CACxB,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CACtF,CACF,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,KAAK,CAAC,QAAQ,KAAK,gBAAgB;QACnC,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;QAChC,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5D,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,CAC9B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,KAAK,CAAC,QAAQ,KAAK,gBAAgB;QACnC,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;QAChC,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;QACjC,OAAO,IAAI,KAAK;QAChB,CAAC,KAAK,CAAC,cAAc,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,cAAc,KAAK,QAAQ,CAAC,CACjF,CAAC;AACJ,CAAC;AAED,+DAA+D;AAC/D,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,KAAK;IACT,MAAM,EAAE,GAAG;IACX,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC;CAC5E,CAAC;AAEX,MAAM,UAAU,mBAAmB,CAAC,OAA6B;IAC/D,KAAK,UAAU,IAAI;QACjB,MAAM,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;QAClD,OAAO,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,EAAE,CAAC;IAC9F,CAAC;IAED,KAAK,UAAU,aAAa,CAAC,IAAa;QACxC,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC;YAAE,OAAO,WAAW,CAAC;QACtD,MAAM,OAAO,GAAkB,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QAC3E,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;YAC1F,OAAO;gBACL,EAAE,EAAE,IAAI;gBACR,QAAQ,EAAE,CAAC,MAAM,OAAO,CAAC,gBAAgB,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,IAAI,QAAQ;aAC5E,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,gBAAgB;gBACnC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;YAC/D,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,UAAU,UAAU,CAAC,IAAa;QACrC,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC;YAAE,OAAO,WAAW,CAAC;QACtD,MAAM,OAAO,GAAkB,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QAC3E,IAAI,CAAC;YACH,OAAO;gBACL,EAAE,EAAE,IAAI;gBACR,QAAQ,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC;aACnF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,gBAAgB;gBACnC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;YAC/D,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
package/dist/testing.d.ts CHANGED
@@ -1,8 +1,13 @@
1
- import { type ActionDescriptor, type ActionEffects, type Capabilities, type ClientContext, type DeclaredError, type IslandDescriptor, type IslandEntry, type IslandError, type ScreenMeta } from "@client-compose/protocol";
2
- /** A response for the fake composer to send. */
1
+ import { type ActionDescriptor, type ActionEffects, type Capabilities, type ClientContext, type DeclaredError, type IslandDescriptor, type IslandEntry, type IslandError, type PendingIsland, type ScreenMeta } from "@client-compose/protocol";
2
+ /**
3
+ * A response for the fake composer to send: a JSON body, or a screen's stream (spec §4.6), as
4
+ * lines that come when they're given.
5
+ */
3
6
  export interface FakeResponse {
4
7
  status: number;
5
- body: unknown;
8
+ body?: unknown;
9
+ /** Opens the stream's body, for each request answered with it. */
10
+ stream?: () => ReadableStream<Uint8Array>;
6
11
  }
7
12
  /** A request the fake composer received. */
8
13
  export interface RecordedRequest {
@@ -19,6 +24,15 @@ export interface RecordedScreenRequest extends RecordedRequest {
19
24
  client: ClientContext | undefined;
20
25
  islands: Capabilities;
21
26
  }
27
+ /** An island request (spec §4.5): a page of one island, or the island resolved again. */
28
+ export interface RecordedIslandRequest extends RecordedRequest {
29
+ screen: string;
30
+ key: string;
31
+ version: number;
32
+ params: Record<string, string>;
33
+ cursor: string | undefined;
34
+ client: ClientContext | undefined;
35
+ }
22
36
  export interface RecordedActionRequest extends RecordedRequest {
23
37
  action: string;
24
38
  version: number;
@@ -31,6 +45,8 @@ export interface FakeGatewayHandlers {
31
45
  screens?: Record<string, (request: RecordedScreenRequest) => FakeResponse>;
32
46
  /** By action name. */
33
47
  actions?: Record<string, (request: RecordedActionRequest) => FakeResponse>;
48
+ /** Island requests, by `"{screen}/{key}"`. */
49
+ islands?: Record<string, (request: RecordedIslandRequest) => FakeResponse>;
34
50
  /** Anything else, e.g. uploads to storage: return undefined to leave a request to the others. */
35
51
  other?: (request: RecordedRequest) => FakeResponse | undefined;
36
52
  }
@@ -40,6 +56,7 @@ export interface FakeGateway {
40
56
  /** Every request received, in order. */
41
57
  requests: RecordedRequest[];
42
58
  screenRequests(): RecordedScreenRequest[];
59
+ islandRequests(): RecordedIslandRequest[];
43
60
  actionRequests(): RecordedActionRequest[];
44
61
  }
45
62
  /** A composer whose screens and actions are the handlers given. */
@@ -53,12 +70,38 @@ export interface InstalledFakeGateway extends FakeGateway {
53
70
  * app rendered in a test). Call `restore()` after the test.
54
71
  */
55
72
  export declare function installFakeGateway(handlers?: FakeGatewayHandlers): InstalledFakeGateway;
56
- /** An island in a screen response, typed by its generated descriptor. */
57
- export declare function island<P>(key: string, descriptor: IslandDescriptor<P>, props: P): IslandEntry<P>;
73
+ /**
74
+ * An island in a screen or island response, typed by its generated descriptor. Give `next` for an
75
+ * island with more pages (spec §4.5).
76
+ */
77
+ export declare function island<P>(key: string, descriptor: IslandDescriptor<P>, props: P, next?: string): IslandEntry<P>;
58
78
  /** An island that failed on the composer (spec §3.4). */
59
79
  export declare function islandError(key: string, descriptor: IslandDescriptor<unknown>, error: IslandError): IslandEntry;
60
- /** A screen response. */
80
+ /** A screen, all of it at once: its stream's head with every island, then its end (spec §4.6). */
61
81
  export declare function screen(id: string, islands: IslandEntry<unknown>[], meta?: ScreenMeta): FakeResponse;
82
+ /** An island still being resolved, in a streamed screen's head (spec §4.6). */
83
+ export declare function pending(key: string, descriptor: IslandDescriptor<unknown>): PendingIsland;
84
+ /** A streamed screen whose lines a test sends, so it controls when each island arrives. */
85
+ export interface StreamedScreen extends FakeResponse {
86
+ /** Sends a pending island's entry. */
87
+ send(entry: IslandEntry<unknown>): void;
88
+ /** Sends the end line. */
89
+ end(): void;
90
+ /** Ends the stream without its end line, as a dropped connection would. */
91
+ cut(): void;
92
+ }
93
+ /**
94
+ * A screen whose head is sent at once, with its islands resolved or `pending(…)`, and whose other
95
+ * lines are sent when the test says (spec §4.6). Answer one request with it.
96
+ *
97
+ * const results = streamedScreen("search", [island("box", BoxV1, {}), pending("results", ResultsV1)]);
98
+ * // render, check the placeholder, then:
99
+ * results.send(island("results", ResultsV1, { items: [] }));
100
+ * results.end();
101
+ */
102
+ export declare function streamedScreen(id: string, islands: (IslandEntry<unknown> | PendingIsland)[], meta?: ScreenMeta): StreamedScreen;
103
+ /** An island response (spec §4.5): a page of one island, or the island resolved again. */
104
+ export declare function islandResponse(screen: string, entry: IslandEntry<unknown>): FakeResponse;
62
105
  /** A screen request that fails, e.g. `screenError(404, "not_found")` or `screenError(401, "unauthenticated")`. */
63
106
  export declare function screenError(status: number, code: string): FakeResponse;
64
107
  /** An action that succeeded, with its result typed by the descriptor. */
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
package/dist/testing.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // const gateway = createFakeGateway({
6
6
  // screens: { profile: () => screen("profile", [island("header", ProfileHeaderV1, { name: "Ada" })]) },
7
7
  // actions: { follow: () => actionOk(FollowUserV1, { followers: 8 }) },
8
+ // islands: { "profile/posts": (r) => islandResponse("profile", island("posts", PostsV1, …, "page:3")) },
8
9
  // });
9
10
  // // pass gateway.fetch as the client's fetch, then assert on gateway.actionRequests()
10
11
  //
@@ -12,13 +13,15 @@
12
13
  import { PROTOCOL_VERSION, } from "@client-compose/protocol";
13
14
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
15
  function strings(value) {
15
- if (!isRecord(value))
16
+ if (!isRecord(value)) {
16
17
  return {};
18
+ }
17
19
  return Object.fromEntries(Object.entries(value).filter((e) => typeof e[1] === "string"));
18
20
  }
19
21
  function capabilities(value) {
20
- if (!isRecord(value))
22
+ if (!isRecord(value)) {
21
23
  return {};
24
+ }
22
25
  return Object.fromEntries(Object.entries(value).map(([type, versions]) => [
23
26
  type,
24
27
  Array.isArray(versions) ? versions.filter((v) => typeof v === "number") : [],
@@ -27,13 +30,19 @@ function capabilities(value) {
27
30
  function clientContext(value) {
28
31
  if (!isRecord(value) ||
29
32
  typeof value.platform !== "string" ||
30
- typeof value.appVersion !== "string")
33
+ typeof value.appVersion !== "string") {
31
34
  return undefined;
32
- return { ...value, platform: value.platform, appVersion: value.appVersion };
35
+ }
36
+ return {
37
+ ...value,
38
+ platform: value.platform,
39
+ appVersion: value.appVersion,
40
+ };
33
41
  }
34
42
  function parseBody(body) {
35
- if (typeof body !== "string")
43
+ if (typeof body !== "string") {
36
44
  return body;
45
+ }
37
46
  try {
38
47
  return JSON.parse(body);
39
48
  }
@@ -42,8 +51,9 @@ function parseBody(body) {
42
51
  }
43
52
  }
44
53
  function urlOf(input) {
45
- if (typeof input === "string")
54
+ if (typeof input === "string") {
46
55
  return input;
56
+ }
47
57
  return input instanceof URL ? input.href : input.url;
48
58
  }
49
59
  function headersOf(init) {
@@ -51,10 +61,15 @@ function headersOf(init) {
51
61
  new Headers(init).forEach((value, name) => (out[name] = value));
52
62
  return out;
53
63
  }
64
+ function isIslandURL(url) {
65
+ return /\/screens\/[^/]+\/islands\/[^/?]+/.test(url);
66
+ }
54
67
  function asScreenRequest(request) {
55
68
  const { body } = request;
56
- if (!request.url.includes("/screens/") || !isRecord(body) || typeof body.screen !== "string")
69
+ const isScreen = request.url.includes("/screens/") && !isIslandURL(request.url);
70
+ if (!isScreen || !isRecord(body) || typeof body.screen !== "string") {
57
71
  return undefined;
72
+ }
58
73
  return {
59
74
  ...request,
60
75
  screen: body.screen,
@@ -63,10 +78,29 @@ function asScreenRequest(request) {
63
78
  islands: capabilities(body.islands),
64
79
  };
65
80
  }
81
+ function asIslandRequest(request) {
82
+ const { body } = request;
83
+ if (!isIslandURL(request.url) || !isRecord(body)) {
84
+ return undefined;
85
+ }
86
+ if (typeof body.screen !== "string" || typeof body.key !== "string") {
87
+ return undefined;
88
+ }
89
+ return {
90
+ ...request,
91
+ screen: body.screen,
92
+ key: body.key,
93
+ version: typeof body.version === "number" ? body.version : 0,
94
+ params: strings(body.params),
95
+ cursor: typeof body.cursor === "string" ? body.cursor : undefined,
96
+ client: clientContext(body.client),
97
+ };
98
+ }
66
99
  function asActionRequest(request) {
67
100
  const { body } = request;
68
- if (!request.url.includes("/actions/") || !isRecord(body) || typeof body.action !== "string")
101
+ if (!request.url.includes("/actions/") || !isRecord(body) || typeof body.action !== "string") {
69
102
  return undefined;
103
+ }
70
104
  return {
71
105
  ...request,
72
106
  action: body.action,
@@ -77,6 +111,12 @@ function asActionRequest(request) {
77
111
  };
78
112
  }
79
113
  function respond(response) {
114
+ if (response.stream) {
115
+ return new Response(response.stream(), {
116
+ status: response.status,
117
+ headers: { "content-type": "application/jsonl" },
118
+ });
119
+ }
80
120
  return new Response(JSON.stringify(response.body), {
81
121
  status: response.status,
82
122
  headers: { "content-type": "application/json" },
@@ -94,22 +134,31 @@ export function createFakeGateway(handlers = {}) {
94
134
  };
95
135
  requests.push(request);
96
136
  const other = handlers.other?.(request);
97
- if (other)
137
+ if (other) {
98
138
  return respond(other);
139
+ }
140
+ const islandRequest = asIslandRequest(request);
141
+ const islandHandler = islandRequest && handlers.islands?.[`${islandRequest.screen}/${islandRequest.key}`];
142
+ if (islandRequest && islandHandler) {
143
+ return respond(islandHandler(islandRequest));
144
+ }
99
145
  const screenRequest = asScreenRequest(request);
100
146
  const screenHandler = screenRequest && handlers.screens?.[screenRequest.screen];
101
- if (screenRequest && screenHandler)
147
+ if (screenRequest && screenHandler) {
102
148
  return respond(screenHandler(screenRequest));
149
+ }
103
150
  const actionRequest = asActionRequest(request);
104
151
  const actionHandler = actionRequest && handlers.actions?.[actionRequest.action];
105
- if (actionRequest && actionHandler)
152
+ if (actionRequest && actionHandler) {
106
153
  return respond(actionHandler(actionRequest));
154
+ }
107
155
  throw new Error(`fake gateway: no handler for ${request.method} ${request.url} ${JSON.stringify(request.body)}`);
108
156
  }
109
157
  return {
110
158
  fetch: fakeFetch,
111
159
  requests,
112
160
  screenRequests: () => requests.flatMap((r) => asScreenRequest(r) ?? []),
161
+ islandRequests: () => requests.flatMap((r) => asIslandRequest(r) ?? []),
113
162
  actionRequests: () => requests.flatMap((r) => asActionRequest(r) ?? []),
114
163
  };
115
164
  }
@@ -129,19 +178,128 @@ export function installFakeGateway(handlers = {}) {
129
178
  };
130
179
  }
131
180
  // ---- Responses ----
132
- /** An island in a screen response, typed by its generated descriptor. */
133
- export function island(key, descriptor, props) {
134
- return { key, type: descriptor.type, version: descriptor.version, status: "ok", props };
181
+ /**
182
+ * An island in a screen or island response, typed by its generated descriptor. Give `next` for an
183
+ * island with more pages (spec §4.5).
184
+ */
185
+ export function island(key, descriptor, props, next) {
186
+ return {
187
+ key,
188
+ type: descriptor.type,
189
+ version: descriptor.version,
190
+ status: "ok",
191
+ props,
192
+ ...(next !== undefined && { next }),
193
+ };
135
194
  }
136
195
  /** An island that failed on the composer (spec §3.4). */
137
196
  export function islandError(key, descriptor, error) {
138
- return { key, type: descriptor.type, version: descriptor.version, status: "error", error };
197
+ return {
198
+ key,
199
+ type: descriptor.type,
200
+ version: descriptor.version,
201
+ status: "error",
202
+ error,
203
+ };
139
204
  }
140
- /** A screen response. */
205
+ /** A screen, all of it at once: its stream's head with every island, then its end (spec §4.6). */
141
206
  export function screen(id, islands, meta) {
207
+ const head = {
208
+ kind: "head",
209
+ protocol: PROTOCOL_VERSION,
210
+ screen: id,
211
+ islands,
212
+ ...(meta && { meta }),
213
+ };
142
214
  return {
143
215
  status: 200,
144
- body: { protocol: PROTOCOL_VERSION, screen: id, islands, ...(meta && { meta }) },
216
+ stream: () => linesStream([head, { kind: "end" }]),
217
+ };
218
+ }
219
+ /** An island still being resolved, in a streamed screen's head (spec §4.6). */
220
+ export function pending(key, descriptor) {
221
+ return {
222
+ key,
223
+ type: descriptor.type,
224
+ version: descriptor.version,
225
+ status: "pending",
226
+ };
227
+ }
228
+ /**
229
+ * A screen whose head is sent at once, with its islands resolved or `pending(…)`, and whose other
230
+ * lines are sent when the test says (spec §4.6). Answer one request with it.
231
+ *
232
+ * const results = streamedScreen("search", [island("box", BoxV1, {}), pending("results", ResultsV1)]);
233
+ * // render, check the placeholder, then:
234
+ * results.send(island("results", ResultsV1, { items: [] }));
235
+ * results.end();
236
+ */
237
+ export function streamedScreen(id, islands, meta) {
238
+ const encoder = new TextEncoder();
239
+ const queued = [];
240
+ let controller;
241
+ let closed = false;
242
+ const write = (line) => {
243
+ const text = `${JSON.stringify(line)}\n`;
244
+ if (controller) {
245
+ controller.enqueue(encoder.encode(text));
246
+ }
247
+ else {
248
+ queued.push(text);
249
+ }
250
+ };
251
+ const close = () => {
252
+ closed = true;
253
+ controller?.close();
254
+ };
255
+ write({
256
+ kind: "head",
257
+ protocol: PROTOCOL_VERSION,
258
+ screen: id,
259
+ islands,
260
+ ...(meta && { meta }),
261
+ });
262
+ return {
263
+ status: 200,
264
+ stream: () => new ReadableStream({
265
+ start(c) {
266
+ controller = c;
267
+ for (const text of queued.splice(0)) {
268
+ c.enqueue(encoder.encode(text));
269
+ }
270
+ if (closed) {
271
+ c.close();
272
+ }
273
+ },
274
+ }),
275
+ send: (entry) => write({ kind: "island", island: entry }),
276
+ end: () => {
277
+ write({ kind: "end" });
278
+ close();
279
+ },
280
+ cut: close,
281
+ };
282
+ }
283
+ function linesStream(lines) {
284
+ const encoder = new TextEncoder();
285
+ return new ReadableStream({
286
+ start(controller) {
287
+ for (const line of lines) {
288
+ controller.enqueue(encoder.encode(`${JSON.stringify(line)}\n`));
289
+ }
290
+ controller.close();
291
+ },
292
+ });
293
+ }
294
+ /** An island response (spec §4.5): a page of one island, or the island resolved again. */
295
+ export function islandResponse(screen, entry) {
296
+ return {
297
+ status: 200,
298
+ body: {
299
+ protocol: PROTOCOL_VERSION,
300
+ screen,
301
+ island: entry,
302
+ },
145
303
  };
146
304
  }
147
305
  /** A screen request that fails, e.g. `screenError(404, "not_found")` or `screenError(401, "unauthenticated")`. */
@@ -150,13 +308,36 @@ export function screenError(status, code) {
150
308
  }
151
309
  /** An action that succeeded, with its result typed by the descriptor. */
152
310
  export function actionOk(action, result, effects) {
153
- const base = { protocol: PROTOCOL_VERSION, action: action.action, version: action.version };
154
- return { status: 200, body: { ...base, status: "ok", result, ...(effects && { effects }) } };
311
+ const base = {
312
+ protocol: PROTOCOL_VERSION,
313
+ action: action.action,
314
+ version: action.version,
315
+ };
316
+ return {
317
+ status: 200,
318
+ body: {
319
+ ...base,
320
+ status: "ok",
321
+ result,
322
+ ...(effects && { effects }),
323
+ },
324
+ };
155
325
  }
156
326
  /** An action that ended with one of its declared errors. */
157
327
  export function actionError(action, error) {
158
- const base = { protocol: PROTOCOL_VERSION, action: action.action, version: action.version };
159
- return { status: 200, body: { ...base, status: "error", error } };
328
+ const base = {
329
+ protocol: PROTOCOL_VERSION,
330
+ action: action.action,
331
+ version: action.version,
332
+ };
333
+ return {
334
+ status: 200,
335
+ body: {
336
+ ...base,
337
+ status: "error",
338
+ error,
339
+ },
340
+ };
160
341
  }
161
342
  /** An action the composer didn't perform, e.g. `actionFailure(401, "unauthenticated")` (spec §5.4). */
162
343
  export function actionFailure(status, code) {