@lincyaw/workbench 0.0.0-stage → 0.2.0-rc.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 +202 -0
- package/README.md +66 -2
- package/package.json +68 -3
- package/src/MediaClipEditor.tsx +122 -0
- package/src/StepProductionEditor.tsx +130 -0
- package/src/approvals.tsx +60 -0
- package/src/assistant-mascot.ts +62 -0
- package/src/audio-recording-messages.ts +44 -0
- package/src/audio-recording.ts +192 -0
- package/src/audio-wav.ts +25 -0
- package/src/audio-waveform.ts +110 -0
- package/src/audition.ts +84 -0
- package/src/authoring-client.ts +136 -0
- package/src/authoring-draft-fragment.ts +28 -0
- package/src/base-controls.tsx +47 -0
- package/src/caption-timing-drafts.ts +61 -0
- package/src/caption-timing-messages.ts +14 -0
- package/src/caption-timing.css +20 -0
- package/src/caption-timing.ts +14 -0
- package/src/component-draft-actions.ts +54 -0
- package/src/component-draft-inspection.ts +35 -0
- package/src/component-properties.css +24 -0
- package/src/component-properties.ts +45 -0
- package/src/component-property-drafts.ts +273 -0
- package/src/component-property-messages.ts +31 -0
- package/src/components.tsx +631 -0
- package/src/context.tsx +27 -0
- package/src/diagnostics.ts +113 -0
- package/src/document-draft-store.ts +179 -0
- package/src/document-drafts.ts +178 -0
- package/src/document-editor/BlockNoteSurface.tsx +209 -0
- package/src/document-editor/ComponentMaterials.tsx +62 -0
- package/src/document-editor/ComponentPropertiesDialog.tsx +282 -0
- package/src/document-editor/DocumentBlockFallback.tsx +31 -0
- package/src/document-editor/DocumentBlockPreview.tsx +239 -0
- package/src/document-editor/DocumentComments.tsx +282 -0
- package/src/document-editor/DocumentEditor.tsx +192 -0
- package/src/document-editor/DocumentStepNarration.tsx +93 -0
- package/src/document-editor/NarrationEditor.tsx +110 -0
- package/src/document-editor/NewCommentDrafts.tsx +224 -0
- package/src/document-editor/authoring.tsx +259 -0
- package/src/document-editor/block-playback.ts +90 -0
- package/src/document-editor/block-preview-messages.ts +13 -0
- package/src/document-editor/blocknote-messages.ts +98 -0
- package/src/document-editor/clipboard.ts +40 -0
- package/src/document-editor/colors.ts +57 -0
- package/src/document-editor/comment-draft-actions.ts +58 -0
- package/src/document-editor/comment-draft-inspection.ts +38 -0
- package/src/document-editor/comment-drafts.ts +361 -0
- package/src/document-editor/comments.ts +410 -0
- package/src/document-editor/file-input.ts +71 -0
- package/src/document-editor/focus.ts +54 -0
- package/src/document-editor/inline.ts +104 -0
- package/src/document-editor/math.tsx +63 -0
- package/src/document-editor/messages.ts +14 -0
- package/src/document-editor/narration-schema.tsx +85 -0
- package/src/document-editor/narration.ts +37 -0
- package/src/document-editor/popover.tsx +107 -0
- package/src/document-editor/projection.ts +423 -0
- package/src/document-editor/rich-text.ts +152 -0
- package/src/document-editor/schema.tsx +117 -0
- package/src/document-editor/selection.ts +152 -0
- package/src/document-editor/style.css +9 -0
- package/src/document-editor/styles.d.ts +1 -0
- package/src/document-editor/table-identities.ts +67 -0
- package/src/document-preview.ts +112 -0
- package/src/document-return-point.ts +59 -0
- package/src/document-session.ts +678 -0
- package/src/document-timing-drafts.ts +61 -0
- package/src/document-voices.ts +2 -0
- package/src/history.ts +167 -0
- package/src/index.ts +36 -0
- package/src/jobs.ts +111 -0
- package/src/mascot-preference.ts +37 -0
- package/src/media-drafts.ts +83 -0
- package/src/media-messages.ts +53 -0
- package/src/media-time.ts +12 -0
- package/src/messages.ts +781 -0
- package/src/outline.ts +97 -0
- package/src/panels/ChangeTrail.tsx +79 -0
- package/src/panels/DocumentNarrationPanel.tsx +27 -0
- package/src/panels/DocumentView.tsx +462 -0
- package/src/panels/DocumentVoiceDialog.tsx +179 -0
- package/src/panels/DocumentVoiceField.tsx +35 -0
- package/src/panels/ExportPanel.tsx +133 -0
- package/src/panels/HistoryPanel.tsx +88 -0
- package/src/panels/HomePanel.tsx +184 -0
- package/src/panels/PageDeleteDialog.tsx +33 -0
- package/src/panels/PageRenameDialog.tsx +27 -0
- package/src/panels/PreviewPanel.tsx +79 -0
- package/src/panels/ProductionCaptionTiming.tsx +122 -0
- package/src/panels/ProductionMediaClips.tsx +156 -0
- package/src/panels/ProductionPanel.tsx +272 -0
- package/src/panels/ProductionRecording.tsx +107 -0
- package/src/panels/ProductionRecordingRange.tsx +118 -0
- package/src/panels/ProductionTimeline.tsx +62 -0
- package/src/panels/ProductionTranscription.tsx +140 -0
- package/src/panels/SourceDocumentPanel.tsx +215 -0
- package/src/panels/SpeechSettingsPanel.tsx +64 -0
- package/src/panels/TranscriptionAdoption.tsx +147 -0
- package/src/panels/VoiceCloneDialog.tsx +152 -0
- package/src/panels/VoicePreviewDialog.tsx +74 -0
- package/src/panels/VoiceScopeField.tsx +30 -0
- package/src/panels/VoiceoverDialog.tsx +216 -0
- package/src/places.ts +102 -0
- package/src/preview-playback.ts +125 -0
- package/src/preview-pool.ts +320 -0
- package/src/preview.ts +110 -0
- package/src/production-messages.ts +151 -0
- package/src/production-playback.ts +109 -0
- package/src/production-voice-playback.ts +114 -0
- package/src/recording-adoption.ts +33 -0
- package/src/schema-form/PmSchemaForm.tsx +187 -0
- package/src/schema-form/context.ts +26 -0
- package/src/schema-form/fields.tsx +59 -0
- package/src/schema-form/templates.tsx +50 -0
- package/src/schema-form/validator.ts +36 -0
- package/src/schema-form/widgets.tsx +60 -0
- package/src/shortcuts.ts +8 -0
- package/src/sortable-list.tsx +201 -0
- package/src/speech-settings-messages.ts +40 -0
- package/src/step-production.css +5 -0
- package/src/step-production.ts +51 -0
- package/src/styles.css +1025 -0
- package/src/telemetry.ts +152 -0
- package/src/transcription-messages.ts +26 -0
- package/src/transcription-session.ts +159 -0
- package/src/use-recording-adoption.ts +84 -0
- package/src/voice-clone-messages.ts +92 -0
- package/src/voice-drafts.ts +89 -0
- package/src/voice-messages.ts +146 -0
- package/src/voice-snapshot.ts +55 -0
- package/src/voiceover-jobs.ts +87 -0
- package/src/workbench.ts +2328 -0
- package/src/workspace-client.ts +480 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { isRecord } from '@lincyaw/kernel'
|
|
2
|
+
import type { CanonicalSource, ClientComponentDraftReference, JsonObject } from '@lincyaw/kernel'
|
|
3
|
+
import { DocumentDraftStore } from './document-draft-store.ts'
|
|
4
|
+
import type { DraftStoreCommit } from './document-draft-store.ts'
|
|
5
|
+
|
|
6
|
+
export interface ComponentPropertyTarget { identity: string, memberId: string, blockId: string, stepId?: string }
|
|
7
|
+
export interface ComponentPropertyDraft {
|
|
8
|
+
format: 1
|
|
9
|
+
id: string
|
|
10
|
+
revision: number
|
|
11
|
+
target: ComponentPropertyTarget
|
|
12
|
+
source: CanonicalSource
|
|
13
|
+
definitionId: string
|
|
14
|
+
baseline: JsonObject
|
|
15
|
+
value: JsonObject
|
|
16
|
+
reset: boolean
|
|
17
|
+
composing: boolean
|
|
18
|
+
updatedAt: string
|
|
19
|
+
}
|
|
20
|
+
export interface ComponentPropertyBackup { record: ComponentPropertyDraft, token: string, key: string }
|
|
21
|
+
export interface ComponentPropertyBackupRead { candidates: ComponentPropertyBackup[], unreadable: number }
|
|
22
|
+
export interface ComponentPropertyAgentCandidate extends ComponentPropertyBackup { current: boolean, storage: 'durable' | 'session', status: 'available' | 'pending' | 'session-only' }
|
|
23
|
+
interface Entry { record: ComponentPropertyDraft, store: DocumentDraftStore, operation?: DraftStoreCommit, failed: boolean, origins: ComponentPropertyBackup[] }
|
|
24
|
+
const PREFIX = 'presmium.component-properties.v1:'
|
|
25
|
+
const SETTLED = 'presmium.component-properties-settled.v1:'
|
|
26
|
+
const MAX_CHARS = 1 << 20
|
|
27
|
+
const scope = (target: Pick<ComponentPropertyTarget, 'identity' | 'memberId'>) => JSON.stringify([target.identity, target.memberId])
|
|
28
|
+
const targetKey = (target: ComponentPropertyTarget) => JSON.stringify([target.identity, target.memberId, target.blockId, target.stepId ?? null])
|
|
29
|
+
const prefix = (target: ComponentPropertyTarget) => `${PREFIX}${targetKey(target)}:`
|
|
30
|
+
const markerPrefix = (target: ComponentPropertyTarget) => `${SETTLED}${targetKey(target)}:`
|
|
31
|
+
const scopedPrefix = (kind: string, identity: string, memberId: string) => `${kind}${JSON.stringify([identity, memberId]).slice(0, -1)},`
|
|
32
|
+
const markerOf = (target: ComponentPropertyTarget, reference: { id: string, revision: number, token: string }) => `${markerPrefix(target)}${JSON.stringify([reference.id, reference.revision, reference.token])}`
|
|
33
|
+
const marker = (backup: ComponentPropertyBackup) => markerOf(backup.record.target, { id: backup.record.id, revision: backup.record.revision, token: backup.token })
|
|
34
|
+
const keyOf = (record: ComponentPropertyDraft) => `${prefix(record.target)}${record.id}`
|
|
35
|
+
const tokenOf = async (raw: string) => [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(raw)))].map(byte => byte.toString(16).padStart(2, '0')).join('')
|
|
36
|
+
|
|
37
|
+
function valid(value: unknown, target: ComponentPropertyTarget): value is ComponentPropertyDraft {
|
|
38
|
+
return isRecord(value) && value.format === 1 && typeof value.id === 'string' && value.id.length > 0
|
|
39
|
+
&& Number.isSafeInteger(value.revision) && (value.revision as number) > 0 && isRecord(value.target)
|
|
40
|
+
&& value.target.identity === target.identity && value.target.memberId === target.memberId && value.target.blockId === target.blockId && value.target.stepId === target.stepId
|
|
41
|
+
&& isRecord(value.source) && typeof value.source.documentId === 'string' && typeof value.source.version === 'string' && typeof value.source.fingerprint === 'string'
|
|
42
|
+
&& typeof value.definitionId === 'string' && isRecord(value.baseline) && isRecord(value.value)
|
|
43
|
+
&& typeof value.reset === 'boolean' && typeof value.composing === 'boolean' && typeof value.updatedAt === 'string' && Number.isFinite(Date.parse(value.updatedAt))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Unapplied form input borrows the existing strict backup actor; only lecture.json is authoring content. */
|
|
47
|
+
export class ComponentPropertyDrafts {
|
|
48
|
+
readonly #entries = new Map<string, Entry>()
|
|
49
|
+
readonly #listeners = new Set<() => void>()
|
|
50
|
+
readonly #revisions = new Map<string, number>()
|
|
51
|
+
readonly #settling = new Map<string, number>()
|
|
52
|
+
#agentReader?: { identity: string, memberId: string, store: DocumentDraftStore }
|
|
53
|
+
readonly #writerOwner = crypto.randomUUID()
|
|
54
|
+
#revision = 0
|
|
55
|
+
#closed = false
|
|
56
|
+
readonly #leave = (event: BeforeUnloadEvent) => {
|
|
57
|
+
if ([...this.#entries.values()].some(entry => entry.failed || entry.operation || entry.record.composing)) event.preventDefault()
|
|
58
|
+
}
|
|
59
|
+
constructor() { if (typeof window !== 'undefined') window.addEventListener('beforeunload', this.#leave) }
|
|
60
|
+
subscribe = (listener: () => void): (() => void) => { this.#listeners.add(listener); return () => this.#listeners.delete(listener) }
|
|
61
|
+
snapshot = (): number => this.#revision
|
|
62
|
+
revision(identity: string, memberId: string): number { return this.#revisions.get(JSON.stringify([identity, memberId])) ?? 0 }
|
|
63
|
+
readable(identity: string, memberId: string): boolean { return !this.#closed && !this.#settling.get(JSON.stringify([identity, memberId])) }
|
|
64
|
+
#settle(target: ComponentPropertyTarget): () => void {
|
|
65
|
+
const key = scope(target)
|
|
66
|
+
this.#settling.set(key, (this.#settling.get(key) ?? 0) + 1); this.#touch(target)
|
|
67
|
+
return () => {
|
|
68
|
+
const remaining = (this.#settling.get(key) ?? 1) - 1
|
|
69
|
+
if (remaining) this.#settling.set(key, remaining); else this.#settling.delete(key)
|
|
70
|
+
this.#touch(target)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
#touch(target: Pick<ComponentPropertyTarget, 'identity' | 'memberId'>): void {
|
|
74
|
+
if (this.#closed) return
|
|
75
|
+
this.#revision++
|
|
76
|
+
this.#revisions.set(scope(target), (this.#revisions.get(scope(target)) ?? 0) + 1)
|
|
77
|
+
for (const listener of this.#listeners) listener()
|
|
78
|
+
}
|
|
79
|
+
get(target: ComponentPropertyTarget): ComponentPropertyDraft | undefined { return this.#entries.get(targetKey(target))?.record }
|
|
80
|
+
canKeep(target: ComponentPropertyTarget): boolean { return !this.#closed && (this.#entries.has(targetKey(target)) || this.#entries.size < 512) }
|
|
81
|
+
status(target: ComponentPropertyTarget): 'available' | 'pending' | 'session-only' {
|
|
82
|
+
const entry = this.#entries.get(targetKey(target))
|
|
83
|
+
return entry?.failed ? 'session-only' : entry?.operation ? 'pending' : 'available'
|
|
84
|
+
}
|
|
85
|
+
has(identity: string, memberId: string): boolean { return [...this.#entries.values()].some(entry => entry.record.target.identity === identity && entry.record.target.memberId === memberId) }
|
|
86
|
+
stopAgentRead(): void {
|
|
87
|
+
const reader = this.#agentReader
|
|
88
|
+
this.#agentReader = undefined
|
|
89
|
+
reader?.store.close()
|
|
90
|
+
if (reader) this.#touch(reader)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Explicit reads never settle backups; current input shadows older disk bytes even after admission failure. */
|
|
94
|
+
async readForAgent(context: { identity: string, memberId: string, source: CanonicalSource }, signal: AbortSignal, reference?: ClientComponentDraftReference): Promise<{ revision: number, candidates: ComponentPropertyAgentCandidate[], coverage: 'stored-and-session' | 'session-only', skippedInvalid: number }> {
|
|
95
|
+
const { identity, memberId, source } = context
|
|
96
|
+
signal.throwIfAborted()
|
|
97
|
+
if (!this.readable(identity, memberId)) throw new Error('client-component-drafts-unavailable')
|
|
98
|
+
if (this.#agentReader && (this.#agentReader.identity !== identity || this.#agentReader.memberId !== memberId)) this.stopAgentRead()
|
|
99
|
+
if (!this.#agentReader) {
|
|
100
|
+
const store = new DocumentDraftStore(JSON.stringify([identity, memberId]), () => { if (this.#agentReader?.store === store) this.#touch({ identity, memberId }) }, { owner: this.#writerOwner, family: 'component' })
|
|
101
|
+
this.#agentReader = { identity, memberId, store }
|
|
102
|
+
}
|
|
103
|
+
const reader = this.#agentReader, revision = this.revision(identity, memberId), writing = [...this.#entries.values()].filter(entry => entry.record.target.identity === identity && entry.record.target.memberId === memberId)
|
|
104
|
+
const target = reference ? { identity, memberId, blockId: reference.blockId, ...reference.stepId !== undefined ? { stepId: reference.stepId } : {} } : undefined
|
|
105
|
+
const key = target && reference ? `${prefix(target)}${reference.id}` : undefined
|
|
106
|
+
const known = reference && writing.find(entry => keyOf(entry.record) === key)
|
|
107
|
+
if (known && known.record.revision !== reference!.revision) throw new Error('client-component-draft-changed')
|
|
108
|
+
const current = () => { signal.throwIfAborted(); if (this.#agentReader !== reader || !this.readable(identity, memberId) || revision !== this.revision(identity, memberId)) throw new Error('client-component-drafts-changed') }
|
|
109
|
+
let stored: Map<string, unknown>, coverage: 'stored-and-session' | 'session-only' = 'stored-and-session'
|
|
110
|
+
try { stored = await reader.store.read(key && target && reference ? [key, markerOf(target, reference)] : [scopedPrefix(PREFIX, identity, memberId), scopedPrefix(SETTLED, identity, memberId)], signal, 2048) }
|
|
111
|
+
catch (error) {
|
|
112
|
+
current()
|
|
113
|
+
if (error instanceof Error && error.message === 'document-draft-read-limit') throw new Error('client-component-drafts-limit')
|
|
114
|
+
if (reference ? !known : !writing.length) throw new Error('client-component-drafts-unavailable')
|
|
115
|
+
stored = new Map(); coverage = 'session-only'
|
|
116
|
+
}
|
|
117
|
+
current()
|
|
118
|
+
const entries = new Map<string, { raw: unknown, storage: 'durable' | 'session', status: ComponentPropertyAgentCandidate['status'], current: boolean }>()
|
|
119
|
+
for (const [name, raw] of stored) if (name.startsWith(scopedPrefix(PREFIX, identity, memberId)) && (!key || name === key)) entries.set(name, { raw, storage: 'durable', status: 'available', current: false })
|
|
120
|
+
for (const entry of writing) if (!key || keyOf(entry.record) === key) {
|
|
121
|
+
const raw = JSON.stringify(entry.record), name = keyOf(entry.record)
|
|
122
|
+
entries.set(name, { raw, storage: stored.get(name) === raw ? 'durable' : 'session', status: entry.failed ? 'session-only' : entry.operation ? 'pending' : 'available', current: true })
|
|
123
|
+
}
|
|
124
|
+
const candidates: ComponentPropertyAgentCandidate[] = []; let skippedInvalid = 0
|
|
125
|
+
for (const [name, entry] of entries) {
|
|
126
|
+
try {
|
|
127
|
+
if (typeof entry.raw !== 'string' || entry.storage === 'durable' && entry.raw.length > MAX_CHARS) throw new Error('component-draft-data')
|
|
128
|
+
const value: unknown = JSON.parse(entry.raw)
|
|
129
|
+
if (!isRecord(value) || !isRecord(value.target) || typeof value.target.blockId !== 'string' || !value.target.blockId || value.target.blockId.length > 200
|
|
130
|
+
|| value.target.stepId !== undefined && (typeof value.target.stepId !== 'string' || !value.target.stepId || value.target.stepId.length > 200)) throw new Error('component-draft-data')
|
|
131
|
+
const owned = { identity, memberId, blockId: value.target.blockId, ...value.target.stepId !== undefined ? { stepId: value.target.stepId as string } : {} }
|
|
132
|
+
if (!valid(value, owned) || name !== keyOf(value) || value.id.length > 200 || value.definitionId.length > 200 || !value.definitionId
|
|
133
|
+
|| value.source.documentId !== source.documentId || !value.source.version || value.source.version.length > 200 || !value.source.fingerprint || value.source.fingerprint.length > 200) throw new Error('component-draft-data')
|
|
134
|
+
const token = await tokenOf(entry.raw); current()
|
|
135
|
+
const marked = stored.get(marker({ key: name, token, record: value })) === token
|
|
136
|
+
if (marked && !entry.current) continue
|
|
137
|
+
// Discarding a backup in another tab does not discard this tab's visible input.
|
|
138
|
+
const candidate: ComponentPropertyAgentCandidate = { key: name, token, record: value, current: entry.current, storage: marked ? 'session' : entry.storage, status: marked ? 'session-only' : entry.status }
|
|
139
|
+
if (reference && (value.id !== reference.id || value.revision !== reference.revision || token !== reference.token)) throw new Error('client-component-draft-changed')
|
|
140
|
+
candidates.push(candidate)
|
|
141
|
+
} catch { current(); if (reference) throw new Error('client-component-draft-changed'); skippedInvalid++ }
|
|
142
|
+
}
|
|
143
|
+
current()
|
|
144
|
+
if (reference && candidates.length !== 1) throw new Error('client-component-draft-changed')
|
|
145
|
+
return { revision, candidates: candidates.sort((a, b) => a.record.id.localeCompare(b.record.id)), coverage, skippedInvalid }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
keep(target: ComponentPropertyTarget, value: { source: CanonicalSource, definitionId: string, baseline: JsonObject, value: JsonObject, reset?: boolean, composing?: boolean }, origins: ComponentPropertyBackup[] = []): ComponentPropertyDraft {
|
|
149
|
+
if (this.#closed) throw new Error('component-draft-closed')
|
|
150
|
+
const key = targetKey(target), previous = this.#entries.get(key)
|
|
151
|
+
if (!previous && this.#entries.size >= 512) throw new Error('component-draft-limit')
|
|
152
|
+
const record: ComponentPropertyDraft = { format: 1, id: previous?.record.id ?? crypto.randomUUID(), revision: (previous?.record.revision ?? 0) + 1,
|
|
153
|
+
target: structuredClone(target), source: structuredClone(value.source), definitionId: value.definitionId, baseline: structuredClone(value.baseline), value: structuredClone(value.value),
|
|
154
|
+
reset: Boolean(value.reset), composing: Boolean(value.composing), updatedAt: new Date().toISOString() }
|
|
155
|
+
const entry: Entry = previous ?? { record, store: new DocumentDraftStore(scope(target), undefined, { owner: this.#writerOwner, family: 'component' }), failed: false, origins: [] }
|
|
156
|
+
entry.record = record
|
|
157
|
+
for (const origin of origins) if (!entry.origins.some(candidate => candidate.key === origin.key && candidate.token === origin.token)) entry.origins.push(origin)
|
|
158
|
+
this.#entries.set(key, entry)
|
|
159
|
+
const raw = JSON.stringify(record)
|
|
160
|
+
const operation = raw.length <= MAX_CHARS ? entry.store.put(keyOf(record), raw) : undefined
|
|
161
|
+
entry.operation = operation
|
|
162
|
+
entry.failed = !operation
|
|
163
|
+
this.#touch(target)
|
|
164
|
+
if (operation) void operation.done.then(result => {
|
|
165
|
+
if (this.#closed || this.#entries.get(key) !== entry || entry.operation !== operation) return
|
|
166
|
+
entry.operation = undefined; entry.failed = result !== 'committed'; this.#touch(target)
|
|
167
|
+
})
|
|
168
|
+
return record
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Reads never settle a foreign backup or replace current input. Recovery is an explicit caller action. */
|
|
172
|
+
async read(target: ComponentPropertyTarget, signal?: AbortSignal): Promise<ComponentPropertyBackupRead> {
|
|
173
|
+
if (this.#closed) throw new Error('component-draft-closed')
|
|
174
|
+
const reader = new DocumentDraftStore(scope(target), undefined, { owner: this.#writerOwner, family: 'component' })
|
|
175
|
+
try {
|
|
176
|
+
const stored = await reader.read([prefix(target), markerPrefix(target)], signal), result: ComponentPropertyBackup[] = []
|
|
177
|
+
if (stored.size > 1024) throw new Error('component-draft-limit')
|
|
178
|
+
let unreadable = 0
|
|
179
|
+
for (const [key, raw] of stored) {
|
|
180
|
+
signal?.throwIfAborted()
|
|
181
|
+
if (!key.startsWith(prefix(target))) continue
|
|
182
|
+
if (typeof raw !== 'string' || raw.length > MAX_CHARS) { unreadable++; continue }
|
|
183
|
+
let record: unknown
|
|
184
|
+
try { record = JSON.parse(raw) } catch { unreadable++; continue }
|
|
185
|
+
if (!valid(record, target) || key !== keyOf(record)) { unreadable++; continue }
|
|
186
|
+
const token = await tokenOf(raw), candidate = { record, token, key }
|
|
187
|
+
signal?.throwIfAborted()
|
|
188
|
+
if (stored.get(marker(candidate)) === token) continue
|
|
189
|
+
const current = this.#entries.get(targetKey(target))
|
|
190
|
+
if (current?.record.id === record.id || current?.origins.some(origin => origin.key === key && origin.token === token)) continue
|
|
191
|
+
result.push(candidate)
|
|
192
|
+
}
|
|
193
|
+
return { candidates: result.sort((a, b) => b.record.updatedAt.localeCompare(a.record.updatedAt)), unreadable }
|
|
194
|
+
} finally { reader.close() }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Recovery creates a confirmed branch before replacing the current in-memory draft. */
|
|
198
|
+
async recover(backup: ComponentPropertyBackup, expected: ComponentPropertyDraft | undefined, signal?: AbortSignal, guard?: () => void): Promise<boolean> {
|
|
199
|
+
const target = backup.record.target, key = targetKey(target), previous = this.#entries.get(key)
|
|
200
|
+
const owns = () => !this.#closed && this.#entries.get(key) === previous && previous?.record === expected
|
|
201
|
+
&& this.status(target) === 'available' && this.canKeep(target)
|
|
202
|
+
signal?.throwIfAborted(); guard?.()
|
|
203
|
+
if (!owns()) return false
|
|
204
|
+
const settled = this.#settle(target)
|
|
205
|
+
const store = new DocumentDraftStore(scope(target), undefined, { owner: this.#writerOwner, family: 'component' })
|
|
206
|
+
let adopted = false
|
|
207
|
+
try {
|
|
208
|
+
const values = await store.read([backup.key, marker(backup)], signal), raw = values.get(backup.key)
|
|
209
|
+
if (typeof raw !== 'string' || raw.length > MAX_CHARS || await tokenOf(raw) !== backup.token || values.get(marker(backup)) === backup.token) return false
|
|
210
|
+
signal?.throwIfAborted()
|
|
211
|
+
if (!owns()) return false
|
|
212
|
+
let value: unknown
|
|
213
|
+
try { value = JSON.parse(raw) } catch { return false }
|
|
214
|
+
if (!valid(value, target) || backup.key !== keyOf(value)) return false
|
|
215
|
+
guard?.()
|
|
216
|
+
const record: ComponentPropertyDraft = { ...structuredClone(value), id: crypto.randomUUID(), revision: 1, composing: false, updatedAt: new Date().toISOString() }
|
|
217
|
+
const fork = JSON.stringify(record)
|
|
218
|
+
if (fork.length > MAX_CHARS) return false
|
|
219
|
+
const operation = store.put(keyOf(record), fork)
|
|
220
|
+
if (!operation || await operation.done !== 'committed') return false
|
|
221
|
+
signal?.throwIfAborted()
|
|
222
|
+
// A later keystroke retains ownership; the committed branch remains recoverable.
|
|
223
|
+
guard?.()
|
|
224
|
+
if (!owns()) return false
|
|
225
|
+
this.#entries.set(key, { record, store, failed: false, origins: [backup] })
|
|
226
|
+
previous?.store.close()
|
|
227
|
+
adopted = true; this.#touch(target)
|
|
228
|
+
return true
|
|
229
|
+
} finally { if (!adopted) store.close(); settled() }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Both the in-memory revision and strict storage owner must still match the acknowledged input. */
|
|
233
|
+
async clear(expected: ComponentPropertyDraft, options?: { preserveOrigins?: boolean }): Promise<boolean> {
|
|
234
|
+
const key = targetKey(expected.target), entry = this.#entries.get(key)
|
|
235
|
+
if (this.#closed || !entry || entry.record !== expected) return false
|
|
236
|
+
const settled = this.#settle(expected.target)
|
|
237
|
+
try {
|
|
238
|
+
const operation = entry.store.remove(keyOf(expected), options?.preserveOrigins ? [] : entry.origins.map(origin => [marker(origin), origin.token] as [string, string]))
|
|
239
|
+
if (!operation) { entry.failed = true; this.#touch(expected.target); return false }
|
|
240
|
+
entry.operation = operation; this.#touch(expected.target)
|
|
241
|
+
const result = await operation.done
|
|
242
|
+
if (this.#closed || this.#entries.get(key) !== entry) return false
|
|
243
|
+
if (entry.operation === operation) { entry.operation = undefined; entry.failed = result !== 'committed' }
|
|
244
|
+
if (result === 'committed' && entry.record === expected) { this.#entries.delete(key); entry.store.close(); this.#touch(expected.target); return true }
|
|
245
|
+
this.#touch(expected.target)
|
|
246
|
+
return false
|
|
247
|
+
} finally { settled() }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** A discarded foreign revision is marked exactly; newer input under that key remains recoverable. */
|
|
251
|
+
async discard(backup: ComponentPropertyBackup, signal?: AbortSignal, guard?: () => void): Promise<boolean> {
|
|
252
|
+
signal?.throwIfAborted(); guard?.()
|
|
253
|
+
if (this.#closed) return false
|
|
254
|
+
const settled = this.#settle(backup.record.target)
|
|
255
|
+
const reader = new DocumentDraftStore(scope(backup.record.target), undefined, { owner: this.#writerOwner, family: 'component' })
|
|
256
|
+
try {
|
|
257
|
+
const values = await reader.read([backup.key], signal), raw = values.get(backup.key)
|
|
258
|
+
if (typeof raw !== 'string' || await tokenOf(raw) !== backup.token) return false
|
|
259
|
+
signal?.throwIfAborted(); guard?.()
|
|
260
|
+
const result = await reader.mark([[marker(backup), backup.token]])
|
|
261
|
+
this.#touch(backup.record.target)
|
|
262
|
+
return result
|
|
263
|
+
} finally { reader.close(); settled() }
|
|
264
|
+
}
|
|
265
|
+
close(): void {
|
|
266
|
+
if (this.#closed) return
|
|
267
|
+
this.#closed = true
|
|
268
|
+
this.stopAgentRead()
|
|
269
|
+
if (typeof window !== 'undefined') window.removeEventListener('beforeunload', this.#leave)
|
|
270
|
+
for (const entry of this.#entries.values()) entry.store.close()
|
|
271
|
+
this.#listeners.clear()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const COMPONENT_PROPERTY_MESSAGES = {
|
|
2
|
+
'zh-CN': {
|
|
3
|
+
properties: '属性', scope: '参数范围', base: '基础参数', step: '当前步骤', materials: '素材与引用', back: '返回参数',
|
|
4
|
+
apply: '应用', adopt: '采用', cancel: '取消', done: '完成', undo: '撤消', reset: '使用继承参数', resetPending: '将使用继承参数', edit: '继续编辑',
|
|
5
|
+
loading: '正在加载参数…', empty: '此内容没有公开参数。', changed: '文稿已更新,请重新检查。', recheck: '再次检查',
|
|
6
|
+
invalid: '请检查参数。', failed: '无法应用参数。输入已保留,请重试。', denied: '没有修改权限。', missing: '此内容已移除。',
|
|
7
|
+
storage: '输入仅在本次会话中保留。', storagePending: '正在备份输入…', storageRead: '无法读取备份。', limit: '无法再保留参数草稿。',
|
|
8
|
+
retry: '重试', backup: '参数备份', recover: '恢复草稿', discard: '放弃', discardChanges: '放弃更改', unreadable: '部分备份无法读取。',
|
|
9
|
+
backupChanged: '备份或输入已变化,请重新加载。', definitionChanged: '素材已改变,请核对保留的输入。',
|
|
10
|
+
preserved: '保留已有步骤覆盖', impact: (count: number) => `影响 ${count} 个步骤`,
|
|
11
|
+
search: '搜索素材', definitions: '已有素材', noDefinitions: '没有兼容素材。', results: (count: number) => `${count} 项`,
|
|
12
|
+
current: '当前素材', incompatible: '不适用于当前内容', module: '暂不支持此素材', references: '引用位置', locate: '定位',
|
|
13
|
+
actions: '素材操作', copy: '创建独立副本', remove: '删除', unavailable: '尚未提供独立副本或删除能力。', copyUnavailable: '尚未提供独立副本。', inUse: '此素材正在被引用。',
|
|
14
|
+
saved: '参数已应用到此内容。', resetReason: '之后将使用继承的参数。',
|
|
15
|
+
},
|
|
16
|
+
en: {
|
|
17
|
+
properties: 'Properties', scope: 'Parameter scope', base: 'Base parameters', step: 'Current step', materials: 'Assets and References', back: 'Back to Parameters',
|
|
18
|
+
apply: 'Apply', adopt: 'Use', cancel: 'Cancel', done: 'Done', undo: 'Undo', reset: 'Use Inherited Parameters', resetPending: 'Inherited parameters will be used', edit: 'Keep Editing',
|
|
19
|
+
loading: 'Loading parameters…', empty: 'No public parameters.', changed: 'The document changed. Review again.', recheck: 'Check Again',
|
|
20
|
+
invalid: 'Check the parameters.', failed: 'Couldn’t apply the parameters. Your input is kept; try again.', denied: 'Editing is not permitted.', missing: 'This content was removed.',
|
|
21
|
+
storage: 'Input is kept only in this session.', storagePending: 'Backing up input…', storageRead: 'Couldn’t read the backups.', limit: 'No more parameter drafts can be kept.',
|
|
22
|
+
retry: 'Retry', backup: 'Parameter backup', recover: 'Recover Draft', discard: 'Discard', discardChanges: 'Discard Changes', unreadable: 'Some backups could not be read.',
|
|
23
|
+
backupChanged: 'The backup or input changed. Reload it.', definitionChanged: 'The asset changed. Review the input that was kept.',
|
|
24
|
+
preserved: 'Keep existing step overrides', impact: (count: number) => `Affects ${count} ${count === 1 ? 'step' : 'steps'}`,
|
|
25
|
+
search: 'Search assets', definitions: 'Existing assets', noDefinitions: 'No compatible assets.', results: (count: number) => `${count} ${count === 1 ? 'item' : 'items'}`,
|
|
26
|
+
current: 'Current asset', incompatible: 'Not compatible with this content', module: 'This asset is not supported yet', references: 'References', locate: 'Locate',
|
|
27
|
+
actions: 'Asset Actions', copy: 'Make an Independent Copy', remove: 'Delete', unavailable: 'Independent copies and deletion are not available yet.', copyUnavailable: 'Independent copies are not available yet.', inUse: 'This asset is in use.',
|
|
28
|
+
saved: 'Parameters applied to this content.', resetReason: 'The inherited parameters will be used.',
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
export type ComponentPropertyMessages = typeof COMPONENT_PROPERTY_MESSAGES['en']
|