iterate 0.2.7 → 0.4.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 (73) hide show
  1. package/README.md +173 -81
  2. package/dist/api.d.ts +643 -0
  3. package/dist/api.mjs +0 -0
  4. package/dist/app-server.d.ts +51 -0
  5. package/dist/app-server.mjs +481 -0
  6. package/dist/app-server.mjs.map +1 -0
  7. package/dist/app-session.d.ts +49 -0
  8. package/dist/app-session.mjs +235 -0
  9. package/dist/app-session.mjs.map +1 -0
  10. package/dist/app.d.ts +29 -0
  11. package/dist/app.mjs +180 -0
  12. package/dist/app.mjs.map +1 -0
  13. package/dist/client/live-state.d.ts +63 -0
  14. package/dist/client/oauth.d.ts +17 -0
  15. package/dist/client/react.d.ts +77 -0
  16. package/dist/client/socket.d.ts +7 -0
  17. package/dist/client.mjs +156 -0
  18. package/dist/client.mjs.map +1 -0
  19. package/dist/expression.d.ts +88 -0
  20. package/dist/expression.mjs +301 -0
  21. package/dist/expression.mjs.map +1 -0
  22. package/dist/lib-BWr-5mFO.mjs +36 -0
  23. package/dist/lib-BWr-5mFO.mjs.map +1 -0
  24. package/dist/lib.d.ts +70 -0
  25. package/dist/lib.mjs +228 -0
  26. package/dist/lib.mjs.map +1 -0
  27. package/dist/node.d.ts +15 -0
  28. package/dist/node.mjs +47 -0
  29. package/dist/node.mjs.map +1 -0
  30. package/dist/oauth-scopes.d.ts +32 -0
  31. package/dist/oauth-scopes.mjs +40 -0
  32. package/dist/oauth-scopes.mjs.map +1 -0
  33. package/dist/oauth.mjs +41 -0
  34. package/dist/oauth.mjs.map +1 -0
  35. package/dist/principal.d.ts +8 -0
  36. package/dist/principal.mjs +8 -0
  37. package/dist/principal.mjs.map +1 -0
  38. package/dist/project-ingress.d.ts +58 -0
  39. package/dist/project-ingress.mjs +104 -0
  40. package/dist/project-ingress.mjs.map +1 -0
  41. package/dist/react.mjs +285 -0
  42. package/dist/react.mjs.map +1 -0
  43. package/dist/sdk/auth.d.ts +25 -0
  44. package/dist/sdk/index.d.ts +155 -0
  45. package/dist/sdk/record-pipelined-steps.d.ts +19 -0
  46. package/dist/sdk.mjs +245 -0
  47. package/dist/sdk.mjs.map +1 -0
  48. package/dist/stream/processor.d.ts +383 -0
  49. package/dist/stream/processor.mjs +605 -0
  50. package/dist/stream/processor.mjs.map +1 -0
  51. package/dist/stream/run.d.ts +61 -0
  52. package/dist/stream/run.mjs +45 -0
  53. package/dist/stream/run.mjs.map +1 -0
  54. package/dist/stream/test-support.d.ts +45 -0
  55. package/dist/stream/test-support.mjs +196 -0
  56. package/dist/stream/test-support.mjs.map +1 -0
  57. package/dist/usingCtx-inzbY1Qz.mjs +57 -0
  58. package/package.json +93 -30
  59. package/bin/iterate.js +0 -86
  60. package/dist/cli-DMS4kJph.mjs +0 -868
  61. package/dist/cli-DMS4kJph.mjs.map +0 -1
  62. package/dist/config-DtnR7Lv7.mjs +0 -170
  63. package/dist/config-DtnR7Lv7.mjs.map +0 -1
  64. package/dist/index.d.mts +0 -5
  65. package/dist/index.d.mts.map +0 -1
  66. package/dist/index.mjs +0 -8
  67. package/dist/index.mjs.map +0 -1
  68. package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
  69. package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
  70. package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
  71. package/dist/worker.d.mts +0 -33
  72. package/dist/worker.mjs +0 -18
  73. package/dist/worker.mjs.map +0 -1
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ /** The platform's OAuth scopes — what an app may ask for and what a consent grants:
3
+ * - `iterate` — reach the projects the person grants (every app; implied, always granted)
4
+ * - `account` — manage the person's sessions and personal access tokens
5
+ * - `organizations:write` — the person's organizations: list every one they belong to, create new ones
6
+ * Consent is task-based (the shape Cloudflare's own OAuth consent took in August 2026: a client
7
+ * requests a set, the person may deselect the optional ones, the token carries what was granted):
8
+ * `iterate` is required, every other requested scope is optional on the consent page, and an app
9
+ * reads the granted set from `session.info().scopes` rather than assuming its request. */
10
+ export declare const OAuthScope: z.ZodEnum<{
11
+ iterate: "iterate";
12
+ account: "account";
13
+ "organizations:write": "organizations:write";
14
+ }>;
15
+ export type OAuthScope = z.infer<typeof OAuthScope>;
16
+ export declare const OAuthScopes: z.ZodPipe<z.ZodArray<z.ZodEnum<{
17
+ iterate: "iterate";
18
+ account: "account";
19
+ "organizations:write": "organizations:write";
20
+ }>>, z.ZodTransform<string[], ("iterate" | "account" | "organizations:write")[]>>;
21
+ /** A requested scope as the consent page shows it: its name, what it means to the person, and
22
+ * whether they may untick it (`iterate` never). */
23
+ export interface ConsentScope {
24
+ name: OAuthScope;
25
+ title: string;
26
+ note: string;
27
+ required: boolean;
28
+ }
29
+ /** What each scope means to the person — the consent page's copy, sent with `consent.describe` so
30
+ * the page renders what it is given: a scope added to `OAuthScope` is described here or it does
31
+ * not compile. */
32
+ export declare const OAuthScopeDescriptions: Record<OAuthScope, Omit<ConsentScope, "name">>;
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ //#region src/oauth-scopes.ts
3
+ /** The platform's OAuth scopes — what an app may ask for and what a consent grants:
4
+ * - `iterate` — reach the projects the person grants (every app; implied, always granted)
5
+ * - `account` — manage the person's sessions and personal access tokens
6
+ * - `organizations:write` — the person's organizations: list every one they belong to, create new ones
7
+ * Consent is task-based (the shape Cloudflare's own OAuth consent took in August 2026: a client
8
+ * requests a set, the person may deselect the optional ones, the token carries what was granted):
9
+ * `iterate` is required, every other requested scope is optional on the consent page, and an app
10
+ * reads the granted set from `session.info().scopes` rather than assuming its request. */
11
+ const OAuthScope = z.enum([
12
+ "iterate",
13
+ "account",
14
+ "organizations:write"
15
+ ]);
16
+ const OAuthScopes = z.array(OAuthScope).transform((scopes) => [...new Set(["iterate", ...scopes])]);
17
+ /** What each scope means to the person — the consent page's copy, sent with `consent.describe` so
18
+ * the page renders what it is given: a scope added to `OAuthScope` is described here or it does
19
+ * not compile. */
20
+ const OAuthScopeDescriptions = {
21
+ iterate: {
22
+ title: "Read and make changes in the projects you grant it",
23
+ note: "Required — what the app is for.",
24
+ required: true
25
+ },
26
+ account: {
27
+ title: "See and end your sessions, and mint personal access tokens",
28
+ note: "Optional.",
29
+ required: false
30
+ },
31
+ "organizations:write": {
32
+ title: "See all your organizations and create new ones",
33
+ note: "Optional.",
34
+ required: false
35
+ }
36
+ };
37
+ //#endregion
38
+ export { OAuthScope, OAuthScopeDescriptions, OAuthScopes };
39
+
40
+ //# sourceMappingURL=oauth-scopes.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-scopes.mjs","names":[],"sources":["../src/oauth-scopes.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/** The platform's OAuth scopes — what an app may ask for and what a consent grants:\n * - `iterate` — reach the projects the person grants (every app; implied, always granted)\n * - `account` — manage the person's sessions and personal access tokens\n * - `organizations:write` — the person's organizations: list every one they belong to, create new ones\n * Consent is task-based (the shape Cloudflare's own OAuth consent took in August 2026: a client\n * requests a set, the person may deselect the optional ones, the token carries what was granted):\n * `iterate` is required, every other requested scope is optional on the consent page, and an app\n * reads the granted set from `session.info().scopes` rather than assuming its request. */\nexport const OAuthScope = z.enum([\"iterate\", \"account\", \"organizations:write\"]);\nexport type OAuthScope = z.infer<typeof OAuthScope>;\n\nexport const OAuthScopes = z\n .array(OAuthScope)\n .transform((scopes) => [...new Set([\"iterate\", ...scopes])]);\n\n/** A requested scope as the consent page shows it: its name, what it means to the person, and\n * whether they may untick it (`iterate` never). */\nexport interface ConsentScope {\n name: OAuthScope;\n title: string;\n note: string;\n required: boolean;\n}\n\n/** What each scope means to the person — the consent page's copy, sent with `consent.describe` so\n * the page renders what it is given: a scope added to `OAuthScope` is described here or it does\n * not compile. */\nexport const OAuthScopeDescriptions: Record<OAuthScope, Omit<ConsentScope, \"name\">> = {\n iterate: {\n title: \"Read and make changes in the projects you grant it\",\n note: \"Required — what the app is for.\",\n required: true,\n },\n account: {\n title: \"See and end your sessions, and mint personal access tokens\",\n note: \"Optional.\",\n required: false,\n },\n \"organizations:write\": {\n title: \"See all your organizations and create new ones\",\n note: \"Optional.\",\n required: false,\n },\n};\n"],"mappings":";;;;;;;;;;AAUA,MAAa,aAAa,EAAE,KAAK;CAAC;CAAW;CAAW;AAAqB,CAAC;AAG9E,MAAa,cAAc,EACxB,MAAM,UAAU,CAAC,CACjB,WAAW,WAAW,CAAC,GAAG,IAAI,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC;;;;AAc7D,MAAa,yBAAyE;CACpF,SAAS;EACP,OAAO;EACP,MAAM;EACN,UAAU;CACZ;CACA,SAAS;EACP,OAAO;EACP,MAAM;EACN,UAAU;CACZ;CACA,uBAAuB;EACrB,OAAO;EACP,MAAM;EACN,UAAU;CACZ;AACF"}
package/dist/oauth.mjs ADDED
@@ -0,0 +1,41 @@
1
+ import { OAuthScopes } from "./oauth-scopes.mjs";
2
+ import * as oauth from "oauth4webapi";
3
+ //#region src/client/oauth.ts
4
+ /** The platform's issuer as oauth4webapi's authorization server: its endpoints under `/oauth2`, and
5
+ * the `iss` it adds to every authorization response (RFC 9207), which `validateAuthResponse` then
6
+ * requires. The same description for an app's browser session and the CLI. */
7
+ function authorizationServer(issuer) {
8
+ return {
9
+ issuer,
10
+ authorization_endpoint: `${issuer}/oauth2/auth`,
11
+ token_endpoint: `${issuer}/oauth2/token`,
12
+ registration_endpoint: `${issuer}/oauth2/register`,
13
+ authorization_response_iss_parameter_supported: true
14
+ };
15
+ }
16
+ /** The same code/PKCE parameters for browser login and a console-minted token. */
17
+ async function authorizationCodeRequest(input) {
18
+ const verifier = oauth.generateRandomCodeVerifier();
19
+ const challenge = await oauth.calculatePKCECodeChallenge(verifier);
20
+ const state = oauth.generateRandomState();
21
+ const url = new URL("/oauth2/auth", input.issuer);
22
+ url.search = new URLSearchParams({
23
+ response_type: "code",
24
+ client_id: input.clientId,
25
+ redirect_uri: input.redirectUri,
26
+ scope: OAuthScopes.parse(input.scopes || []).join(" "),
27
+ state,
28
+ code_challenge: challenge,
29
+ code_challenge_method: "S256"
30
+ }).toString();
31
+ for (const resource of input.resources) url.searchParams.append("resource", resource);
32
+ return {
33
+ url,
34
+ state,
35
+ verifier
36
+ };
37
+ }
38
+ //#endregion
39
+ export { authorizationCodeRequest, authorizationServer };
40
+
41
+ //# sourceMappingURL=oauth.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.mjs","names":[],"sources":["../src/client/oauth.ts"],"sourcesContent":["import * as oauth from \"oauth4webapi\";\nimport { OAuthScopes } from \"../oauth-scopes.ts\";\n\n/** The platform's issuer as oauth4webapi's authorization server: its endpoints under `/oauth2`, and\n * the `iss` it adds to every authorization response (RFC 9207), which `validateAuthResponse` then\n * requires. The same description for an app's browser session and the CLI. */\nexport function authorizationServer(issuer: string): oauth.AuthorizationServer {\n return {\n issuer,\n authorization_endpoint: `${issuer}/oauth2/auth`,\n token_endpoint: `${issuer}/oauth2/token`,\n registration_endpoint: `${issuer}/oauth2/register`,\n authorization_response_iss_parameter_supported: true,\n };\n}\n\n/** The same code/PKCE parameters for browser login and a console-minted token. */\nexport async function authorizationCodeRequest(input: {\n issuer: string;\n clientId: string;\n redirectUri: string;\n resources: string[];\n scopes?: string[];\n}) {\n const verifier = oauth.generateRandomCodeVerifier();\n const challenge = await oauth.calculatePKCECodeChallenge(verifier);\n const state = oauth.generateRandomState();\n const url = new URL(\"/oauth2/auth\", input.issuer);\n url.search = new URLSearchParams({\n response_type: \"code\",\n client_id: input.clientId,\n redirect_uri: input.redirectUri,\n scope: OAuthScopes.parse(input.scopes || []).join(\" \"),\n state,\n code_challenge: challenge,\n code_challenge_method: \"S256\",\n }).toString();\n for (const resource of input.resources) url.searchParams.append(\"resource\", resource);\n return { url, state, verifier };\n}\n"],"mappings":";;;;;;AAMA,SAAgB,oBAAoB,QAA2C;CAC7E,OAAO;EACL;EACA,wBAAwB,GAAG,OAAO;EAClC,gBAAgB,GAAG,OAAO;EAC1B,uBAAuB,GAAG,OAAO;EACjC,gDAAgD;CAClD;AACF;;AAGA,eAAsB,yBAAyB,OAM5C;CACD,MAAM,WAAW,MAAM,2BAA2B;CAClD,MAAM,YAAY,MAAM,MAAM,2BAA2B,QAAQ;CACjE,MAAM,QAAQ,MAAM,oBAAoB;CACxC,MAAM,MAAM,IAAI,IAAI,gBAAgB,MAAM,MAAM;CAChD,IAAI,SAAS,IAAI,gBAAgB;EAC/B,eAAe;EACf,WAAW,MAAM;EACjB,cAAc,MAAM;EACpB,OAAO,YAAY,MAAM,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;EACrD;EACA,gBAAgB;EAChB,uBAAuB;CACzB,CAAC,CAAC,CAAC,SAAS;CACZ,KAAK,MAAM,YAAY,MAAM,WAAW,IAAI,aAAa,OAAO,YAAY,QAAQ;CACpF,OAAO;EAAE;EAAK;EAAO;CAAS;AAChC"}
@@ -0,0 +1,8 @@
1
+ /** Who is acting: a stable actor id (the control plane's user id) and, when known, an email. */
2
+ export type Principal = {
3
+ actor: string;
4
+ email?: string;
5
+ };
6
+ /** The header the edge sets on a Request it forwards on a principal's behalf — the ingress after
7
+ * the cookie check, a session's terminal `fetch` — and strips from every inbound Request. */
8
+ export declare const ITX_PRINCIPAL_HEADER = "x-itx-principal";
@@ -0,0 +1,8 @@
1
+ //#region src/principal.ts
2
+ /** The header the edge sets on a Request it forwards on a principal's behalf — the ingress after
3
+ * the cookie check, a session's terminal `fetch` — and strips from every inbound Request. */
4
+ const ITX_PRINCIPAL_HEADER = "x-itx-principal";
5
+ //#endregion
6
+ export { ITX_PRINCIPAL_HEADER };
7
+
8
+ //# sourceMappingURL=principal.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"principal.mjs","names":[],"sources":["../src/principal.ts"],"sourcesContent":["// principal.ts — WHO user code is acting for, as the platform tells it: the verified `Principal`\n// (`itx.whoami()`, `source.principal` on an event) and the header the platform stamps it in on a\n// Request it forwards to a project's worker (sdk/auth.ts reads it). How the platform admits and\n// carries a caller, and signs its own tokens, stays in the platform.\n\n/** Who is acting: a stable actor id (the control plane's user id) and, when known, an email. */\nexport type Principal = { actor: string; email?: string };\n\n/** The header the edge sets on a Request it forwards on a principal's behalf — the ingress after\n * the cookie check, a session's terminal `fetch` — and strips from every inbound Request. */\nexport const ITX_PRINCIPAL_HEADER = \"x-itx-principal\";\n"],"mappings":";;;AAUA,MAAa,uBAAuB"}
@@ -0,0 +1,58 @@
1
+ /** THE ROUTING SLUG a project host named (`notes` for `notes--<project>.<hostname>`), as the project's
2
+ * config worker `fetch` reads it; absent on the apex. Written only by the platform: the edge sets or
3
+ * deletes it on every project-host request, and the context DO deletes it from every other
4
+ * expression fetch, so neither a visitor nor loaded code can pick a routing slug. */
5
+ export declare const ITERATE_ROUTING_SLUG_HEADER = "x-iterate-routing-slug";
6
+ /** How projects are reached over HTTP; null ⇒ no ingress (`/api` and `/mcp` still answer). */
7
+ export type IngressRouting = {
8
+ type: "subdomains";
9
+ hostname: string;
10
+ } | {
11
+ type: "paths";
12
+ } | null;
13
+ /** What a request names: the project (its slug, as written — whether it EXISTS is the directory's
14
+ * answer), the routing slug (null ⇒ the apex; either way the project's config worker answers), and
15
+ * the path prefix the edge strips before the config worker sees the URL ("" under subdomains;
16
+ * "/projects/<project>" or "/projects/<project>/<routingSlug>" under paths). */
17
+ export type ProjectAddress = {
18
+ project: string;
19
+ routingSlug: string | null;
20
+ basePath: string;
21
+ };
22
+ /** The project + routing slug `url` names under `routing`, or null when it names none. Pure. */
23
+ export declare function projectAddressOf(routing: IngressRouting, url: URL, platformOrigin: string): ProjectAddress | null;
24
+ /** A PROJECT WILDCARD — an owned zone served as one project's apex (apps/os
25
+ * `urls.projectWildcard`, `{ hostname: "iterate.com", project: "iterate" }`): the zone's apex and
26
+ * every first-level name under it but the excluded ones, in the apex shape, `routingSlug: null`, so the
27
+ * project's config worker `fetch` answers exactly as it does on `<project>.<hostname>`. Null for
28
+ * anything else. Case and a trailing dot are forgiven. Pure. */
29
+ export declare function projectWildcardHostOf(hostname: string, wildcard: {
30
+ hostname: string;
31
+ project: string;
32
+ excludedHostnames?: string[];
33
+ } | undefined): {
34
+ routingSlug: null;
35
+ project: string;
36
+ } | null;
37
+ /** A PROJECT'S OWN HOSTNAME — `iterate.example.com`, added by the project (apps/os
38
+ * project/custom-hostnames.ts) — is that project's apex, and one label under it names a routing
39
+ * slug: `notes.iterate.example.com` carries `notes`, as `notes--<project>.<hostname>` does. The
40
+ * hostnames a request's host could be a project's own hostname for, most specific first: the host
41
+ * itself (the apex), then its parent with the first label as the routing slug. The caller
42
+ * looks them up in that order; the first a project holds wins. Case and a trailing dot are
43
+ * forgiven. Pure. */
44
+ export declare function customHostnameCandidatesOf(host: string): {
45
+ hostname: string;
46
+ routingSlug: string | null;
47
+ }[];
48
+ /** The URL of `routingSlug` (null ⇒ the apex) in `project` under `routing`, at `path` (default "/",
49
+ * must start with "/"). Null when there is no ingress, or when the result would not parse back to
50
+ * the same address (a bad slug; a `path` that climbs out of its routing slug). subdomains: the
51
+ * protocol and port are `platformOrigin`'s (local dev is `http://localhost:8788`, so
52
+ * `http://<routingSlug>--<project>.localhost:8788/…`); paths:
53
+ * `<platformOrigin>/projects/<project>[/<routingSlug>]<path>`. Pure. */
54
+ export declare function projectUrlOf(routing: IngressRouting, platformOrigin: string, target: {
55
+ project: string;
56
+ routingSlug?: string | null;
57
+ path?: string;
58
+ }): URL | null;
@@ -0,0 +1,104 @@
1
+ //#region src/project-ingress.ts
2
+ /** THE ROUTING SLUG a project host named (`notes` for `notes--<project>.<hostname>`), as the project's
3
+ * config worker `fetch` reads it; absent on the apex. Written only by the platform: the edge sets or
4
+ * deletes it on every project-host request, and the context DO deletes it from every other
5
+ * expression fetch, so neither a visitor nor loaded code can pick a routing slug. */
6
+ const ITERATE_ROUTING_SLUG_HEADER = "x-iterate-routing-slug";
7
+ /** A DNS label: lowercase letters and digits, single hyphens inside. */
8
+ const DNS_LABEL = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9
+ /** A routing slug: a DNS label starting with a letter. */
10
+ const ROUTING_SLUG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
11
+ /** The labels `host` has under `hostname` — `site--p.iterate.app` ⇒ `["site--p"]` — lowercased, a
12
+ * trailing dot (a fully-qualified Host, `site--p.base.`) dropped; null when `host` is not under
13
+ * `hostname` at all. */
14
+ function labelsUnder(host, hostname) {
15
+ const name = host.toLowerCase().replace(/\.$/, "");
16
+ const suffix = `.${hostname.toLowerCase()}`;
17
+ return name.endsWith(suffix) ? name.slice(0, -suffix.length).split(".") : null;
18
+ }
19
+ /** The project + routing slug `url` names under `routing`, or null when it names none. Pure. */
20
+ function projectAddressOf(routing, url, platformOrigin) {
21
+ if (!routing) return null;
22
+ if (routing.type === "subdomains") {
23
+ const labels = labelsUnder(url.hostname, routing.hostname);
24
+ if (!labels || labels.length > 2) return null;
25
+ const [first, second] = labels;
26
+ const separator = first.startsWith("xn--") ? -1 : first.indexOf("--");
27
+ const [routingSlug, project] = second !== void 0 ? [first, second] : separator === -1 ? [null, first] : [first.slice(0, separator), first.slice(separator + 2)];
28
+ if (!DNS_LABEL.test(project) || routingSlug !== null && !ROUTING_SLUG.test(routingSlug)) return null;
29
+ return {
30
+ routingSlug,
31
+ project,
32
+ basePath: ""
33
+ };
34
+ }
35
+ if (url.origin !== new URL(platformOrigin).origin) return null;
36
+ const [, prefix, project = "", routingSlug] = url.pathname.split("/");
37
+ if (prefix !== "projects" || !DNS_LABEL.test(project)) return null;
38
+ if (routingSlug === void 0 || routingSlug === "") return {
39
+ routingSlug: null,
40
+ project,
41
+ basePath: `/projects/${project}`
42
+ };
43
+ if (!ROUTING_SLUG.test(routingSlug)) return null;
44
+ return {
45
+ routingSlug,
46
+ project,
47
+ basePath: `/projects/${project}/${routingSlug}`
48
+ };
49
+ }
50
+ /** A PROJECT WILDCARD — an owned zone served as one project's apex (apps/os
51
+ * `urls.projectWildcard`, `{ hostname: "iterate.com", project: "iterate" }`): the zone's apex and
52
+ * every first-level name under it but the excluded ones, in the apex shape, `routingSlug: null`, so the
53
+ * project's config worker `fetch` answers exactly as it does on `<project>.<hostname>`. Null for
54
+ * anything else. Case and a trailing dot are forgiven. Pure. */
55
+ function projectWildcardHostOf(hostname, wildcard) {
56
+ if (!wildcard) return null;
57
+ const normalized = hostname.toLowerCase().replace(/\.$/, "");
58
+ if (wildcard.excludedHostnames?.includes(normalized)) return null;
59
+ const suffix = `.${wildcard.hostname}`;
60
+ const label = normalized.endsWith(suffix) ? normalized.slice(0, -suffix.length) : null;
61
+ return normalized === wildcard.hostname || label && !label.includes(".") ? {
62
+ routingSlug: null,
63
+ project: wildcard.project
64
+ } : null;
65
+ }
66
+ /** A PROJECT'S OWN HOSTNAME — `iterate.example.com`, added by the project (apps/os
67
+ * project/custom-hostnames.ts) — is that project's apex, and one label under it names a routing
68
+ * slug: `notes.iterate.example.com` carries `notes`, as `notes--<project>.<hostname>` does. The
69
+ * hostnames a request's host could be a project's own hostname for, most specific first: the host
70
+ * itself (the apex), then its parent with the first label as the routing slug. The caller
71
+ * looks them up in that order; the first a project holds wins. Case and a trailing dot are
72
+ * forgiven. Pure. */
73
+ function customHostnameCandidatesOf(host) {
74
+ const hostname = host.toLowerCase().replace(/\.$/, "");
75
+ const dot = hostname.indexOf(".");
76
+ const [routingSlug, parent] = [hostname.slice(0, dot), hostname.slice(dot + 1)];
77
+ return [{
78
+ hostname,
79
+ routingSlug: null
80
+ }, ...dot > 0 && parent.includes(".") && ROUTING_SLUG.test(routingSlug) ? [{
81
+ hostname: parent,
82
+ routingSlug
83
+ }] : []];
84
+ }
85
+ /** The URL of `routingSlug` (null ⇒ the apex) in `project` under `routing`, at `path` (default "/",
86
+ * must start with "/"). Null when there is no ingress, or when the result would not parse back to
87
+ * the same address (a bad slug; a `path` that climbs out of its routing slug). subdomains: the
88
+ * protocol and port are `platformOrigin`'s (local dev is `http://localhost:8788`, so
89
+ * `http://<routingSlug>--<project>.localhost:8788/…`); paths:
90
+ * `<platformOrigin>/projects/<project>[/<routingSlug>]<path>`. Pure. */
91
+ function projectUrlOf(routing, platformOrigin, target) {
92
+ if (!routing) return null;
93
+ const path = target.path || "/";
94
+ if (!path.startsWith("/")) throw new Error(`projectUrlOf: path must start with "/": ${path}`);
95
+ const routingSlug = target.routingSlug || null;
96
+ const origin = new URL(platformOrigin);
97
+ const url = routing.type === "subdomains" ? new URL(path, `${origin.protocol}//${routingSlug ? `${routingSlug}--` : ""}${target.project}.${routing.hostname}${origin.port ? `:${origin.port}` : ""}`) : new URL(`/projects/${target.project}${routingSlug ? `/${routingSlug}` : ""}${path}`, origin.origin);
98
+ const parsed = projectAddressOf(routing, url, platformOrigin);
99
+ return parsed && parsed.project === target.project && parsed.routingSlug === routingSlug ? url : null;
100
+ }
101
+ //#endregion
102
+ export { ITERATE_ROUTING_SLUG_HEADER, customHostnameCandidatesOf, projectAddressOf, projectUrlOf, projectWildcardHostOf };
103
+
104
+ //# sourceMappingURL=project-ingress.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-ingress.mjs","names":[],"sources":["../src/project-ingress.ts"],"sourcesContent":["// project-ingress.ts — HOW PROJECTS ARE REACHED OVER HTTP, both directions in ONE pure file. A\n// deployment's `urls.ingressRouting` (apps/os app-config.ts) names the mechanism; `projectAddressOf`\n// parses a request's URL into the project and routing slug it names, `projectUrlOf` composes the URL\n// of a routing slug in a project. The platform's edge parses; the platform, the dash and an app\n// compose — one implementation, table-tested to round-trip (project-ingress.test.ts). No imports.\n//\n// The edge picks the PROJECT only: every host of a project reaches the project's config worker\n// `fetch`, with the routing slug in `x-iterate-routing-slug` (absent on the apex), and the config\n// worker routes on it in plain code.\n//\n// subdomains `<routingSlug>--<project>.<hostname>`, `<routingSlug>.<project>.<hostname>`, the apex\n// `<project>.<hostname>` — every routing slug its own origin, under one wildcard on `hostname`.\n// paths `<platformOrigin>/projects/<project>/<routingSlug>/…`, the apex `<platformOrigin>/projects/<project>/`\n// — one origin (workers.dev has no wildcard), every project under `/projects/` so the\n// platform's own paths (`/api`, `/mcp`, `/login`, …) need no reserved list; the edge\n// sandboxes what a project serves.\n\n/** THE ROUTING SLUG a project host named (`notes` for `notes--<project>.<hostname>`), as the project's\n * config worker `fetch` reads it; absent on the apex. Written only by the platform: the edge sets or\n * deletes it on every project-host request, and the context DO deletes it from every other\n * expression fetch, so neither a visitor nor loaded code can pick a routing slug. */\nexport const ITERATE_ROUTING_SLUG_HEADER = \"x-iterate-routing-slug\";\n\n/** How projects are reached over HTTP; null ⇒ no ingress (`/api` and `/mcp` still answer). */\nexport type IngressRouting = { type: \"subdomains\"; hostname: string } | { type: \"paths\" } | null;\n\n/** What a request names: the project (its slug, as written — whether it EXISTS is the directory's\n * answer), the routing slug (null ⇒ the apex; either way the project's config worker answers), and\n * the path prefix the edge strips before the config worker sees the URL (\"\" under subdomains;\n * \"/projects/<project>\" or \"/projects/<project>/<routingSlug>\" under paths). */\nexport type ProjectAddress = { project: string; routingSlug: string | null; basePath: string };\n\n/** A DNS label: lowercase letters and digits, single hyphens inside. */\nconst DNS_LABEL = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n/** A routing slug: a DNS label starting with a letter. */\nconst ROUTING_SLUG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;\n\n/** The labels `host` has under `hostname` — `site--p.iterate.app` ⇒ `[\"site--p\"]` — lowercased, a\n * trailing dot (a fully-qualified Host, `site--p.base.`) dropped; null when `host` is not under\n * `hostname` at all. */\nfunction labelsUnder(host: string, hostname: string): string[] | null {\n const name = host.toLowerCase().replace(/\\.$/, \"\");\n const suffix = `.${hostname.toLowerCase()}`;\n return name.endsWith(suffix) ? name.slice(0, -suffix.length).split(\".\") : null;\n}\n\n/** The project + routing slug `url` names under `routing`, or null when it names none. Pure. */\nexport function projectAddressOf(\n routing: IngressRouting,\n url: URL,\n platformOrigin: string,\n): ProjectAddress | null {\n if (!routing) return null;\n if (routing.type === \"subdomains\") {\n const labels = labelsUnder(url.hostname, routing.hostname);\n if (!labels || labels.length > 2) return null; // deeper than `<routingSlug>.<project>` is not a project host\n const [first, second] = labels as [string, string?];\n const separator = first.startsWith(\"xn--\") ? -1 : first.indexOf(\"--\"); // `xn--…` is an IDN label (punycode), never `<routingSlug>--<project>`\n const [routingSlug, project] =\n // oxlint-disable-next-line iterate/simple-truthiness-check -- a PRESENT-but-empty second label (`<routingSlug>..<base>`) is the `<routingSlug>.<project>` shape (rejected below by DNS_LABEL), not the single-label `<project>` shape a truthiness check would route it to\n second !== undefined\n ? [first, second] // `<routingSlug>.<project>`\n : separator === -1\n ? [null, first] // the apex, `<project>`\n : [first.slice(0, separator), first.slice(separator + 2)]; // `<routingSlug>--<project>`\n // oxlint-disable-next-line iterate/simple-truthiness-check -- an empty routing slug (`--<project>.<base>`) must still be rejected by ROUTING_SLUG; truthiness would skip the check and admit it\n if (!DNS_LABEL.test(project) || (routingSlug !== null && !ROUTING_SLUG.test(routingSlug)))\n return null;\n return { routingSlug, project, basePath: \"\" };\n }\n if (url.origin !== new URL(platformOrigin).origin) return null;\n const [, prefix, project = \"\", routingSlug] = url.pathname.split(\"/\");\n if (prefix !== \"projects\" || !DNS_LABEL.test(project)) return null;\n // oxlint-disable-next-line iterate/simple-truthiness-check -- `/projects/<project>` and `/projects/<project>/` are both the apex; a present-but-empty next segment is not a routing slug\n if (routingSlug === undefined || routingSlug === \"\")\n return { routingSlug: null, project, basePath: `/projects/${project}` };\n if (!ROUTING_SLUG.test(routingSlug)) return null;\n return { routingSlug, project, basePath: `/projects/${project}/${routingSlug}` };\n}\n\n/** A PROJECT WILDCARD — an owned zone served as one project's apex (apps/os\n * `urls.projectWildcard`, `{ hostname: \"iterate.com\", project: \"iterate\" }`): the zone's apex and\n * every first-level name under it but the excluded ones, in the apex shape, `routingSlug: null`, so the\n * project's config worker `fetch` answers exactly as it does on `<project>.<hostname>`. Null for\n * anything else. Case and a trailing dot are forgiven. Pure. */\nexport function projectWildcardHostOf(\n hostname: string,\n wildcard: { hostname: string; project: string; excludedHostnames?: string[] } | undefined,\n): { routingSlug: null; project: string } | null {\n if (!wildcard) return null;\n const normalized = hostname.toLowerCase().replace(/\\.$/, \"\");\n if (wildcard.excludedHostnames?.includes(normalized)) return null;\n const suffix = `.${wildcard.hostname}`;\n const label = normalized.endsWith(suffix) ? normalized.slice(0, -suffix.length) : null;\n return normalized === wildcard.hostname || (label && !label.includes(\".\"))\n ? { routingSlug: null, project: wildcard.project }\n : null;\n}\n\n/** A PROJECT'S OWN HOSTNAME — `iterate.example.com`, added by the project (apps/os\n * project/custom-hostnames.ts) — is that project's apex, and one label under it names a routing\n * slug: `notes.iterate.example.com` carries `notes`, as `notes--<project>.<hostname>` does. The\n * hostnames a request's host could be a project's own hostname for, most specific first: the host\n * itself (the apex), then its parent with the first label as the routing slug. The caller\n * looks them up in that order; the first a project holds wins. Case and a trailing dot are\n * forgiven. Pure. */\nexport function customHostnameCandidatesOf(\n host: string,\n): { hostname: string; routingSlug: string | null }[] {\n const hostname = host.toLowerCase().replace(/\\.$/, \"\");\n const dot = hostname.indexOf(\".\");\n const [routingSlug, parent] = [hostname.slice(0, dot), hostname.slice(dot + 1)];\n return [\n { hostname, routingSlug: null },\n ...(dot > 0 && parent.includes(\".\") && ROUTING_SLUG.test(routingSlug)\n ? [{ hostname: parent, routingSlug }]\n : []),\n ];\n}\n\n/** The URL of `routingSlug` (null ⇒ the apex) in `project` under `routing`, at `path` (default \"/\",\n * must start with \"/\"). Null when there is no ingress, or when the result would not parse back to\n * the same address (a bad slug; a `path` that climbs out of its routing slug). subdomains: the\n * protocol and port are `platformOrigin`'s (local dev is `http://localhost:8788`, so\n * `http://<routingSlug>--<project>.localhost:8788/…`); paths:\n * `<platformOrigin>/projects/<project>[/<routingSlug>]<path>`. Pure. */\nexport function projectUrlOf(\n routing: IngressRouting,\n platformOrigin: string,\n target: { project: string; routingSlug?: string | null; path?: string },\n): URL | null {\n if (!routing) return null;\n const path = target.path || \"/\";\n if (!path.startsWith(\"/\")) throw new Error(`projectUrlOf: path must start with \"/\": ${path}`);\n const routingSlug = target.routingSlug || null;\n const origin = new URL(platformOrigin);\n const url =\n routing.type === \"subdomains\"\n ? new URL(\n path,\n `${origin.protocol}//${routingSlug ? `${routingSlug}--` : \"\"}${target.project}.${routing.hostname}${origin.port ? `:${origin.port}` : \"\"}`,\n )\n : new URL(\n `/projects/${target.project}${routingSlug ? `/${routingSlug}` : \"\"}${path}`,\n origin.origin,\n );\n const parsed = projectAddressOf(routing, url, platformOrigin);\n return parsed && parsed.project === target.project && parsed.routingSlug === routingSlug\n ? url\n : null;\n}\n"],"mappings":"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"}
package/dist/react.mjs ADDED
@@ -0,0 +1,285 @@
1
+ import { connectLiveState } from "./client.mjs";
2
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ //#region src/client/react.tsx
4
+ /** @jsxImportSource react */
5
+ /** Subscribe to a producer's live state and render its latest value. Pass a ready `itx` (a capnweb
6
+ * `api.authenticate(credentials).user` or `.projects.get(id)`), the producer's `key`, and a `readSeed`
7
+ * thunk that reads `{rev, state}` (`() => itx.invoke("itx.facets.get('slug').liveSnapshot()")`).
8
+ * Re-subscribes when the session, `key`, or `name` changes; unmount (and every re-subscribe)
9
+ * disposes the previous server-side subscription. */
10
+ function useLiveState(itx, opts) {
11
+ const [store, setStore] = useState();
12
+ const [status, setStatus] = useState("connecting");
13
+ const [error, setError] = useState();
14
+ const readSeedRef = useRef(opts.readSeed);
15
+ readSeedRef.current = opts.readSeed;
16
+ useEffect(() => {
17
+ setStore(void 0);
18
+ setStatus("connecting");
19
+ setError(void 0);
20
+ if (!itx) return;
21
+ const readSeed = readSeedRef.current;
22
+ let disposed = false;
23
+ let dispose;
24
+ const unmounted = new AbortController();
25
+ connectLiveState(itx, {
26
+ key: opts.key,
27
+ name: opts.name,
28
+ readSeed,
29
+ signal: unmounted.signal,
30
+ onResync: (r) => {
31
+ if (disposed) return;
32
+ if (r === "healed") {
33
+ setStatus("live");
34
+ setError(void 0);
35
+ } else {
36
+ setStatus("error");
37
+ setError(r.message);
38
+ }
39
+ }
40
+ }).then((conn) => {
41
+ dispose = conn.dispose;
42
+ if (disposed) {
43
+ conn.dispose();
44
+ return;
45
+ }
46
+ setStore(conn.store);
47
+ setStatus("live");
48
+ }, (e) => {
49
+ if (disposed) return;
50
+ setError(e instanceof Error ? e.message : String(e));
51
+ setStatus("error");
52
+ });
53
+ return () => {
54
+ disposed = true;
55
+ unmounted.abort();
56
+ dispose?.();
57
+ };
58
+ }, [
59
+ itx,
60
+ opts.key,
61
+ opts.name
62
+ ]);
63
+ return {
64
+ value: useSyncExternalStore(useCallback((cb) => store ? store.subscribe(cb) : () => {}, [store]), () => store?.get(), () => void 0),
65
+ rev: store?.rev() ?? null,
66
+ status,
67
+ error
68
+ };
69
+ }
70
+ /** A wire event (a capnweb proxy value or a plain object) as a `StreamEvent`, or null when
71
+ * it is not a committed row. Structural, not a schema: the transport validated it; this only refuses
72
+ * a shape the view cannot place (no offset, type or time). */
73
+ function toStreamEvent(raw) {
74
+ const value = JSON.parse(JSON.stringify(raw));
75
+ if (!value || typeof value.offset !== "number" || typeof value.type !== "string" || typeof value.createdAt !== "string") return null;
76
+ return value;
77
+ }
78
+ /** A named live state before its first seed lands — and before the effect that opens it has run. */
79
+ const LIVE_STATE_CONNECTING = {
80
+ value: void 0,
81
+ rev: null,
82
+ status: "connecting"
83
+ };
84
+ /** THE ITERATE CONTEXT, live — one hook, one stream subscription. THE LOG: subscribe to every
85
+ * committed event (or `consumes`) BEFORE the catch-up read, so nothing lands between the two;
86
+ * pushes and pages both dedupe into one map by offset; `caughtUp` once the read reached the head;
87
+ * `error` when the connect failed. Off that same log, THE PROCESSORS TABLE, re-read whenever the
88
+ * log grows a row-changing event (a subscription configured, halted or resumed — the table is core
89
+ * state, one call away, no push of its own), and WHO IS HERE: the rpc stubs lent right now
90
+ * (`itx.rpcStubs.list()` — physical, re-read at every new head, since presence changes are
91
+ * ephemeral facts) and, from the log, every principal that acted, newest first. And named facets'
92
+ * LIVE STATE, each seeded through `itx.facets.get('<name>').liveSnapshot()` — one entry per name,
93
+ * always. `liveState` OMITTED opens `core` (the core reduce answers under that name) plus every
94
+ * hosted facet in the processors table the hook holds, following the table as it loads and changes;
95
+ * `liveState` GIVEN is exactly the names to open, no implicit `core`. Re-connects when `itx`
96
+ * changes; unmount disposes every server-side subscription. */
97
+ function useIterateContext(itx, opts = {}) {
98
+ const [events, setEvents] = useState(() => /* @__PURE__ */ new Map());
99
+ const [caughtUp, setCaughtUp] = useState(false);
100
+ const [error, setError] = useState();
101
+ const consumesKey = JSON.stringify(opts.consumes || ["*"]);
102
+ useEffect(() => {
103
+ setEvents(/* @__PURE__ */ new Map());
104
+ setCaughtUp(false);
105
+ setError(void 0);
106
+ if (!itx) return;
107
+ let disposed = false;
108
+ const merge = (batch) => setEvents((held) => {
109
+ const next = new Map(held);
110
+ for (const raw of batch) {
111
+ const event = toStreamEvent(raw);
112
+ if (event) next.set(event.offset, event);
113
+ }
114
+ return next;
115
+ });
116
+ let subscription;
117
+ (async () => {
118
+ const handle = await itx.subscribe({
119
+ consumes: JSON.parse(consumesKey),
120
+ target: (batch) => !disposed && merge(batch)
121
+ });
122
+ if (disposed) {
123
+ handle[Symbol.dispose]();
124
+ return;
125
+ }
126
+ subscription = handle;
127
+ for (let after = 0;;) {
128
+ const page = await itx.readEvents(after, 500);
129
+ if (disposed) return;
130
+ merge(page.events);
131
+ if (page.atHead || page.scannedThroughOffset <= after) break;
132
+ after = page.scannedThroughOffset;
133
+ }
134
+ setCaughtUp(true);
135
+ })().catch((e) => !disposed && setError(e instanceof Error ? e.message : String(e)));
136
+ return () => {
137
+ disposed = true;
138
+ subscription?.[Symbol.dispose]();
139
+ };
140
+ }, [itx, consumesKey]);
141
+ const sorted = useMemo(() => [...events.values()].sort((a, b) => a.offset - b.offset), [events]);
142
+ const [table, setTable] = useState();
143
+ const [failure, setFailure] = useState();
144
+ useEffect(() => {
145
+ if (!itx) return;
146
+ let disposed = false;
147
+ Promise.resolve(itx.processors.list()).then((list) => {
148
+ if (disposed) return;
149
+ setTable({
150
+ itx,
151
+ rows: list
152
+ });
153
+ setFailure(void 0);
154
+ }, (e) => !disposed && setFailure({
155
+ itx,
156
+ message: e instanceof Error ? e.message : String(e)
157
+ }));
158
+ return () => {
159
+ disposed = true;
160
+ };
161
+ }, [itx, sorted.reduce((last, event) => event.type.startsWith("events.iterate.com/itx/subscription-") ? event.offset : last, 0)]);
162
+ const currentTable = itx && table?.itx === itx ? table : void 0;
163
+ const [census, setCensus] = useState();
164
+ useEffect(() => {
165
+ if (!itx) return;
166
+ let disposed = false;
167
+ Promise.resolve(itx.rpcStubs.list()).then((list) => !disposed && setCensus({
168
+ itx,
169
+ rpcStubs: list
170
+ }), () => void 0);
171
+ return () => {
172
+ disposed = true;
173
+ };
174
+ }, [itx, sorted.at(-1)?.offset ?? 0]);
175
+ const rpcStubs = itx && census?.itx === itx ? census.rpcStubs : [];
176
+ const actors = useMemo(() => {
177
+ const byActor = /* @__PURE__ */ new Map();
178
+ for (const event of sorted) {
179
+ const principal = event.source?.principal;
180
+ if (!principal) continue;
181
+ byActor.set(principal.actor, {
182
+ actor: principal.actor,
183
+ email: principal.email,
184
+ grant: event.source?.grant,
185
+ lastSeenAt: event.createdAt
186
+ });
187
+ }
188
+ return [...byActor.values()].sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
189
+ }, [sorted]);
190
+ const liveStateKey = JSON.stringify(opts.liveState || ["core", ...(currentTable?.rows || []).flatMap((row) => row.hostedFacet ? [row.hostedFacet.name] : [])]);
191
+ const [liveStates, setLiveStates] = useState();
192
+ useEffect(() => {
193
+ if (!itx) return;
194
+ const names = JSON.parse(liveStateKey);
195
+ if (names.length === 0) return;
196
+ let disposed = false;
197
+ const unmounted = new AbortController();
198
+ const disposers = [];
199
+ const patch = (name, change) => setLiveStates((held) => held && held.itx === itx && held.key === liveStateKey ? {
200
+ ...held,
201
+ entries: {
202
+ ...held.entries,
203
+ [name]: {
204
+ ...held.entries[name],
205
+ ...change
206
+ }
207
+ }
208
+ } : held);
209
+ setLiveStates({
210
+ itx,
211
+ key: liveStateKey,
212
+ entries: Object.fromEntries(names.map((name) => [name, LIVE_STATE_CONNECTING]))
213
+ });
214
+ for (const name of names) connectLiveState(itx, {
215
+ key: name,
216
+ readSeed: async () => await itx.invoke(`itx.facets.get('${name}').liveSnapshot()`),
217
+ signal: unmounted.signal,
218
+ onResync: (result) => {
219
+ if (disposed) return;
220
+ if (result === "healed") patch(name, {
221
+ status: "live",
222
+ error: void 0
223
+ });
224
+ else patch(name, {
225
+ status: "error",
226
+ error: result.message
227
+ });
228
+ }
229
+ }).then((connection) => {
230
+ if (disposed) {
231
+ connection.dispose();
232
+ return;
233
+ }
234
+ disposers.push(connection.dispose);
235
+ disposers.push(connection.store.subscribe(() => patch(name, {
236
+ value: connection.store.get(),
237
+ rev: connection.store.rev()
238
+ })));
239
+ patch(name, {
240
+ value: connection.store.get(),
241
+ rev: connection.store.rev(),
242
+ status: "live"
243
+ });
244
+ }, (e) => {
245
+ if (disposed) return;
246
+ patch(name, {
247
+ status: "error",
248
+ error: e instanceof Error ? e.message : String(e)
249
+ });
250
+ });
251
+ return () => {
252
+ disposed = true;
253
+ unmounted.abort();
254
+ for (const dispose of disposers) dispose();
255
+ };
256
+ }, [itx, liveStateKey]);
257
+ const liveState = useMemo(() => {
258
+ const names = JSON.parse(liveStateKey);
259
+ const held = itx && liveStates?.itx === itx && liveStates.key === liveStateKey ? liveStates.entries : {};
260
+ return Object.fromEntries(names.map((name) => [name, held[name] || LIVE_STATE_CONNECTING]));
261
+ }, [
262
+ itx,
263
+ liveStateKey,
264
+ liveStates
265
+ ]);
266
+ return {
267
+ events: sorted,
268
+ caughtUp,
269
+ error,
270
+ processors: {
271
+ rows: currentTable?.rows || [],
272
+ loaded: Boolean(currentTable),
273
+ error: itx && failure?.itx === itx ? failure.message : void 0
274
+ },
275
+ presence: {
276
+ actors,
277
+ rpcStubs
278
+ },
279
+ liveState
280
+ };
281
+ }
282
+ //#endregion
283
+ export { useIterateContext, useLiveState };
284
+
285
+ //# sourceMappingURL=react.mjs.map