iterate 0.3.0 → 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.
- package/README.md +164 -82
- package/dist/{next/api.d.ts → api.d.ts} +197 -33
- package/dist/{next/app-server.d.ts → app-server.d.ts} +7 -0
- package/dist/{next/app-server.mjs → app-server.mjs} +17 -15
- package/dist/app-server.mjs.map +1 -0
- package/dist/{next/app-session.mjs → app-session.mjs} +12 -15
- package/dist/app-session.mjs.map +1 -0
- package/dist/{next/app.mjs → app.mjs} +53 -14
- package/dist/app.mjs.map +1 -0
- package/dist/{next/client → client}/live-state.d.ts +11 -11
- package/dist/client/oauth.d.ts +17 -0
- package/dist/{next/client → client}/react.d.ts +10 -42
- package/dist/{next/client → client}/socket.d.ts +1 -0
- package/dist/client.mjs +156 -4
- package/dist/client.mjs.map +1 -0
- package/dist/{next/expression.d.ts → expression.d.ts} +11 -69
- package/dist/{next/expression.mjs → expression.mjs} +11 -109
- package/dist/expression.mjs.map +1 -0
- package/dist/lib-BWr-5mFO.mjs +36 -0
- package/dist/lib-BWr-5mFO.mjs.map +1 -0
- package/dist/{next/lib.d.ts → lib.d.ts} +16 -2
- package/dist/{next/lib.mjs → lib.mjs} +32 -3
- package/dist/lib.mjs.map +1 -0
- package/dist/node.d.ts +15 -3
- package/dist/node.mjs +36 -174
- package/dist/node.mjs.map +1 -1
- package/dist/{next/oauth-scopes.mjs → oauth-scopes.mjs} +1 -1
- package/dist/oauth-scopes.mjs.map +1 -0
- package/dist/{next/oauth.mjs → oauth.mjs} +14 -2
- package/dist/oauth.mjs.map +1 -0
- package/dist/principal.d.ts +8 -0
- package/dist/principal.mjs +8 -0
- package/dist/principal.mjs.map +1 -0
- package/dist/project-ingress.d.ts +58 -0
- package/dist/project-ingress.mjs +104 -0
- package/dist/project-ingress.mjs.map +1 -0
- package/dist/{next/react.mjs → react.mjs} +12 -12
- package/dist/react.mjs.map +1 -0
- package/dist/sdk/auth.d.ts +25 -0
- package/dist/sdk/index.d.ts +155 -0
- package/dist/sdk/record-pipelined-steps.d.ts +19 -0
- package/dist/sdk.mjs +245 -2
- package/dist/sdk.mjs.map +1 -0
- package/dist/{next/stream → stream}/processor.d.ts +28 -23
- package/dist/{next/stream → stream}/processor.mjs +48 -25
- package/dist/stream/processor.mjs.map +1 -0
- package/dist/{next/stream → stream}/run.d.ts +9 -6
- package/dist/{next/stream → stream}/run.mjs +13 -8
- package/dist/stream/run.mjs.map +1 -0
- package/dist/stream/test-support.d.ts +45 -0
- package/dist/stream/test-support.mjs +196 -0
- package/dist/stream/test-support.mjs.map +1 -0
- package/package.json +65 -219
- package/THIRD_PARTY_NOTICES.md +0 -55
- package/bin/iterate.js +0 -94
- package/dist/api-url-B6404M82.mjs +0 -17
- package/dist/api-url-B6404M82.mjs.map +0 -1
- package/dist/app-ref-BipL0feU.mjs +0 -35
- package/dist/app-ref-BipL0feU.mjs.map +0 -1
- package/dist/app-ref-C1CrgXqX.mjs +0 -7
- package/dist/app-ref-C1CrgXqX.mjs.map +0 -1
- package/dist/app-ref-DYai_om1.mjs +0 -7
- package/dist/app-ref-DYai_om1.mjs.map +0 -1
- package/dist/cli-D0c-pDL_.mjs +0 -1010
- package/dist/cli-D0c-pDL_.mjs.map +0 -1
- package/dist/client.d.ts +0 -3
- package/dist/cloudflare-BTm90gQ4.mjs +0 -951
- package/dist/cloudflare-BTm90gQ4.mjs.map +0 -1
- package/dist/contract-s4FW4eES.mjs +0 -309
- package/dist/contract-s4FW4eES.mjs.map +0 -1
- package/dist/document-review/index.d.ts +0 -5
- package/dist/document-review/types.d.ts +0 -107
- package/dist/document-review.mjs +0 -7015
- package/dist/document-review.mjs.map +0 -1
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs +0 -205
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +0 -1
- package/dist/idempotency-DleloJNt.mjs +0 -28
- package/dist/idempotency-DleloJNt.mjs.map +0 -1
- package/dist/index.d.mts +0 -5
- package/dist/index.mjs +0 -8
- package/dist/index.mjs.map +0 -1
- package/dist/itx/api-url.d.ts +0 -6
- package/dist/itx/itx-node-client.d.ts +0 -65
- package/dist/itx/itx-session.d.ts +0 -215
- package/dist/itx/owned-rpc-session.d.ts +0 -14
- package/dist/itx/query-client.d.ts +0 -10
- package/dist/itx-api.generated.d.ts +0 -6195
- package/dist/itx-session-sjud8GiT.mjs +0 -534
- package/dist/itx-session-sjud8GiT.mjs.map +0 -1
- package/dist/live-state-BJNqOwFw.mjs +0 -299
- package/dist/live-state-BJNqOwFw.mjs.map +0 -1
- package/dist/next/app-server.mjs.map +0 -1
- package/dist/next/app-session.mjs.map +0 -1
- package/dist/next/app.mjs.map +0 -1
- package/dist/next/client/oauth.d.ts +0 -12
- package/dist/next/client.mjs +0 -156
- package/dist/next/client.mjs.map +0 -1
- package/dist/next/expression.mjs.map +0 -1
- package/dist/next/lib.mjs.map +0 -1
- package/dist/next/oauth-scopes.mjs.map +0 -1
- package/dist/next/oauth.mjs.map +0 -1
- package/dist/next/principal.d.ts +0 -64
- package/dist/next/principal.mjs +0 -98
- package/dist/next/principal.mjs.map +0 -1
- package/dist/next/project-ingress.d.ts +0 -37
- package/dist/next/project-ingress.mjs +0 -75
- package/dist/next/project-ingress.mjs.map +0 -1
- package/dist/next/react.mjs.map +0 -1
- package/dist/next/sdk/auth.d.ts +0 -5
- package/dist/next/sdk/index.d.ts +0 -112
- package/dist/next/sdk.mjs +0 -139
- package/dist/next/sdk.mjs.map +0 -1
- package/dist/next/stream/processor.mjs.map +0 -1
- package/dist/next/stream/run.mjs.map +0 -1
- package/dist/next-node.d.ts +0 -15
- package/dist/next-node.mjs +0 -51
- package/dist/next-node.mjs.map +0 -1
- package/dist/processor-host-capabilities-BMFH3KTM.mjs +0 -56
- package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +0 -1
- package/dist/processors/cloudflare.d.ts +0 -3
- package/dist/processors/durable-object-processor-durability.d.ts +0 -79
- package/dist/processors/event-consumption-metrics.d.ts +0 -82
- package/dist/processors/idempotency.d.ts +0 -13
- package/dist/processors/index.d.ts +0 -12
- package/dist/processors/processor-contracts.d.ts +0 -342
- package/dist/processors/processor-facet.d.ts +0 -186
- package/dist/processors/processor-host-capabilities.d.ts +0 -60
- package/dist/processors/prompt-sections.d.ts +0 -17
- package/dist/processors/rpc-types.d.ts +0 -515
- package/dist/processors/schemas.d.ts +0 -102
- package/dist/processors/stream-handle.d.ts +0 -45
- package/dist/processors/stream-processor-keepalive.d.ts +0 -95
- package/dist/processors/stream-processor-registry.d.ts +0 -233
- package/dist/processors/stream-processor-runner.d.ts +0 -289
- package/dist/processors/stream-processor.d.ts +0 -339
- package/dist/processors/stream-runtime-metrics.d.ts +0 -107
- package/dist/processors/testing.d.ts +0 -302
- package/dist/processors-BoNyeBfQ.mjs +0 -10
- package/dist/processors-BoNyeBfQ.mjs.map +0 -1
- package/dist/processors-cloudflare.mjs +0 -3
- package/dist/processors-testing.mjs +0 -435
- package/dist/processors-testing.mjs.map +0 -1
- package/dist/processors.mjs +0 -52
- package/dist/processors.mjs.map +0 -1
- package/dist/protocol-DnK_f2m6.mjs +0 -251
- package/dist/protocol-DnK_f2m6.mjs.map +0 -1
- package/dist/sdk/capnweb/index.d.ts +0 -2
- package/dist/sdk/capnweb/live-state/compact.d.ts +0 -5
- package/dist/sdk/capnweb/live-state/diff.d.ts +0 -41
- package/dist/sdk/capnweb/live-state/engine.d.ts +0 -44
- package/dist/sdk/capnweb/live-state/index.d.ts +0 -41
- package/dist/sdk/capnweb/live-state/protocol.d.ts +0 -87
- package/dist/sdk/capnweb/live-state/retain.d.ts +0 -23
- package/dist/sdk/capnweb/live-state/store.d.ts +0 -20
- package/dist/sdk/capnweb/live-state/types.d.ts +0 -11
- package/dist/sdk/capnweb/react.d.ts +0 -45
- package/dist/sdk/capnweb/react.mjs +0 -316
- package/dist/sdk/capnweb/react.mjs.map +0 -1
- package/dist/sdk/capnweb.mjs +0 -4
- package/dist/sdk/itx/react.d.ts +0 -191
- package/dist/sdk/itx/react.mjs +0 -383
- package/dist/sdk/itx/react.mjs.map +0 -1
- package/dist/sdk-DMB-IM11.mjs +0 -933
- package/dist/sdk-DMB-IM11.mjs.map +0 -1
- package/dist/sdk.d.ts +0 -339
- package/dist/serve-itx.d.ts +0 -46
- package/dist/starter-apps/flake-dashboard/app-ref.d.ts +0 -31
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs +0 -1055
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/flake-dashboard/contract.d.ts +0 -4839
- package/dist/starter-apps/flake-dashboard/contract.mjs +0 -2
- package/dist/starter-apps/flake-dashboard/index.d.ts +0 -17
- package/dist/starter-apps/flake-dashboard/index.mjs +0 -56
- package/dist/starter-apps/flake-dashboard/index.mjs.map +0 -1
- package/dist/starter-apps/flake-dashboard/worker.d.ts +0 -4607
- package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +0 -8914
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs +0 -17987
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/github-ai-linter/contract.d.ts +0 -9193
- package/dist/starter-apps/github-ai-linter/index.d.ts +0 -10
- package/dist/starter-apps/github-ai-linter/index.mjs +0 -36
- package/dist/starter-apps/github-ai-linter/index.mjs.map +0 -1
- package/dist/starter-apps/github-ai-linter/prompt.d.ts +0 -13
- package/dist/starter-apps/github-ai-linter/review-bot.d.ts +0 -808
- package/dist/starter-apps/github-ai-linter/rules.d.ts +0 -34
- package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +0 -19
- package/dist/starter-apps/github-ai-linter/worker.d.ts +0 -19
- package/dist/starter-apps/github-ai-linter/worker.mjs +0 -947
- package/dist/starter-apps/github-ai-linter/worker.mjs.map +0 -1
- package/dist/starter-apps/guestbook/app-ref.d.ts +0 -27
- package/dist/starter-apps/guestbook/client.d.ts +0 -7
- package/dist/starter-apps/guestbook/client.mjs +0 -59
- package/dist/starter-apps/guestbook/configured-worker.mjs +0 -205
- package/dist/starter-apps/guestbook/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/guestbook/index.d.ts +0 -9
- package/dist/starter-apps/guestbook/index.mjs +0 -31
- package/dist/starter-apps/guestbook/index.mjs.map +0 -1
- package/dist/starter-apps/guestbook/processor.d.ts +0 -2267
- package/dist/starter-apps/guestbook/worker.d.ts +0 -26
- package/dist/starter-apps/guestbook/worker.mjs +0 -191
- package/dist/starter-apps/guestbook/worker.mjs.map +0 -1
- package/dist/starter-apps/media/configured-worker.mjs +0 -577
- package/dist/starter-apps/media/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/media/index.mjs +0 -36
- package/dist/starter-apps/media/index.mjs.map +0 -1
- package/dist/starter-apps/media/ref.mjs +0 -20
- package/dist/starter-apps/media/ref.mjs.map +0 -1
- package/dist/starter-apps/media/worker.mjs +0 -579
- package/dist/starter-apps/media/worker.mjs.map +0 -1
- package/dist/starter-apps/notes/configured-worker.mjs +0 -6134
- package/dist/starter-apps/notes/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/notes/index.mjs +0 -23
- package/dist/starter-apps/notes/index.mjs.map +0 -1
- package/dist/starter-apps/notes/ref.mjs +0 -21
- package/dist/starter-apps/notes/ref.mjs.map +0 -1
- package/dist/starter-apps/notes/worker.mjs +0 -427
- package/dist/starter-apps/notes/worker.mjs.map +0 -1
- package/dist/starter-apps/todo/client.mjs +0 -59
- package/dist/starter-apps/todo/configured-worker.mjs +0 -2864
- package/dist/starter-apps/todo/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/todo/index.d.ts +0 -8
- package/dist/starter-apps/todo/index.mjs +0 -29
- package/dist/starter-apps/todo/index.mjs.map +0 -1
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs +0 -2082
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +0 -1
- package/dist/usingCtx-mZx5nsAW.mjs +0 -11800
- package/dist/usingCtx-mZx5nsAW.mjs.map +0 -1
- package/dist/worker-ref-DZxPDmb_.mjs +0 -390
- package/dist/worker-ref-DZxPDmb_.mjs.map +0 -1
- package/dist/worker.d.mts +0 -33
- package/dist/worker.mjs +0 -18
- package/dist/worker.mjs.map +0 -1
- package/menubar/Iterate.entitlements +0 -12
- package/menubar/Iterate.swift +0 -914
- package/menubar/IterateIcon.swift +0 -145
- package/menubar/README.md +0 -28
- package/menubar/build-menubar-app.sh +0 -59
- /package/dist/{next/api.mjs → api.mjs} +0 -0
- /package/dist/{next/app-session.d.ts → app-session.d.ts} +0 -0
- /package/dist/{next/app.d.ts → app.d.ts} +0 -0
- /package/dist/{next/oauth-scopes.d.ts → oauth-scopes.d.ts} +0 -0
|
@@ -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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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { connectLiveState } from "./client.mjs";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/client/react.tsx
|
|
4
4
|
/** @jsxImportSource react */
|
|
5
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 `
|
|
6
|
+
* `api.authenticate(credentials).user` or `.projects.get(id)`), the producer's `key`, and a `readSeed`
|
|
7
7
|
* thunk that reads `{rev, state}` (`() => itx.invoke("itx.facets.get('slug').liveSnapshot()")`).
|
|
8
8
|
* Re-subscribes when the session, `key`, or `name` changes; unmount (and every re-subscribe)
|
|
9
9
|
* disposes the previous server-side subscription. */
|
|
@@ -11,21 +11,21 @@ function useLiveState(itx, opts) {
|
|
|
11
11
|
const [store, setStore] = useState();
|
|
12
12
|
const [status, setStatus] = useState("connecting");
|
|
13
13
|
const [error, setError] = useState();
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const readSeedRef = useRef(opts.readSeed);
|
|
15
|
+
readSeedRef.current = opts.readSeed;
|
|
16
16
|
useEffect(() => {
|
|
17
17
|
setStore(void 0);
|
|
18
18
|
setStatus("connecting");
|
|
19
19
|
setError(void 0);
|
|
20
20
|
if (!itx) return;
|
|
21
|
-
const
|
|
21
|
+
const readSeed = readSeedRef.current;
|
|
22
22
|
let disposed = false;
|
|
23
23
|
let dispose;
|
|
24
24
|
const unmounted = new AbortController();
|
|
25
25
|
connectLiveState(itx, {
|
|
26
26
|
key: opts.key,
|
|
27
27
|
name: opts.name,
|
|
28
|
-
|
|
28
|
+
readSeed,
|
|
29
29
|
signal: unmounted.signal,
|
|
30
30
|
onResync: (r) => {
|
|
31
31
|
if (disposed) return;
|
|
@@ -67,10 +67,10 @@ function useLiveState(itx, opts) {
|
|
|
67
67
|
error
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
-
/** A wire event (a capnweb proxy value or a plain object) as
|
|
70
|
+
/** A wire event (a capnweb proxy value or a plain object) as a `StreamEvent`, or null when
|
|
71
71
|
* it is not a committed row. Structural, not a schema: the transport validated it; this only refuses
|
|
72
72
|
* a shape the view cannot place (no offset, type or time). */
|
|
73
|
-
function
|
|
73
|
+
function toStreamEvent(raw) {
|
|
74
74
|
const value = JSON.parse(JSON.stringify(raw));
|
|
75
75
|
if (!value || typeof value.offset !== "number" || typeof value.type !== "string" || typeof value.createdAt !== "string") return null;
|
|
76
76
|
return value;
|
|
@@ -108,7 +108,7 @@ function useIterateContext(itx, opts = {}) {
|
|
|
108
108
|
const merge = (batch) => setEvents((held) => {
|
|
109
109
|
const next = new Map(held);
|
|
110
110
|
for (const raw of batch) {
|
|
111
|
-
const event =
|
|
111
|
+
const event = toStreamEvent(raw);
|
|
112
112
|
if (event) next.set(event.offset, event);
|
|
113
113
|
}
|
|
114
114
|
return next;
|
|
@@ -158,11 +158,11 @@ function useIterateContext(itx, opts = {}) {
|
|
|
158
158
|
return () => {
|
|
159
159
|
disposed = true;
|
|
160
160
|
};
|
|
161
|
-
}, [itx, sorted.reduce((last, event) => event.type.startsWith("events.iterate.com/
|
|
161
|
+
}, [itx, sorted.reduce((last, event) => event.type.startsWith("events.iterate.com/itx/subscription-") ? event.offset : last, 0)]);
|
|
162
162
|
const currentTable = itx && table?.itx === itx ? table : void 0;
|
|
163
163
|
const [census, setCensus] = useState();
|
|
164
164
|
useEffect(() => {
|
|
165
|
-
if (!itx
|
|
165
|
+
if (!itx) return;
|
|
166
166
|
let disposed = false;
|
|
167
167
|
Promise.resolve(itx.rpcStubs.list()).then((list) => !disposed && setCensus({
|
|
168
168
|
itx,
|
|
@@ -213,7 +213,7 @@ function useIterateContext(itx, opts = {}) {
|
|
|
213
213
|
});
|
|
214
214
|
for (const name of names) connectLiveState(itx, {
|
|
215
215
|
key: name,
|
|
216
|
-
|
|
216
|
+
readSeed: async () => await itx.invoke(`itx.facets.get('${name}').liveSnapshot()`),
|
|
217
217
|
signal: unmounted.signal,
|
|
218
218
|
onResync: (result) => {
|
|
219
219
|
if (disposed) return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.mjs","names":[],"sources":["../src/client/react.tsx"],"sourcesContent":["/** @jsxImportSource react */\n// client/react.tsx — the React binding for live state, shared by every UI. `useLiveState` subscribes a component to a producer's live\n// state (a processor slug, a mini-app key), seeds from it, and re-renders on every synced\n// delta via `useSyncExternalStore` over the LiveStateStore. The transport and the store\n// (client/live-state.ts) stay framework-free, so this is the ONE file that imports React.\n//\n// Kept to the one shape a UI or test needs — no reconnect/backoff/ping-watchdog (that policy belongs\n// to whoever owns the capnweb session; here the caller passes a ready `itx`).\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { SubscriptionListEntry } from \"../api.ts\";\nimport type { StreamEvent } from \"../stream/processor.ts\";\nimport {\n connectLiveState,\n type LiveStateItx,\n type LiveStateSeed,\n type LiveStateStore,\n} from \"./live-state.ts\";\n\nexport type LiveStateStatus = \"connecting\" | \"live\" | \"error\";\n\n/** One live state as a component reads it: the latest value (undefined until the first seed lands),\n * the revision it is at, whether its subscription is connecting, live or failed, and the failure. */\nexport type LiveStateResult<S = unknown> = {\n value: S | undefined;\n rev: number | null;\n status: LiveStateStatus;\n error?: string;\n};\n\n/** Subscribe to a producer's live state and render its latest value. Pass a ready `itx` (a capnweb\n * `api.authenticate(credentials).user` or `.projects.get(id)`), the producer's `key`, and a `readSeed`\n * thunk that reads `{rev, state}` (`() => itx.invoke(\"itx.facets.get('slug').liveSnapshot()\")`).\n * Re-subscribes when the session, `key`, or `name` changes; unmount (and every re-subscribe)\n * disposes the previous server-side subscription. */\nexport function useLiveState<S>(\n itx: LiveStateItx | undefined,\n opts: { key: string; name?: string; readSeed: () => Promise<LiveStateSeed<S>> },\n): LiveStateResult<S> {\n const [store, setStore] = useState<LiveStateStore<S> | undefined>();\n const [status, setStatus] = useState<LiveStateStatus>(\"connecting\");\n const [error, setError] = useState<string | undefined>();\n // The readSeed thunk is a fresh arrow every render; hold the latest so the effect need not re-run per\n // render. The effect SNAPSHOTS it at connect time, so an old subscription's gap heal can never\n // read a NEWER key's seed (cross-key contamination after a key/session switch).\n const readSeedRef = useRef(opts.readSeed);\n readSeedRef.current = opts.readSeed;\n\n useEffect(() => {\n setStore(undefined);\n setStatus(\"connecting\");\n setError(undefined);\n if (!itx) return;\n const readSeed = readSeedRef.current; // pinned to THIS key/session for the connection's whole life\n let disposed = false;\n let dispose: (() => Promise<void>) | undefined;\n const unmounted = new AbortController(); // an unmount while the first seed is pending recalls the row\n connectLiveState<S>(itx, {\n key: opts.key,\n name: opts.name,\n readSeed,\n signal: unmounted.signal,\n onResync: (r) => {\n if (disposed) return;\n if (r === \"healed\") {\n setStatus(\"live\");\n setError(undefined);\n } else {\n // the store keeps its last value; the next delta retries the heal\n setStatus(\"error\");\n setError(r.message);\n }\n },\n }).then(\n (conn) => {\n dispose = conn.dispose;\n if (disposed) {\n void conn.dispose(); // unmounted while connecting — still tear the mount down\n return;\n }\n setStore(conn.store);\n setStatus(\"live\");\n },\n (e: unknown) => {\n if (disposed) return;\n setError(e instanceof Error ? e.message : String(e));\n setStatus(\"error\");\n },\n );\n return () => {\n disposed = true;\n unmounted.abort();\n void dispose?.();\n };\n }, [itx, opts.key, opts.name]);\n\n const subscribe = useCallback(\n (cb: () => void) => (store ? store.subscribe(cb) : () => {}),\n [store],\n );\n const value = useSyncExternalStore(\n subscribe,\n () => store?.get(),\n () => undefined,\n );\n return { value, rev: store?.rev() ?? null, status, error };\n}\n\n// ── the iterate context ── the data half of a general-purpose context view (packages/ui\n// `components/context-view`, the rendering half): every committed event of a context, live; the rows\n// of its processors table; who is here; named facets' live state. ONE hook here, pure components\n// there, so the UI kit stays free of the SDK and any app — the dash, the agents app — composes the two.\n\n/** One presence: who acted on the context and when last, from the log's stamps. */\nexport type IterateContextPresence = {\n actor: string;\n email?: string;\n grant?: string;\n lastSeenAt: string;\n};\n\n/** The slice of a context handle `useIterateContext` reads — a capnweb `IterateContextApi` stub\n * satisfies it structurally. `invoke` seeds a named facet's live state\n * (`itx.facets.get('<name>').liveSnapshot()`, as an expression). */\nexport type IterateContextHandle = LiveStateItx & {\n readEvents(\n afterOffset?: number,\n limit?: number,\n ): Promise<{ events: unknown[]; atHead: boolean; scannedThroughOffset: number }>;\n processors: { list(): Promise<SubscriptionListEntry[]> | SubscriptionListEntry[] };\n rpcStubs: { list(): Promise<string[]> | string[] };\n invoke(call: string): Promise<unknown>;\n};\n\n/** A wire event (a capnweb proxy value or a plain object) as a `StreamEvent`, or null when\n * it is not a committed row. Structural, not a schema: the transport validated it; this only refuses\n * a shape the view cannot place (no offset, type or time). */\nfunction toStreamEvent(raw: unknown): StreamEvent | null {\n const value = JSON.parse(JSON.stringify(raw)) as Record<string, unknown> | null;\n if (\n !value ||\n typeof value.offset !== \"number\" ||\n typeof value.type !== \"string\" ||\n typeof value.createdAt !== \"string\"\n )\n return null;\n return value as unknown as StreamEvent; // the three fields checked are all the hook indexes by\n}\n\n/** A named live state before its first seed lands — and before the effect that opens it has run. */\nconst LIVE_STATE_CONNECTING: LiveStateResult = {\n value: undefined,\n rev: null,\n status: \"connecting\",\n};\n\n/** THE ITERATE CONTEXT, live — one hook, one stream subscription. THE LOG: subscribe to every\n * committed event (or `consumes`) BEFORE the catch-up read, so nothing lands between the two;\n * pushes and pages both dedupe into one map by offset; `caughtUp` once the read reached the head;\n * `error` when the connect failed. Off that same log, THE PROCESSORS TABLE, re-read whenever the\n * log grows a row-changing event (a subscription configured, halted or resumed — the table is core\n * state, one call away, no push of its own), and WHO IS HERE: the rpc stubs lent right now\n * (`itx.rpcStubs.list()` — physical, re-read at every new head, since presence changes are\n * ephemeral facts) and, from the log, every principal that acted, newest first. And named facets'\n * LIVE STATE, each seeded through `itx.facets.get('<name>').liveSnapshot()` — one entry per name,\n * always. `liveState` OMITTED opens `core` (the core reduce answers under that name) plus every\n * hosted facet in the processors table the hook holds, following the table as it loads and changes;\n * `liveState` GIVEN is exactly the names to open, no implicit `core`. Re-connects when `itx`\n * changes; unmount disposes every server-side subscription. */\nexport function useIterateContext(\n itx: IterateContextHandle | undefined,\n opts: { consumes?: string[]; liveState?: string[] } = {},\n): {\n events: StreamEvent[];\n caughtUp: boolean;\n error?: string;\n processors: { rows: SubscriptionListEntry[]; loaded: boolean; error?: string };\n presence: { actors: IterateContextPresence[]; rpcStubs: string[] };\n liveState: Record<string, LiveStateResult>;\n} {\n // ── the log ──\n const [events, setEvents] = useState<Map<number, StreamEvent>>(() => new Map());\n const [caughtUp, setCaughtUp] = useState(false);\n const [error, setError] = useState<string | undefined>();\n const consumesKey = JSON.stringify(opts.consumes || [\"*\"]);\n useEffect(() => {\n setEvents(new Map());\n setCaughtUp(false);\n setError(undefined);\n if (!itx) return;\n let disposed = false;\n const merge = (batch: unknown[]) =>\n setEvents((held) => {\n const next = new Map(held);\n for (const raw of batch) {\n const event = toStreamEvent(raw);\n if (event) next.set(event.offset, event);\n }\n return next;\n });\n let subscription: { [Symbol.dispose](): void } | undefined;\n (async () => {\n const handle = await itx.subscribe({\n consumes: JSON.parse(consumesKey) as string[],\n target: (batch) => !disposed && merge(batch),\n });\n // An unmount while the subscribe was pending ran the cleanup before this handle existed:\n // release it here, or the server keeps delivering to nobody.\n if (disposed) {\n handle[Symbol.dispose]();\n return;\n }\n subscription = handle;\n for (let after = 0; ; ) {\n const page = await itx.readEvents(after, 500);\n if (disposed) return;\n merge(page.events);\n if (page.atHead || page.scannedThroughOffset <= after) break;\n after = page.scannedThroughOffset;\n }\n setCaughtUp(true);\n })().catch((e: unknown) => !disposed && setError(e instanceof Error ? e.message : String(e)));\n return () => {\n disposed = true;\n subscription?.[Symbol.dispose]();\n };\n }, [itx, consumesKey]);\n const sorted = useMemo(() => [...events.values()].sort((a, b) => a.offset - b.offset), [events]);\n\n // ── the processors table ──\n // The table and the last failure remember WHICH itx they came from: a page that swaps contexts\n // (one route, another organization) shows an empty, not-yet-loaded table for the new one rather\n // than the old one's rows or error until the new read lands.\n const [table, setTable] = useState<{\n itx: IterateContextHandle;\n rows: SubscriptionListEntry[];\n }>();\n const [failure, setFailure] = useState<{ itx: IterateContextHandle; message: string }>();\n const tableVersion = sorted.reduce(\n (last, event) =>\n event.type.startsWith(\"events.iterate.com/itx/subscription-\") ? event.offset : last,\n 0,\n );\n useEffect(() => {\n if (!itx) return;\n let disposed = false;\n Promise.resolve(itx.processors.list()).then(\n (list) => {\n if (disposed) return;\n setTable({ itx, rows: list });\n setFailure(undefined); // a read that recovered clears the last failure\n },\n (e: unknown) =>\n !disposed && setFailure({ itx, message: e instanceof Error ? e.message : String(e) }),\n );\n return () => {\n disposed = true;\n };\n }, [itx, tableVersion]);\n const currentTable = itx && table?.itx === itx ? table : undefined;\n\n // ── who is here ──\n const [census, setCensus] = useState<{ itx: IterateContextHandle; rpcStubs: string[] }>();\n const head = sorted.at(-1)?.offset ?? 0;\n useEffect(() => {\n if (!itx) return;\n let disposed = false;\n Promise.resolve(itx.rpcStubs.list()).then(\n (list) => !disposed && setCensus({ itx, rpcStubs: list }),\n () => undefined, // presence is nice to have; a failed census shows nothing\n );\n return () => {\n disposed = true;\n };\n }, [itx, head]);\n // keyed by its itx: a swapped context shows no census until its own lands\n const rpcStubs = itx && census?.itx === itx ? census.rpcStubs : [];\n const actors = useMemo(() => {\n const byActor = new Map<string, IterateContextPresence>();\n for (const event of sorted) {\n const principal = event.source?.principal;\n if (!principal) continue;\n byActor.set(principal.actor, {\n actor: principal.actor,\n email: principal.email,\n grant: event.source?.grant,\n lastSeenAt: event.createdAt,\n });\n }\n return [...byActor.values()].sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));\n }, [sorted]);\n\n // ── named facets' live state ──\n // N subscriptions in ONE effect keyed by the name set — it changes at runtime as the processors\n // table loads (the default set is `core` plus the table's hosted facets) — since hooks cannot run\n // in a loop: client/live-state.ts's store reduces each, and this mirrors every change into React\n // state. The entries remember WHICH itx and name set they came from (as the table does), so a\n // swapped context or a changed set shows fresh connecting entries, never the last one's values.\n const liveStateKey = JSON.stringify(\n opts.liveState || [\n \"core\",\n ...(currentTable?.rows || []).flatMap((row) =>\n row.hostedFacet ? [row.hostedFacet.name] : [],\n ),\n ],\n );\n const [liveStates, setLiveStates] = useState<{\n itx: IterateContextHandle;\n key: string;\n entries: Record<string, LiveStateResult>;\n }>();\n useEffect(() => {\n if (!itx) return;\n const names = JSON.parse(liveStateKey) as string[];\n if (names.length === 0) return;\n let disposed = false;\n const unmounted = new AbortController(); // an unmount while a first seed is pending recalls that row\n const disposers: Array<() => void | Promise<void>> = [];\n const patch = (name: string, change: Partial<LiveStateResult>) =>\n setLiveStates((held) =>\n held && held.itx === itx && held.key === liveStateKey\n ? { ...held, entries: { ...held.entries, [name]: { ...held.entries[name], ...change } } }\n : held,\n );\n setLiveStates({\n itx,\n key: liveStateKey,\n entries: Object.fromEntries(names.map((name) => [name, LIVE_STATE_CONNECTING])),\n });\n for (const name of names) {\n connectLiveState<unknown>(itx, {\n key: name,\n readSeed: async () =>\n // the engine's own `{ rev, state }` seed, as `liveSnapshot()` answers it\n (await itx.invoke(`itx.facets.get('${name}').liveSnapshot()`)) as LiveStateSeed<unknown>,\n signal: unmounted.signal,\n onResync: (result) => {\n if (disposed) return;\n if (result === \"healed\") patch(name, { status: \"live\", error: undefined });\n // the store keeps its last value; the next delta retries the heal\n else patch(name, { status: \"error\", error: result.message });\n },\n }).then(\n (connection) => {\n if (disposed) {\n void connection.dispose(); // unmounted while connecting — still tear the row down\n return;\n }\n disposers.push(connection.dispose);\n disposers.push(\n connection.store.subscribe(() =>\n patch(name, { value: connection.store.get(), rev: connection.store.rev() }),\n ),\n );\n patch(name, {\n value: connection.store.get(),\n rev: connection.store.rev(),\n status: \"live\",\n });\n },\n (e: unknown) => {\n if (disposed) return;\n patch(name, { status: \"error\", error: e instanceof Error ? e.message : String(e) });\n },\n );\n }\n return () => {\n disposed = true;\n unmounted.abort();\n for (const dispose of disposers) void dispose();\n };\n }, [itx, liveStateKey]);\n // One entry per name, always: a name the effect has not reached yet (the render right after the\n // set changed) reads as connecting rather than missing.\n const liveState = useMemo(() => {\n const names = JSON.parse(liveStateKey) as string[];\n const held =\n itx && liveStates?.itx === itx && liveStates.key === liveStateKey ? liveStates.entries : {};\n return Object.fromEntries(names.map((name) => [name, held[name] || LIVE_STATE_CONNECTING]));\n }, [itx, liveStateKey, liveStates]);\n\n return {\n events: sorted,\n caughtUp,\n error,\n processors: {\n rows: currentTable?.rows || [],\n loaded: Boolean(currentTable),\n error: itx && failure?.itx === itx ? failure.message : undefined,\n },\n presence: { actors, rpcStubs },\n liveState,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Project ingress strips public identity headers and stamps `x-itx-principal` for a project member
|
|
2
|
+
* only: a visitor signed out, signed in without this project, or riding a session cookie on a
|
|
3
|
+
* cross-site write arrives without one. This guard runs in the config worker, before it proxies an
|
|
4
|
+
* app.
|
|
5
|
+
*
|
|
6
|
+
* Signed out, every request gets `401` with `WWW-Authenticate: Bearer realm="iterate"`: the
|
|
7
|
+
* platform's edge turns that answer into the sign-in for a page load (or into "sign in again with
|
|
8
|
+
* this project" for someone signed in without it), whatever path the app is served under, and hands
|
|
9
|
+
* a fetch, a write or a WebSocket upgrade the 401 itself. Any app can ask for a signed-in visitor
|
|
10
|
+
* the same way:
|
|
11
|
+
*
|
|
12
|
+
* ```js
|
|
13
|
+
* if (!request.headers.get("x-itx-principal"))
|
|
14
|
+
* return new Response("Sign in\n", { status: 401, headers: { "WWW-Authenticate": 'Bearer realm="iterate"' } });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* A write or a WebSocket upgrade must also come from this origin (or carry no Origin, a non-browser
|
|
18
|
+
* client), else 403. The edge already sends such a cookie request on anonymous; this repeats the
|
|
19
|
+
* check where the app runs. The handshake is a GET, but it opens a two-way channel, and the app
|
|
20
|
+
* session cookie is `SameSite=Lax`: every `<routingSlug>--<project>.iterate.app` host is same-site with
|
|
21
|
+
* every other, so a page on another project's host could otherwise open a socket to this app with
|
|
22
|
+
* the visitor's cookie. */
|
|
23
|
+
export declare const auth: {
|
|
24
|
+
require(request: Request): Response | null;
|
|
25
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
|
2
|
+
import type { IterateContextApi } from "../api.ts";
|
|
3
|
+
import { type ScannedRange, type StreamProcessor, type StreamEvent, type StreamEventInput } from "../stream/processor.ts";
|
|
4
|
+
import { withItx } from "./record-pipelined-steps.ts";
|
|
5
|
+
export { withItx };
|
|
6
|
+
export { LiveState, StreamProcessor, defineProcessorContract, type ConsumedEvent, type EventCatalog, type EventDefinition, type EmittedEventInput, type EventInput, type LiveStateSink, type ProcessorContract, type ProcessorState, type ProcessorStream, type ProcessEventArgs, type ReduceArgs, type ScannedRange, type StreamEvent, type StreamEventInput, } from "../stream/processor.ts";
|
|
7
|
+
export { z } from "zod";
|
|
8
|
+
export { newHttpBatchRpcSession, newWebSocketRpcSession, newWorkersRpcResponse } from "capnweb";
|
|
9
|
+
export { applyPatch, diff, jsonEqual, type PatchOp } from "../lib.ts";
|
|
10
|
+
/** What the parent mints a facet's class with — the whole identity, and one fact about its feed. */
|
|
11
|
+
export type FacetProps = {
|
|
12
|
+
iterateContextName: string;
|
|
13
|
+
name: string;
|
|
14
|
+
/** Set when, as this facet started, a subscription row of its context pushed it every commit it
|
|
15
|
+
* consumes (`processEventBatch`, the delivery loop's push): a processor's engine then trusts the
|
|
16
|
+
* head a catch-up read until the next push (stream/processor.ts, the read verbs). Absent, only a
|
|
17
|
+
* push is proof, so a processor no row pushes reads its log on every read. */
|
|
18
|
+
fedByPushes?: true;
|
|
19
|
+
};
|
|
20
|
+
/** THE FACET SHELL: a `DurableObject` a context hosts as a facet — `itx.facets.get(name, { source,
|
|
21
|
+
* className })`, a rule naming it, or a processor's row. A caller reaches a facet by itx expression
|
|
22
|
+
* (`itx.facets.get(name).<method>(…)`) only through what its class lists in `publicMethods`: the
|
|
23
|
+
* context refuses any other first step FORBIDDEN before the call reaches the facet
|
|
24
|
+
* (apps/os context/facet-public-methods.ts). The platform's own calls — the delivery loop's push
|
|
25
|
+
* and catch-up, the alarm's revive — never go through the list. A loaded class that does not
|
|
26
|
+
* extend this shell lists nothing, so no caller reaches it by expression. */
|
|
27
|
+
export declare abstract class FacetDurableObject<Env = unknown> extends DurableObject<Env, FacetProps> {
|
|
28
|
+
/** What a caller may reach by itx expression: the FIRST step of `itx.facets.get(name).<step>…`, a
|
|
29
|
+
* method or a property of this class. A subclass lists its own on top of its parent's:
|
|
30
|
+
* `static override publicMethods = [...super.publicMethods, "send"]`. */
|
|
31
|
+
static publicMethods: readonly string[];
|
|
32
|
+
/** This class's `publicMethods`, for the context that loaded it — a static does not cross the
|
|
33
|
+
* isolate. On no list: only the context asks it. */
|
|
34
|
+
listPublicMethods(): readonly string[];
|
|
35
|
+
}
|
|
36
|
+
/** The itx scope `withItx` hands its callback: a context's declared API (api.ts) — a capnweb stub
|
|
37
|
+
* of apps/os's `IterateContextRpcTarget`, which satisfies it. */
|
|
38
|
+
export type ItxScope = IterateContextApi;
|
|
39
|
+
/** What hands the scope over: the loopback entrypoint a loaded worker has as `env.ITX`, or the one a
|
|
40
|
+
* class of the platform's own worker mints from `ctx.exports`. */
|
|
41
|
+
export type ItxEntrypointService = {
|
|
42
|
+
get(): ItxScope;
|
|
43
|
+
};
|
|
44
|
+
/** The least a host needs of its scope: the fixed-point log calls the engine makes. The platform's own
|
|
45
|
+
* facets pass the Workers-RPC STUB of a context (every dotted step pipelined; a property there is a
|
|
46
|
+
* promise), which no plain-promise interface can name — so the constraint is this, not `ItxScope`. */
|
|
47
|
+
export type ProcessorScope = {
|
|
48
|
+
append(...events: StreamEventInput[]): Promise<unknown>;
|
|
49
|
+
readEvents(afterOffset?: number, limit?: number): Promise<unknown>;
|
|
50
|
+
/** The engine's claim on the context's alarm (processor.ts rule 3): "come back by `at`", or null. */
|
|
51
|
+
processors: {
|
|
52
|
+
claim(name: string, at: number | null): Promise<unknown>;
|
|
53
|
+
};
|
|
54
|
+
/** Another context of the project by its dotted surface (`.append`), which the platform's handle
|
|
55
|
+
* and a loaded worker's alike answer — how an entity's processor cross-posts its certificate to
|
|
56
|
+
* `/` (`withItx((itx) => itx.cd("/").append(certificate))`). Through the table like every other
|
|
57
|
+
* word here: a loaded processor's `cd` goes down only (the app wall), the platform's own go
|
|
58
|
+
* anywhere within the project. */
|
|
59
|
+
cd(path: string): {
|
|
60
|
+
append(...events: StreamEventInput[]): Promise<unknown>;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/** THE SCOPE ACCESSOR a host hands its processor: one pipelined round trip on the context's itx,
|
|
64
|
+
* released after (`StreamProcessorDurableObject.withItx`). A processor that needs an effect —
|
|
65
|
+
* `itx.cfArtifacts.create(path)`, `itx.ai.run(…)` — takes this and nothing else, so a unit test
|
|
66
|
+
* hands it a fake and the e2e lends one by rule on the context. */
|
|
67
|
+
export type WithItx<Scope = ItxScope> = <T>(call: (itx: Scope) => T) => Promise<Awaited<T>>;
|
|
68
|
+
export declare abstract class StreamProcessorDurableObject<State = unknown, Env extends {
|
|
69
|
+
ITX?: ItxEntrypointService;
|
|
70
|
+
} = {
|
|
71
|
+
ITX: ItxEntrypointService;
|
|
72
|
+
}, Scope extends ProcessorScope = ItxScope> extends FacetDurableObject<Env> {
|
|
73
|
+
#private;
|
|
74
|
+
/** The reads a caller reaches on every processor: `fetch`, and the state caught up through the log
|
|
75
|
+
* (`snapshot`, `liveSnapshot`) or awaited (`waitUntilProcessed`). What feeds the processor —
|
|
76
|
+
* `processEventBatch`, `catchUpFromLog`, `revive` — is the platform's, never a caller's. */
|
|
77
|
+
static publicMethods: string[];
|
|
78
|
+
/** The processor this object hosts — `processor = new PresenceProcessor()` at the top of the subclass. */
|
|
79
|
+
abstract readonly processor: StreamProcessor<State>;
|
|
80
|
+
/** After a runtime field on the processor moved OUTSIDE a batch (an RPC method on this object);
|
|
81
|
+
* inside `processEvent` the engine re-projects on its own. */
|
|
82
|
+
protected publishLiveState(): void;
|
|
83
|
+
/** THE push: the context hands over each committed batch with its scanned-range proof. */
|
|
84
|
+
processEventBatch(events: StreamEvent[], range: ScannedRange): Promise<void>;
|
|
85
|
+
/** Catch up from the log (the delivery loop's, when a row is configured or resumed). */
|
|
86
|
+
catchUpFromLog(): Promise<void>;
|
|
87
|
+
/** THE REVIVE: the context's alarm pass calls it for a due claim — catch up, then run the
|
|
88
|
+
* at-head pass, so an attempt the last incarnation was running is started again from state. */
|
|
89
|
+
revive(): Promise<void>;
|
|
90
|
+
/** Caught up through the log, then `{ offset, state }`. */
|
|
91
|
+
snapshot(): Promise<{
|
|
92
|
+
offset: number;
|
|
93
|
+
state: State;
|
|
94
|
+
}>;
|
|
95
|
+
/** The live-state seed read: `{ rev, state: projectLiveState(reduced) }`. */
|
|
96
|
+
liveSnapshot(): Promise<{
|
|
97
|
+
rev: number;
|
|
98
|
+
state: unknown;
|
|
99
|
+
}>;
|
|
100
|
+
/** The barrier: resolves once processed at least through `offset` (default timeout 10s). */
|
|
101
|
+
waitUntilProcessed(input: {
|
|
102
|
+
offset: number;
|
|
103
|
+
timeoutMs?: number;
|
|
104
|
+
}): Promise<void>;
|
|
105
|
+
/** ONE round trip on the itx scope, then RELEASE EVERYTHING IT REACHED: the get, and every call the
|
|
106
|
+
* callback made through it — not only the last. A Workers-RPC value this facet leaves undisposed —
|
|
107
|
+
* the `itx.cd(path)` of `itx.cd(path).append(…)`, the `cfArtifacts.get(p)` of `.remote()`, an
|
|
108
|
+
* answer awaited inside the callback (`const { state } = await context.invoke(…)`), data included —
|
|
109
|
+
* keeps THIS FACET running after its context is evicted, until V8 collects the value, which an
|
|
110
|
+
* idle isolate may not do for many minutes: each new incarnation of the context reattaches to the
|
|
111
|
+
* facet, and the object stays billed (measured 2026-09-23: a new website project's `/` and
|
|
112
|
+
* `/repos/config` billed 60 s of every minute for 30 min with no request). The context's own
|
|
113
|
+
* `invoke` cannot end this from its side: the facet holds the value (context-residency.e2e.test.ts,
|
|
114
|
+
* "… does not outlive …"). Protected: a host with methods of its own (the workspace,
|
|
115
|
+
* src/workspace/durable-object.ts) reaches its context the same way. */
|
|
116
|
+
protected withItx<T>(call: (itx: Scope) => T): Promise<Awaited<T>>;
|
|
117
|
+
}
|
|
118
|
+
export type ConfigEventArgs = {
|
|
119
|
+
event: StreamEvent;
|
|
120
|
+
range: ScannedRange;
|
|
121
|
+
itx: ItxScope;
|
|
122
|
+
};
|
|
123
|
+
export declare abstract class ConfigWorker<Env extends {
|
|
124
|
+
ITX: ItxEntrypointService;
|
|
125
|
+
} = {
|
|
126
|
+
ITX: ItxEntrypointService;
|
|
127
|
+
}> extends WorkerEntrypoint<Env> {
|
|
128
|
+
/** At fetch entry: `const denied = this.auth.require(request); if (denied) return denied;`
|
|
129
|
+
* `x-itx-principal` is on a request only when a project member (or the operator) sent it, safe
|
|
130
|
+
* to act on. A private route written by hand answers the platform's sign-in challenge, which
|
|
131
|
+
* the edge turns into the sign-in for a page load (`auth.require` does the same):
|
|
132
|
+
*
|
|
133
|
+
* ```js
|
|
134
|
+
* if (!request.headers.get("x-itx-principal"))
|
|
135
|
+
* return new Response("Sign in\n", { status: 401, headers: { "WWW-Authenticate": 'Bearer realm="iterate"' } });
|
|
136
|
+
* ``` */
|
|
137
|
+
protected readonly auth: {
|
|
138
|
+
require(request: Request): Response | null;
|
|
139
|
+
};
|
|
140
|
+
/** Process an explicitly subscribed batch with this worker's context scope. */
|
|
141
|
+
processEventBatch(events: StreamEvent[], range: ScannedRange): Promise<void>;
|
|
142
|
+
/** ONE round trip on the itx scope, then release the scope and every call made through it
|
|
143
|
+
* (`StreamProcessorDurableObject.withItx` says why an undisposed step keeps a context billed). */
|
|
144
|
+
protected withItx<T>(call: (itx: ItxScope) => T): Promise<Awaited<T>>;
|
|
145
|
+
/** THE AUTHOR HOOK — one event at a time, in offset order. Append reactions through the itx scope;
|
|
146
|
+
* make them idempotent (a redelivery must be a no-op). Default: ignore the event. */
|
|
147
|
+
processEvent(_args: ConfigEventArgs): void | Promise<void>;
|
|
148
|
+
/** THE WEB ROOT — every Request on a host of the project (the project's configured ingress
|
|
149
|
+
* target). The host's routing slug is in `x-iterate-routing-slug` (`notes` for
|
|
150
|
+
* `notes--<project>.<hostname>`; absent on the apex), written only by the platform: route on it
|
|
151
|
+
* in plain code, answering here (reaching the context through `this.withItx`) or forwarding the
|
|
152
|
+
* Request. Default: not found. */
|
|
153
|
+
fetch(_request: Request): Response | Promise<Response>;
|
|
154
|
+
}
|
|
155
|
+
export { RunContract, RunRequested, RunSettled } from "../stream/run.ts";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** ONE round trip on `entrypoint.get()`, then RELEASE EVERYTHING IT REACHED: the scope and every call
|
|
2
|
+
* `call` made through it or through a handle it awaited, the last first. A release that throws is reported and the rest still run
|
|
3
|
+
* (lib.ts `releaseRpcSessions`), so the call's answer stands. Data it answers stays usable; a stub or
|
|
4
|
+
* handle it answers is released with the rest, so return data.
|
|
5
|
+
*
|
|
6
|
+
* const { projectSlug } = await withItx(this.env.ITX, (itx) => itx.whoami());
|
|
7
|
+
*/
|
|
8
|
+
export declare function withItx<Scope, T>(entrypoint: {
|
|
9
|
+
get(): Scope;
|
|
10
|
+
}, call: (itx: Scope) => T): Promise<Awaited<T>>;
|
|
11
|
+
/** `stub` as the caller sees it, except that every CALL made through it — at any depth, on the stub,
|
|
12
|
+
* on a call's result, or on the handle a call's result resolves to once awaited — is pushed onto
|
|
13
|
+
* `steps`, so the caller can dispose each one: a Workers-RPC call's result is a stub-bearing promise
|
|
14
|
+
* that keeps its session open until disposed, awaited or not. Awaiting hands back a handle (a stub
|
|
15
|
+
* is callable, in workerd and capnweb alike) recorded and pushed too, and plain data untouched, so
|
|
16
|
+
* data still copies across RPC. `catch`/`finally` and symbol members (`Symbol.dispose`) are the
|
|
17
|
+
* value's own, bound to it, so disposing behaves exactly as on the bare stub; an argument that is
|
|
18
|
+
* itself a recorded value crosses the wire as the stub it wraps. */
|
|
19
|
+
export declare function recordPipelinedSteps<T>(stub: T, steps: unknown[]): T;
|