@volter/world-runtime 3.0.32 → 3.0.34
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/src/served-world.js +52 -12
- package/package.json +4 -4
- package/src/served-world.ts +48 -10
package/dist/src/served-world.js
CHANGED
|
@@ -30,7 +30,7 @@ import { boardFrameKey, getActiveWorldStore, withHistoryLocks, captureHistory, h
|
|
|
30
30
|
// to `.volter/token` and `.volter/token.read` (0600).
|
|
31
31
|
// One writer per world by construction: the doors run in this process, serialized per twin.
|
|
32
32
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
33
|
-
import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, siteUrlOf, ORIGINAL_PATH_HEADER, validateRemoteOrigin } from '@volter/world-core/runtime';
|
|
33
|
+
import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, TWIN_SITE_HEADER, siteUrlOf, ORIGINAL_PATH_HEADER, validateRemoteOrigin } from '@volter/world-core/runtime';
|
|
34
34
|
import { resolveTwinHost } from '@volter/world-core/host-routing';
|
|
35
35
|
import { assertBrowserName, validateBrowserState, cookieMatchesHost } from '@volter/world-core';
|
|
36
36
|
import { worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, materializeBrowserFiles, browserLoad, putBrowserLoad, browserLoadId } from "./browsers.js";
|
|
@@ -1095,8 +1095,8 @@ export class WorldDoors {
|
|
|
1095
1095
|
...(packFacts()[vendor]?.hosts ?? []).flatMap(rule => typeof rule.host === 'string' && !rule.exclude ? [rule.host] : []),
|
|
1096
1096
|
])] }));
|
|
1097
1097
|
}
|
|
1098
|
-
async browserRoutes(origin) {
|
|
1099
|
-
const listed = await this.claimedHosts(origin);
|
|
1098
|
+
async browserRoutes(origin, hosts) {
|
|
1099
|
+
const listed = hosts ?? await this.claimedHosts(origin);
|
|
1100
1100
|
const facts = packFacts();
|
|
1101
1101
|
const map = Object.fromEntries(Object.keys(this.up().services).map(vendor => [vendor, vendor]));
|
|
1102
1102
|
const claimed = {};
|
|
@@ -1196,17 +1196,32 @@ export class WorldDoors {
|
|
|
1196
1196
|
if (browser && !grant)
|
|
1197
1197
|
return Response.json({ error: "open this browser site from the World's board" }, { status: 401 });
|
|
1198
1198
|
const url = new URL(request.url);
|
|
1199
|
-
const
|
|
1199
|
+
const pageHost = host;
|
|
1200
|
+
// This prefix belongs only to a hosted site's session, never to a bare twin or the content plane.
|
|
1201
|
+
const resource = grant && this.siteShaped(url) && url.pathname.startsWith('/_host/');
|
|
1202
|
+
const listed = await this.claimedHosts(url.origin);
|
|
1203
|
+
const routes = browser || resource ? await this.browserRoutes(url.origin, listed) : null;
|
|
1204
|
+
let path = url.pathname;
|
|
1200
1205
|
let vendor;
|
|
1201
|
-
if (routes)
|
|
1202
|
-
vendor = resolveTwinHost(routes.rules, host, routes.map,
|
|
1206
|
+
if (browser && routes)
|
|
1207
|
+
vendor = resolveTwinHost(routes.rules, host, routes.map, resource ? '/' : path, routes.claimed)?.origin;
|
|
1203
1208
|
else
|
|
1204
|
-
for (const row of
|
|
1209
|
+
for (const row of listed) {
|
|
1205
1210
|
if (!this.host.twinFetch(row.vendor) || !row.hosts.includes(host))
|
|
1206
1211
|
continue;
|
|
1207
1212
|
vendor = row.vendor;
|
|
1208
1213
|
break;
|
|
1209
1214
|
}
|
|
1215
|
+
if (resource && routes) {
|
|
1216
|
+
const target = /^\/_host\/([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(\/.*)?$/.exec(path);
|
|
1217
|
+
const targetHost = target?.[1];
|
|
1218
|
+
const targetPath = target?.[2] ?? '/';
|
|
1219
|
+
const targetVendor = targetHost ? resolveTwinHost(routes.rules, targetHost, routes.map, targetPath, routes.claimed)?.origin : undefined;
|
|
1220
|
+
if (!vendor || targetVendor !== vendor)
|
|
1221
|
+
return Response.json({ error: 'this site serves only its own vendor hosts' }, { status: 404 });
|
|
1222
|
+
host = targetHost;
|
|
1223
|
+
path = targetPath;
|
|
1224
|
+
}
|
|
1210
1225
|
const twin = vendor ? this.host.twinFetch(vendor) : undefined;
|
|
1211
1226
|
if (vendor && twin) {
|
|
1212
1227
|
const reads = request.method === 'GET' || request.method === 'HEAD';
|
|
@@ -1214,7 +1229,7 @@ export class WorldDoors {
|
|
|
1214
1229
|
if (browser && grant?.scope === 'read' && !reads && !enforced)
|
|
1215
1230
|
return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
|
|
1216
1231
|
const headers = browser ? new Headers(request.headers) : new Headers();
|
|
1217
|
-
for (const key of [TOKEN_HEADER, TWINS_KEY_HEADER, CALLER_HEADER, ORIGINAL_PATH_HEADER, READ_ONLY_HEADER, 'host', TWIN_PREFIX_HEADER])
|
|
1232
|
+
for (const key of [TOKEN_HEADER, TWINS_KEY_HEADER, CALLER_HEADER, ORIGINAL_PATH_HEADER, READ_ONLY_HEADER, 'host', TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, TWIN_SITE_HEADER])
|
|
1218
1233
|
headers.delete(key);
|
|
1219
1234
|
for (const name of ['accept', 'accept-language', 'if-none-match', 'range', 'user-agent']) {
|
|
1220
1235
|
const v = request.headers.get(name);
|
|
@@ -1226,9 +1241,12 @@ export class WorldDoors {
|
|
|
1226
1241
|
if (cookies.length)
|
|
1227
1242
|
headers.set('cookie', cookies.join('; '));
|
|
1228
1243
|
headers.set('x-volter-world-browser', browser);
|
|
1244
|
+
}
|
|
1245
|
+
if (grant && this.siteShaped(url)) {
|
|
1229
1246
|
const template = this.host.sitesTemplate?.();
|
|
1230
1247
|
if (template)
|
|
1231
1248
|
headers.set(TWIN_SITES_HEADER, template);
|
|
1249
|
+
headers.set(TWIN_SITE_HEADER, pageHost);
|
|
1232
1250
|
headers.set('x-forwarded-host', url.host);
|
|
1233
1251
|
headers.set('x-forwarded-proto', url.protocol.slice(0, -1));
|
|
1234
1252
|
}
|
|
@@ -1241,14 +1259,17 @@ export class WorldDoors {
|
|
|
1241
1259
|
headers.set('traceparent', traceparentForDelivery(request.headers.get('traceparent')) ?? newTraceparent());
|
|
1242
1260
|
}
|
|
1243
1261
|
const body = reads ? undefined : await request.arrayBuffer();
|
|
1244
|
-
const answer = await twin(new Request(`${url.origin}${
|
|
1262
|
+
const answer = await twin(new Request(`${url.origin}${path}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
|
|
1245
1263
|
// a redirect the site answers names its own hostname: in the World it stays under the World's origin
|
|
1246
1264
|
const out = new Headers(answer.headers);
|
|
1247
1265
|
const location = out.get('location');
|
|
1248
1266
|
if (location) {
|
|
1249
1267
|
try {
|
|
1250
|
-
const to = new URL(location, `https://${host}`);
|
|
1251
|
-
|
|
1268
|
+
const to = new URL(location, `https://${host}${path}${url.search}`);
|
|
1269
|
+
const sameVendor = resource && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed)?.origin === vendor;
|
|
1270
|
+
if (sameVendor)
|
|
1271
|
+
out.set('location', `${to.hostname === pageHost ? '' : `/_host/${to.hostname}`}${to.pathname}${to.search}${to.hash}`);
|
|
1272
|
+
else if (to.hostname === host)
|
|
1252
1273
|
out.set('location', `${to.pathname}${to.search}${to.hash}`);
|
|
1253
1274
|
else if (browser && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed) && routes.sites.some(row => row.hosts.includes(to.hostname))) {
|
|
1254
1275
|
const at = this.host.sitesTemplate?.();
|
|
@@ -2656,11 +2677,30 @@ export class WorldDoors {
|
|
|
2656
2677
|
if (home)
|
|
2657
2678
|
listed = [{ id: `home:${home.id}`, kind: 'home', title: vendor, section: { id: 'home', title: 'Home' }, url: home.path, covers: [], drawn: true }];
|
|
2658
2679
|
}
|
|
2680
|
+
const entries = wholeLog(this.stateOf(vendor), this.controlRoot(vendor));
|
|
2681
|
+
// One index per vendor, rather than scanning its history for every page on every board read.
|
|
2682
|
+
const positions = new Map();
|
|
2683
|
+
let vendorPosition = 0;
|
|
2684
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2685
|
+
const entry = entries[i];
|
|
2686
|
+
if (entry.subject.type.startsWith('_'))
|
|
2687
|
+
continue;
|
|
2688
|
+
vendorPosition = i + 1;
|
|
2689
|
+
positions.set(`${entry.subject.type}:${entry.subject.id}`, vendorPosition);
|
|
2690
|
+
if (entry.aliasOf)
|
|
2691
|
+
positions.set(`${entry.subject.type}:${entry.aliasOf}`, vendorPosition);
|
|
2692
|
+
}
|
|
2659
2693
|
for (const f of listed) {
|
|
2660
2694
|
const covered = new Set(f.covers.map(c => `${c.type}:${c.id}`));
|
|
2661
2695
|
const mine = changes.filter(x => x.service === vendor && (fallback || covered.has(`${x.action.subject.type}:${x.action.subject.id}`)));
|
|
2696
|
+
// A page's revision includes already-cut writes and landed receipts, independent of the board's filter.
|
|
2697
|
+
// Another page's writes never move it. A fallback home covers the vendor's public writes, as its change count does.
|
|
2698
|
+
let position = fallback ? vendorPosition : 0;
|
|
2699
|
+
if (!fallback)
|
|
2700
|
+
for (const key of covered)
|
|
2701
|
+
position = Math.max(position, positions.get(key) ?? 0);
|
|
2662
2702
|
const add = (at, browser, unseen) => {
|
|
2663
|
-
frames.push({ frame: { vendor, ...(browser ? { browser } : {}), id: f.id, kind: f.kind, title: f.title, section: f.section, url: f.url, at, ...(f.parent ? { parent: f.parent } : {}), ...(f.ours ? { ours: true } : {}), ...(unseen ? { unseen } : {}) }, mine });
|
|
2703
|
+
frames.push({ frame: { vendor, ...(browser ? { browser } : {}), id: f.id, kind: f.kind, title: f.title, section: f.section, url: f.url, position, at, ...(f.parent ? { parent: f.parent } : {}), ...(f.ours ? { ours: true } : {}), ...(unseen ? { unseen } : {}) }, mine });
|
|
2664
2704
|
};
|
|
2665
2705
|
let plain = false;
|
|
2666
2706
|
const openedBrowsers = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.34",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -68,14 +68,14 @@
|
|
|
68
68
|
"node": ">=22.3"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@volter/world-core": "3.0.
|
|
72
|
-
"@volter/world-console": "3.0.
|
|
71
|
+
"@volter/world-core": "3.0.34",
|
|
72
|
+
"@volter/world-console": "3.0.29"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"@electric-sql/pglite": "0.5.8",
|
|
76
76
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
77
77
|
"@volter/world-access": "3.0.1",
|
|
78
|
-
"@volter/world-core": "3.0.
|
|
78
|
+
"@volter/world-core": "3.0.34",
|
|
79
79
|
"pg-gateway": "0.3.0-beta.4",
|
|
80
80
|
"smol-toml": "^1.8.0",
|
|
81
81
|
"yaml": "2.9.0"
|
package/src/served-world.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { boardFrameKey, getActiveWorldStore, withHistoryLocks, captureHistory, h
|
|
|
30
30
|
// to `.volter/token` and `.volter/token.read` (0600).
|
|
31
31
|
// One writer per world by construction: the doors run in this process, serialized per twin.
|
|
32
32
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync , rmSync} from 'node:fs';
|
|
33
|
-
import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, type TwinRequestJournalCursor, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, siteLabel, siteUrlOf, ORIGINAL_PATH_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink, type BoardFrame } from '@volter/world-core/runtime';
|
|
33
|
+
import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, type TwinRequestJournalCursor, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, TWIN_SITE_HEADER, siteLabel, siteUrlOf, ORIGINAL_PATH_HEADER, validateRemoteOrigin, type SealedCredential, type TwinStreamConnection, type TwinStreamSink, type BoardFrame } from '@volter/world-core/runtime';
|
|
34
34
|
import { resolveTwinHost, type HostPredicate } from '@volter/world-core/host-routing';
|
|
35
35
|
import { assertBrowserName, validateBrowserState, cookieMatchesHost } from '@volter/world-core';
|
|
36
36
|
import { worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, materializeBrowserFiles, browserLoad, putBrowserLoad, browserLoadId } from './browsers.ts';
|
|
@@ -997,8 +997,8 @@ export class WorldDoors {
|
|
|
997
997
|
])] }));
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
|
-
private async browserRoutes(origin: string): Promise<{ sites: Array<{ vendor: string; hosts: string[] }>; rules: Record<string, HostPredicate>; map: Record<string, string>; claimed: Record<string, Set<string>> }> {
|
|
1001
|
-
const listed = await this.claimedHosts(origin);
|
|
1000
|
+
private async browserRoutes(origin: string, hosts?: Array<{ vendor: string; hosts: string[] }>): Promise<{ sites: Array<{ vendor: string; hosts: string[] }>; rules: Record<string, HostPredicate>; map: Record<string, string>; claimed: Record<string, Set<string>> }> {
|
|
1001
|
+
const listed = hosts ?? await this.claimedHosts(origin);
|
|
1002
1002
|
const facts = packFacts();
|
|
1003
1003
|
const map: Record<string, string> = Object.fromEntries(Object.keys(this.up().services).map(vendor => [vendor, vendor]));
|
|
1004
1004
|
const claimed: Record<string, Set<string>> = {};
|
|
@@ -1078,28 +1078,45 @@ export class WorldDoors {
|
|
|
1078
1078
|
if (!browser && request.method !== 'GET' && request.method !== 'HEAD') return Response.json({ error: 'a site in a World is read: GET and HEAD' }, { status: 405 });
|
|
1079
1079
|
if (browser && !grant) return Response.json({ error: "open this browser site from the World's board" }, { status: 401 });
|
|
1080
1080
|
const url = new URL(request.url);
|
|
1081
|
-
const
|
|
1081
|
+
const pageHost = host;
|
|
1082
|
+
// This prefix belongs only to a hosted site's session, never to a bare twin or the content plane.
|
|
1083
|
+
const resource = grant && this.siteShaped(url) && url.pathname.startsWith('/_host/');
|
|
1084
|
+
const listed = await this.claimedHosts(url.origin);
|
|
1085
|
+
const routes = browser || resource ? await this.browserRoutes(url.origin, listed) : null;
|
|
1086
|
+
let path = url.pathname;
|
|
1082
1087
|
let vendor: string | undefined;
|
|
1083
|
-
if (routes) vendor = resolveTwinHost(routes.rules, host, routes.map,
|
|
1084
|
-
else for (const row of
|
|
1088
|
+
if (browser && routes) vendor = resolveTwinHost(routes.rules, host, routes.map, resource ? '/' : path, routes.claimed)?.origin;
|
|
1089
|
+
else for (const row of listed) {
|
|
1085
1090
|
if (!this.host.twinFetch(row.vendor) || !row.hosts.includes(host)) continue;
|
|
1086
1091
|
vendor = row.vendor;
|
|
1087
1092
|
break;
|
|
1088
1093
|
}
|
|
1094
|
+
if (resource && routes) {
|
|
1095
|
+
const target = /^\/_host\/([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(\/.*)?$/.exec(path);
|
|
1096
|
+
const targetHost = target?.[1];
|
|
1097
|
+
const targetPath = target?.[2] ?? '/';
|
|
1098
|
+
const targetVendor = targetHost ? resolveTwinHost(routes.rules, targetHost, routes.map, targetPath, routes.claimed)?.origin : undefined;
|
|
1099
|
+
if (!vendor || targetVendor !== vendor) return Response.json({ error: 'this site serves only its own vendor hosts' }, { status: 404 });
|
|
1100
|
+
host = targetHost!;
|
|
1101
|
+
path = targetPath;
|
|
1102
|
+
}
|
|
1089
1103
|
const twin = vendor ? this.host.twinFetch(vendor) : undefined;
|
|
1090
1104
|
if (vendor && twin) {
|
|
1091
1105
|
const reads = request.method === 'GET' || request.method === 'HEAD';
|
|
1092
1106
|
const enforced = browser && grant?.scope === 'read' && await this.enforcesRead(vendor, twin, url.origin);
|
|
1093
1107
|
if (browser && grant?.scope === 'read' && !reads && !enforced) return Response.json({ error: 'the read token opens only reads' }, { status: 403 });
|
|
1094
1108
|
const headers = browser ? new Headers(request.headers) : new Headers();
|
|
1095
|
-
for (const key of [TOKEN_HEADER, TWINS_KEY_HEADER, CALLER_HEADER, ORIGINAL_PATH_HEADER, READ_ONLY_HEADER, 'host', TWIN_PREFIX_HEADER]) headers.delete(key);
|
|
1109
|
+
for (const key of [TOKEN_HEADER, TWINS_KEY_HEADER, CALLER_HEADER, ORIGINAL_PATH_HEADER, READ_ONLY_HEADER, 'host', TWIN_PREFIX_HEADER, TWIN_SITES_HEADER, TWIN_SITE_HEADER]) headers.delete(key);
|
|
1096
1110
|
for (const name of ['accept', 'accept-language', 'if-none-match', 'range', 'user-agent']) { const v = request.headers.get(name); if (v !== null) headers.set(name, v); }
|
|
1097
1111
|
if (browser) {
|
|
1098
1112
|
const cookies = (request.headers.get('cookie') ?? '').split(/;\s*/).filter(c => !/^(?:__Host-)?volter_(?:world|pages|browser)=/.test(c));
|
|
1099
1113
|
if (cookies.length) headers.set('cookie', cookies.join('; '));
|
|
1100
1114
|
headers.set('x-volter-world-browser', browser);
|
|
1115
|
+
}
|
|
1116
|
+
if (grant && this.siteShaped(url)) {
|
|
1101
1117
|
const template = this.host.sitesTemplate?.();
|
|
1102
1118
|
if (template) headers.set(TWIN_SITES_HEADER, template);
|
|
1119
|
+
headers.set(TWIN_SITE_HEADER, pageHost);
|
|
1103
1120
|
headers.set('x-forwarded-host', url.host);
|
|
1104
1121
|
headers.set('x-forwarded-proto', url.protocol.slice(0, -1));
|
|
1105
1122
|
}
|
|
@@ -1110,11 +1127,17 @@ export class WorldDoors {
|
|
|
1110
1127
|
headers.set('traceparent', traceparentForDelivery(request.headers.get('traceparent')) ?? newTraceparent());
|
|
1111
1128
|
}
|
|
1112
1129
|
const body = reads ? undefined : await request.arrayBuffer();
|
|
1113
|
-
const answer = await twin(new Request(`${url.origin}${
|
|
1130
|
+
const answer = await twin(new Request(`${url.origin}${path}${url.search}`, { method: request.method, headers, ...(body === undefined ? {} : { body }), redirect: 'manual' }));
|
|
1114
1131
|
// a redirect the site answers names its own hostname: in the World it stays under the World's origin
|
|
1115
1132
|
const out = new Headers(answer.headers);
|
|
1116
1133
|
const location = out.get('location');
|
|
1117
|
-
if (location) { try {
|
|
1134
|
+
if (location) { try {
|
|
1135
|
+
const to = new URL(location, `https://${host}${path}${url.search}`);
|
|
1136
|
+
const sameVendor = resource && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed)?.origin === vendor;
|
|
1137
|
+
if (sameVendor) out.set('location', `${to.hostname === pageHost ? '' : `/_host/${to.hostname}`}${to.pathname}${to.search}${to.hash}`);
|
|
1138
|
+
else if (to.hostname === host) out.set('location', `${to.pathname}${to.search}${to.hash}`);
|
|
1139
|
+
else if (browser && routes && resolveTwinHost(routes.rules, to.hostname, routes.map, to.pathname, routes.claimed) && routes.sites.some(row => row.hosts.includes(to.hostname))) { const at = this.host.sitesTemplate?.(); const next = at ? siteUrlOf(at, to.hostname, browser) : null; if (next) out.set('location', `${next}${to.pathname}${to.search}${to.hash}`); }
|
|
1140
|
+
} catch { /* left as answered */ } }
|
|
1118
1141
|
return new Response(request.method === 'HEAD' ? null : answer.body, { status: answer.status, statusText: answer.statusText, headers: out });
|
|
1119
1142
|
}
|
|
1120
1143
|
// a person's browser is told in words (often the site is not there yet, or was not in a copy of the World as it was)
|
|
@@ -2212,11 +2235,26 @@ export class WorldDoors {
|
|
|
2212
2235
|
homeHost = home?.host;
|
|
2213
2236
|
if (home) listed = [{ id: `home:${home.id}`, kind: 'home', title: vendor, section: { id: 'home', title: 'Home' }, url: home.path, covers: [], drawn: true }];
|
|
2214
2237
|
}
|
|
2238
|
+
const entries = wholeLog(this.stateOf(vendor), this.controlRoot(vendor));
|
|
2239
|
+
// One index per vendor, rather than scanning its history for every page on every board read.
|
|
2240
|
+
const positions = new Map<string, number>();
|
|
2241
|
+
let vendorPosition = 0;
|
|
2242
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2243
|
+
const entry = entries[i]!;
|
|
2244
|
+
if (entry.subject.type.startsWith('_')) continue;
|
|
2245
|
+
vendorPosition = i + 1;
|
|
2246
|
+
positions.set(`${entry.subject.type}:${entry.subject.id}`, vendorPosition);
|
|
2247
|
+
if (entry.aliasOf) positions.set(`${entry.subject.type}:${entry.aliasOf}`, vendorPosition);
|
|
2248
|
+
}
|
|
2215
2249
|
for (const f of listed) {
|
|
2216
2250
|
const covered = new Set(f.covers.map(c => `${c.type}:${c.id}`));
|
|
2217
2251
|
const mine = changes.filter(x => x.service === vendor && (fallback || covered.has(`${x.action.subject.type}:${x.action.subject.id}`)));
|
|
2252
|
+
// A page's revision includes already-cut writes and landed receipts, independent of the board's filter.
|
|
2253
|
+
// Another page's writes never move it. A fallback home covers the vendor's public writes, as its change count does.
|
|
2254
|
+
let position = fallback ? vendorPosition : 0;
|
|
2255
|
+
if (!fallback) for (const key of covered) position = Math.max(position, positions.get(key) ?? 0);
|
|
2218
2256
|
const add = (at: { site: string } | { screen: string } | null, browser?: string, unseen?: string): void => {
|
|
2219
|
-
frames.push({ frame: { vendor, ...(browser ? { browser } : {}), id: f.id, kind: f.kind, title: f.title, section: f.section, url: f.url, at, ...(f.parent ? { parent: f.parent } : {}), ...(f.ours ? { ours: true } : {}), ...(unseen ? { unseen } : {}) }, mine });
|
|
2257
|
+
frames.push({ frame: { vendor, ...(browser ? { browser } : {}), id: f.id, kind: f.kind, title: f.title, section: f.section, url: f.url, position, at, ...(f.parent ? { parent: f.parent } : {}), ...(f.ours ? { ours: true } : {}), ...(unseen ? { unseen } : {}) }, mine });
|
|
2220
2258
|
};
|
|
2221
2259
|
let plain = false;
|
|
2222
2260
|
const openedBrowsers: Array<{ browser: string; at: string }> = [];
|