diff --git a/manifest.json b/manifest.json index 04def06..cbc4ff2 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "mdbase-obsidian", "name": "mdbase", - "version": "0.4.19", + "version": "0.4.20", "minAppVersion": "1.11.4", "description": "Edit mdbase types and mirror hosted collections through mdbase Connect.", "author": "calluma", diff --git a/package-lock.json b/package-lock.json index 2f8ffb8..8023064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "mdbase-obsidian", - "version": "0.4.19", + "version": "0.4.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mdbase-obsidian", - "version": "0.4.19", + "version": "0.4.20", "license": "MIT", "dependencies": { "@callumalpass/mdbase-interop": "0.1.0-rc.2", - "@mdbase-dev/sdk": "file:vendor/sdk-1907e67f-production-beta187.tgz", + "@mdbase-dev/sdk": "file:vendor/sdk-e8ffb806-production-beta188.tgz", "@noble/hashes": "2.4.0", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", @@ -1300,9 +1300,9 @@ "license": "MIT" }, "node_modules/@mdbase-dev/sdk": { - "version": "0.1.0-beta.187", - "resolved": "file:vendor/sdk-1907e67f-production-beta187.tgz", - "integrity": "sha512-bNsqlaeBKwA5ig3e4WZaCBRKmnlJs/b1FB11pogNtb7ur/bQf8txlcJWawKU2Fjto5/7+8MYz9bZFzM7JOfK/Q==", + "version": "0.1.0-beta.188", + "resolved": "file:vendor/sdk-e8ffb806-production-beta188.tgz", + "integrity": "sha512-EDaInyP8q2Lz0Akz8P+CNj5O6OvinMJWR1bE04fKtWRFDt9mVRBRBYRja5okOtb3gtnKYyMdQFGLlFln7F1RrA==", "bundleDependencies": [ "@mdbase-dev/connect-protocol" ], diff --git a/package.json b/package.json index 344d7cf..35455a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mdbase-obsidian", - "version": "0.4.19", + "version": "0.4.20", "description": "mdbase types and sync for Obsidian", "main": "main.js", "type": "module", @@ -58,7 +58,7 @@ }, "dependencies": { "@callumalpass/mdbase-interop": "0.1.0-rc.2", - "@mdbase-dev/sdk": "file:vendor/sdk-1907e67f-production-beta187.tgz", + "@mdbase-dev/sdk": "file:vendor/sdk-e8ffb806-production-beta188.tgz", "@noble/hashes": "2.4.0", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", diff --git a/src/sync/controller.ts b/src/sync/controller.ts index a3cc744..7c6ba11 100644 --- a/src/sync/controller.ts +++ b/src/sync/controller.ts @@ -1,4 +1,4 @@ -import type { ConflictEntry, ConflictValue, Hold, MdbaseClient, SyncStatus } from "@mdbase-dev/sdk/thin-client"; +import type { ConflictEntry, ConflictValue, Hold, MdbaseClient, MdbaseSync, SyncStatus } from "@mdbase-dev/sdk/thin-client"; import { hexToBytes } from "@noble/hashes/utils.js"; import { collectionIdFrom, @@ -59,7 +59,7 @@ export interface SyncSnapshot { direct?: { text: string; phase: { - kind: string; sync?: { kind: string; pending: number; health?: string | null; live?: boolean | null; holds?: number; conflicts?: number }; problem?: unknown; + kind: string; sync?: Pick & { installing?: boolean }; problem?: unknown; /** A Private collection waiting for its key: how the account unlocks it. */ unlock?: { mode: "password" | "strict" | "none"; unlocking: boolean; name: string | null } | null; /** Still starting after the plugin stopped waiting: offer Restart sync. */ @@ -452,7 +452,6 @@ export class SyncController { private readonly decided = new Set(); private async showMerged(client: MdbaseClient, entries: ConflictEntry[]): Promise { - const sdk = await import("@mdbase-dev/sdk/thin-client"); // Choosing a held version also records the conflict it settled; the person // already decided, so don't ask again. const already = entries.filter((entry) => this.decided.has(entry.conflict.id)); @@ -466,14 +465,14 @@ export class SyncController { const text = (value: ConflictValue | undefined): string => { if (!value) return ""; if (value.form === "text") return value.text; - if (value.form === "value") return JSON.stringify(sdk.toPlain(value.value)); + if (value.form === "value") return JSON.stringify(value.value); return value.form === "deleted" ? "(deleted)" : "(a file)"; }; const lost = entry.conflict.lost; return { entry, path: this.paths.get(id) ?? "", kind: entry.conflict.kind, field: entry.conflict.field ?? null, kept: text(entry.conflict.kept), lost: text(lost), - lostValue: lost.form === "value" ? sdk.toPlain(lost.value) : undefined, + lostValue: lost.form === "value" ? lost.value : undefined, }; })); if (this.client === client) this.set({ merged }); diff --git a/src/sync/native/page.ts b/src/sync/native/page.ts index f445de7..a27d7d2 100644 --- a/src/sync/native/page.ts +++ b/src/sync/native/page.ts @@ -46,13 +46,11 @@ const confirmed = async (w: any) => { // revision a read-back returns (checked against production; the view's own // `state` is the stale optimistic one), which saves that read. The caller keeps // only identity and revision. -const written = async (ws: any[], id: string) => { - for (const w of ws) { - const receipt: any = await w.confirmed; - const r = receipt?.state === "confirmed" && receipt.status === "applied" ? receipt.records?.find((x: any) => x.id === id) : undefined; - if (r && typeof r.revision === "string" && typeof r.path === "string") return { id: r.id, path: r.path, revision: r.revision, via: "receipt" }; - } - return plain(await records().get(id, { document: true })); +const written = async (w: any, id: string) => { + const receipt: any = await w.confirmed; + const r = receipt?.state === "confirmed" && receipt.status === "applied" ? receipt.records?.find((x: any) => x.id === id) : undefined; + if (r && typeof r.revision === "string" && typeof r.path === "string") return { id: r.id, path: r.path, revision: r.revision, via: "receipt" }; + return plain(await records().get(id, { include: { document: true } })); }; function phaseSummary() { @@ -61,7 +59,9 @@ function phaseSummary() { return { kind: p.kind, collectionId: p.collectionId ?? null, - sync: s.sync ? { kind: s.sync.kind, text: s.sync.text, pending: s.sync.pending, health: s.sync.health, live: s.sync.live ?? null, holds: s.sync.holds ?? 0, conflicts: s.sync.conflicts ?? 0 } : null, + sync: s.sync ? { state: s.sync.state, connection: s.sync.connection, pending: s.sync.pending, health: s.sync.health, live: s.sync.live, holds: s.sync.holds, conflicts: s.sync.conflicts, + // The SDK reads a first download as catching up; the replica says which it is. + installing: p.connection?.records?.status?.installing !== undefined } : null, // `action` is the SDK's suggested next step ("approve" to request access again). problem: p.problem ? { code: p.problem.code, reason: p.problem.reason, key: p.problem.key ?? null, title: p.problem.title, message: p.problem.message, action: p.problem.action ?? null } : null, // A Private collection without its key on this device: how the account @@ -112,7 +112,7 @@ const methods: Record Promise> = { for (let limit = 64; ; limit = Math.max(1, Math.floor(limit / 2))) { const out: any[] = []; try { - for await (const page of records().queryPages({ limit }, { document: true }, undefined, { onReset: () => { out.length = 0; } })) + for await (const page of records().queryPages({ limit }, { include: { document: true }, onReset: () => { out.length = 0; } })) for (const r of page.records) out.push(plain(r)); return out; } catch (e: any) { @@ -120,13 +120,13 @@ const methods: Record Promise> = { } } }, - get: async (id: string) => plain(await records().get(id, { document: true })), + get: async (id: string) => plain(await records().get(id, { include: { document: true } })), // Bounded ordinary reads for an event's affected paths. Only not_found means // absent: permission, budget and transient errors must never look like deletes. at: async (paths: string[]) => { const out: any[] = []; let next = 0; const read = async (path: string) => { - try { return { ...plain(await records().get({ path }, { document: true })), kind: "record" }; } + try { return { ...plain(await records().get({ path }, { include: { document: true } })), kind: "record" }; } catch (e: any) { if (e?.code !== "not_found") throw e; } try { return { ...fplain(await files().get({ path })), kind: "file" }; } catch (e: any) { if (e?.code !== "not_found") throw e; } @@ -139,8 +139,8 @@ const methods: Record Promise> = { }, watch: async (cursor?: string) => { watch?.close?.(); watch?.stop?.(); - watch = records().watchChanges(cursor, (batch: any) => - post({ type: "changes", batch: { cursor: batch.cursor, reset: batch.reset, changes: batch.changes.map((c: any) => ({ id: c.id, path: c.path, kind: c.kind, entity: c.entity ?? null })) } })); + watch = records().watchChanges((batch: any) => + post({ type: "changes", batch: { cursor: batch.cursor, reset: batch.reset, changes: batch.changes.map((c: any) => ({ id: c.id, path: c.path, kind: c.kind, entity: c.entity ?? null })) } }), { cursor }); // The ACK is delivered only after the initial reset/replay callback. Keep // startup's barrier closed until that callback has reached the parent. await watch!.ready; @@ -148,12 +148,11 @@ const methods: Record Promise> = { }, create: async (path: string, document: string) => { const w: any = await confirmed(await records().create({ path, document }, SYNC_WRITE)); - return written([w], w.recordId); + return written(w, w.recordId); }, replace: async (id: string, base: { path: string; doc: string }, next: { path: string; doc: string }, revision: string) => { - const ws: any[] = await records().submit([{ kind: "document", id, base, new: next, ifRevision: revision }], SYNC_WRITE); - await Promise.all(ws.map(confirmed)); - return written(ws, id); + const w = await confirmed(await records().submit([{ kind: "document", id, base, new: next, ifRevision: revision }], SYNC_WRITE)); + return written(w, id); }, // The collection's file policy (which attachments sync, for every device). inclusion: async () => { @@ -245,7 +244,7 @@ const methods: Record Promise> = { const until = Date.now() + 120_000; while (true) { const status = await records().getStatus(); - const writes: any = mdbase.getSnapshot().writes; + const writes: any = mdbase.getSnapshot().sync?.writes; if (writes?.rejected?.length || status.holds > 0 || status.unresolved > 0) { log(`drain blocked: rejected=${writes?.rejected?.length ?? 0} holds=${status.holds} unresolved=${status.unresolved} pending=${status.pending}`); throw new Error("Unresolved writes/holds; ownership retained"); diff --git a/src/sync/native/plugin.ts b/src/sync/native/plugin.ts index 79ff745..31c1f61 100644 --- a/src/sync/native/plugin.ts +++ b/src/sync/native/plugin.ts @@ -12,7 +12,7 @@ const { spawnSync } = nativeModule("child_p const { brotliDecompressSync } = nativeModule("zlib"); import packedAssets from "embedded:assets"; import { validVaultPort } from "../ownership"; -import { AWAITING_APPROVAL, HANDED_OVER, NEEDS_APPROVAL, PRIVATE_COLLECTIONS_HINT, SIGN_IN_STOPPED, STARTING_STALLED, failedText, stoppedText } from "../words"; +import { AWAITING_APPROVAL, HANDED_OVER, NEEDS_APPROVAL, PRIVATE_COLLECTIONS_HINT, SIGN_IN_STOPPED, STARTING_STALLED, failedText, readyText, stoppedText, syncOffline } from "../words"; import { stageDownload } from "./download"; import { retryUpload, type UploadIdentity } from "./upload"; import { conflictPath, reconcileDecision, remoteMoveDecision, localMovePairs, Synced } from "./reconcile"; @@ -544,17 +544,17 @@ export default class NativeSync extends Component { const sameStop = phase.kind !== "ready" && phase.kind !== "needs_unlock" && phase.kind === was.kind && phase.problem && JSON.stringify(phase.problem) === JSON.stringify(was.problem); if (this.admitting && sameStop) this.setStatus(this.statusText); else if (this.admitting && !(phase.kind === "ready" && (this.syncingFiles || this.startingMirror || this.reconcileRetryTimer !== null))) - this.setStatus(phase.kind === "ready" ? phase.sync?.text ?? "Checking your files" : phaseText[phase.kind] ?? "Starting sync"); + this.setStatus(phase.kind === "ready" ? phase.sync ? readyText(phase.sync) : "Checking your files" : phaseText[phase.kind] ?? "Starting sync"); // A pass in progress keeps its own progress text, but going offline or // coming back must still reach the UI; "Uploading 83 of 500" would // otherwise stay up for as long as the connection is down (QA2-1). - else if (this.admitting && phase.sync?.kind !== was.sync?.kind) this.setStatus(this.statusText); + else if (this.admitting && phase.sync?.state !== was.sync?.state) this.setStatus(this.statusText); // Names/documents never belong in this log. - this.log(`phase ${phase.kind}${phase.problem ? ` ${phase.problem.code ?? ""} ${phase.problem.reason ?? ""}` : ""}${phase.sync ? ` sync=${phase.sync.kind} pending=${phase.sync.pending}` : ""}`); + this.log(`phase ${phase.kind}${phase.problem ? ` ${phase.problem.code ?? ""} ${phase.problem.reason ?? ""}` : ""}${phase.sync ? ` sync=${phase.sync.state} pending=${phase.sync.pending}` : ""}`); // A wrong password is shown beside the form; it doesn't stop sync. if (this.admitting && phase.kind !== "needs_unlock" && phase.problem && JSON.stringify(phase.problem) !== JSON.stringify(was.problem)) this.fail("sync", phase.problem, this.needsRestart && this.canStartOver ? SIGN_IN_STOPPED : stoppedText(phase.problem), true); - if (phase.sync?.kind === "offline" && was.sync?.kind !== "offline") + if (syncOffline(phase.sync) && !syncOffline(was.sync)) new Notice("mdbase: sync service unreachable. Changes are saved on this device and will sync when the connection returns.", 10000); if (this.admitting && (phase.kind === "opening" || phase.kind === "needs_unlock") && phase.collectionId) { try { this.claim(phase.collectionId); } catch (e) { this.pauseForOwnership(e); return; } @@ -1273,7 +1273,7 @@ export default class NativeSync extends Component { this.lastSynced = Date.now(); if (this.activityTimer === null) this.activityTimer = window.setTimeout(() => { this.activityTimer = null; this.writeActivity(); }, 2000); } - this.passStatus(this.phase.sync?.text ?? "Synced"); + this.passStatus(this.phase.sync ? readyText(this.phase.sync) : "Synced"); // Notes refused under the old definitions get another chance now. if (definitionsChanged && failed.length) window.setTimeout(() => this.requestReconcile(), 0); return failed.length === 0; diff --git a/src/sync/words.ts b/src/sync/words.ts index c1f906f..ca47576 100644 --- a/src/sync/words.ts +++ b/src/sync/words.ts @@ -1,3 +1,4 @@ +import type { MdbaseSync } from "@mdbase-dev/sdk/thin-client"; import type { SyncSnapshot } from "./controller"; export type Tone = "ok" | "busy" | "quiet" | "attention" | "problem"; @@ -119,17 +120,23 @@ const STATE_REASONS: Record = { sync_not_eligible: "This computer's mdbase sign-in stopped working, so sync is stopped here. Your notes here are untouched.", }; -/** The ready state in words (the SDK's own text is "Synced through "). */ -export function readyText(sync: { kind: string; pending: number; health?: string | null } | undefined): string { +type ReadySync = Pick & { installing?: boolean }; + +/** + * Sync can't reach mdbase from here: no network, or the service did not answer. + * A busy or rate-limited service, or a replica still connecting, is not offline. + */ +export function syncOffline(sync: ReadySync | null | undefined): boolean { + return sync?.connection === "offline" || sync?.health === "unreachable"; +} + +/** The ready state in words. */ +export function readyText(sync: ReadySync | null | undefined): string { const waiting = sync?.pending ? `${sync.pending === 1 ? "1 change" : `${sync.pending.toLocaleString()} changes`} waiting to send.` : ""; - switch (sync?.kind) { - case "offline": return `Offline. Changes are saved here and sync when the connection returns.${waiting ? ` ${waiting}` : ""}`; - case "catching_up": return "Getting the latest changes from your other devices."; - case "installing": return "Downloading the collection."; - default: - if (sync?.health === "server_busy" || sync?.health === "rate_limited") return "mdbase is busy. Sync carries on shortly."; - return waiting || "Everything is up to date."; - } + if (syncOffline(sync)) return `Offline. Changes are saved here and sync when the connection returns.${waiting ? ` ${waiting}` : ""}`; + if (sync?.state === "catching_up") return sync.installing ? "Downloading the collection." : "Getting the latest changes from your other devices."; + if (sync?.health === "server_busy" || sync?.health === "rate_limited") return "mdbase is busy. Sync carries on shortly."; + return waiting || "Everything is up to date."; } export function summarize(s: SyncSnapshot): SyncSummary { @@ -165,10 +172,10 @@ export function summarize(s: SyncSnapshot): SyncSummary { } // Offline outranks progress text: an upload that can't reach mdbase isn't // progressing, and the offline notice alone disappears (QA-3). - const offline = ready && !problem && phase.sync?.kind === "offline"; + const offline = ready && !problem && syncOffline(phase.sync); if (offline) return { label: "Offline", detail: readyText(phase.sync), tone: "attention", syncing: true }; const checking = /checking your files|uploading|downloading/i.test(text); - const label = problem ? "Sync needs attention" : checking ? "Syncing" : ready ? (phase.sync?.kind === "offline" ? "Offline" : phase.sync?.pending ? "Syncing" : "Synced") : phase.kind === "signed_out" ? "Sync off" : phase.kind === "choosing" ? "Choose collection" : phase.kind === "authorizing" ? "Finish sign-in" : "Starting sync"; + const label = problem ? "Sync needs attention" : checking ? "Syncing" : ready ? (syncOffline(phase.sync) ? "Offline" : phase.sync?.pending ? "Syncing" : "Synced") : phase.kind === "signed_out" ? "Sync off" : phase.kind === "choosing" ? "Choose collection" : phase.kind === "authorizing" ? "Finish sign-in" : "Starting sync"; const detail = ready && !problem && !checking ? readyText(phase.sync) : text; return { label, detail, tone: problem ? "problem" : checking ? "busy" : ready ? "ok" : "quiet", syncing: ready || problem }; } diff --git a/test/native/device.test.mjs b/test/native/device.test.mjs index 5992865..858ad5d 100644 --- a/test/native/device.test.mjs +++ b/test/native/device.test.mjs @@ -48,10 +48,13 @@ test("activity keeps the most recent entries first, up to the limit", () => { }); test("the ready state is described in words, not log positions", () => { - assert.equal(readyText({ kind: "synced", pending: 0 }), "Everything is up to date."); - assert.equal(readyText({ kind: "synced", pending: 3 }), "3 changes waiting to send."); - assert.match(readyText({ kind: "offline", pending: 1 }), /^Offline\. .* 1 change waiting to send\.$/); - assert.equal(readyText({ kind: "catching_up", pending: 0 }), "Getting the latest changes from your other devices."); + assert.equal(readyText({ state: "saved", connection: "online", health: null, pending: 0 }), "Everything is up to date."); + assert.equal(readyText({ state: "saving", connection: "online", health: null, pending: 3 }), "3 changes waiting to send."); + assert.match(readyText({ state: "saved_on_device", connection: "reconnecting", health: "unreachable", pending: 1 }), /^Offline\. .* 1 change waiting to send\.$/); + assert.equal(readyText({ state: "catching_up", connection: "online", health: null, pending: 0 }), "Getting the latest changes from your other devices."); + assert.equal(readyText({ state: "catching_up", connection: "online", health: null, pending: 0, installing: true }), "Downloading the collection."); + assert.equal(readyText({ state: "reconnecting", connection: "reconnecting", health: null, pending: 0 }), "Everything is up to date.", "a replica still connecting is not offline"); + assert.equal(readyText({ state: "reconnecting", connection: "reconnecting", health: "server_busy", pending: 0 }), "mdbase is busy. Sync carries on shortly.", "a busy service is not offline"); }); test("the collection's file policy leaves out attachments only, as Connect does", () => { diff --git a/test/native/sync.test.mjs b/test/native/sync.test.mjs index 0056048..d178d26 100644 --- a/test/native/sync.test.mjs +++ b/test/native/sync.test.mjs @@ -500,20 +500,20 @@ test("a pending full-scan retry keeps the status busy when the SDK reports synce f.native.admitting = true; f.native.syncingFiles = false; f.native.phase = { kind: "ready", collectionId: "c" }; f.native.reconcileRetryTimer = 1; - await f.native.onPhase({ kind: "ready", collectionId: "c", sync: { kind: "synced", text: "Synced through 9", pending: 0 } }); - assert.ok(!seen.includes("Synced through 9")); + await f.native.onPhase({ kind: "ready", collectionId: "c", sync: { state: "saved", connection: "online", health: null, pending: 0 } }); + assert.ok(!seen.includes("Everything is up to date.")); }); test("going offline during an upload reaches the UI without replacing the progress text (QA2-1)", async t => { const f = fixture(t); const seen = []; - f.native.changed = (text, phase) => seen.push([text, phase.sync?.kind]); + f.native.changed = (text, phase) => seen.push([text, phase.sync?.state]); f.native.admitting = true; f.native.syncingFiles = true; f.native.statusText = "Uploading 83 of 500 notes"; - f.native.phase = { kind: "ready", collectionId: "c", sync: { kind: "synced", text: "Synced through 9, 16 changes waiting", pending: 16 } }; - await f.native.onPhase({ kind: "ready", collectionId: "c", sync: { kind: "synced", text: "Synced through 10, 15 changes waiting", pending: 15 } }); + f.native.phase = { kind: "ready", collectionId: "c", sync: { state: "saving", connection: "online", health: null, pending: 16 } }; + await f.native.onPhase({ kind: "ready", collectionId: "c", sync: { state: "saving", connection: "online", health: null, pending: 15 } }); assert.deepEqual(seen, [], "routine progress during a pass is not republished"); - await f.native.onPhase({ kind: "ready", collectionId: "c", sync: { kind: "offline", text: "Can't reach the sync service.", pending: 16 } }); - await f.native.onPhase({ kind: "ready", collectionId: "c", sync: { kind: "synced", text: "Synced through 11", pending: 14 } }); - assert.deepEqual(seen, [["Uploading 83 of 500 notes", "offline"], ["Uploading 83 of 500 notes", "synced"]]); + await f.native.onPhase({ kind: "ready", collectionId: "c", sync: { state: "saved_on_device", connection: "reconnecting", health: "unreachable", pending: 16 } }); + await f.native.onPhase({ kind: "ready", collectionId: "c", sync: { state: "saving", connection: "online", health: null, pending: 14 } }); + assert.deepEqual(seen, [["Uploading 83 of 500 notes", "saved_on_device"], ["Uploading 83 of 500 notes", "saving"]]); }); test("a locked Private collection waits for its key instead of stopping sync", async t => { const f = fixture(t); const seen = [], calls = []; @@ -544,10 +544,10 @@ test("access removed: one clean sentence that later notices and a finishing pass const f = fixture(t); const seen = [], calls = []; f.native.changed = (text, phase) => seen.push([text, phase.kind]); f.native.admitting = true; f.native.mirroring = true; - f.native.phase = { kind: "ready", collectionId: "c", sync: { kind: "synced", text: "Synced through 9", pending: 0 } }; + f.native.phase = { kind: "ready", collectionId: "c", sync: { state: "saved", connection: "online", health: null, pending: 0 } }; const problem = { code: "access_removed", reason: null, key: "installation_scope_removed", action: "approve", title: "Access was removed", message: "This app's previous installation cannot regain access to this collection. Request access again to approve a new installation for this collection. Your other collections keep their access." }; - const stopped = { kind: "problem", collectionId: "c", sync: { kind: "synced", text: "Synced through 9", pending: 0 }, problem }; + const stopped = { kind: "problem", collectionId: "c", sync: { state: "saved", connection: "online", health: null, pending: 0 }, problem }; await f.native.onPhase(stopped); const [text, kind] = seen.at(-1); assert.equal(kind, "problem"); @@ -557,7 +557,7 @@ test("access removed: one clean sentence that later notices and a finishing pass await f.native.onPhase({ ...stopped, sync: { ...stopped.sync, pending: 1 }, collections: [] }); assert.deepEqual(seen.at(-1), [text, "problem"], "a later notice keeps the reason"); // A pass that was running when access went keeps its counts but not "Synced". - f.native.passStatus("Synced through 9"); + f.native.passStatus("Everything is up to date."); f.native.passStatus("Checking your files"); assert.deepEqual(seen.at(-1), [text, "problem"]); // Request access again uses the SDK's approve for this collection. diff --git a/test/sync.test.ts b/test/sync.test.ts index 3d76797..43d4d69 100644 --- a/test/sync.test.ts +++ b/test/sync.test.ts @@ -165,11 +165,11 @@ test("Sync now says what the press did", () => { test("offline outranks upload progress in the sync status (QA-3)", () => { const s = { daemon: "running", collection: null, status: null, error: null, busy: null, live: null, - direct: { text: "Syncing: uploading 100% (0 MiB)", phase: { kind: "ready", sync: { kind: "offline", pending: 3, health: "unreachable" } } } } as unknown as SyncSnapshot; + direct: { text: "Syncing: uploading 100% (0 MiB)", phase: { kind: "ready", sync: { state: "saved_on_device", connection: "reconnecting", pending: 3, health: "unreachable" } } } } as unknown as SyncSnapshot; const summary = summarize(s); assert.equal(summary.label, "Offline"); assert.match(summary.detail, /Offline\. Changes are saved here/); - const catching = { ...s, direct: { text: "Checking your files: catching up with changes from other devices", phase: { kind: "ready", sync: { kind: "synced", pending: 0 } } } } as unknown as SyncSnapshot; + const catching = { ...s, direct: { text: "Checking your files: catching up with changes from other devices", phase: { kind: "ready", sync: { state: "saved", connection: "online", health: null, pending: 0 } } } } as unknown as SyncSnapshot; assert.equal(summarize(catching).label, "Syncing", "a pending full-scan retry isn't 'Synced' (QA-2)"); assert.doesNotMatch(summarize(catching).detail, /up to date/); }); @@ -186,11 +186,11 @@ test("a Private collection waiting for its key reads as locked, not as a problem test("a vault whose sync stopped keeps the status bar, saying it stopped (LAB F1)", () => { const base = { daemon: "running", collection: null, status: null, error: null, busy: null, live: null } as unknown as SyncSnapshot; - const synced = summarize({ ...base, direct: { text: "Synced through 9", phase: { kind: "ready", sync: { kind: "synced", pending: 0 } } } } as SyncSnapshot); + const synced = summarize({ ...base, direct: { text: "Everything is up to date.", phase: { kind: "ready", sync: { state: "saved", connection: "online", health: null, pending: 0 } } } } as unknown as SyncSnapshot); assert.deepEqual([synced.label, synced.syncing], ["Synced", true]); // The SDK keeps its last sync summary on the problem phase. const text = stoppedText({ key: "installation_scope_removed", title: "Access was removed", message: "Engine words." }); - const stopped = summarize({ ...base, direct: { text, phase: { kind: "problem", sync: { kind: "synced", pending: 0 }, problem: { key: "installation_scope_removed" } } } } as SyncSnapshot); + const stopped = summarize({ ...base, direct: { text, phase: { kind: "problem", sync: { state: "saved", connection: "online", health: null, pending: 0 }, problem: { key: "installation_scope_removed" } } } } as unknown as SyncSnapshot); assert.deepEqual([stopped.label, stopped.tone, stopped.syncing], ["Sync needs attention", "problem", true], "shown, not hidden with its old words"); assert.equal(stopped.detail, text); }); diff --git a/vendor/sdk-e8ffb806-production-beta188.tgz b/vendor/sdk-e8ffb806-production-beta188.tgz new file mode 100644 index 0000000..b4d999c Binary files /dev/null and b/vendor/sdk-e8ffb806-production-beta188.tgz differ diff --git a/vendor/sdk-production-beta188.json b/vendor/sdk-production-beta188.json new file mode 100644 index 0000000..7b3bc27 --- /dev/null +++ b/vendor/sdk-production-beta188.json @@ -0,0 +1,46 @@ +{ + "schema": "tnapp/production-sdk-original/1", + "artifact": "sdk-e8ffb806-production-beta188.tgz", + "version": "0.1.0-beta.188", + "sourceCommit": "e8ffb806d16c58fe03cf50c96fb66e29ebb612ba", + "environment": "production", + "bytes": 4819840, + "sha256": "9ac39582b5440272f7e95177ac4f4840aebd4c8ddf832b24545232abe99c6b27", + "sri": "sha512-EDaInyP8q2Lz0Akz8P+CNj5O6OvinMJWR1bE04fKtWRFDt9mVRBRBYRja5okOtb3gtnKYyMdQFGLlFln7F1RrA==", + "publication": { + "repository": "mdbase-dev/mdbase-connect", + "workflow": ".github/workflows/publish-sdk-npm.yml", + "ref": "refs/tags/v0.1.0-beta.188", + "npm": "@mdbase-dev/sdk@0.1.0-beta.188", + "integrity": "sha512-EDaInyP8q2Lz0Akz8P+CNj5O6OvinMJWR1bE04fKtWRFDt9mVRBRBYRja5okOtb3gtnKYyMdQFGLlFln7F1RrA==", + "tarball": "https://registry.npmjs.org/@mdbase-dev/sdk/-/sdk-0.1.0-beta.188.tgz", + "attestations": "https://registry.npmjs.org/-/npm/v1/attestations/@mdbase-dev%2fsdk@0.1.0-beta.188" + }, + "trust": { + "package": "@mdbase-dev/sdk", + "sha256": "7912e40838213e60b65848350d319a48019021317b112e3326429b250f0133d7", + "bytes": 840, + "cpOrigin": "https://connect.mdbase.dev", + "logOrigin": "https://log.mdbase.dev", + "sourceCommit": "36abd4b065be9495a7f5ae0808865a1f2279af04", + "sourceVersion": "0.1.0-beta.141" + }, + "runtime": { + "sha256": "7004351583c3a79411e644fafe9f5786816d32ea058605d1905fd50666012f40", + "bytes": 3875815, + "maxBytes": 3875815, + "source": { + "repository": "mdbase-dev/mdbase-connect", + "commit": "aae332572834fbfa065c0f696017c0de3540649d" + } + }, + "packagedTrustModuleSha256": "cf9690968d68b3c1eed2356ccfa2c72198b2f8cf07b9b24b1bb7be499b05bffb", + "bundledDependencies": [ + { + "name": "@mdbase-dev/connect-protocol", + "member": "package/vendor/mdbase-dev-connect-protocol-0.1.0-beta.145-e6e66bcd172f.tgz", + "bytes": 106404, + "sha256": "e901144b8d93dc96fcf71d9d8f85dddbf647e9908913be21191c484e9224bb98" + } + ] +} diff --git a/versions.json b/versions.json index 321f808..1caac80 100644 --- a/versions.json +++ b/versions.json @@ -22,5 +22,6 @@ "0.4.16": "1.11.4", "0.4.17": "1.11.4", "0.4.18": "1.11.4", - "0.4.19": "1.11.4" + "0.4.19": "1.11.4", + "0.4.20": "1.11.4" }