The two storage channels every Application has — when to use each and how to avoid the pitfalls.
|
|
appContent | ||
appContent | ||
appContent | ||
appState | ||
appState | ||
appState |
// the Application's app-content (its first), kept current: null while there is none window.charmiq.appContent.content$().subscribe(content => { show(content ? content.text : 'Nothing yet'); }); // a named app-content window.charmiq.appContent.content$("[name='editor']").subscribe(content => { /* … */ });
content$() gives the app-content as it stands now, then again every time anyone changes it, your own writes included. It is null while no app-content matches, and becomes null the moment the app-content is removed: the Platform knows exactly when, so there is nothing to check. Once an app-content is removed, the next app-content the selector names is followed. A read that fails errors the stream with its code.id, its revision (its exact state), its text, and its lines (the text as lines, without the line break that ends each).app/content-conflict and nothing is written, so a write never lands on text it wasn't measured on.await content.appendLine('Jam'); // adds a line after the last await content.removeLine(0); // removes the first line, with its line break await content.replaceLine(1, 'Tea'); // replaces the text of a line await content.setLines(['Milk', 'Tea']); // makes the app-content these lines, writing only the runs that differ await content.edit([{ from: 0, to: 4, insert: 'Oat milk' }]); // any range, each edit measured after the ones before it
appendLine puts a line break first when the last line has none, and if another writer got there first it is measured again and sent again on its own: adding a line never depends on what the app-content held. Every other write is the caller's to retry, because only it knows whether its intent still holds. setLines writes only the runs of lines that differ, so another writer's change to a line you kept stays; when the lines differ too much to tell which stayed the same, it is refused (functions/out-of-range) and nothing is written — change them with replaceLine and removeLine instead. Every edit must lie inside the app-content; an edit outside it is refused (functions/invalid-argument).const log = await window.charmiq.appContent.create('', 'log'); // the app-content named log: a new one, or the one already there await log.remove(); // refused if it changed since
create(text, name) never writes an app-content that exists, empty or not: when one of that name is there, it is returned with its text. Two collaborators creating one at the same moment each make one; contents$("[name='log']") shows both. set(text, selector) replaces an app-content's whole text (and creates one when the selector matches none): anything another writer changed meanwhile is lost, so use it only to replace everything on purpose.content$() gives, never what you last wrote: your own writes come back through it, merged with whatever others did meanwhile.set() replaces the whole app-content and drops anyone else's concurrent edit.content$: ask the user to reload rather than writing another way.const appContent = window.charmiq.appContent; appContent.content$().subscribe({ next: content => render(content), // null: the list isn't in the document error: failure => showError(failure), // the list could not be read }); // a refused write says why: the list changed since, or anything else const refused = (failure, what) => say((failure?.code === 'app/content-conflict') ? `The list changed, so ${what}. Check it and try again.` : `${what[0].toUpperCase()}${what.slice(1)}: ${failure?.message ?? failure}.`); // each row removes through the app-content it was rendered from const render = content => { list.replaceChildren(...(content?.lines ?? []).map((line, index) => row(line, () => content.removeLine(index).catch(failure => refused(failure, `"${line}" was not removed`)) ))); addButton.onclick = () => content?.appendLine(input.value).catch(failure => refused(failure, `"${input.value}" was not added`)); };
edits$(selector) streams the app-content as edits. It opens with where the app-content stands (created), then sends each change in order. An [id='…'] selector follows that app-content whether or not it exists yet, opening with absent when it does not; any other selector names the app-content it matches now and fails with functions/not-found when none does. Each changes carries the revision it applies to (base), its edits, and the revision it makes; then removed when the app-content goes. A base that is not the revision last seen means an edit was missed.applyChanges() takes a fourth argument, { editId }, and the changes the write makes carries it back as ack. The editor recognizes its own edit whether the write's answer or the edit arrives first.apps/codemirror-editor in the CharmIQ Applications repository) is the worked example: it runs CodeMirror's collaboration library over these, and its Charm command waits for its write.// every app-content, each one you can write through window.charmiq.appContent.contents$().subscribe(contents => renderTabs(contents)); // every app-content of a name: a second one is visible, never hidden behind the first window.charmiq.appContent.contents$("[name='model']").subscribe(models => { if (models.length > 1) say('This document has two models.'); });
content$() and contents$() are built over primitives that stay for what they do not cover: onChange$(selector) (each change; with no selector, every app-content; its deleted notice is a reason to check, not proof), snapshot(selector) (an app-content with its revision, or null), applyChanges(changes, selector, guard) (guarded edits: the guard is a snapshot's id and revision), edits$(selector) and the write tag (for a live text editor), and get, set and remove.// Read current state const config = await window.charmiq.appState.get(); // → { theme: 'dark', language: 'javascript', fontSize: 14 } // Watch for changes (fires immediately with current state) window.charmiq.appState.onChange$().subscribe(state => { applyTheme(state?.theme ?? 'light'); }); // Write (replaces the entire object) await window.charmiq.appState.set({ theme: 'light', language: 'python', fontSize: 16, });
onChange$() delivers, not what you last set().content$(), get(), set(), applyChanges(), remove(): the first app-content. contents$(), onChange$(): every app-content | |
[id='kRP9xNbCmQ'] | |
[name='editor'] | |
[0], [1] |
appContent and appState always act on your own Application, in this instance, and on nothing else. Two copies of the same Application in one document each keep their own. To work with another Application, use what it advertises: see Application Discovery. Another Application reaches your data only the way any tool does: through authorized Platform APIs, including Commands and MCP, with the User's consent.window.charmiq API reference including appContent, appState, OAuth, and MCP.