@world-engines/project-host 0.1.0-alpha.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/LICENSE +46 -0
- package/assets/gui/WELogo-title.svg +11 -0
- package/assets/gui/app.js +1326 -0
- package/assets/gui/index.html +118 -0
- package/assets/gui/local-preview-asset-urls.js +61 -0
- package/assets/gui/local-preview-bridge.js +393 -0
- package/assets/gui/spatial-template-panel.js +233 -0
- package/assets/gui/styles.css +115 -0
- package/assets/gui/terrain-render.js +120 -0
- package/assets/gui/workspace-status.css +17 -0
- package/assets/gui/workspace-status.js +255 -0
- package/assets/gui/world-map-globe.js +187 -0
- package/assets/gui/world-map-worker.js +62 -0
- package/assets/gui/world-map.css +30 -0
- package/assets/gui/world-map.js +2466 -0
- package/dist/account-model-proxy.d.ts +21 -0
- package/dist/account-model-proxy.js +470 -0
- package/dist/authoring-browser-runtime.d.ts +7 -0
- package/dist/authoring-browser-runtime.js +110 -0
- package/dist/authoring-ingestion.d.ts +38 -0
- package/dist/authoring-ingestion.js +410 -0
- package/dist/authoring-model-connection.d.ts +166 -0
- package/dist/authoring-model-connection.js +757 -0
- package/dist/chinese-lexical.d.ts +152 -0
- package/dist/chinese-lexical.js +555 -0
- package/dist/chromium-preview-browser-driver.d.ts +43 -0
- package/dist/chromium-preview-browser-driver.js +575 -0
- package/dist/cli/download.d.ts +12 -0
- package/dist/cli/download.js +50 -0
- package/dist/cli/dump.d.ts +32 -0
- package/dist/cli/dump.js +91 -0
- package/dist/cli/ingest.d.ts +49 -0
- package/dist/cli/ingest.js +225 -0
- package/dist/cli/review.d.ts +10 -0
- package/dist/cli/review.js +44 -0
- package/dist/cli/upload.d.ts +57 -0
- package/dist/cli/upload.js +112 -0
- package/dist/cli-command.d.ts +60 -0
- package/dist/cli-command.js +613 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/desktop-authoring-runtime.d.ts +88 -0
- package/dist/desktop-authoring-runtime.js +1602 -0
- package/dist/desktop-sidecar.d.ts +11 -0
- package/dist/desktop-sidecar.js +143 -0
- package/dist/document-dump.d.ts +113 -0
- package/dist/document-dump.js +431 -0
- package/dist/document-graph-extractor.d.ts +125 -0
- package/dist/document-graph-extractor.js +588 -0
- package/dist/document-ingestion-journal.d.ts +68 -0
- package/dist/document-ingestion-journal.js +471 -0
- package/dist/document-ingestion.d.ts +183 -0
- package/dist/document-ingestion.js +976 -0
- package/dist/document-query-path-proof.d.ts +30 -0
- package/dist/document-query-path-proof.js +34 -0
- package/dist/document-query-path-renderer.d.ts +50 -0
- package/dist/document-query-path-renderer.js +75 -0
- package/dist/document-query-proof-paths.d.ts +22 -0
- package/dist/document-query-proof-paths.js +71 -0
- package/dist/document-query.d.ts +97 -0
- package/dist/document-query.js +1106 -0
- package/dist/document-relation-role.d.ts +14 -0
- package/dist/document-relation-role.js +86 -0
- package/dist/document-source.d.ts +107 -0
- package/dist/document-source.js +255 -0
- package/dist/embedding-cache.d.ts +35 -0
- package/dist/embedding-cache.js +317 -0
- package/dist/frozen-review-local-pipeline.d.ts +88 -0
- package/dist/frozen-review-local-pipeline.js +609 -0
- package/dist/frozen-review-source-capture.d.ts +15 -0
- package/dist/frozen-review-source-capture.js +551 -0
- package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
- package/dist/frozen-review-wasm-encryptor.js +72 -0
- package/dist/frozen-review.d.ts +131 -0
- package/dist/frozen-review.js +802 -0
- package/dist/graph-traversal.d.ts +23 -0
- package/dist/graph-traversal.js +100 -0
- package/dist/gui/graph-layout-worker.js +1 -0
- package/dist/gui/graph-layout.wasm +0 -0
- package/dist/gui/graph.css +1 -0
- package/dist/gui/graph.js +76 -0
- package/dist/gui/plane-generation.js +112 -0
- package/dist/gui/plane-generator.js +310 -0
- package/dist/gui/plane-generator_bg.wasm +0 -0
- package/dist/gui/procedural-city.js +595 -0
- package/dist/gui/road-generation.js +129 -0
- package/dist/gui/road-parcels.js +1584 -0
- package/dist/gui/score.css +1 -0
- package/dist/gui/score.js +54 -0
- package/dist/gui/spatial-runtime.js +1231 -0
- package/dist/gui/spatial-templates.js +200 -0
- package/dist/gui/trigger.css +1 -0
- package/dist/gui/trigger.js +142 -0
- package/dist/harness-callback-transport.d.ts +17 -0
- package/dist/harness-callback-transport.js +128 -0
- package/dist/harness-document-ingestion.d.ts +77 -0
- package/dist/harness-document-ingestion.js +343 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/ingestion-source-reconciliation.d.ts +88 -0
- package/dist/ingestion-source-reconciliation.js +253 -0
- package/dist/ladybug-runtime.d.ts +117 -0
- package/dist/ladybug-runtime.js +886 -0
- package/dist/local-account-embedding.d.ts +34 -0
- package/dist/local-account-embedding.js +294 -0
- package/dist/local-gallery.d.ts +35 -0
- package/dist/local-gallery.js +227 -0
- package/dist/local-gui-auth.d.ts +96 -0
- package/dist/local-gui-auth.js +703 -0
- package/dist/local-gui-contract.d.ts +53 -0
- package/dist/local-gui-contract.js +5 -0
- package/dist/local-gui-image-assets.d.ts +17 -0
- package/dist/local-gui-image-assets.js +96 -0
- package/dist/local-gui-preview-client.d.ts +3 -0
- package/dist/local-gui-preview-client.js +111 -0
- package/dist/local-gui-score.d.ts +72 -0
- package/dist/local-gui-score.js +184 -0
- package/dist/local-gui-server.d.ts +10 -0
- package/dist/local-gui-server.js +987 -0
- package/dist/local-gui-version.d.ts +17 -0
- package/dist/local-gui-version.js +147 -0
- package/dist/local-gui-workspace.d.ts +14 -0
- package/dist/local-gui-workspace.js +171 -0
- package/dist/local-model-settings.d.ts +28 -0
- package/dist/local-model-settings.js +124 -0
- package/dist/local-preview-assets.d.ts +28 -0
- package/dist/local-preview-assets.js +124 -0
- package/dist/local-preview-bound-resources.d.ts +12 -0
- package/dist/local-preview-bound-resources.js +80 -0
- package/dist/local-preview-message-store.d.ts +63 -0
- package/dist/local-preview-message-store.js +214 -0
- package/dist/local-preview-player-read-models.d.ts +47 -0
- package/dist/local-preview-player-read-models.js +90 -0
- package/dist/local-preview-projections.d.ts +61 -0
- package/dist/local-preview-projections.js +117 -0
- package/dist/local-preview-runtime.d.ts +105 -0
- package/dist/local-preview-runtime.js +553 -0
- package/dist/local-preview-session.d.ts +73 -0
- package/dist/local-preview-session.js +395 -0
- package/dist/local-preview-source.d.ts +40 -0
- package/dist/local-preview-source.js +192 -0
- package/dist/local-preview-view-events.d.ts +101 -0
- package/dist/local-preview-view-events.js +179 -0
- package/dist/local-preview-view-state-sqlite.d.ts +24 -0
- package/dist/local-preview-view-state-sqlite.js +263 -0
- package/dist/local-preview-view-state.d.ts +93 -0
- package/dist/local-preview-view-state.js +108 -0
- package/dist/local-preview-vite-config.d.ts +6 -0
- package/dist/local-preview-vite-config.js +122 -0
- package/dist/local-preview-world-effects.d.ts +9 -0
- package/dist/local-preview-world-effects.js +55 -0
- package/dist/local-preview-worldline-store.d.ts +47 -0
- package/dist/local-preview-worldline-store.js +282 -0
- package/dist/local-preview-worldlines.d.ts +95 -0
- package/dist/local-preview-worldlines.js +136 -0
- package/dist/mcp-async-dispatcher.d.ts +3 -0
- package/dist/mcp-async-dispatcher.js +129 -0
- package/dist/mcp-line-queue.d.ts +14 -0
- package/dist/mcp-line-queue.js +184 -0
- package/dist/mcp-pipe-internal.d.ts +6 -0
- package/dist/mcp-pipe-internal.js +151 -0
- package/dist/mcp-pipe-response.d.ts +14 -0
- package/dist/mcp-pipe-response.js +50 -0
- package/dist/mcp-stdio-response.d.ts +12 -0
- package/dist/mcp-stdio-response.js +27 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +194 -0
- package/dist/prepared-embedding-cache.d.ts +38 -0
- package/dist/prepared-embedding-cache.js +238 -0
- package/dist/preview-browser-adapter.d.ts +53 -0
- package/dist/preview-browser-adapter.js +171 -0
- package/dist/preview-runtime.d.ts +67 -0
- package/dist/preview-runtime.js +460 -0
- package/dist/project-auth-transport.d.ts +27 -0
- package/dist/project-auth-transport.js +121 -0
- package/dist/project-auth.d.ts +51 -0
- package/dist/project-auth.js +108 -0
- package/dist/project-git-capture-recovery.d.ts +16 -0
- package/dist/project-git-capture-recovery.js +347 -0
- package/dist/project-git-cli.d.ts +5 -0
- package/dist/project-git-cli.js +40 -0
- package/dist/project-git-scenario-source.d.ts +10 -0
- package/dist/project-git-scenario-source.js +240 -0
- package/dist/project-git-view-source.d.ts +13 -0
- package/dist/project-git-view-source.js +134 -0
- package/dist/project-git.d.ts +97 -0
- package/dist/project-git.js +656 -0
- package/dist/project-host.d.ts +59 -0
- package/dist/project-host.js +471 -0
- package/dist/project-writer-lease.d.ts +18 -0
- package/dist/project-writer-lease.js +430 -0
- package/dist/public-error.d.ts +7 -0
- package/dist/public-error.js +61 -0
- package/dist/retrieval-benchmark.d.ts +46 -0
- package/dist/retrieval-benchmark.js +58 -0
- package/dist/safe-project-path.d.ts +8 -0
- package/dist/safe-project-path.js +93 -0
- package/dist/scenario-writer.d.ts +32 -0
- package/dist/scenario-writer.js +191 -0
- package/dist/transfer/download.d.ts +159 -0
- package/dist/transfer/download.js +598 -0
- package/dist/transfer/transport.d.ts +26 -0
- package/dist/transfer/transport.js +1 -0
- package/dist/transfer/upload.d.ts +162 -0
- package/dist/transfer/upload.js +354 -0
- package/dist/transfer-types.d.ts +95 -0
- package/dist/transfer-types.js +1 -0
- package/dist/transfer.d.ts +18 -0
- package/dist/transfer.js +273 -0
- package/dist/trigger-runtime.d.ts +121 -0
- package/dist/trigger-runtime.js +153 -0
- package/dist/vendor/wasm-core/asset-hashes.json +5 -0
- package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
- package/dist/vendor/wasm-core/wasm_core.js +1 -0
- package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
- package/dist/view-watcher.d.ts +9 -0
- package/dist/view-watcher.js +34 -0
- package/package.json +110 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type TmwAuthorViewEventV1, type ViewEventDeliveryMessageV1, type ViewInteractionEventV3 } from "@world-engines/tmw";
|
|
2
|
+
export interface LocalPreviewViewRuntimeBinding {
|
|
3
|
+
/** LocalPreviewRuntime.readViewInteractionBinding() 的 Host-only 原始 layout。 */
|
|
4
|
+
readonly source: unknown;
|
|
5
|
+
readonly epoch: number;
|
|
6
|
+
readonly artifact_digest: string;
|
|
7
|
+
readonly runtime_contract_hash: string;
|
|
8
|
+
}
|
|
9
|
+
export type LocalPreviewViewSurface = "pc" | "mobile";
|
|
10
|
+
export interface LocalPreviewViewFrameContext {
|
|
11
|
+
readonly content_window: object;
|
|
12
|
+
readonly opaque_origin: "null";
|
|
13
|
+
readonly capture_sha256: string;
|
|
14
|
+
}
|
|
15
|
+
export interface LocalPreviewViewMessage {
|
|
16
|
+
readonly source: unknown;
|
|
17
|
+
readonly origin: string;
|
|
18
|
+
readonly data: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface LocalPreviewViewExecutionContext {
|
|
21
|
+
readonly operation_id: string;
|
|
22
|
+
readonly turn_id: string;
|
|
23
|
+
readonly world_time?: number | null;
|
|
24
|
+
}
|
|
25
|
+
export interface LocalPreviewViewEventExecutionInput extends LocalPreviewViewExecutionContext {
|
|
26
|
+
readonly event_type: string;
|
|
27
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Runtime owner 必须从已持久化的 executeCommittedEvent receipt 返回这些字段。
|
|
31
|
+
* 只有 status 不足以安全生成 tmw.delivery。
|
|
32
|
+
*/
|
|
33
|
+
export interface LocalPreviewViewDurableExecutionReceipt {
|
|
34
|
+
readonly status: "committed" | "already_committed";
|
|
35
|
+
readonly operation_id: string;
|
|
36
|
+
readonly author_view_events: ReadonlyArray<TmwAuthorViewEventV1>;
|
|
37
|
+
}
|
|
38
|
+
export type LocalPreviewViewEventResult = Readonly<{
|
|
39
|
+
ok: true;
|
|
40
|
+
status: LocalPreviewViewDurableExecutionReceipt["status"];
|
|
41
|
+
operation_id: string;
|
|
42
|
+
deliveries: ReadonlyArray<ViewEventDeliveryMessageV1>;
|
|
43
|
+
}> | Readonly<{
|
|
44
|
+
ok: false;
|
|
45
|
+
code: string;
|
|
46
|
+
player_message: string;
|
|
47
|
+
committed: boolean;
|
|
48
|
+
delivered_count: number;
|
|
49
|
+
}>;
|
|
50
|
+
/** 可交给 iframe SDK 的最小配置;不包含 payload schema、capability、delivery 或 source 内容。 */
|
|
51
|
+
export interface LocalPreviewViewInteractionConfigV3 {
|
|
52
|
+
readonly schema: "view-interaction-config.v3";
|
|
53
|
+
readonly generation: 3;
|
|
54
|
+
readonly manifest_digest: string;
|
|
55
|
+
readonly epoch: number;
|
|
56
|
+
readonly artifact_digest: string;
|
|
57
|
+
readonly runtime_contract_hash: string;
|
|
58
|
+
readonly events: ReadonlyArray<Readonly<{
|
|
59
|
+
event_id: string;
|
|
60
|
+
event_type: string;
|
|
61
|
+
}>>;
|
|
62
|
+
}
|
|
63
|
+
export interface LocalPreviewViewEventIngress {
|
|
64
|
+
handle(message: LocalPreviewViewMessage): Promise<LocalPreviewViewEventResult>;
|
|
65
|
+
/** 仅供已在浏览器父窗口完成 source/origin/capture gate 的同源 Host receiver 使用。 */
|
|
66
|
+
handleTrustedOpaqueFrameData(data: unknown): Promise<LocalPreviewViewEventResult>;
|
|
67
|
+
clearReplayWindow(): void;
|
|
68
|
+
}
|
|
69
|
+
export interface CreateLocalPreviewViewEventIngressOptions {
|
|
70
|
+
readonly capture_sha256: string;
|
|
71
|
+
readonly binding: LocalPreviewViewRuntimeBinding;
|
|
72
|
+
/** 本地 GUI 当前只打开 PC View;surface 必须由 Host 选择,不能由 iframe 声称。 */
|
|
73
|
+
readonly surface: LocalPreviewViewSurface;
|
|
74
|
+
readonly readCurrentFrame: () => LocalPreviewViewFrameContext | null;
|
|
75
|
+
/** Host 从当前 turn/session context 派生身份;iframe 不能提供 operation_id/turn_id。 */
|
|
76
|
+
readonly deriveExecutionContext: (event: ViewInteractionEventV3) => LocalPreviewViewExecutionContext | Promise<LocalPreviewViewExecutionContext>;
|
|
77
|
+
readonly executeEvent: (input: LocalPreviewViewEventExecutionInput) => Promise<LocalPreviewViewDurableExecutionReceipt>;
|
|
78
|
+
/** Browser bridge 可提供此回调直接发送通知;server receiver 留空并把 deliveries 回传 bridge。 */
|
|
79
|
+
readonly publishDelivery?: (topic: "tmw.delivery", delivery: ViewEventDeliveryMessageV1) => void | Promise<void>;
|
|
80
|
+
readonly maxReplayNonces?: number;
|
|
81
|
+
}
|
|
82
|
+
export declare class LocalPreviewViewEventConfigurationError extends Error {
|
|
83
|
+
readonly code: string;
|
|
84
|
+
constructor(code: string);
|
|
85
|
+
}
|
|
86
|
+
export declare function readLocalPreviewViewInteractionConfig(input: Readonly<{
|
|
87
|
+
capture_sha256: string;
|
|
88
|
+
binding: LocalPreviewViewRuntimeBinding;
|
|
89
|
+
surface: LocalPreviewViewSurface;
|
|
90
|
+
}>): Promise<LocalPreviewViewInteractionConfigV3>;
|
|
91
|
+
export declare function projectLocalPreviewAuthorViewEvents(input: Readonly<{
|
|
92
|
+
capture_sha256: string;
|
|
93
|
+
binding: LocalPreviewViewRuntimeBinding;
|
|
94
|
+
surface: LocalPreviewViewSurface;
|
|
95
|
+
author_view_events: ReadonlyArray<TmwAuthorViewEventV1>;
|
|
96
|
+
}>): Promise<ReadonlyArray<ViewEventDeliveryMessageV1>>;
|
|
97
|
+
/**
|
|
98
|
+
* 把 opaque View 的 declared action 接入真实 local TMW commit;所有下行消息均来自
|
|
99
|
+
* committed receipt 的 authorViewEvents 与 manifest view_deliveries 的交集。
|
|
100
|
+
*/
|
|
101
|
+
export declare function createLocalPreviewViewEventIngress(options: CreateLocalPreviewViewEventIngressOptions): Promise<LocalPreviewViewEventIngress>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createViewInteractionEventGateV3, digestViewInteractionManifestV3, parseViewInteractionSourceV3, projectAuthorViewEventToViewDeliveries, } from "@world-engines/tmw";
|
|
2
|
+
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
3
|
+
const STABLE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
4
|
+
export class LocalPreviewViewEventConfigurationError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code) {
|
|
7
|
+
super(code);
|
|
8
|
+
this.name = "LocalPreviewViewEventConfigurationError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const invalid = (code, playerMessage, committed = false, deliveredCount = 0) => Object.freeze({
|
|
13
|
+
ok: false,
|
|
14
|
+
code,
|
|
15
|
+
player_message: playerMessage,
|
|
16
|
+
committed,
|
|
17
|
+
delivered_count: deliveredCount,
|
|
18
|
+
});
|
|
19
|
+
const isExecutionContext = (value) => {
|
|
20
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
const candidate = value;
|
|
23
|
+
if (!STABLE_ID.test(String(candidate.operation_id ?? "")) || !STABLE_ID.test(String(candidate.turn_id ?? ""))) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return candidate.world_time === undefined
|
|
27
|
+
|| candidate.world_time === null
|
|
28
|
+
|| (typeof candidate.world_time === "number" && Number.isFinite(candidate.world_time));
|
|
29
|
+
};
|
|
30
|
+
const isDurableReceipt = (value, operationId) => {
|
|
31
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
32
|
+
return false;
|
|
33
|
+
const receipt = value;
|
|
34
|
+
return (receipt.status === "committed" || receipt.status === "already_committed")
|
|
35
|
+
&& receipt.operation_id === operationId
|
|
36
|
+
&& Array.isArray(receipt.author_view_events);
|
|
37
|
+
};
|
|
38
|
+
const publicExecutionCode = (error) => {
|
|
39
|
+
if (typeof error !== "object" || error === null)
|
|
40
|
+
return "E_OPERATION_UNCERTAIN";
|
|
41
|
+
const code = error.code;
|
|
42
|
+
return typeof code === "string" && /^E_[A-Z0-9_]{1,63}$/u.test(code)
|
|
43
|
+
? code
|
|
44
|
+
: "E_OPERATION_UNCERTAIN";
|
|
45
|
+
};
|
|
46
|
+
const assertBoundSource = async (binding, surface, captureSha256) => {
|
|
47
|
+
if (!SHA256.test(captureSha256)) {
|
|
48
|
+
throw new LocalPreviewViewEventConfigurationError("local_preview_capture_digest_invalid");
|
|
49
|
+
}
|
|
50
|
+
if (typeof binding.source !== "object" || binding.source === null || Array.isArray(binding.source)) {
|
|
51
|
+
throw new LocalPreviewViewEventConfigurationError("local_preview_view_interaction_layout_missing");
|
|
52
|
+
}
|
|
53
|
+
const layout = binding.source;
|
|
54
|
+
const source = parseViewInteractionSourceV3(layout[`view_interaction_${surface}`]);
|
|
55
|
+
if (source === null) {
|
|
56
|
+
throw new LocalPreviewViewEventConfigurationError("local_preview_view_interaction_source_missing");
|
|
57
|
+
}
|
|
58
|
+
const digest = await digestViewInteractionManifestV3(source.manifest);
|
|
59
|
+
if (digest !== source.manifest_digest) {
|
|
60
|
+
throw new LocalPreviewViewEventConfigurationError("local_preview_view_manifest_digest_mismatch");
|
|
61
|
+
}
|
|
62
|
+
if (source.manifest.epoch !== binding.epoch
|
|
63
|
+
|| source.manifest.artifact_digest !== binding.artifact_digest
|
|
64
|
+
|| source.manifest.runtime_contract_hash !== binding.runtime_contract_hash) {
|
|
65
|
+
throw new LocalPreviewViewEventConfigurationError("local_preview_view_runtime_binding_mismatch");
|
|
66
|
+
}
|
|
67
|
+
return source;
|
|
68
|
+
};
|
|
69
|
+
export async function readLocalPreviewViewInteractionConfig(input) {
|
|
70
|
+
const source = await assertBoundSource(input.binding, input.surface, input.capture_sha256);
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
schema: "view-interaction-config.v3",
|
|
73
|
+
generation: 3,
|
|
74
|
+
manifest_digest: source.manifest_digest,
|
|
75
|
+
epoch: source.manifest.epoch,
|
|
76
|
+
artifact_digest: source.manifest.artifact_digest,
|
|
77
|
+
runtime_contract_hash: source.manifest.runtime_contract_hash,
|
|
78
|
+
events: Object.freeze(source.manifest.events.map((event) => Object.freeze({
|
|
79
|
+
event_id: event.event_id,
|
|
80
|
+
event_type: event.event_type,
|
|
81
|
+
}))),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export async function projectLocalPreviewAuthorViewEvents(input) {
|
|
85
|
+
const source = await assertBoundSource(input.binding, input.surface, input.capture_sha256);
|
|
86
|
+
const deliveries = [];
|
|
87
|
+
for (const event of input.author_view_events) {
|
|
88
|
+
const projected = await projectAuthorViewEventToViewDeliveries(event, source.manifest.view_deliveries ?? []);
|
|
89
|
+
if (!projected.ok)
|
|
90
|
+
throw new LocalPreviewViewEventConfigurationError(projected.code);
|
|
91
|
+
deliveries.push(...projected.deliveries);
|
|
92
|
+
}
|
|
93
|
+
return Object.freeze(deliveries);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 把 opaque View 的 declared action 接入真实 local TMW commit;所有下行消息均来自
|
|
97
|
+
* committed receipt 的 authorViewEvents 与 manifest view_deliveries 的交集。
|
|
98
|
+
*/
|
|
99
|
+
export async function createLocalPreviewViewEventIngress(options) {
|
|
100
|
+
const source = await assertBoundSource(options.binding, options.surface, options.capture_sha256);
|
|
101
|
+
const gate = createViewInteractionEventGateV3(source.manifest, source.manifest_digest, options.maxReplayNonces);
|
|
102
|
+
const handleTrustedOpaqueFrameData = async (data) => {
|
|
103
|
+
const parsed = gate.parse(data);
|
|
104
|
+
if (!parsed.ok)
|
|
105
|
+
return invalid(parsed.code, "此操作无效或已过期,请刷新后重试");
|
|
106
|
+
let context;
|
|
107
|
+
try {
|
|
108
|
+
context = await options.deriveExecutionContext(parsed.event);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return invalid("view_event_execution_context_unavailable", "当前回合尚未准备好,请稍后重试");
|
|
112
|
+
}
|
|
113
|
+
if (!isExecutionContext(context)) {
|
|
114
|
+
return invalid("view_event_execution_context_invalid", "当前回合尚未准备好,请稍后重试");
|
|
115
|
+
}
|
|
116
|
+
const executionInput = Object.freeze({
|
|
117
|
+
event_type: parsed.event.event_type,
|
|
118
|
+
payload: parsed.event.payload,
|
|
119
|
+
operation_id: context.operation_id,
|
|
120
|
+
turn_id: context.turn_id,
|
|
121
|
+
...(context.world_time === undefined ? {} : { world_time: context.world_time }),
|
|
122
|
+
});
|
|
123
|
+
let receipt;
|
|
124
|
+
try {
|
|
125
|
+
const executed = await options.executeEvent(executionInput);
|
|
126
|
+
if (!isDurableReceipt(executed, context.operation_id)) {
|
|
127
|
+
return invalid("view_event_durable_receipt_invalid", "操作结果未能确认,请检查预览状态");
|
|
128
|
+
}
|
|
129
|
+
receipt = executed;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return invalid(publicExecutionCode(error), "操作未提交,请检查预览状态后重试");
|
|
133
|
+
}
|
|
134
|
+
const deliveries = [];
|
|
135
|
+
for (const authorViewEvent of receipt.author_view_events) {
|
|
136
|
+
const projected = await projectAuthorViewEventToViewDeliveries(authorViewEvent, source.manifest.view_deliveries ?? []);
|
|
137
|
+
if (!projected.ok)
|
|
138
|
+
return invalid(projected.code, "操作已提交,但界面更新数据无效", true, deliveries.length);
|
|
139
|
+
deliveries.push(...projected.deliveries);
|
|
140
|
+
}
|
|
141
|
+
let deliveredCount = 0;
|
|
142
|
+
if (options.publishDelivery !== undefined) {
|
|
143
|
+
try {
|
|
144
|
+
for (const delivery of deliveries) {
|
|
145
|
+
await options.publishDelivery("tmw.delivery", delivery);
|
|
146
|
+
deliveredCount += 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return invalid("view_delivery_publish_failed", "操作已提交,但界面更新失败,请刷新查看结果", true, deliveredCount);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
ok: true,
|
|
155
|
+
status: receipt.status,
|
|
156
|
+
operation_id: receipt.operation_id,
|
|
157
|
+
deliveries: Object.freeze([...deliveries]),
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
return Object.freeze({
|
|
161
|
+
async handle(message) {
|
|
162
|
+
const frame = options.readCurrentFrame();
|
|
163
|
+
if (frame === null
|
|
164
|
+
|| message.source !== frame.content_window
|
|
165
|
+
|| message.origin !== frame.opaque_origin
|
|
166
|
+
|| frame.opaque_origin !== "null") {
|
|
167
|
+
return invalid("view_event_frame_authority_denied", "此操作不属于当前预览页面");
|
|
168
|
+
}
|
|
169
|
+
if (frame.capture_sha256 !== options.capture_sha256) {
|
|
170
|
+
return invalid("stale_content_capture", "预览内容已更新,请刷新后重试");
|
|
171
|
+
}
|
|
172
|
+
return await handleTrustedOpaqueFrameData(message.data);
|
|
173
|
+
},
|
|
174
|
+
handleTrustedOpaqueFrameData,
|
|
175
|
+
clearReplayWindow() {
|
|
176
|
+
gate.clear();
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { LocalPreviewCustomStateEntry, LocalPreviewRuntimeHead, LocalPreviewViewStateRuntimePort, LocalPreviewViewStateScope, LocalPreviewViewStateSnapshot } from "./local-preview-view-state.js";
|
|
3
|
+
type PublicScope = Omit<LocalPreviewViewStateScope, "principalId">;
|
|
4
|
+
export declare class LocalPreviewViewStateSqlitePort implements LocalPreviewViewStateRuntimePort {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(input: Readonly<{
|
|
7
|
+
database: DatabaseSync;
|
|
8
|
+
scope: PublicScope;
|
|
9
|
+
}>);
|
|
10
|
+
initialize(scope: PublicScope): Promise<LocalPreviewViewStateSnapshot>;
|
|
11
|
+
read(scope: PublicScope): Promise<LocalPreviewViewStateSnapshot>;
|
|
12
|
+
commitRuntimeOutput(input: Readonly<{
|
|
13
|
+
scope: PublicScope;
|
|
14
|
+
operationId: string;
|
|
15
|
+
expectedRuntimeHead: LocalPreviewRuntimeHead;
|
|
16
|
+
expectedGeneration: number;
|
|
17
|
+
entries: readonly LocalPreviewCustomStateEntry[];
|
|
18
|
+
}>): Promise<Readonly<{
|
|
19
|
+
snapshot: LocalPreviewViewStateSnapshot;
|
|
20
|
+
changedKeys: readonly string[];
|
|
21
|
+
replayed: boolean;
|
|
22
|
+
}>>;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* The durable local Preview implementation of the View-state Runtime port.
|
|
4
|
+
*
|
|
5
|
+
* It intentionally owns neither an SQLite file nor a runtime head. The
|
|
6
|
+
* Runtime passes its already-open activation.sqlite connection and this port
|
|
7
|
+
* stores its small, capture-bound read model inside `projection.views`. A
|
|
8
|
+
* worldline checkout consequently brings the exact same projection (including
|
|
9
|
+
* custom state) along with the SAV instead of creating a second authority.
|
|
10
|
+
*/
|
|
11
|
+
const NAMESPACE = "chatplay.custom-state.v1";
|
|
12
|
+
const ID = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
13
|
+
const MAX_ENTRIES = 256;
|
|
14
|
+
const MAX_AUTHORIZED_USERS = 64;
|
|
15
|
+
const MAX_ENTRY_JSON_BYTES = 64 * 1024;
|
|
16
|
+
const MAX_VIEW_SNAPSHOT_JSON_BYTES = 16 * 1024 * 1024;
|
|
17
|
+
export class LocalPreviewViewStateSqlitePort {
|
|
18
|
+
#db;
|
|
19
|
+
#scope;
|
|
20
|
+
constructor(input) {
|
|
21
|
+
assertScope(input.scope);
|
|
22
|
+
this.#db = input.database;
|
|
23
|
+
this.#scope = Object.freeze({ ...input.scope });
|
|
24
|
+
// Kept separate from the TMW activation receipts: operation ids share a
|
|
25
|
+
// caller namespace only within this View-state protocol, not across TMW.
|
|
26
|
+
this.#db.exec("CREATE TABLE IF NOT EXISTS local_preview_view_state_receipt (scope_key TEXT NOT NULL, operation_id TEXT NOT NULL, request_digest TEXT NOT NULL, result_json TEXT NOT NULL, PRIMARY KEY(scope_key,operation_id));");
|
|
27
|
+
}
|
|
28
|
+
async initialize(scope) {
|
|
29
|
+
this.#assertScope(scope);
|
|
30
|
+
return this.#transaction(() => {
|
|
31
|
+
const head = this.#readHead();
|
|
32
|
+
const projection = this.#readProjection();
|
|
33
|
+
const stored = this.#storedSnapshot(projection);
|
|
34
|
+
if (stored !== null)
|
|
35
|
+
return snapshot(this.#scope, head, stored);
|
|
36
|
+
const initialized = Object.freeze({ scope: this.#scope, generation: 0, entries: Object.freeze([]) });
|
|
37
|
+
this.#writeProjection(withStoredSnapshot(projection, initialized));
|
|
38
|
+
return snapshot(this.#scope, head, initialized);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async read(scope) {
|
|
42
|
+
this.#assertScope(scope);
|
|
43
|
+
const stored = this.#storedSnapshot(this.#readProjection());
|
|
44
|
+
if (stored === null)
|
|
45
|
+
fail("reader_missing");
|
|
46
|
+
return snapshot(this.#scope, this.#readHead(), stored);
|
|
47
|
+
}
|
|
48
|
+
async commitRuntimeOutput(input) {
|
|
49
|
+
this.#assertScope(input.scope);
|
|
50
|
+
assertId(input.operationId, "operation_id");
|
|
51
|
+
assertHead(input.expectedRuntimeHead);
|
|
52
|
+
if (!Number.isSafeInteger(input.expectedGeneration) || input.expectedGeneration < 0)
|
|
53
|
+
fail("invalid_generation");
|
|
54
|
+
assertEntries(input.entries);
|
|
55
|
+
const requestDigest = digest({ scope: this.#scope, operationId: input.operationId, expectedRuntimeHead: input.expectedRuntimeHead, expectedGeneration: input.expectedGeneration, entries: input.entries });
|
|
56
|
+
return this.#transaction(() => {
|
|
57
|
+
const prior = this.#receipt(input.operationId);
|
|
58
|
+
if (prior !== null) {
|
|
59
|
+
if (prior.requestDigest !== requestDigest)
|
|
60
|
+
fail("operation_id_conflict");
|
|
61
|
+
return Object.freeze({ snapshot: clone(prior.snapshot), changedKeys: Object.freeze([...prior.changedKeys]), replayed: true });
|
|
62
|
+
}
|
|
63
|
+
const head = this.#readHead();
|
|
64
|
+
if (!sameHead(head, input.expectedRuntimeHead))
|
|
65
|
+
fail("cas_conflict");
|
|
66
|
+
const projection = this.#readProjection();
|
|
67
|
+
const current = this.#storedSnapshot(projection);
|
|
68
|
+
if (current === null)
|
|
69
|
+
fail("reader_missing");
|
|
70
|
+
if (current.generation !== input.expectedGeneration)
|
|
71
|
+
fail("cas_conflict");
|
|
72
|
+
const changedKeys = changed(current.entries, input.entries);
|
|
73
|
+
const next = changedKeys.length === 0
|
|
74
|
+
? current
|
|
75
|
+
: Object.freeze({ scope: this.#scope, generation: current.generation + 1, entries: freezeEntries(input.entries) });
|
|
76
|
+
const nextSnapshot = snapshot(this.#scope, head, next);
|
|
77
|
+
// A no-op deliberately records its receipt but does not advance the view
|
|
78
|
+
// generation nor rewrite projection. This keeps next formal outputs
|
|
79
|
+
// correctly fenced while making retries deterministic.
|
|
80
|
+
if (changedKeys.length > 0)
|
|
81
|
+
this.#writeProjection(withStoredSnapshot(projection, next));
|
|
82
|
+
const receipt = Object.freeze({ requestDigest, snapshot: nextSnapshot, changedKeys: Object.freeze(changedKeys) });
|
|
83
|
+
this.#db.prepare("INSERT INTO local_preview_view_state_receipt(scope_key,operation_id,request_digest,result_json) VALUES (?,?,?,?)").run(scopeKey(this.#scope), input.operationId, requestDigest, JSON.stringify(receipt));
|
|
84
|
+
return Object.freeze({ snapshot: clone(nextSnapshot), changedKeys: Object.freeze(changedKeys), replayed: false });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
#assertScope(scope) {
|
|
88
|
+
assertScope(scope);
|
|
89
|
+
if (scope.captureId !== this.#scope.captureId || scope.namespace !== this.#scope.namespace || scope.saveId !== this.#scope.saveId || scope.viewId !== this.#scope.viewId)
|
|
90
|
+
fail("scope_mismatch");
|
|
91
|
+
}
|
|
92
|
+
#readHead() {
|
|
93
|
+
const row = this.#db.prepare("SELECT value FROM local_preview_state WHERE key='head'").get();
|
|
94
|
+
if (row === undefined)
|
|
95
|
+
fail("runtime_head_missing");
|
|
96
|
+
let value;
|
|
97
|
+
try {
|
|
98
|
+
value = JSON.parse(row.value);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
fail("runtime_head_invalid");
|
|
102
|
+
}
|
|
103
|
+
assertHead(value);
|
|
104
|
+
return Object.freeze({ ...value });
|
|
105
|
+
}
|
|
106
|
+
#readProjection() {
|
|
107
|
+
const row = this.#db.prepare("SELECT value FROM local_preview_state WHERE key='projection'").get();
|
|
108
|
+
if (row === undefined)
|
|
109
|
+
fail("projection_missing");
|
|
110
|
+
let value;
|
|
111
|
+
try {
|
|
112
|
+
value = JSON.parse(row.value);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
fail("projection_invalid");
|
|
116
|
+
}
|
|
117
|
+
if (!isRecord(value) || !isRecord(value.entities) || !Array.isArray(value.emitted_events))
|
|
118
|
+
fail("projection_invalid");
|
|
119
|
+
if (value.views !== undefined && !isRecord(value.views))
|
|
120
|
+
fail("projection_invalid");
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
#storedSnapshot(projection) {
|
|
124
|
+
const namespaced = projection.views?.[NAMESPACE];
|
|
125
|
+
if (namespaced === undefined)
|
|
126
|
+
return null;
|
|
127
|
+
if (!isRecord(namespaced))
|
|
128
|
+
fail("projection_view_namespace_invalid");
|
|
129
|
+
const candidate = namespaced[this.#scope.viewId];
|
|
130
|
+
if (candidate === undefined)
|
|
131
|
+
return null;
|
|
132
|
+
return parseStoredSnapshot(candidate, this.#scope);
|
|
133
|
+
}
|
|
134
|
+
#writeProjection(projection) {
|
|
135
|
+
const serialized = JSON.stringify(projection);
|
|
136
|
+
if (new TextEncoder().encode(serialized).byteLength > MAX_VIEW_SNAPSHOT_JSON_BYTES + 64 * 1024 * 1024)
|
|
137
|
+
fail("projection_oversize");
|
|
138
|
+
this.#db.prepare("INSERT OR REPLACE INTO local_preview_state(key,value) VALUES ('projection',?)").run(serialized);
|
|
139
|
+
}
|
|
140
|
+
#receipt(operationId) {
|
|
141
|
+
const row = this.#db.prepare("SELECT request_digest,result_json FROM local_preview_view_state_receipt WHERE scope_key=? AND operation_id=?").get(scopeKey(this.#scope), operationId);
|
|
142
|
+
if (row === undefined)
|
|
143
|
+
return null;
|
|
144
|
+
let stored;
|
|
145
|
+
try {
|
|
146
|
+
stored = JSON.parse(row.result_json);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
fail("receipt_invalid");
|
|
150
|
+
}
|
|
151
|
+
if (!isRecord(stored) || typeof stored.requestDigest !== "string" || !Array.isArray(stored.changedKeys) || !isRecord(stored.snapshot))
|
|
152
|
+
fail("receipt_invalid");
|
|
153
|
+
if (stored.requestDigest !== row.request_digest || stored.changedKeys.some((key) => typeof key !== "string" || !ID.test(key)) || new Set(stored.changedKeys).size !== stored.changedKeys.length)
|
|
154
|
+
fail("receipt_invalid");
|
|
155
|
+
const result = Object.freeze({ requestDigest: stored.requestDigest, snapshot: parseSnapshot(stored.snapshot, this.#scope), changedKeys: Object.freeze([...stored.changedKeys]) });
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
#transaction(work) {
|
|
159
|
+
if (this.#db.isTransaction === true)
|
|
160
|
+
return work();
|
|
161
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
162
|
+
try {
|
|
163
|
+
const result = work();
|
|
164
|
+
this.#db.exec("COMMIT");
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
try {
|
|
169
|
+
this.#db.exec("ROLLBACK");
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function withStoredSnapshot(projection, stored) {
|
|
177
|
+
const copy = clone(projection);
|
|
178
|
+
const views = isRecord(copy.views) ? copy.views : {};
|
|
179
|
+
const namespaced = isRecord(views[NAMESPACE]) ? views[NAMESPACE] : {};
|
|
180
|
+
namespaced[stored.scope.viewId] = clone(stored);
|
|
181
|
+
views[NAMESPACE] = namespaced;
|
|
182
|
+
copy.views = views;
|
|
183
|
+
return copy;
|
|
184
|
+
}
|
|
185
|
+
function parseStoredSnapshot(value, scope) {
|
|
186
|
+
if (!isRecord(value) || !isRecord(value.scope) || typeof value.generation !== "number" || !Number.isSafeInteger(value.generation) || value.generation < 0 || !Array.isArray(value.entries))
|
|
187
|
+
fail("stored_snapshot_invalid");
|
|
188
|
+
const storedScope = value.scope;
|
|
189
|
+
const generation = value.generation;
|
|
190
|
+
const entries = value.entries;
|
|
191
|
+
assertScope(storedScope);
|
|
192
|
+
if (!sameScope(storedScope, scope))
|
|
193
|
+
fail("stored_scope_mismatch");
|
|
194
|
+
assertEntries(entries);
|
|
195
|
+
const stored = Object.freeze({ scope: Object.freeze({ ...scope }), generation, entries: freezeEntries(entries) });
|
|
196
|
+
if (new TextEncoder().encode(JSON.stringify(stored)).byteLength > MAX_VIEW_SNAPSHOT_JSON_BYTES)
|
|
197
|
+
fail("stored_snapshot_oversize");
|
|
198
|
+
return stored;
|
|
199
|
+
}
|
|
200
|
+
function parseSnapshot(value, scope) {
|
|
201
|
+
if (!isRecord(value.scope) || !isRecord(value.runtimeHead) || typeof value.generation !== "number" || !Number.isSafeInteger(value.generation) || value.generation < 0 || !Array.isArray(value.entries))
|
|
202
|
+
fail("receipt_invalid");
|
|
203
|
+
const storedScope = value.scope;
|
|
204
|
+
const storedHead = value.runtimeHead;
|
|
205
|
+
const generation = value.generation;
|
|
206
|
+
const entries = value.entries;
|
|
207
|
+
assertScope(storedScope);
|
|
208
|
+
if (!sameScope(storedScope, scope))
|
|
209
|
+
fail("receipt_invalid");
|
|
210
|
+
assertHead(storedHead);
|
|
211
|
+
assertEntries(entries);
|
|
212
|
+
return Object.freeze({ scope: Object.freeze({ ...scope }), runtimeHead: Object.freeze({ ...storedHead }), generation, entries: freezeEntries(entries) });
|
|
213
|
+
}
|
|
214
|
+
function snapshot(scope, runtimeHead, stored) {
|
|
215
|
+
return Object.freeze({ scope: Object.freeze({ ...scope }), runtimeHead: Object.freeze({ ...runtimeHead }), generation: stored.generation, entries: freezeEntries(stored.entries) });
|
|
216
|
+
}
|
|
217
|
+
function assertScope(scope) {
|
|
218
|
+
if (!isRecord(scope) || !ID.test(scope.captureId) || scope.namespace !== NAMESPACE || !ID.test(scope.saveId) || !ID.test(scope.viewId))
|
|
219
|
+
fail("invalid_scope");
|
|
220
|
+
}
|
|
221
|
+
function assertId(value, label) { if (typeof value !== "string" || !ID.test(value))
|
|
222
|
+
fail(`invalid_${label}`); }
|
|
223
|
+
function assertHead(value) {
|
|
224
|
+
if (!isRecord(value) || typeof value.revisionId !== "string" || !ID.test(value.revisionId)
|
|
225
|
+
|| typeof value.revisionSequence !== "number" || !Number.isSafeInteger(value.revisionSequence) || value.revisionSequence < 0
|
|
226
|
+
|| (value.ledgerHead !== null && (typeof value.ledgerHead !== "string" || !ID.test(value.ledgerHead))))
|
|
227
|
+
fail("invalid_runtime_head");
|
|
228
|
+
}
|
|
229
|
+
function assertEntries(entries) {
|
|
230
|
+
if (entries.length > MAX_ENTRIES || new Set(entries.map((entry) => entry.key)).size !== entries.length)
|
|
231
|
+
fail("invalid_entries");
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
if (!isRecord(entry) || !ID.test(entry.key) || (entry.scope !== "public" && entry.scope !== "protected") || !Number.isSafeInteger(entry.revision) || entry.revision < 0)
|
|
234
|
+
fail("invalid_entry");
|
|
235
|
+
if (!Array.isArray(entry.authorizedUserIds) || entry.authorizedUserIds.length > MAX_AUTHORIZED_USERS || new Set(entry.authorizedUserIds).size !== entry.authorizedUserIds.length || entry.authorizedUserIds.some((id) => !ID.test(id)))
|
|
236
|
+
fail("invalid_authorized_users");
|
|
237
|
+
if ((entry.scope === "public" && entry.authorizedUserIds.length !== 0) || (entry.scope === "protected" && entry.authorizedUserIds.length === 0))
|
|
238
|
+
fail("invalid_authorized_users");
|
|
239
|
+
if (!validJson(entry.value) || new TextEncoder().encode(JSON.stringify(entry.value)).byteLength > MAX_ENTRY_JSON_BYTES)
|
|
240
|
+
fail("invalid_or_oversize_value");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function validJson(value) {
|
|
244
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
245
|
+
return true;
|
|
246
|
+
if (typeof value === "number")
|
|
247
|
+
return Number.isFinite(value);
|
|
248
|
+
if (Array.isArray(value))
|
|
249
|
+
return value.every(validJson);
|
|
250
|
+
return isRecord(value) && Object.values(value).every(validJson);
|
|
251
|
+
}
|
|
252
|
+
function freezeEntries(entries) { return Object.freeze(entries.map((entry) => Object.freeze({ ...entry, value: clone(entry.value), authorizedUserIds: Object.freeze([...entry.authorizedUserIds]) }))); }
|
|
253
|
+
function changed(before, after) {
|
|
254
|
+
const keys = new Set([...before.map((entry) => entry.key), ...after.map((entry) => entry.key)]);
|
|
255
|
+
return [...keys].filter((key) => JSON.stringify(before.find((entry) => entry.key === key)) !== JSON.stringify(after.find((entry) => entry.key === key))).sort();
|
|
256
|
+
}
|
|
257
|
+
function sameScope(left, right) { return left.captureId === right.captureId && left.namespace === right.namespace && left.saveId === right.saveId && left.viewId === right.viewId; }
|
|
258
|
+
function sameHead(left, right) { return left.revisionId === right.revisionId && left.revisionSequence === right.revisionSequence && left.ledgerHead === right.ledgerHead; }
|
|
259
|
+
function scopeKey(scope) { return digest(scope); }
|
|
260
|
+
function digest(value) { return createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
|
|
261
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)); }
|
|
262
|
+
function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; }
|
|
263
|
+
function fail(code) { throw new Error(`local_preview_view_state_sqlite:${code}`); }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地 Preview 的 View custom state read model。
|
|
3
|
+
*
|
|
4
|
+
* 这个模块不拥有文件或 SQLite。Runtime owner 必须把 snapshot/commit 接到同一
|
|
5
|
+
* runtime SAV envelope 的 projection[viewId],并复用 runtime 的 head CAS 与
|
|
6
|
+
* operation receipt;因此不会产生第二个 local save authority。
|
|
7
|
+
*/
|
|
8
|
+
declare const NAMESPACE = "chatplay.custom-state.v1";
|
|
9
|
+
/** 单个 view custom-state entry 的真实持久化上限,供 Host capability 投影复用。 */
|
|
10
|
+
export declare const LOCAL_PREVIEW_CUSTOM_STATE_MAX_BYTES: number;
|
|
11
|
+
export type LocalPreviewCustomStateScope = "public" | "protected";
|
|
12
|
+
export type LocalPreviewJsonValue = null | boolean | number | string | readonly LocalPreviewJsonValue[] | {
|
|
13
|
+
readonly [key: string]: LocalPreviewJsonValue;
|
|
14
|
+
};
|
|
15
|
+
export interface LocalPreviewViewStateScope {
|
|
16
|
+
/** Host capture 的 immutable identity;不可来自 iframe request。 */
|
|
17
|
+
readonly captureId: string;
|
|
18
|
+
readonly namespace: typeof NAMESPACE;
|
|
19
|
+
readonly saveId: string;
|
|
20
|
+
readonly viewId: string;
|
|
21
|
+
/** Host 已认证的 principal;null 时只允许 public entry。 */
|
|
22
|
+
readonly principalId: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface LocalPreviewCustomStateEntry {
|
|
25
|
+
readonly key: string;
|
|
26
|
+
readonly value: LocalPreviewJsonValue;
|
|
27
|
+
readonly scope: LocalPreviewCustomStateScope;
|
|
28
|
+
readonly authorizedUserIds: readonly string[];
|
|
29
|
+
readonly revision: number;
|
|
30
|
+
}
|
|
31
|
+
/** 与 LocalPreviewActivationStore activeHead 同一份 runtime authority。 */
|
|
32
|
+
export interface LocalPreviewRuntimeHead {
|
|
33
|
+
readonly revisionId: string;
|
|
34
|
+
readonly revisionSequence: number;
|
|
35
|
+
readonly ledgerHead: string | null;
|
|
36
|
+
}
|
|
37
|
+
/** 已持久化且已绑定本 Host scope 的 view projection。空 entries 是新 save 的合法状态。 */
|
|
38
|
+
export interface LocalPreviewViewStateSnapshot {
|
|
39
|
+
readonly scope: Omit<LocalPreviewViewStateScope, "principalId">;
|
|
40
|
+
readonly runtimeHead: LocalPreviewRuntimeHead;
|
|
41
|
+
readonly generation: number;
|
|
42
|
+
readonly entries: readonly LocalPreviewCustomStateEntry[];
|
|
43
|
+
}
|
|
44
|
+
export interface LocalPreviewRuntimeStateOutput {
|
|
45
|
+
/** 只有 Runtime commit 或正式 action output 可构造并调用这个输入。 */
|
|
46
|
+
readonly source: "runtime-action-output";
|
|
47
|
+
readonly operationId: string;
|
|
48
|
+
readonly expectedRuntimeHead: LocalPreviewRuntimeHead;
|
|
49
|
+
readonly expectedGeneration: number;
|
|
50
|
+
readonly entries: readonly LocalPreviewCustomStateEntry[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Runtime owner 的唯一 persistence seam。
|
|
54
|
+
* initialize 必须真实创建 envelope projection 中的空 view store;read 不得把
|
|
55
|
+
* 缺少 reader/snapshot 伪装成空数组。commit 必须在 runtime 同一 SQLite transaction
|
|
56
|
+
* 中检查 head/generation、保存 operation receipt,并返回 replayed receipt。
|
|
57
|
+
*/
|
|
58
|
+
export interface LocalPreviewViewStateRuntimePort {
|
|
59
|
+
initialize(scope: Omit<LocalPreviewViewStateScope, "principalId">): Promise<LocalPreviewViewStateSnapshot>;
|
|
60
|
+
read(scope: Omit<LocalPreviewViewStateScope, "principalId">): Promise<LocalPreviewViewStateSnapshot>;
|
|
61
|
+
commitRuntimeOutput(input: Readonly<{
|
|
62
|
+
scope: Omit<LocalPreviewViewStateScope, "principalId">;
|
|
63
|
+
operationId: string;
|
|
64
|
+
expectedRuntimeHead: LocalPreviewRuntimeHead;
|
|
65
|
+
expectedGeneration: number;
|
|
66
|
+
entries: readonly LocalPreviewCustomStateEntry[];
|
|
67
|
+
}>): Promise<Readonly<{
|
|
68
|
+
snapshot: LocalPreviewViewStateSnapshot;
|
|
69
|
+
changedKeys: readonly string[];
|
|
70
|
+
replayed: boolean;
|
|
71
|
+
}>>;
|
|
72
|
+
}
|
|
73
|
+
export interface LocalPreviewCustomStateCommitResult {
|
|
74
|
+
readonly generation: number;
|
|
75
|
+
readonly changedKeys: readonly string[];
|
|
76
|
+
readonly entries: readonly LocalPreviewCustomStateEntry[];
|
|
77
|
+
readonly replayed: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** Host-only facade. It deliberately contains no request-selected identity or iframe mutation API. */
|
|
80
|
+
export declare class LocalPreviewViewStateStore {
|
|
81
|
+
#private;
|
|
82
|
+
private constructor();
|
|
83
|
+
static open(scope: LocalPreviewViewStateScope, port: LocalPreviewViewStateRuntimePort): Promise<LocalPreviewViewStateStore>;
|
|
84
|
+
keys(): Promise<readonly string[]>;
|
|
85
|
+
/** 给 LocalPreviewReadOnlyProjection 的 customState reader 使用,结果已经按 Host principal 过滤。 */
|
|
86
|
+
entries(): Promise<readonly LocalPreviewCustomStateEntry[]>;
|
|
87
|
+
get(key: string): Promise<LocalPreviewJsonValue | undefined>;
|
|
88
|
+
getEntry(key: string): Promise<LocalPreviewCustomStateEntry | undefined>;
|
|
89
|
+
/** Runtime owner calls this after a formal action output; View RPC never gets this method. */
|
|
90
|
+
commitRuntimeOutput(output: LocalPreviewRuntimeStateOutput): Promise<LocalPreviewCustomStateCommitResult>;
|
|
91
|
+
}
|
|
92
|
+
export declare const LOCAL_PREVIEW_VIEW_STATE_NAMESPACE = "chatplay.custom-state.v1";
|
|
93
|
+
export {};
|