fix: thread builder save crash — await Automerge doc open before write
Two bugs fixed:
- attachShadow called unconditionally in connectedCallback, crashing on
re-insertion ("Shadow root cannot be created on a host which already
hosts a shadow tree")
- saveToAutomerge/deleteFromAutomerge called runtime.change() before the
async subscribeOffline() had resolved, causing "Document not open" errors
Now tracks the subscribe promise and awaits it before any write operation.
Also guards shadow root creation in gallery and campaign manager components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0318f0a7e1
commit
0abfce2991
|
|
@ -18,7 +18,7 @@ export class FolkCampaignManager extends HTMLElement {
|
||||||
static get observedAttributes() { return ['space']; }
|
static get observedAttributes() { return ['space']; }
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.attachShadow({ mode: 'open' });
|
if (!this.shadowRoot) this.attachShadow({ mode: 'open' });
|
||||||
this._space = this.getAttribute('space') || 'demo';
|
this._space = this.getAttribute('space') || 'demo';
|
||||||
// Start with demo campaign
|
// Start with demo campaign
|
||||||
this._campaigns = [{ ...MYCOFI_CAMPAIGN, createdAt: Date.now(), updatedAt: Date.now() }];
|
this._campaigns = [{ ...MYCOFI_CAMPAIGN, createdAt: Date.now(), updatedAt: Date.now() }];
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export class FolkThreadBuilder extends HTMLElement {
|
||||||
private _tweetImages: Record<string, string> = {};
|
private _tweetImages: Record<string, string> = {};
|
||||||
private _autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
private _autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
private _offlineUnsub: (() => void) | null = null;
|
private _offlineUnsub: (() => void) | null = null;
|
||||||
|
private _offlineReady: Promise<void> | null = null;
|
||||||
private _tweetImageUploadIdx: string | null = null;
|
private _tweetImageUploadIdx: string | null = null;
|
||||||
|
|
||||||
// SVG icons
|
// SVG icons
|
||||||
|
|
@ -44,7 +45,9 @@ export class FolkThreadBuilder extends HTMLElement {
|
||||||
static get observedAttributes() { return ['space', 'thread-id', 'mode']; }
|
static get observedAttributes() { return ['space', 'thread-id', 'mode']; }
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.attachShadow({ mode: 'open' });
|
if (!this.shadowRoot) {
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
this._space = this.getAttribute('space') || 'demo';
|
this._space = this.getAttribute('space') || 'demo';
|
||||||
this._threadId = this.getAttribute('thread-id') || null;
|
this._threadId = this.getAttribute('thread-id') || null;
|
||||||
this._mode = (this.getAttribute('mode') as any) || 'new';
|
this._mode = (this.getAttribute('mode') as any) || 'new';
|
||||||
|
|
@ -60,7 +63,7 @@ export class FolkThreadBuilder extends HTMLElement {
|
||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
if (this._space !== 'demo') {
|
if (this._space !== 'demo') {
|
||||||
this.subscribeOffline();
|
this._offlineReady = this.subscribeOffline();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,22 +115,35 @@ export class FolkThreadBuilder extends HTMLElement {
|
||||||
return (window as any).__rspaceOfflineRuntime;
|
return (window as any).__rspaceOfflineRuntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveToAutomerge(thread: ThreadData) {
|
private async saveToAutomerge(thread: ThreadData) {
|
||||||
const runtime = this.getRuntime();
|
const runtime = this.getRuntime();
|
||||||
if (!runtime?.isInitialized) return;
|
if (!runtime?.isInitialized) return;
|
||||||
|
|
||||||
const docId = socialsDocId(this._space) as DocumentId;
|
// Ensure the document is open before writing
|
||||||
runtime.change(docId, `Save thread ${thread.title || thread.id}`, (d: SocialsDoc) => {
|
if (this._offlineReady) {
|
||||||
if (!d.threads) d.threads = {} as any;
|
await this._offlineReady;
|
||||||
thread.updatedAt = Date.now();
|
}
|
||||||
d.threads[thread.id] = thread;
|
|
||||||
});
|
try {
|
||||||
|
const docId = socialsDocId(this._space) as DocumentId;
|
||||||
|
runtime.change(docId, `Save thread ${thread.title || thread.id}`, (d: SocialsDoc) => {
|
||||||
|
if (!d.threads) d.threads = {} as any;
|
||||||
|
thread.updatedAt = Date.now();
|
||||||
|
d.threads[thread.id] = thread;
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[thread-builder] Failed to save:', e.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteFromAutomerge(id: string) {
|
private async deleteFromAutomerge(id: string) {
|
||||||
const runtime = this.getRuntime();
|
const runtime = this.getRuntime();
|
||||||
if (!runtime?.isInitialized) return;
|
if (!runtime?.isInitialized) return;
|
||||||
|
|
||||||
|
if (this._offlineReady) {
|
||||||
|
await this._offlineReady;
|
||||||
|
}
|
||||||
|
|
||||||
const docId = socialsDocId(this._space) as DocumentId;
|
const docId = socialsDocId(this._space) as DocumentId;
|
||||||
runtime.change(docId, `Delete thread ${id}`, (d: SocialsDoc) => {
|
runtime.change(docId, `Delete thread ${id}`, (d: SocialsDoc) => {
|
||||||
if (d.threads?.[id]) delete d.threads[id];
|
if (d.threads?.[id]) delete d.threads[id];
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export class FolkThreadGallery extends HTMLElement {
|
||||||
static get observedAttributes() { return ['space']; }
|
static get observedAttributes() { return ['space']; }
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.attachShadow({ mode: 'open' });
|
if (!this.shadowRoot) this.attachShadow({ mode: 'open' });
|
||||||
this._space = this.getAttribute('space') || 'demo';
|
this._space = this.getAttribute('space') || 'demo';
|
||||||
this.render();
|
this.render();
|
||||||
if (this._space === 'demo') {
|
if (this._space === 'demo') {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue