@trylight/app 0.3.1 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/lookup.d.ts +1 -1
- package/dist/auth/lookup.js +1 -1
- package/dist/auth/mode.js +2 -1
- package/dist/auth/next-path.js +10 -4
- package/dist/auth/plaza.js +4 -3
- package/dist/chat/paths.d.ts +1 -8
- package/dist/chat/paths.js +1 -8
- package/dist/chat/server.js +6 -10
- package/dist/client.js +2 -1
- package/dist/entities.d.ts +1 -4
- package/dist/entities.js +5 -5
- package/dist/images.d.ts +0 -1
- package/dist/images.js +1 -1
- package/dist/invoke/index.d.ts +0 -4
- package/dist/invoke/index.js +4 -8
- package/dist/invoke/server.js +5 -5
- package/dist/platform/contract.d.ts +46 -0
- package/dist/platform/contract.js +56 -0
- package/dist/platform/index.js +4 -3
- package/dist/platform/keep-scroll.d.ts +5 -0
- package/dist/platform/keep-scroll.js +75 -0
- package/dist/platform/live-data.d.ts +0 -2
- package/dist/platform/live-data.js +5 -5
- package/dist/platform/new-build-mount.d.ts +4 -0
- package/dist/platform/new-build-mount.js +65 -0
- package/dist/platform/new-build.d.ts +21 -0
- package/dist/platform/new-build.js +63 -0
- package/dist/platform/pane-bridge.js +12 -11
- package/dist/platform/timing.d.ts +12 -0
- package/dist/platform/timing.js +13 -0
- package/dist/platform.d.ts +1 -0
- package/dist/platform.js +2 -1
- package/dist/server/entities.js +2 -1
- package/package.json +12 -10
- package/dist/invoke/paths.d.ts +0 -2
- package/dist/invoke/paths.js +0 -2
package/dist/auth/lookup.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AuthContext } from "./context.js";
|
|
2
2
|
import { type PlazaSession } from "./plaza.js";
|
|
3
3
|
/** The cookie holding this app's own session. Nothing of the platform's is read here. */
|
|
4
|
-
export
|
|
4
|
+
export { SESSION_COOKIE } from "../platform/contract.js";
|
|
5
5
|
/**
|
|
6
6
|
* How often a session asks the platform whether it still stands. Someone removed from
|
|
7
7
|
* the org is signed out of the app within this long; their chat stops at once,
|
package/dist/auth/lookup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { checkPlazaSession, revokePlazaSession } from "./plaza.js";
|
|
3
3
|
/** The cookie holding this app's own session. Nothing of the platform's is read here. */
|
|
4
|
-
export
|
|
4
|
+
export { SESSION_COOKIE } from "../platform/contract.js";
|
|
5
5
|
/**
|
|
6
6
|
* How often a session asks the platform whether it still stands. Someone removed from
|
|
7
7
|
* the org is signed out of the app within this long; their chat stops at once,
|
package/dist/auth/mode.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
import { miiAuthConfig } from "./config.js";
|
|
3
|
+
import { PLATFORM_AUTH_PATHS } from "../platform/contract.js";
|
|
3
4
|
// Who may open this app, as set in the platform (Settings → Workspaces → "Who can open
|
|
4
5
|
// it"). Read from the platform rather than baked into the build so the switch takes
|
|
5
6
|
// effect on the next request, not the next deploy.
|
|
@@ -40,7 +41,7 @@ export async function getAuthMode() {
|
|
|
40
41
|
}
|
|
41
42
|
async function fetchMode() {
|
|
42
43
|
const { plazaUrl, appId } = miiAuthConfig();
|
|
43
|
-
const response = await fetch(`${plazaUrl}
|
|
44
|
+
const response = await fetch(`${plazaUrl}${PLATFORM_AUTH_PATHS.config}?app_id=${encodeURIComponent(appId)}`, { cache: "no-store" });
|
|
44
45
|
if (!response.ok) {
|
|
45
46
|
throw new Error(`The platform's app-auth config answered ${response.status}.`);
|
|
46
47
|
}
|
package/dist/auth/next-path.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
1
|
+
import { SAME_ORIGIN_BASE } from "../platform/contract.js";
|
|
2
|
+
// Only a path on this app is a valid place to land after signing in. Parsed,
|
|
3
|
+
// not pattern-matched: browsers read `//host`, `/\host` and `/\t/host` as
|
|
4
|
+
// another site. The candidate is returned as given, not normalised: its
|
|
5
|
+
// pathname could read `//host`.
|
|
3
6
|
export function safeNext(candidate) {
|
|
4
7
|
if (candidate === null ||
|
|
5
8
|
!candidate.startsWith("/") ||
|
|
6
|
-
|
|
9
|
+
// The parser drops tabs and line breaks; a Location header refuses them.
|
|
10
|
+
[...candidate].some((character) => character < " ")) {
|
|
7
11
|
return "/";
|
|
8
12
|
}
|
|
9
|
-
return candidate
|
|
13
|
+
return new URL(candidate, SAME_ORIGIN_BASE).origin === SAME_ORIGIN_BASE
|
|
14
|
+
? candidate
|
|
15
|
+
: "/";
|
|
10
16
|
}
|
package/dist/auth/plaza.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
import { miiAuthConfig } from "./config.js";
|
|
3
|
+
import { PLATFORM_AUTH_PATHS } from "../platform/contract.js";
|
|
3
4
|
/**
|
|
4
5
|
* Check an email and password with the platform, and get back who the person is and
|
|
5
6
|
* the session the platform signed for them.
|
|
@@ -8,7 +9,7 @@ export async function signInWithPlaza(email, password) {
|
|
|
8
9
|
const { plazaUrl, appId } = miiAuthConfig();
|
|
9
10
|
let response;
|
|
10
11
|
try {
|
|
11
|
-
response = await fetch(`${plazaUrl}
|
|
12
|
+
response = await fetch(`${plazaUrl}${PLATFORM_AUTH_PATHS.signIn}`, {
|
|
12
13
|
method: "POST",
|
|
13
14
|
headers: { "Content-Type": "application/json" },
|
|
14
15
|
body: JSON.stringify({ appId, email, password }),
|
|
@@ -50,7 +51,7 @@ export async function requestPasswordResetWithPlaza(email, returnTo) {
|
|
|
50
51
|
const { plazaUrl, appId } = miiAuthConfig();
|
|
51
52
|
let response;
|
|
52
53
|
try {
|
|
53
|
-
response = await fetch(`${plazaUrl}
|
|
54
|
+
response = await fetch(`${plazaUrl}${PLATFORM_AUTH_PATHS.requestPasswordReset}`, {
|
|
54
55
|
method: "POST",
|
|
55
56
|
headers: { "Content-Type": "application/json" },
|
|
56
57
|
body: JSON.stringify({ appId, email, returnTo }),
|
|
@@ -80,7 +81,7 @@ export async function requestPasswordResetWithPlaza(email, returnTo) {
|
|
|
80
81
|
*/
|
|
81
82
|
export async function redeemWithPlaza(code) {
|
|
82
83
|
const { plazaUrl, appId } = miiAuthConfig();
|
|
83
|
-
const response = await fetch(`${plazaUrl}
|
|
84
|
+
const response = await fetch(`${plazaUrl}${PLATFORM_AUTH_PATHS.redeemPaneCode}`, {
|
|
84
85
|
method: "POST",
|
|
85
86
|
headers: { "Content-Type": "application/json" },
|
|
86
87
|
body: JSON.stringify({ appId, code }),
|
package/dist/chat/paths.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare const CHAT_BASE_PATH = "/api/mii/chat";
|
|
3
|
-
/**
|
|
4
|
-
* What the app's chat server can do beyond the first contract, listed on its
|
|
5
|
-
* answers. The platform serves the widget to apps on every runtime version, so the
|
|
6
|
-
* widget offers only what this app's server says it forwards.
|
|
7
|
-
*/
|
|
8
|
-
export declare const CHAT_FEATURES_HEADER = "X-Mii-Chat-Features";
|
|
1
|
+
/** Features listed in CHAT_FEATURES_HEADER. */
|
|
9
2
|
export declare const REACTIONS_FEATURE = "reactions";
|
|
10
3
|
export declare const CREDITS_FEATURE = "credits";
|
package/dist/chat/paths.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export const CHAT_BASE_PATH = "/api/mii/chat";
|
|
3
|
-
/**
|
|
4
|
-
* What the app's chat server can do beyond the first contract, listed on its
|
|
5
|
-
* answers. The platform serves the widget to apps on every runtime version, so the
|
|
6
|
-
* widget offers only what this app's server says it forwards.
|
|
7
|
-
*/
|
|
8
|
-
export const CHAT_FEATURES_HEADER = "X-Mii-Chat-Features";
|
|
1
|
+
/** Features listed in CHAT_FEATURES_HEADER. */
|
|
9
2
|
export const REACTIONS_FEATURE = "reactions";
|
|
10
3
|
export const CREDITS_FEATURE = "credits";
|
package/dist/chat/server.js
CHANGED
|
@@ -15,12 +15,8 @@
|
|
|
15
15
|
// the platform signed when the person signed in. The platform checks that token and the
|
|
16
16
|
// person's membership on every request.
|
|
17
17
|
import { miiAuthConfig } from "../auth/config.js";
|
|
18
|
-
import { CHAT_BASE_PATH, CHAT_FEATURES_HEADER,
|
|
19
|
-
|
|
20
|
-
// server forwards any path beneath it: a widget route the platform adds needs no
|
|
21
|
-
// release of this package. The platform's server-only routes (session) are outside it,
|
|
22
|
-
// and each route under it validates its own ids.
|
|
23
|
-
const PLAZA_CHAT_PATH = "/api/app-chat/v1/chat";
|
|
18
|
+
import { CHAT_BASE_PATH, CHAT_FEATURES_HEADER, PLATFORM_CHAT_PATH } from "../platform/contract.js";
|
|
19
|
+
import { CREDITS_FEATURE, REACTIONS_FEATURE } from "./paths.js";
|
|
24
20
|
// Plain segments only: no dots, no percent-encoding, no empty segment. A path
|
|
25
21
|
// that could climb out of the platform's prefix (`..`, `%2e%2e`, `%2f`, `//`) never
|
|
26
22
|
// leaves this server.
|
|
@@ -28,7 +24,7 @@ const FORWARDABLE_PATH = /^(\/[A-Za-z0-9_-]+)+$/;
|
|
|
28
24
|
// What the thread's poll and its pages may carry through. Everything else in
|
|
29
25
|
// the query string is dropped rather than forwarded.
|
|
30
26
|
const FORWARDED_QUERY = ["after", "afterId", "limit", "before", "beforeId"];
|
|
31
|
-
export function createMiiChatHandler(auth, { reportError = (error, context) => console.error("
|
|
27
|
+
export function createMiiChatHandler(auth, { reportError = (error, context) => console.error("chat route error", context, error) } = {}) {
|
|
32
28
|
return async (request) => {
|
|
33
29
|
const url = new URL(request.url);
|
|
34
30
|
if (!url.pathname.startsWith(`${CHAT_BASE_PATH}/`)) {
|
|
@@ -56,7 +52,7 @@ export function createMiiChatHandler(auth, { reportError = (error, context) => c
|
|
|
56
52
|
return refuse(401, "sign_in_again", "Sign out and back in to use it here.");
|
|
57
53
|
}
|
|
58
54
|
const { plazaUrl } = miiAuthConfig();
|
|
59
|
-
const target = new URL(`${plazaUrl}${
|
|
55
|
+
const target = new URL(`${plazaUrl}${PLATFORM_CHAT_PATH}${path}`);
|
|
60
56
|
for (const key of FORWARDED_QUERY) {
|
|
61
57
|
const value = url.searchParams.get(key);
|
|
62
58
|
if (value !== null)
|
|
@@ -83,7 +79,7 @@ export function createMiiChatHandler(auth, { reportError = (error, context) => c
|
|
|
83
79
|
answer = await fetch(target, init);
|
|
84
80
|
}
|
|
85
81
|
catch (error) {
|
|
86
|
-
reportError(error, { where: "
|
|
82
|
+
reportError(error, { where: "chat-route", step: "forward" });
|
|
87
83
|
return refuse(502, "unavailable", "Couldn't reach the chat. Try again in a moment.");
|
|
88
84
|
}
|
|
89
85
|
// The platform no longer stands behind this session: the token is no good, or the
|
|
@@ -93,7 +89,7 @@ export function createMiiChatHandler(auth, { reportError = (error, context) => c
|
|
|
93
89
|
await auth.revokeSession(token, answer.status === 403 ? "not_a_member" : "token_rejected");
|
|
94
90
|
}
|
|
95
91
|
if (answer.status >= 500) {
|
|
96
|
-
reportError(new Error(`Platform chat answered ${answer.status}: ${(await answer.clone().text()).slice(0, 300)}`), { where: "
|
|
92
|
+
reportError(new Error(`Platform chat answered ${answer.status}: ${(await answer.clone().text()).slice(0, 300)}`), { where: "chat-route", step: "plaza" });
|
|
97
93
|
}
|
|
98
94
|
// An avatar may be cached for the person; chat data never is.
|
|
99
95
|
const cacheControl = answer.headers.get("cache-control");
|
package/dist/client.js
CHANGED
|
@@ -10,7 +10,8 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
10
10
|
import { createInvoker } from "./invoke/index.js";
|
|
11
11
|
import { getTableColumns, getTableName } from "drizzle-orm";
|
|
12
12
|
import { z } from "zod/v4";
|
|
13
|
-
import { ENTITIES_PATH
|
|
13
|
+
import { ENTITIES_PATH } from "./platform/contract.js";
|
|
14
|
+
import { EntityError, LIST_LIMIT, tableQueryKey } from "./entities.js";
|
|
14
15
|
export { EntityError, LIST_LIMIT } from "./entities.js";
|
|
15
16
|
export { siteOf } from "./schema.js";
|
|
16
17
|
export { imageSrc } from "./images.js";
|
package/dist/entities.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { Entity, PlatformColumn } from "./schema.js";
|
|
2
|
-
|
|
3
|
-
/** Which tables changed since a cursor (open pages' poll): `?since=<cursor>` → {cursor, changedTables}. */
|
|
4
|
-
export declare const CHANGES_PATH = "/api/light/entities/_changes";
|
|
5
|
-
/** The TanStack Query key everything cached from one table sits under; a change to the table invalidates it. */
|
|
2
|
+
/** Everything cached from one table sits under this key; a change to the table invalidates it. */
|
|
6
3
|
export declare const tableQueryKey: (table: string) => string[];
|
|
7
4
|
/** The most rows one list returns; page past it with `offset` (useInfiniteList does). */
|
|
8
5
|
export declare const LIST_LIMIT = 1000;
|
package/dist/entities.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
export const tableQueryKey = (table) => [
|
|
1
|
+
// What the browser client and the entities route agree on: the path, the
|
|
2
|
+
// shapes of a request, and the row types a schema gives them.
|
|
3
|
+
import { ENTITIES_PATH, QUERY_ROOT } from "./platform/contract.js";
|
|
4
|
+
/** Everything cached from one table sits under this key; a change to the table invalidates it. */
|
|
5
|
+
export const tableQueryKey = (table) => [QUERY_ROOT, `${ENTITIES_PATH}/${table}`];
|
|
6
6
|
/** The most rows one list returns; page past it with `offset` (useInfiniteList does). */
|
|
7
7
|
export const LIST_LIMIT = 1000;
|
|
8
8
|
/** A refused or failed entities call, with the route's own reason. */
|
package/dist/images.d.ts
CHANGED
package/dist/images.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// What the browser and the image route agree on: where a row's photo or logo
|
|
2
2
|
// loads from. Nothing here may touch a database or the DOM.
|
|
3
|
+
import { IMAGE_PATH } from "./platform/contract.js";
|
|
3
4
|
import { webUrl } from "./schema.js";
|
|
4
|
-
export const IMAGE_PATH = "/api/light/image";
|
|
5
5
|
/**
|
|
6
6
|
* Where a row's photo or logo loads from: the app's own copy (kept from the
|
|
7
7
|
* first view on) for an address on the web, the value itself for one of the
|
package/dist/invoke/index.d.ts
CHANGED
|
@@ -4,10 +4,6 @@ export { InvocationError };
|
|
|
4
4
|
export type { Invocation, InvocationState } from "./contracts.js";
|
|
5
5
|
/** How long one read asks the platform to hold a pending invocation; the platform clamps it to its own limit. */
|
|
6
6
|
export declare const INVOCATION_WAIT_SECONDS = 60;
|
|
7
|
-
/** The pause after a read that failed before asking again. */
|
|
8
|
-
export declare const INVOCATION_RETRY_MS = 1500;
|
|
9
|
-
/** How long a run started with `remember` is picked up again in this browser. */
|
|
10
|
-
export declare const INVOCATION_REMEMBER_MS: number;
|
|
11
7
|
type Result = Record<string, unknown>;
|
|
12
8
|
/**
|
|
13
9
|
* `id` and `input` are null until the platform has the invocation. `input` is the
|
package/dist/invoke/index.js
CHANGED
|
@@ -5,16 +5,12 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
5
5
|
import { z } from "zod/v4";
|
|
6
6
|
import { errorBody } from "../request-error.js";
|
|
7
7
|
import { failedRun, InvocationError } from "./error.js";
|
|
8
|
-
import { INVOKE_BASE_PATH } from "
|
|
8
|
+
import { INVOKE_BASE_PATH, STORAGE_KEYS } from "../platform/contract.js";
|
|
9
|
+
import { INVOCATION_REMEMBER_MS, INVOCATION_RETRY_MS } from "../platform/timing.js";
|
|
9
10
|
export { InvocationError };
|
|
10
11
|
/** How long one read asks the platform to hold a pending invocation; the platform clamps it to its own limit. */
|
|
11
12
|
export const INVOCATION_WAIT_SECONDS = 60;
|
|
12
|
-
/** The pause after a read that failed before asking again. */
|
|
13
|
-
export const INVOCATION_RETRY_MS = 1500;
|
|
14
|
-
/** How long a run started with `remember` is picked up again in this browser. */
|
|
15
|
-
export const INVOCATION_REMEMBER_MS = 24 * 60 * 60 * 1000;
|
|
16
13
|
const ELAPSED_TICK_MS = 1000;
|
|
17
|
-
const REMEMBERED_PREFIX = "mii-invocation:";
|
|
18
14
|
export function createInvoker() {
|
|
19
15
|
return {
|
|
20
16
|
async invoke(key, input, signal = null) {
|
|
@@ -49,7 +45,7 @@ async function readAnswered(id, holdSeconds, signal = null, onPoll = unwatched)
|
|
|
49
45
|
// invocation, which carries on either way.
|
|
50
46
|
if (!(error instanceof InvocationError) || (error.status !== 0 && error.status < 500))
|
|
51
47
|
throw error;
|
|
52
|
-
console.error("
|
|
48
|
+
console.error("invoke: a read failed; trying again", error);
|
|
53
49
|
onPoll(null, error);
|
|
54
50
|
await wait(INVOCATION_RETRY_MS, signal);
|
|
55
51
|
}
|
|
@@ -162,7 +158,7 @@ const IDLE = { state: "idle" };
|
|
|
162
158
|
* With `remember`, `restore()` picks up the run this browser last started with it.
|
|
163
159
|
*/
|
|
164
160
|
export function createInvocationController(key, onStatus, { remember = null } = {}) {
|
|
165
|
-
const name = remember === null ? null : `${
|
|
161
|
+
const name = remember === null ? null : `${STORAGE_KEYS.invocationPrefix}${key}:${remember}`;
|
|
166
162
|
// The run this button follows. `note` goes false once it is replaced or
|
|
167
163
|
// reset, so a start still in flight is never noted; an unmount leaves it.
|
|
168
164
|
let current = null;
|
package/dist/invoke/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isSameOrigin, readCookie } from "../chat/server.js";
|
|
|
6
6
|
import { errorBody } from "../request-error.js";
|
|
7
7
|
import { isUuid } from "../uuid.js";
|
|
8
8
|
import { failedRun, InvocationError } from "./error.js";
|
|
9
|
-
import { INVOKE_BASE_PATH } from "
|
|
9
|
+
import { INVOKE_BASE_PATH } from "../platform/contract.js";
|
|
10
10
|
export { InvocationError };
|
|
11
11
|
function routeOf(request) {
|
|
12
12
|
const { pathname } = new URL(request.url);
|
|
@@ -125,7 +125,7 @@ async function askPlaza(auth, person, call) {
|
|
|
125
125
|
// refinement or a transform. The app's own Zod has the last word.
|
|
126
126
|
function resultMismatch(id, error, reportError) {
|
|
127
127
|
const problem = `The result of invocation ${id} does not match this app's output schema:\n${z.prettifyError(error)}`;
|
|
128
|
-
reportError(new Error(problem), { where: "
|
|
128
|
+
reportError(new Error(problem), { where: "invoke-route", step: "result" });
|
|
129
129
|
return new InvocationError(422, "result_mismatch", problem);
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
@@ -133,7 +133,7 @@ function resultMismatch(id, error, reportError) {
|
|
|
133
133
|
* Every output is converted to JSON Schema here, once, so one with no JSON
|
|
134
134
|
* Schema form stops the server starting, not someone's press of the button.
|
|
135
135
|
*/
|
|
136
|
-
export function createMiiInvoke(auth, contracts, { reportError = (error, context) => console.error("
|
|
136
|
+
export function createMiiInvoke(auth, contracts, { reportError = (error, context) => console.error("invoke route error", context, error) } = {}) {
|
|
137
137
|
const declared = new Map(Object.entries(contracts).map(([key, contract]) => [key, declare(key, contract)]));
|
|
138
138
|
const handler = async (request) => {
|
|
139
139
|
const route = routeOf(request);
|
|
@@ -168,13 +168,13 @@ export function createMiiInvoke(auth, contracts, { reportError = (error, context
|
|
|
168
168
|
}
|
|
169
169
|
catch (error) {
|
|
170
170
|
if (!request.signal.aborted)
|
|
171
|
-
reportError(error, { where: "
|
|
171
|
+
reportError(error, { where: "invoke-route", step: "forward" });
|
|
172
172
|
return refuse(502, "unavailable", "Couldn't reach the platform. Try again in a moment.");
|
|
173
173
|
}
|
|
174
174
|
if (!answer.ok) {
|
|
175
175
|
if (answer.status >= 500) {
|
|
176
176
|
reportError(new Error(`Platform answered ${answer.status}: ${(await answer.clone().text()).slice(0, 300)}`), {
|
|
177
|
-
where: "
|
|
177
|
+
where: "invoke-route",
|
|
178
178
|
step: "plaza"
|
|
179
179
|
});
|
|
180
180
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const ENTITIES_PATH = "/api/light/entities";
|
|
2
|
+
export declare const CHANGES_PATH = "/api/light/entities/_changes";
|
|
3
|
+
export declare const IMAGE_PATH = "/api/light/image";
|
|
4
|
+
/** The template's src/routes/api/light/build.ts. */
|
|
5
|
+
export declare const BUILD_PATH = "/api/light/build";
|
|
6
|
+
export declare const INVOKE_BASE_PATH = "/api/mii/invoke";
|
|
7
|
+
export declare const CHAT_BASE_PATH = "/api/mii/chat";
|
|
8
|
+
/** A base to resolve an app-relative path against when checking it stays on the app; never contacted. */
|
|
9
|
+
export declare const SAME_ORIGIN_BASE = "https://app.invalid";
|
|
10
|
+
/** The platform's sign-in API, called with an app's own id. */
|
|
11
|
+
export declare const PLATFORM_AUTH_PATHS: {
|
|
12
|
+
signIn: string;
|
|
13
|
+
requestPasswordReset: string;
|
|
14
|
+
redeemPaneCode: string;
|
|
15
|
+
config: string;
|
|
16
|
+
/** "Continue with Google / Microsoft": the platform signs the person in and returns with a one-time code. */
|
|
17
|
+
socialSignIn: string;
|
|
18
|
+
};
|
|
19
|
+
/** The platform's chat API: an app's chat route forwards any path beneath it, so a new widget route needs no SDK release. */
|
|
20
|
+
export declare const PLATFORM_CHAT_PATH = "/api/app-chat/v1/chat";
|
|
21
|
+
/** What this app's chat server forwards beyond the first contract: the widget, served to every app version, offers only these. */
|
|
22
|
+
export declare const CHAT_FEATURES_HEADER = "X-Mii-Chat-Features";
|
|
23
|
+
/** Every TanStack query the SDK owns sits under this key. */
|
|
24
|
+
export declare const QUERY_ROOT = "light";
|
|
25
|
+
export declare const SESSION_COOKIE = "mii_session";
|
|
26
|
+
export declare const STORAGE_KEYS: {
|
|
27
|
+
/** sessionStorage: the build this tab already reloaded for after a missing chunk, so a second miss on it can't loop. */
|
|
28
|
+
chunkRetryBuildId: string;
|
|
29
|
+
/** sessionStorage: scroll positions saved just before a reload onto a new build. */
|
|
30
|
+
scrollBeforeReload: string;
|
|
31
|
+
/** localStorage prefix: a contract invocation remembered across reloads. */
|
|
32
|
+
invocationPrefix: string;
|
|
33
|
+
/** localStorage, and the URL parameter that sets it: this browser is the org's own automation. */
|
|
34
|
+
agentBrowser: string;
|
|
35
|
+
};
|
|
36
|
+
/** postMessage types between an app framed in the platform's App pane and the pane. */
|
|
37
|
+
export declare const PANE_MESSAGES: {
|
|
38
|
+
hello: "mii-preview:hello";
|
|
39
|
+
select: "mii-preview:select";
|
|
40
|
+
locate: "mii-preview:locate";
|
|
41
|
+
ready: "mii-preview:ready";
|
|
42
|
+
route: "mii-preview:route";
|
|
43
|
+
error: "mii-preview:error";
|
|
44
|
+
picked: "mii-preview:picked";
|
|
45
|
+
located: "mii-preview:located";
|
|
46
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The names an app's pages, its server and the platform agree on. Deployed
|
|
2
|
+
// apps and the platform's pane depend on every value here: rename freely, never
|
|
3
|
+
// change a value without both sides moving.
|
|
4
|
+
const NAMESPACE = "light";
|
|
5
|
+
/** The platform's former name: the pane protocol, the session cookie and two routes still carry it. */
|
|
6
|
+
const LEGACY_NAMESPACE = "mii";
|
|
7
|
+
export const ENTITIES_PATH = `/api/${NAMESPACE}/entities`;
|
|
8
|
+
export const CHANGES_PATH = `${ENTITIES_PATH}/_changes`;
|
|
9
|
+
export const IMAGE_PATH = `/api/${NAMESPACE}/image`;
|
|
10
|
+
/** The template's src/routes/api/light/build.ts. */
|
|
11
|
+
export const BUILD_PATH = `/api/${NAMESPACE}/build`;
|
|
12
|
+
export const INVOKE_BASE_PATH = `/api/${LEGACY_NAMESPACE}/invoke`;
|
|
13
|
+
export const CHAT_BASE_PATH = `/api/${LEGACY_NAMESPACE}/chat`;
|
|
14
|
+
/** A base to resolve an app-relative path against when checking it stays on the app; never contacted. */
|
|
15
|
+
export const SAME_ORIGIN_BASE = "https://app.invalid";
|
|
16
|
+
/** The platform's sign-in API, called with an app's own id. */
|
|
17
|
+
export const PLATFORM_AUTH_PATHS = {
|
|
18
|
+
signIn: "/api/app-auth/sign-in",
|
|
19
|
+
requestPasswordReset: "/api/app-auth/request-password-reset",
|
|
20
|
+
redeemPaneCode: "/api/app-auth/redeem",
|
|
21
|
+
config: "/api/app-auth/config",
|
|
22
|
+
/** "Continue with Google / Microsoft": the platform signs the person in and returns with a one-time code. */
|
|
23
|
+
socialSignIn: "/api/app-auth/social"
|
|
24
|
+
};
|
|
25
|
+
/** The platform's chat API: an app's chat route forwards any path beneath it, so a new widget route needs no SDK release. */
|
|
26
|
+
export const PLATFORM_CHAT_PATH = "/api/app-chat/v1/chat";
|
|
27
|
+
/** What this app's chat server forwards beyond the first contract: the widget, served to every app version, offers only these. */
|
|
28
|
+
export const CHAT_FEATURES_HEADER = "X-Mii-Chat-Features";
|
|
29
|
+
/** Every TanStack query the SDK owns sits under this key. */
|
|
30
|
+
export const QUERY_ROOT = NAMESPACE;
|
|
31
|
+
export const SESSION_COOKIE = `${LEGACY_NAMESPACE}_session`;
|
|
32
|
+
export const STORAGE_KEYS = {
|
|
33
|
+
/** sessionStorage: the build this tab already reloaded for after a missing chunk, so a second miss on it can't loop. */
|
|
34
|
+
chunkRetryBuildId: `${NAMESPACE}:chunk-retry-build-id`,
|
|
35
|
+
/** sessionStorage: scroll positions saved just before a reload onto a new build. */
|
|
36
|
+
scrollBeforeReload: `${NAMESPACE}:scroll-before-reload`,
|
|
37
|
+
/** localStorage prefix: a contract invocation remembered across reloads. */
|
|
38
|
+
invocationPrefix: `${LEGACY_NAMESPACE}-invocation:`,
|
|
39
|
+
/** localStorage, and the URL parameter that sets it: this browser is the org's own automation. */
|
|
40
|
+
agentBrowser: `${LEGACY_NAMESPACE}_agent`
|
|
41
|
+
};
|
|
42
|
+
const PANE_PREFIX = `${LEGACY_NAMESPACE}-preview:`;
|
|
43
|
+
function paneMessage(name) {
|
|
44
|
+
return `${PANE_PREFIX}${name}`;
|
|
45
|
+
}
|
|
46
|
+
/** postMessage types between an app framed in the platform's App pane and the pane. */
|
|
47
|
+
export const PANE_MESSAGES = {
|
|
48
|
+
hello: paneMessage("hello"),
|
|
49
|
+
select: paneMessage("select"),
|
|
50
|
+
locate: paneMessage("locate"),
|
|
51
|
+
ready: paneMessage("ready"),
|
|
52
|
+
route: paneMessage("route"),
|
|
53
|
+
error: paneMessage("error"),
|
|
54
|
+
picked: paneMessage("picked"),
|
|
55
|
+
located: paneMessage("located")
|
|
56
|
+
};
|
package/dist/platform/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// events that pollute product data). Frameworks come and go around this file;
|
|
6
6
|
// nothing in it may import one.
|
|
7
7
|
import { APP_ORIGINS } from "../brand.generated.js";
|
|
8
|
+
import { STORAGE_KEYS } from "./contract.js";
|
|
8
9
|
/**
|
|
9
10
|
* Platform origins allowed to frame an org app AND to drive its preview
|
|
10
11
|
* bridge. One list on purpose: who may frame the app and who may talk to it
|
|
@@ -66,11 +67,11 @@ export function miiAnalyticsPolicy({ vercelEnv = "development", ingestHost, forc
|
|
|
66
67
|
*/
|
|
67
68
|
export function isAgentBrowser(win) {
|
|
68
69
|
try {
|
|
69
|
-
if (new URLSearchParams(win.location.search).has(
|
|
70
|
-
win.localStorage.setItem(
|
|
70
|
+
if (new URLSearchParams(win.location.search).has(STORAGE_KEYS.agentBrowser)) {
|
|
71
|
+
win.localStorage.setItem(STORAGE_KEYS.agentBrowser, "1");
|
|
71
72
|
}
|
|
72
73
|
return (win.navigator.webdriver === true ||
|
|
73
|
-
win.localStorage.getItem(
|
|
74
|
+
win.localStorage.getItem(STORAGE_KEYS.agentBrowser) === "1");
|
|
74
75
|
}
|
|
75
76
|
catch {
|
|
76
77
|
// localStorage can be unavailable (privacy modes); never break the app.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function keepScroll(): void;
|
|
2
|
+
export declare function restoreScroll(): void;
|
|
3
|
+
export declare function takeSession(key: string): string | null;
|
|
4
|
+
export declare function readSession(key: string): string | null;
|
|
5
|
+
export declare function writeSession(key: string, value: string): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The router restores the window's scroll before a list inside the page has its rows; these wait until each is tall enough.
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { STORAGE_KEYS } from "./contract.js";
|
|
4
|
+
import { SCROLL_RESTORE_MS } from "./timing.js";
|
|
5
|
+
const scrollPositions = z.array(z.object({ selector: z.string(), top: z.number() }));
|
|
6
|
+
function pathTo(element) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
for (let node = element; node !== null; node = node.parentElement) {
|
|
9
|
+
const at = node.parentElement === null ? 1 : [...node.parentElement.children].indexOf(node) + 1;
|
|
10
|
+
parts.unshift(`${node.tagName.toLowerCase()}:nth-child(${at})`);
|
|
11
|
+
}
|
|
12
|
+
return `:root > ${parts.slice(1).join(" > ")}`;
|
|
13
|
+
}
|
|
14
|
+
export function keepScroll() {
|
|
15
|
+
const scrolled = [...document.body.querySelectorAll("*")].filter((element) => element.scrollTop > 0);
|
|
16
|
+
writeSession(STORAGE_KEYS.scrollBeforeReload, JSON.stringify(scrolled.map((element) => ({ selector: pathTo(element), top: element.scrollTop }))));
|
|
17
|
+
}
|
|
18
|
+
export function restoreScroll() {
|
|
19
|
+
const saved = takeSession(STORAGE_KEYS.scrollBeforeReload);
|
|
20
|
+
if (saved === null)
|
|
21
|
+
return;
|
|
22
|
+
// Another build's shape: start at the top.
|
|
23
|
+
const read = scrollPositions.safeParse(parseJson(saved));
|
|
24
|
+
if (!read.success)
|
|
25
|
+
return;
|
|
26
|
+
let pending = read.data;
|
|
27
|
+
const until = performance.now() + SCROLL_RESTORE_MS;
|
|
28
|
+
const step = () => {
|
|
29
|
+
pending = pending.filter(({ selector, top }) => {
|
|
30
|
+
const element = document.querySelector(selector);
|
|
31
|
+
if (element === null || element.scrollHeight - element.clientHeight < top)
|
|
32
|
+
return true;
|
|
33
|
+
element.scrollTop = top;
|
|
34
|
+
return false;
|
|
35
|
+
});
|
|
36
|
+
if (pending.length > 0 && performance.now() < until)
|
|
37
|
+
requestAnimationFrame(step);
|
|
38
|
+
};
|
|
39
|
+
requestAnimationFrame(step);
|
|
40
|
+
}
|
|
41
|
+
// Storage can be blocked (privacy settings); never worth breaking the page for.
|
|
42
|
+
export function takeSession(key) {
|
|
43
|
+
try {
|
|
44
|
+
const value = sessionStorage.getItem(key);
|
|
45
|
+
sessionStorage.removeItem(key);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function readSession(key) {
|
|
53
|
+
try {
|
|
54
|
+
return sessionStorage.getItem(key);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function writeSession(key, value) {
|
|
61
|
+
try {
|
|
62
|
+
sessionStorage.setItem(key, value);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function parseJson(text) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(text);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type QueryClient } from "@tanstack/react-query";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
/** How often a visible tab asks what changed: an agent's write shows within about this long. */
|
|
4
|
-
export declare const POLL_MS = 3000;
|
|
5
3
|
declare const changes: z.ZodObject<{
|
|
6
4
|
cursor: z.ZodString;
|
|
7
5
|
changedTables: z.ZodArray<z.ZodString>;
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// reconnect), later a pushed one (SSE) behind the same type.
|
|
7
7
|
import { QueryObserver } from "@tanstack/react-query";
|
|
8
8
|
import { z } from "zod/v4";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import { EntityError, tableQueryKey } from "../entities.js";
|
|
10
|
+
import { CHANGES_PATH, QUERY_ROOT } from "./contract.js";
|
|
11
|
+
import { POLL_MS } from "./timing.js";
|
|
12
12
|
const changes = z.object({ cursor: z.string(), changedTables: z.array(z.string()) });
|
|
13
13
|
/**
|
|
14
14
|
* Polls `_changes` as a TanStack query. One the server breaks on (or answers
|
|
@@ -20,7 +20,7 @@ export function pollChanges(queries, fetchChanges) {
|
|
|
20
20
|
let cursor = null;
|
|
21
21
|
let reported = false;
|
|
22
22
|
const observer = new QueryObserver(queries, {
|
|
23
|
-
queryKey: [
|
|
23
|
+
queryKey: [QUERY_ROOT, CHANGES_PATH],
|
|
24
24
|
queryFn: () => fetchChanges(cursor),
|
|
25
25
|
// Signed out: nothing to watch until the tab is shown again.
|
|
26
26
|
refetchInterval: (query) => (query.state.data === "signed-out" ? false : POLL_MS),
|
|
@@ -49,7 +49,7 @@ export function pollChanges(queries, fetchChanges) {
|
|
|
49
49
|
});
|
|
50
50
|
return () => {
|
|
51
51
|
stop();
|
|
52
|
-
queries.removeQueries({ queryKey: [
|
|
52
|
+
queries.removeQueries({ queryKey: [QUERY_ROOT, CHANGES_PATH], exact: true });
|
|
53
53
|
};
|
|
54
54
|
};
|
|
55
55
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// The browser half of new-build.ts, mounted by the template's MiiPlatform.
|
|
3
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { useRouter } from "@tanstack/react-router";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { STORAGE_KEYS } from "./contract.js";
|
|
7
|
+
import { keepScroll, readSession, restoreScroll, writeSession } from "./keep-scroll.js";
|
|
8
|
+
import { fetchLive, watchBuild } from "./new-build.js";
|
|
9
|
+
const TYPING_IN = new Set(["INPUT", "TEXTAREA", "SELECT"]);
|
|
10
|
+
/** Through shadow roots: the platform's chat bubble is one. */
|
|
11
|
+
function focusedElement() {
|
|
12
|
+
let focused = document.activeElement;
|
|
13
|
+
while (focused !== null && focused.shadowRoot !== null && focused.shadowRoot.activeElement !== null)
|
|
14
|
+
focused = focused.shadowRoot.activeElement;
|
|
15
|
+
return focused;
|
|
16
|
+
}
|
|
17
|
+
function busy() {
|
|
18
|
+
const focused = focusedElement();
|
|
19
|
+
if (focused instanceof HTMLElement && (TYPING_IN.has(focused.tagName) || focused.isContentEditable))
|
|
20
|
+
return true;
|
|
21
|
+
return document.querySelector('[role="dialog"], dialog[open]') !== null;
|
|
22
|
+
}
|
|
23
|
+
/** `buildId` null off Vercel: a dev server has HMR, nothing is watched. */
|
|
24
|
+
export function ReloadOnNewBuild({ buildId }) {
|
|
25
|
+
// router.invalidate() re-runs the root loader, which would answer with the live build.
|
|
26
|
+
const [servedBy] = useState(buildId);
|
|
27
|
+
const queries = useQueryClient();
|
|
28
|
+
const router = useRouter();
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (servedBy === null)
|
|
31
|
+
return;
|
|
32
|
+
restoreScroll();
|
|
33
|
+
return watchBuild({
|
|
34
|
+
buildId: servedBy,
|
|
35
|
+
queries,
|
|
36
|
+
fetchLive,
|
|
37
|
+
visible: () => document.visibilityState === "visible",
|
|
38
|
+
busy,
|
|
39
|
+
// The router also announces its own reloads and search-param changes.
|
|
40
|
+
onNavigate: (callback) => router.subscribe("onBeforeNavigate", ({ toLocation, pathChanged }) => {
|
|
41
|
+
if (pathChanged)
|
|
42
|
+
callback(toLocation.publicHref);
|
|
43
|
+
}),
|
|
44
|
+
onChunkMissing: (callback) => {
|
|
45
|
+
// Not prevented, Vite throws the error on to the page.
|
|
46
|
+
const onError = (event) => {
|
|
47
|
+
if (callback())
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
};
|
|
50
|
+
window.addEventListener("vite:preloadError", onError);
|
|
51
|
+
return () => window.removeEventListener("vite:preloadError", onError);
|
|
52
|
+
},
|
|
53
|
+
reload: () => {
|
|
54
|
+
keepScroll();
|
|
55
|
+
window.location.reload();
|
|
56
|
+
},
|
|
57
|
+
go: (href) => window.location.assign(href),
|
|
58
|
+
chunkRetryBuildId: {
|
|
59
|
+
get: () => readSession(STORAGE_KEYS.chunkRetryBuildId),
|
|
60
|
+
set: (build) => writeSession(STORAGE_KEYS.chunkRetryBuildId, build)
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}, [servedBy, queries, router]);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type QueryClient } from "@tanstack/react-query";
|
|
2
|
+
export type BuildEnv = {
|
|
3
|
+
buildId: string;
|
|
4
|
+
queries: QueryClient;
|
|
5
|
+
fetchLive(): Promise<string | null>;
|
|
6
|
+
visible(): boolean;
|
|
7
|
+
/** An input has focus, or a dialog is open. */
|
|
8
|
+
busy(): boolean;
|
|
9
|
+
/** Before a navigation to another page. */
|
|
10
|
+
onNavigate(callback: (href: string) => void): () => void;
|
|
11
|
+
/** vite:preloadError; the callback says whether it reloaded. */
|
|
12
|
+
onChunkMissing(callback: () => boolean): () => void;
|
|
13
|
+
reload(): void;
|
|
14
|
+
go(href: string): void;
|
|
15
|
+
chunkRetryBuildId: {
|
|
16
|
+
get(): string | null;
|
|
17
|
+
set(build: string): void;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function watchBuild(env: BuildEnv): () => void;
|
|
21
|
+
export declare function fetchLive(): Promise<string | null>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// An open page moves onto a newly published build at a moment nobody is mid-task.
|
|
2
|
+
import { QueryObserver } from "@tanstack/react-query";
|
|
3
|
+
import { z } from "zod/v4";
|
|
4
|
+
import { BUILD_PATH, QUERY_ROOT } from "./contract.js";
|
|
5
|
+
import { CHECK_MS, IDLE_MS } from "./timing.js";
|
|
6
|
+
const live = z.object({ buildId: z.string().nullable() });
|
|
7
|
+
export function watchBuild(env) {
|
|
8
|
+
let stale = false;
|
|
9
|
+
let quietCheck = null;
|
|
10
|
+
const stopQuietCheck = () => {
|
|
11
|
+
if (quietCheck !== null)
|
|
12
|
+
clearTimeout(quietCheck);
|
|
13
|
+
};
|
|
14
|
+
const reloadWhenQuiet = () => {
|
|
15
|
+
if (!env.visible())
|
|
16
|
+
return env.reload();
|
|
17
|
+
quietCheck = setTimeout(() => (!env.visible() || !env.busy() ? env.reload() : reloadWhenQuiet()), IDLE_MS);
|
|
18
|
+
};
|
|
19
|
+
const check = new QueryObserver(env.queries, {
|
|
20
|
+
queryKey: [QUERY_ROOT, BUILD_PATH],
|
|
21
|
+
queryFn: () => env.fetchLive(),
|
|
22
|
+
refetchInterval: CHECK_MS,
|
|
23
|
+
// A hidden tab is the best one to reload.
|
|
24
|
+
refetchIntervalInBackground: true,
|
|
25
|
+
refetchOnWindowFocus: "always",
|
|
26
|
+
refetchOnReconnect: "always",
|
|
27
|
+
retry: false,
|
|
28
|
+
notifyOnChangeProps: ["data"]
|
|
29
|
+
});
|
|
30
|
+
const stopCheck = check.subscribe((result) => {
|
|
31
|
+
if (stale || result.status !== "success" || result.data === env.buildId)
|
|
32
|
+
return;
|
|
33
|
+
stale = true;
|
|
34
|
+
stopCheck();
|
|
35
|
+
reloadWhenQuiet();
|
|
36
|
+
});
|
|
37
|
+
const stopNavigate = env.onNavigate((href) => {
|
|
38
|
+
if (!stale)
|
|
39
|
+
return void check.refetch();
|
|
40
|
+
stopQuietCheck();
|
|
41
|
+
env.go(href);
|
|
42
|
+
});
|
|
43
|
+
const stopChunks = env.onChunkMissing(() => {
|
|
44
|
+
if (env.chunkRetryBuildId.get() === env.buildId)
|
|
45
|
+
return false;
|
|
46
|
+
env.chunkRetryBuildId.set(env.buildId);
|
|
47
|
+
env.reload();
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
stopQuietCheck();
|
|
52
|
+
stopCheck();
|
|
53
|
+
stopNavigate();
|
|
54
|
+
stopChunks();
|
|
55
|
+
env.queries.removeQueries({ queryKey: [QUERY_ROOT, BUILD_PATH], exact: true });
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export async function fetchLive() {
|
|
59
|
+
const response = await fetch(BUILD_PATH, { cache: "no-store" });
|
|
60
|
+
if (!response.ok)
|
|
61
|
+
throw new Error(`GET ${BUILD_PATH} failed (${response.status}).`);
|
|
62
|
+
return live.parse(await response.json()).buildId;
|
|
63
|
+
}
|
|
@@ -17,14 +17,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
17
17
|
// This is only the live channel to the pane, and it leaves no trace.
|
|
18
18
|
import { useEffect, useRef } from "react";
|
|
19
19
|
import { z } from "zod/v4";
|
|
20
|
+
import { PANE_MESSAGES } from "./contract.js";
|
|
20
21
|
import { PLAZA_FRAME_ORIGINS } from "./index.js";
|
|
21
22
|
import { LiveData } from "./live-data-mount.js";
|
|
22
23
|
const PLAZA_ORIGINS = PLAZA_FRAME_ORIGINS;
|
|
23
24
|
/** The platform → app. Anything else the platform posts is not for this bridge. */
|
|
24
25
|
const inbound = z.discriminatedUnion("type", [
|
|
25
|
-
z.object({ type: z.literal(
|
|
26
|
-
z.object({ type: z.literal(
|
|
27
|
-
z.object({ type: z.literal(
|
|
26
|
+
z.object({ type: z.literal(PANE_MESSAGES.hello) }),
|
|
27
|
+
z.object({ type: z.literal(PANE_MESSAGES.select), on: z.boolean() }),
|
|
28
|
+
z.object({ type: z.literal(PANE_MESSAGES.locate), id: z.string(), selector: z.string() })
|
|
28
29
|
]);
|
|
29
30
|
/**
|
|
30
31
|
* Route changes without a router dependency: popstate covers back/forward,
|
|
@@ -76,12 +77,12 @@ export function MiiPaneBridge() {
|
|
|
76
77
|
// these too, but Sentry is somewhere you go afterwards — an agent watching
|
|
77
78
|
// its own change land needs to know now.
|
|
78
79
|
const onError = (event) => send({
|
|
79
|
-
type:
|
|
80
|
+
type: PANE_MESSAGES.error,
|
|
80
81
|
message: event.message,
|
|
81
82
|
source: `${event.filename}:${event.lineno}`
|
|
82
83
|
});
|
|
83
84
|
const onRejection = (event) => send({
|
|
84
|
-
type:
|
|
85
|
+
type: PANE_MESSAGES.error,
|
|
85
86
|
message: String(event.reason),
|
|
86
87
|
source: "unhandled rejection"
|
|
87
88
|
});
|
|
@@ -118,7 +119,7 @@ export function MiiPaneBridge() {
|
|
|
118
119
|
// arrives here under the name it already has.
|
|
119
120
|
const label = target.getAttribute("data-el");
|
|
120
121
|
send({
|
|
121
|
-
type:
|
|
122
|
+
type: PANE_MESSAGES.picked,
|
|
122
123
|
label: label === null ? target.textContent.trim().slice(0, 80) : label,
|
|
123
124
|
selector: selectorFor(target),
|
|
124
125
|
tag: target.tagName.toLowerCase(),
|
|
@@ -133,11 +134,11 @@ export function MiiPaneBridge() {
|
|
|
133
134
|
if (!read.success)
|
|
134
135
|
return;
|
|
135
136
|
const message = read.data;
|
|
136
|
-
if (message.type ===
|
|
137
|
-
send({ type:
|
|
137
|
+
if (message.type === PANE_MESSAGES.hello) {
|
|
138
|
+
send({ type: PANE_MESSAGES.ready, ...describe() });
|
|
138
139
|
return;
|
|
139
140
|
}
|
|
140
|
-
if (message.type ===
|
|
141
|
+
if (message.type === PANE_MESSAGES.select) {
|
|
141
142
|
selecting = message.on;
|
|
142
143
|
document.body.style.cursor = selecting ? "crosshair" : "";
|
|
143
144
|
if (!selecting)
|
|
@@ -150,7 +151,7 @@ export function MiiPaneBridge() {
|
|
|
150
151
|
// different size than the one being replayed.
|
|
151
152
|
const found = document.querySelector(message.selector);
|
|
152
153
|
send({
|
|
153
|
-
type:
|
|
154
|
+
type: PANE_MESSAGES.located,
|
|
154
155
|
id: message.id,
|
|
155
156
|
rect: found === null ? null : rectOf(found)
|
|
156
157
|
});
|
|
@@ -160,7 +161,7 @@ export function MiiPaneBridge() {
|
|
|
160
161
|
const plaza = plazaRef.current;
|
|
161
162
|
if (plaza === null)
|
|
162
163
|
return;
|
|
163
|
-
window.parent.postMessage({ type:
|
|
164
|
+
window.parent.postMessage({ type: PANE_MESSAGES.route, ...describe() }, plaza);
|
|
164
165
|
});
|
|
165
166
|
document.body.appendChild(outline);
|
|
166
167
|
window.addEventListener("error", onError);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Live data: an agent's change shows on an open page within about this long. */
|
|
2
|
+
export declare const POLL_MS = 3000;
|
|
3
|
+
/** New build: how often a tab asks which build is live; browsers throttle hidden tabs' timers to about this anyway. */
|
|
4
|
+
export declare const CHECK_MS = 60000;
|
|
5
|
+
/** New build: how long nobody may be typing or have a dialog open before a visible tab reloads. */
|
|
6
|
+
export declare const IDLE_MS = 5000;
|
|
7
|
+
/** After a reload: how long a list may take to be tall enough to scroll back; past it, it stays at the top. */
|
|
8
|
+
export declare const SCROLL_RESTORE_MS = 5000;
|
|
9
|
+
/** A contract read that failed: how long before asking again. */
|
|
10
|
+
export declare const INVOCATION_RETRY_MS = 1500;
|
|
11
|
+
/** How long a page remembers a contract invocation it started, across reloads. */
|
|
12
|
+
export declare const INVOCATION_REMEMBER_MS: number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// How often and how long the SDK waits in the browser: the knobs, in one place.
|
|
2
|
+
/** Live data: an agent's change shows on an open page within about this long. */
|
|
3
|
+
export const POLL_MS = 3_000;
|
|
4
|
+
/** New build: how often a tab asks which build is live; browsers throttle hidden tabs' timers to about this anyway. */
|
|
5
|
+
export const CHECK_MS = 60_000;
|
|
6
|
+
/** New build: how long nobody may be typing or have a dialog open before a visible tab reloads. */
|
|
7
|
+
export const IDLE_MS = 5_000;
|
|
8
|
+
/** After a reload: how long a list may take to be tall enough to scroll back; past it, it stays at the top. */
|
|
9
|
+
export const SCROLL_RESTORE_MS = 5_000;
|
|
10
|
+
/** A contract read that failed: how long before asking again. */
|
|
11
|
+
export const INVOCATION_RETRY_MS = 1500;
|
|
12
|
+
/** How long a page remembers a contract invocation it started, across reloads. */
|
|
13
|
+
export const INVOCATION_REMEMBER_MS = 24 * 60 * 60 * 1000;
|
package/dist/platform.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { FRAME_ANCESTORS, isAgentBrowser, mayCallServerFn, miiAnalyticsPolicy, miiErrorPolicy, PLAZA_FRAME_ORIGINS, type ServerFnName } from "./platform/index.js";
|
|
2
2
|
export { MiiPaneBridge } from "./platform/pane-bridge.js";
|
|
3
|
+
export { ReloadOnNewBuild } from "./platform/new-build-mount.js";
|
package/dist/platform.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The platform contract a page and the global middleware need: framing by
|
|
2
2
|
// the platform's App pane, the server-function door, analytics and error policies,
|
|
3
|
-
//
|
|
3
|
+
// the pane bridge, and moving an open page onto a new build.
|
|
4
4
|
export { FRAME_ANCESTORS, isAgentBrowser, mayCallServerFn, miiAnalyticsPolicy, miiErrorPolicy, PLAZA_FRAME_ORIGINS } from "./platform/index.js";
|
|
5
5
|
export { MiiPaneBridge } from "./platform/pane-bridge.js";
|
|
6
|
+
export { ReloadOnNewBuild } from "./platform/new-build-mount.js";
|
package/dist/server/entities.js
CHANGED
|
@@ -8,7 +8,8 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
8
8
|
import { createSchemaFactory } from "drizzle-zod";
|
|
9
9
|
import { isSameOrigin } from "../chat/server.js";
|
|
10
10
|
import { z } from "zod/v4";
|
|
11
|
-
import {
|
|
11
|
+
import { LIST_LIMIT } from "../entities.js";
|
|
12
|
+
import { CHANGES_PATH, ENTITIES_PATH } from "../platform/contract.js";
|
|
12
13
|
import { PLATFORM_COLUMNS, fieldSource, isEntity, lightChanges } from "../schema.js";
|
|
13
14
|
import { env } from "./env.js";
|
|
14
15
|
/** Drizzle's text column types: what a search reads. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trylight/app",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "The Light SDK every v2 org app depends on: entities (Drizzle tables with a typed client), sign-in, contracts and in-app chat.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"url": "https://github.com/shubh9/mii.git",
|
|
13
13
|
"directory": "packages/app-sdk"
|
|
14
14
|
},
|
|
15
|
+
"packageManager": "pnpm@11.0.9",
|
|
15
16
|
"files": [
|
|
16
17
|
"dist",
|
|
17
18
|
"bin"
|
|
@@ -41,6 +42,15 @@
|
|
|
41
42
|
"default": "./dist/vite.js"
|
|
42
43
|
}
|
|
43
44
|
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"lint": "eslint src test --max-warnings 0",
|
|
49
|
+
"test": "node --test --import tsx \"test/**/*.test.ts\"",
|
|
50
|
+
"check": "tsx scripts/write-brand.ts --check && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
|
|
51
|
+
"prepack": "pnpm run build",
|
|
52
|
+
"brand": "tsx scripts/write-brand.ts"
|
|
53
|
+
},
|
|
44
54
|
"peerDependencies": {
|
|
45
55
|
"@tanstack/react-query": ">=5.69",
|
|
46
56
|
"@tanstack/react-router": ">=1.170",
|
|
@@ -74,13 +84,5 @@
|
|
|
74
84
|
"typescript-eslint": "^8.70.1",
|
|
75
85
|
"vite": "^8.2.0",
|
|
76
86
|
"zod": "^4.6.5"
|
|
77
|
-
},
|
|
78
|
-
"scripts": {
|
|
79
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
80
|
-
"typecheck": "tsc --noEmit",
|
|
81
|
-
"lint": "eslint src test --max-warnings 0",
|
|
82
|
-
"test": "node --test --import tsx \"test/**/*.test.ts\"",
|
|
83
|
-
"check": "tsx scripts/write-brand.ts --check && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
|
|
84
|
-
"brand": "tsx scripts/write-brand.ts"
|
|
85
87
|
}
|
|
86
|
-
}
|
|
88
|
+
}
|
package/dist/invoke/paths.d.ts
DELETED
package/dist/invoke/paths.js
DELETED