@operato/shell 10.29.38 → 10.29.40
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/CHANGELOG.md +17 -0
- package/coverage/lcov-report/index.html +47 -47
- package/coverage/lcov-report/test/fixtures/guarded-define.ts.html +1 -1
- package/coverage/lcov-report/test/fixtures/index.html +1 -1
- package/coverage/lcov-report/test/fixtures/no-registration.ts.html +1 -1
- package/coverage/lcov-report/test/fixtures/registers.ts.html +1 -1
- package/coverage/lcov-report/test/fixtures/unguarded-define.ts.html +1 -1
- package/coverage/lcov.info +1683 -966
- package/dist/src/actions/route.d.ts +1 -8
- package/dist/src/actions/route.js +9 -8
- package/dist/src/actions/route.js.map +1 -1
- package/dist/src/app/app.d.ts +3 -0
- package/dist/src/app/app.js +56 -34
- package/dist/src/app/app.js.map +1 -1
- package/dist/src/app/pages/page-view.d.ts +18 -6
- package/dist/src/app/pages/page-view.js +122 -7
- package/dist/src/app/pages/page-view.js.map +1 -1
- package/dist/src/drafts/draft-store.js +5 -3
- package/dist/src/drafts/draft-store.js.map +1 -1
- package/dist/src/drafts/unsaved-draft-controller.d.ts +9 -3
- package/dist/src/drafts/unsaved-draft-controller.js +77 -20
- package/dist/src/drafts/unsaved-draft-controller.js.map +1 -1
- package/dist/src/router.d.ts +2 -5
- package/dist/src/router.js +25 -22
- package/dist/src/router.js.map +1 -1
- package/dist/test/drafts.test.js +88 -4
- package/dist/test/drafts.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/drafts.test.ts +90 -4
|
@@ -57,9 +57,11 @@ function database() {
|
|
|
57
57
|
async function run(mode, work) {
|
|
58
58
|
const db = await database();
|
|
59
59
|
return new Promise((resolve, reject) => {
|
|
60
|
-
const
|
|
61
|
-
request
|
|
62
|
-
|
|
60
|
+
const transaction = db.transaction(STORE, mode);
|
|
61
|
+
const request = work(transaction.objectStore(STORE));
|
|
62
|
+
transaction.oncomplete = () => resolve(request.result);
|
|
63
|
+
transaction.onabort = () => reject(transaction.error || new Error('Draft transaction aborted'));
|
|
64
|
+
transaction.onerror = () => reject(transaction.error);
|
|
63
65
|
});
|
|
64
66
|
}
|
|
65
67
|
/** Whether this browser keeps drafts at all - false in a private window or with storage blocked. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"draft-store.js","sourceRoot":"","sources":["../../../src/drafts/draft-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA2BH,wEAAwE;AACxE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAExD,MAAM,UAAU,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAgB;IACrE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACvF,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAA;AACvC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAiC,EACjC,aAAqC,EACrC,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,IAAI,GAAG,GAAG,KAAK,CAAC,EAAE,GAAG,iBAAiB;QAAE,OAAO,MAAM,CAAA;IACrD,IAAI,aAAa,IAAI,IAAI,IAAI,aAAa,KAAK,KAAK,CAAC,IAAI;QAAE,OAAO,cAAc,CAAA;IAChF,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,8EAA8E;AAE9E,MAAM,OAAO,GAAG,gBAAgB,CAAA;AAChC,MAAM,KAAK,GAAG,QAAQ,CAAA;AAEtB,IAAI,MAAwC,CAAA;AAE5C,SAAS,QAAQ;IACf,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACvC,IAAI,OAAO,SAAS,KAAK,WAAW;gBAAE,OAAO,MAAM,CAAC,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC,CAAA;YAC9F,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;YAC1C,OAAO,CAAC,eAAe,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;YAC3F,OAAO,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;YACjD,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAC/C,CAAC,CAAC,CAAA;QACF,kEAAkE;QAClE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,KAAK,UAAU,GAAG,CAAI,IAAwB,EAAE,IAA8C;IAC5F,MAAM,EAAE,GAAG,MAAM,QAAQ,EAAE,CAAA;IAC3B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,
|
|
1
|
+
{"version":3,"file":"draft-store.js","sourceRoot":"","sources":["../../../src/drafts/draft-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Unsaved edits kept as drafts in this browser, so a reload, a closed tab or a crash does not lose them\n * (design/ui/unsaved-drafts.md).\n *\n * The browser lets a page ask only its own \"Reload site?\" box while it is being torn down, in its own words\n * and look. So nothing asks at that moment: the edit is already written here, and the screen offers it back\n * with the app's own dialog when it opens again.\n *\n * A database of its own. Adding a store to things-factory-database would need a version upgrade of a\n * database the service worker also opens.\n */\n\nexport interface DraftAddress {\n /** Who edited - a draft is never offered to someone else on the same browser. */\n user: string\n /** The site (domain) it was edited on. */\n domain: string\n /** The screen - 'figure-modeller', 'board-modeller', … */\n screen: string\n /** What was being edited - the figure's or board's id, or a name for something not saved yet. */\n target: string\n}\n\nexport interface Draft<T = unknown> {\n key: string\n /** The value the screen hands back to `restore`. */\n value: T\n /** When the draft was last written (ms). */\n at: number\n /**\n * The saved copy's revision the editing began from, or null for something never saved. It is handed\n * back with the value, and the save after restoring goes out against it - so if someone saved over it\n * in between, the save's own conflict check refuses, and the draft never overwrites that silently.\n */\n base: string | number | null\n}\n\n/** A draft older than this is not offered; it is dropped when found. */\nexport const DRAFT_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000\n\nexport function draftKey({ user, domain, screen, target }: DraftAddress): string {\n return [user, domain, screen, target].map(part => encodeURIComponent(part)).join('/')\n}\n\n/** The key prefix of every draft one user has - for clearing them at sign-out. */\nexport function draftKeyPrefixOf(user: string): string {\n return `${encodeURIComponent(user)}/`\n}\n\n/**\n * What to do with a draft found when a screen opens.\n *\n * - `drop`: too old to trust - remove it without asking.\n * - `offer`: offer it back.\n * - `offer-behind`: offer it back, and say the saved copy has been saved again since this draft began -\n * saving will report that conflict (the save goes out against the draft's base).\n *\n * `savedRevision` is the saved copy's revision now (null when there is none).\n */\nexport function draftDecision(\n draft: Pick<Draft, 'at' | 'base'>,\n savedRevision: string | number | null,\n now: number = Date.now()\n): 'drop' | 'offer' | 'offer-behind' {\n if (now - draft.at > DRAFT_LIFETIME_MS) return 'drop'\n if (savedRevision != null && savedRevision !== draft.base) return 'offer-behind'\n return 'offer'\n}\n\n/* ── storage ───────────────────────────────────────────────────────────── */\n\nconst DB_NAME = 'operato-drafts'\nconst STORE = 'drafts'\n\nlet opened: Promise<IDBDatabase> | undefined\n\nfunction database(): Promise<IDBDatabase> {\n if (!opened) {\n opened = new Promise((resolve, reject) => {\n if (typeof indexedDB === 'undefined') return reject(new Error('this browser keeps no drafts'))\n const request = indexedDB.open(DB_NAME, 1)\n request.onupgradeneeded = () => request.result.createObjectStore(STORE, { keyPath: 'key' })\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n /* A failed open is not remembered - the next call tries again. */\n opened.catch(() => (opened = undefined))\n }\n return opened\n}\n\nasync function run<R>(mode: IDBTransactionMode, work: (store: IDBObjectStore) => IDBRequest<R>): Promise<R> {\n const db = await database()\n return new Promise((resolve, reject) => {\n const transaction = db.transaction(STORE, mode)\n const request = work(transaction.objectStore(STORE))\n transaction.oncomplete = () => resolve(request.result)\n transaction.onabort = () => reject(transaction.error || new Error('Draft transaction aborted'))\n transaction.onerror = () => reject(transaction.error)\n })\n}\n\n/** Whether this browser keeps drafts at all - false in a private window or with storage blocked. */\nexport async function draftsKept(): Promise<boolean> {\n try {\n await database()\n return true\n } catch {\n return false\n }\n}\n\nexport function writeDraft<T>(draft: Draft<T>): Promise<unknown> {\n return run('readwrite', store => store.put(draft))\n}\n\nexport function readDraft<T>(key: string): Promise<Draft<T> | undefined> {\n return run('readonly', store => store.get(key) as IDBRequest<Draft<T> | undefined>)\n}\n\nexport function removeDraft(key: string): Promise<unknown> {\n return run('readwrite', store => store.delete(key))\n}\n\n/** Remove every draft of one user - at sign-out, so a shared computer keeps none of their edits. */\nexport function removeDraftsOf(user: string): Promise<unknown> {\n const prefix = draftKeyPrefixOf(user)\n return run('readwrite', store => store.delete(IDBKeyRange.bound(prefix, prefix + '')))\n}\n"]}
|
|
@@ -29,22 +29,28 @@ export declare class UnsavedDraftController<T> implements ReactiveController {
|
|
|
29
29
|
static WAIT_MS: number;
|
|
30
30
|
private timer?;
|
|
31
31
|
private cannotKeep;
|
|
32
|
+
private persisted?;
|
|
33
|
+
private writes;
|
|
34
|
+
private offerAbort?;
|
|
35
|
+
private connected;
|
|
36
|
+
private identity;
|
|
37
|
+
dispose(): void;
|
|
32
38
|
constructor(host: ReactiveControllerHost, source: UnsavedDraftSource<T>);
|
|
33
39
|
hostConnected(): void;
|
|
34
40
|
hostDisconnected(): void;
|
|
35
|
-
/** Whether
|
|
41
|
+
/** Whether the current edit, including its identity, has a completed durable write. */
|
|
36
42
|
get keeps(): boolean;
|
|
37
43
|
/** The edit changed. It is written after it rests for a moment. */
|
|
38
44
|
changed(): void;
|
|
39
45
|
/** Saved, or discarded on purpose: there is nothing to keep. */
|
|
40
46
|
settled(): Promise<void>;
|
|
41
|
-
/**
|
|
47
|
+
/** Capture before yielding: a query switch must never store its edit under the next query's key. */
|
|
42
48
|
flush(): Promise<void>;
|
|
43
49
|
/**
|
|
44
50
|
* Offer a kept draft back - call once the saved copy is loaded. Resolves true when the draft was restored.
|
|
45
51
|
* A draft too old to trust is dropped without asking.
|
|
46
52
|
*/
|
|
47
53
|
offer(): Promise<boolean>;
|
|
48
|
-
/** Only
|
|
54
|
+
/** Only an edit without a durable copy needs the browser's unload question. */
|
|
49
55
|
private holdIfNotKept;
|
|
50
56
|
}
|
|
@@ -13,33 +13,58 @@ import { draftDecision, draftKey, draftsKept, readDraft, removeDraft, writeDraft
|
|
|
13
13
|
export class UnsavedDraftController {
|
|
14
14
|
/** How long an edit rests before it is written. */
|
|
15
15
|
static { this.WAIT_MS = 1000; }
|
|
16
|
+
identity() {
|
|
17
|
+
const address = this.source.address();
|
|
18
|
+
const snapshot = this.source.snapshot();
|
|
19
|
+
return address && snapshot ? JSON.stringify([draftKey(address), snapshot]) : undefined;
|
|
20
|
+
}
|
|
21
|
+
dispose() {
|
|
22
|
+
this.hostDisconnected();
|
|
23
|
+
this.host.removeController(this);
|
|
24
|
+
}
|
|
16
25
|
constructor(host, source) {
|
|
17
26
|
this.host = host;
|
|
18
27
|
this.source = source;
|
|
19
28
|
this.cannotKeep = false;
|
|
20
|
-
|
|
29
|
+
this.writes = Promise.resolve();
|
|
30
|
+
this.connected = false;
|
|
31
|
+
/** Only an edit without a durable copy needs the browser's unload question. */
|
|
21
32
|
this.holdIfNotKept = (event) => {
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
try {
|
|
34
|
+
if (!this.source.snapshot() || this.keeps)
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* An unreadable edit is not safely kept. */
|
|
39
|
+
}
|
|
40
|
+
void this.flush();
|
|
24
41
|
event.preventDefault();
|
|
25
42
|
event.returnValue = '';
|
|
26
43
|
};
|
|
27
44
|
host.addController(this);
|
|
28
45
|
}
|
|
29
46
|
hostConnected() {
|
|
47
|
+
this.connected = true;
|
|
30
48
|
window.addEventListener('beforeunload', this.holdIfNotKept);
|
|
31
49
|
/* Known before the first edit, so a reload in that first second is not the one that loses it. */
|
|
32
50
|
draftsKept().then(kept => (this.cannotKeep = !kept));
|
|
33
51
|
}
|
|
34
52
|
hostDisconnected() {
|
|
53
|
+
this.connected = false;
|
|
54
|
+
this.offerAbort?.abort();
|
|
35
55
|
window.removeEventListener('beforeunload', this.holdIfNotKept);
|
|
36
56
|
/* Leaving the screen with a write still waiting: write it now, not never. */
|
|
37
57
|
if (this.timer)
|
|
38
58
|
this.flush();
|
|
39
59
|
}
|
|
40
|
-
/** Whether
|
|
60
|
+
/** Whether the current edit, including its identity, has a completed durable write. */
|
|
41
61
|
get keeps() {
|
|
42
|
-
|
|
62
|
+
try {
|
|
63
|
+
return (!this.cannotKeep && (!this.source.snapshot() || (!!this.source.address() && this.identity() === this.persisted)));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
43
68
|
}
|
|
44
69
|
/** The edit changed. It is written after it rests for a moment. */
|
|
45
70
|
changed() {
|
|
@@ -50,40 +75,64 @@ export class UnsavedDraftController {
|
|
|
50
75
|
async settled() {
|
|
51
76
|
clearTimeout(this.timer);
|
|
52
77
|
this.timer = undefined;
|
|
78
|
+
this.offerAbort?.abort();
|
|
79
|
+
this.persisted = undefined;
|
|
53
80
|
const address = this.source.address();
|
|
54
81
|
if (!address)
|
|
55
82
|
return;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
83
|
+
const key = draftKey(address);
|
|
84
|
+
this.writes = this.writes.then(async () => {
|
|
85
|
+
try {
|
|
86
|
+
await removeDraft(key);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
this.cannotKeep = true;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
await this.writes;
|
|
62
93
|
}
|
|
63
|
-
/**
|
|
94
|
+
/** Capture before yielding: a query switch must never store its edit under the next query's key. */
|
|
64
95
|
async flush() {
|
|
65
96
|
clearTimeout(this.timer);
|
|
66
97
|
this.timer = undefined;
|
|
67
98
|
const address = this.source.address();
|
|
68
99
|
if (!address)
|
|
69
100
|
return;
|
|
70
|
-
|
|
101
|
+
let snapshot;
|
|
71
102
|
try {
|
|
72
|
-
|
|
73
|
-
await removeDraft(draftKey(address));
|
|
74
|
-
else
|
|
75
|
-
await writeDraft({ key: draftKey(address), value: snapshot.value, base: snapshot.base, at: Date.now() });
|
|
76
|
-
this.cannotKeep = false;
|
|
103
|
+
snapshot = structuredClone(this.source.snapshot());
|
|
77
104
|
}
|
|
78
105
|
catch {
|
|
79
106
|
this.cannotKeep = true;
|
|
107
|
+
return;
|
|
80
108
|
}
|
|
109
|
+
const key = draftKey(address);
|
|
110
|
+
const identity = snapshot ? JSON.stringify([key, snapshot]) : undefined;
|
|
111
|
+
this.writes = this.writes.then(async () => {
|
|
112
|
+
try {
|
|
113
|
+
if (!snapshot)
|
|
114
|
+
await removeDraft(key);
|
|
115
|
+
else
|
|
116
|
+
await writeDraft({ key, value: snapshot.value, base: snapshot.base, at: Date.now() });
|
|
117
|
+
this.persisted = identity;
|
|
118
|
+
this.cannotKeep = false;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
this.cannotKeep = true;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
await this.writes;
|
|
81
125
|
}
|
|
82
126
|
/**
|
|
83
127
|
* Offer a kept draft back - call once the saved copy is loaded. Resolves true when the draft was restored.
|
|
84
128
|
* A draft too old to trust is dropped without asking.
|
|
85
129
|
*/
|
|
86
130
|
async offer() {
|
|
131
|
+
this.offerAbort?.abort();
|
|
132
|
+
const request = (this.offerAbort = new AbortController());
|
|
133
|
+
await this.writes;
|
|
134
|
+
if (request.signal.aborted)
|
|
135
|
+
return false;
|
|
87
136
|
const address = this.source.address();
|
|
88
137
|
if (!address)
|
|
89
138
|
return false;
|
|
@@ -96,7 +145,11 @@ export class UnsavedDraftController {
|
|
|
96
145
|
this.cannotKeep = true;
|
|
97
146
|
return false;
|
|
98
147
|
}
|
|
99
|
-
|
|
148
|
+
const currentAddress = () => {
|
|
149
|
+
const current = this.source.address();
|
|
150
|
+
return current && draftKey(current) === key;
|
|
151
|
+
};
|
|
152
|
+
if (!draft || request.signal.aborted || !currentAddress())
|
|
100
153
|
return false;
|
|
101
154
|
const decision = draftDecision(draft, this.source.savedRevision());
|
|
102
155
|
if (decision === 'drop') {
|
|
@@ -116,10 +169,14 @@ export class UnsavedDraftController {
|
|
|
116
169
|
title: t('title.unsaved-draft', 'there is an unsaved edit'),
|
|
117
170
|
text: lines.join(' '),
|
|
118
171
|
confirmButton: { text: t('button.resume-editing', 'resume editing') },
|
|
119
|
-
cancelButton: { text: t('button.discard', 'discard') }
|
|
172
|
+
cancelButton: { text: t('button.discard', 'discard') },
|
|
173
|
+
signal: request.signal
|
|
120
174
|
});
|
|
175
|
+
if (request.signal.aborted || !this.connected || !currentAddress())
|
|
176
|
+
return false;
|
|
121
177
|
if (resume) {
|
|
122
178
|
this.source.restore(draft.value, draft.base);
|
|
179
|
+
this.persisted = this.identity();
|
|
123
180
|
return true;
|
|
124
181
|
}
|
|
125
182
|
await removeDraft(key).catch(() => { });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unsaved-draft-controller.js","sourceRoot":"","sources":["../../../src/drafts/unsaved-draft-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup'\n\nimport { draftDecision, draftKey, draftsKept, readDraft, removeDraft, writeDraft, type DraftAddress } from './draft-store.js'\n\nexport interface UnsavedDraftSource<T> {\n /** Where the draft lives - who, which site, which screen, what is edited. Undefined until it is known. */\n address(): DraftAddress | undefined\n /** The unsaved model and the saved revision it began from; undefined when nothing is unsaved. */\n snapshot(): { value: T; base: string | number | null } | undefined\n /** The saved copy's revision now, or null when it was never saved. */\n savedRevision(): string | number | null\n /** Put a draft back on the canvas. The next save goes out against `base`, so a conflict is reported. */\n restore(value: T, base: string | number | null): void\n}\n\n/**\n * Keeps a screen's unsaved edit as a draft, and offers it back when the screen opens (design/ui/unsaved-drafts.md).\n *\n * The screen calls `changed()` when its edit changes, `settled()` when it saved or discarded, and `offer()` once\n * the saved copy is loaded. Nothing asks at reload - the edit is already written.\n *\n * Where a draft cannot be written (a private window, blocked storage), this screen falls back to the browser's\n * own leave question while something is unsaved: without either, the edit would be lost without a word.\n */\nexport class UnsavedDraftController<T> implements ReactiveController {\n /** How long an edit rests before it is written. */\n static WAIT_MS = 1000\n\n private timer?: ReturnType<typeof setTimeout>\n private cannotKeep = false\n\n constructor(\n private host: ReactiveControllerHost,\n private source: UnsavedDraftSource<T>\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n window.addEventListener('beforeunload', this.holdIfNotKept)\n /* Known before the first edit, so a reload in that first second is not the one that loses it. */\n draftsKept().then(kept => (this.cannotKeep = !kept))\n }\n\n hostDisconnected() {\n window.removeEventListener('beforeunload', this.holdIfNotKept)\n /* Leaving the screen with a write still waiting: write it now, not never. */\n if (this.timer) this.flush()\n }\n\n /** Whether this browser keeps drafts for this screen (false after a write failed). */\n get keeps() {\n return !this.cannotKeep\n }\n\n /** The edit changed. It is written after it rests for a moment. */\n changed() {\n clearTimeout(this.timer)\n this.timer = setTimeout(() => this.flush(), UnsavedDraftController.WAIT_MS)\n }\n\n /** Saved, or discarded on purpose: there is nothing to keep. */\n async settled() {\n clearTimeout(this.timer)\n this.timer = undefined\n const address = this.source.address()\n if (!address) return\n try {\n await removeDraft(draftKey(address))\n } catch {\n /* Nothing was kept here, so there is nothing to take away. */\n }\n }\n\n /** Write the current edit now. */\n async flush() {\n clearTimeout(this.timer)\n this.timer = undefined\n const address = this.source.address()\n if (!address) return\n\n const snapshot = this.source.snapshot()\n try {\n if (!snapshot) await removeDraft(draftKey(address))\n else await writeDraft({ key: draftKey(address), value: snapshot.value, base: snapshot.base, at: Date.now() })\n this.cannotKeep = false\n } catch {\n this.cannotKeep = true\n }\n }\n\n /**\n * Offer a kept draft back - call once the saved copy is loaded. Resolves true when the draft was restored.\n * A draft too old to trust is dropped without asking.\n */\n async offer(): Promise<boolean> {\n const address = this.source.address()\n if (!address) return false\n const key = draftKey(address)\n\n let draft\n try {\n draft = await readDraft<T>(key)\n } catch {\n this.cannotKeep = true\n return false\n }\n if (!draft) return false\n\n const decision = draftDecision(draft, this.source.savedRevision())\n if (decision === 'drop') {\n await removeDraft(key).catch(() => {})\n return false\n }\n\n const t = (name: string, fallback: string, params: Record<string, unknown> = {}) =>\n String(i18next.t(name, { defaultValue: fallback, ...params }))\n const minutes = Math.max(1, Math.round((Date.now() - draft.at) / 60000))\n const lines = [\n t('text.unsaved-draft-kept', 'what you changed {minutes} minutes ago is still kept in this browser.', { minutes })\n ]\n if (decision === 'offer-behind') {\n lines.push(\n t('text.unsaved-draft-behind', 'it has been saved again since then. saving will tell you about the conflict.')\n )\n }\n\n const resume = await OxPrompt.open({\n type: 'info',\n title: t('title.unsaved-draft', 'there is an unsaved edit'),\n text: lines.join(' '),\n confirmButton: { text: t('button.resume-editing', 'resume editing') },\n cancelButton: { text: t('button.discard', 'discard') }\n })\n\n if (resume) {\n this.source.restore(draft.value, draft.base)\n return true\n }\n await removeDraft(key).catch(() => {})\n return false\n }\n\n /** Only where drafts cannot be kept: the browser's own leave question, while something is unsaved. */\n private holdIfNotKept = (event: BeforeUnloadEvent) => {\n if (!this.cannotKeep || !this.source.snapshot()) return\n event.preventDefault()\n event.returnValue = ''\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"unsaved-draft-controller.js","sourceRoot":"","sources":["../../../src/drafts/unsaved-draft-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup'\n\nimport {\n draftDecision,\n draftKey,\n draftsKept,\n readDraft,\n removeDraft,\n writeDraft,\n type DraftAddress\n} from './draft-store.js'\n\nexport interface UnsavedDraftSource<T> {\n /** Where the draft lives - who, which site, which screen, what is edited. Undefined until it is known. */\n address(): DraftAddress | undefined\n /** The unsaved model and the saved revision it began from; undefined when nothing is unsaved. */\n snapshot(): { value: T; base: string | number | null } | undefined\n /** The saved copy's revision now, or null when it was never saved. */\n savedRevision(): string | number | null\n /** Put a draft back on the canvas. The next save goes out against `base`, so a conflict is reported. */\n restore(value: T, base: string | number | null): void\n}\n\n/**\n * Keeps a screen's unsaved edit as a draft, and offers it back when the screen opens (design/ui/unsaved-drafts.md).\n *\n * The screen calls `changed()` when its edit changes, `settled()` when it saved or discarded, and `offer()` once\n * the saved copy is loaded. Nothing asks at reload - the edit is already written.\n *\n * Where a draft cannot be written (a private window, blocked storage), this screen falls back to the browser's\n * own leave question while something is unsaved: without either, the edit would be lost without a word.\n */\nexport class UnsavedDraftController<T> implements ReactiveController {\n /** How long an edit rests before it is written. */\n static WAIT_MS = 1000\n\n private timer?: ReturnType<typeof setTimeout>\n private cannotKeep = false\n private persisted?: string\n private writes: Promise<void> = Promise.resolve()\n private offerAbort?: AbortController\n private connected = false\n\n private identity() {\n const address = this.source.address()\n const snapshot = this.source.snapshot()\n return address && snapshot ? JSON.stringify([draftKey(address), snapshot]) : undefined\n }\n\n dispose() {\n this.hostDisconnected()\n this.host.removeController(this)\n }\n\n constructor(\n private host: ReactiveControllerHost,\n private source: UnsavedDraftSource<T>\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n this.connected = true\n window.addEventListener('beforeunload', this.holdIfNotKept)\n /* Known before the first edit, so a reload in that first second is not the one that loses it. */\n draftsKept().then(kept => (this.cannotKeep = !kept))\n }\n\n hostDisconnected() {\n this.connected = false\n this.offerAbort?.abort()\n window.removeEventListener('beforeunload', this.holdIfNotKept)\n /* Leaving the screen with a write still waiting: write it now, not never. */\n if (this.timer) this.flush()\n }\n\n /** Whether the current edit, including its identity, has a completed durable write. */\n get keeps() {\n try {\n return (\n !this.cannotKeep && (!this.source.snapshot() || (!!this.source.address() && this.identity() === this.persisted))\n )\n } catch {\n return false\n }\n }\n\n /** The edit changed. It is written after it rests for a moment. */\n changed() {\n clearTimeout(this.timer)\n this.timer = setTimeout(() => this.flush(), UnsavedDraftController.WAIT_MS)\n }\n\n /** Saved, or discarded on purpose: there is nothing to keep. */\n async settled() {\n clearTimeout(this.timer)\n this.timer = undefined\n this.offerAbort?.abort()\n this.persisted = undefined\n const address = this.source.address()\n if (!address) return\n const key = draftKey(address)\n this.writes = this.writes.then(async () => {\n try {\n await removeDraft(key)\n } catch {\n this.cannotKeep = true\n }\n })\n await this.writes\n }\n\n /** Capture before yielding: a query switch must never store its edit under the next query's key. */\n async flush() {\n clearTimeout(this.timer)\n this.timer = undefined\n const address = this.source.address()\n if (!address) return\n let snapshot: ReturnType<UnsavedDraftSource<T>['snapshot']>\n try {\n snapshot = structuredClone(this.source.snapshot())\n } catch {\n this.cannotKeep = true\n return\n }\n const key = draftKey(address)\n const identity = snapshot ? JSON.stringify([key, snapshot]) : undefined\n this.writes = this.writes.then(async () => {\n try {\n if (!snapshot) await removeDraft(key)\n else await writeDraft({ key, value: snapshot.value, base: snapshot.base, at: Date.now() })\n this.persisted = identity\n this.cannotKeep = false\n } catch {\n this.cannotKeep = true\n }\n })\n await this.writes\n }\n\n /**\n * Offer a kept draft back - call once the saved copy is loaded. Resolves true when the draft was restored.\n * A draft too old to trust is dropped without asking.\n */\n async offer(): Promise<boolean> {\n this.offerAbort?.abort()\n const request = (this.offerAbort = new AbortController())\n await this.writes\n if (request.signal.aborted) return false\n const address = this.source.address()\n if (!address) return false\n const key = draftKey(address)\n\n let draft\n try {\n draft = await readDraft<T>(key)\n } catch {\n this.cannotKeep = true\n return false\n }\n const currentAddress = () => {\n const current = this.source.address()\n return current && draftKey(current) === key\n }\n if (!draft || request.signal.aborted || !currentAddress()) return false\n\n const decision = draftDecision(draft, this.source.savedRevision())\n if (decision === 'drop') {\n await removeDraft(key).catch(() => {})\n return false\n }\n\n const t = (name: string, fallback: string, params: Record<string, unknown> = {}) =>\n String(i18next.t(name, { defaultValue: fallback, ...params }))\n const minutes = Math.max(1, Math.round((Date.now() - draft.at) / 60000))\n const lines = [\n t('text.unsaved-draft-kept', 'what you changed {minutes} minutes ago is still kept in this browser.', { minutes })\n ]\n if (decision === 'offer-behind') {\n lines.push(\n t('text.unsaved-draft-behind', 'it has been saved again since then. saving will tell you about the conflict.')\n )\n }\n\n const resume = await OxPrompt.open({\n type: 'info',\n title: t('title.unsaved-draft', 'there is an unsaved edit'),\n text: lines.join(' '),\n confirmButton: { text: t('button.resume-editing', 'resume editing') },\n cancelButton: { text: t('button.discard', 'discard') },\n signal: request.signal\n })\n\n if (request.signal.aborted || !this.connected || !currentAddress()) return false\n if (resume) {\n this.source.restore(draft.value, draft.base)\n this.persisted = this.identity()\n return true\n }\n await removeDraft(key).catch(() => {})\n return false\n }\n\n /** Only an edit without a durable copy needs the browser's unload question. */\n private holdIfNotKept = (event: BeforeUnloadEvent) => {\n try {\n if (!this.source.snapshot() || this.keeps) return\n } catch {\n /* An unreadable edit is not safely kept. */\n }\n void this.flush()\n event.preventDefault()\n event.returnValue = ''\n }\n}\n"]}
|
package/dist/src/router.d.ts
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* and listens for popstate events. Replaces pwa-helpers/router.js.
|
|
4
|
-
*/
|
|
5
|
-
export declare const installRouter: (locationUpdatedCallback: (location: Location, event: Event | null) => void) => void;
|
|
1
|
+
/** Link requests and committed entry notifications share the same address contract. */
|
|
2
|
+
export declare const installRouter: (locationUpdatedCallback: (location: URL, event: Event | null) => void) => () => void;
|
package/dist/src/router.js
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* and listens for popstate events. Replaces pwa-helpers/router.js.
|
|
4
|
-
*/
|
|
1
|
+
import { addressController, writeAddress } from '@operato/utils/address.js';
|
|
2
|
+
/** Link requests and committed entry notifications share the same address contract. */
|
|
5
3
|
export const installRouter = (locationUpdatedCallback) => {
|
|
6
|
-
|
|
7
|
-
if (
|
|
4
|
+
const click = (event) => {
|
|
5
|
+
if (event.defaultPrevented ||
|
|
6
|
+
event.button !== 0 ||
|
|
7
|
+
event.metaKey ||
|
|
8
|
+
event.ctrlKey ||
|
|
9
|
+
event.shiftKey ||
|
|
10
|
+
event.altKey)
|
|
8
11
|
return;
|
|
9
|
-
const
|
|
10
|
-
|
|
12
|
+
const path = event.composedPath();
|
|
13
|
+
const anchor = path.find((node) => node instanceof HTMLAnchorElement);
|
|
14
|
+
if (!anchor || anchor.target || anchor.hasAttribute('download') || anchor.rel === 'external')
|
|
11
15
|
return;
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
16
|
+
const url = new URL(anchor.href, document.baseURI);
|
|
17
|
+
if (url.origin !== location.origin)
|
|
14
18
|
return;
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
if (href
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
locationUpdatedCallback(window.location, null);
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
const overlay = path.find(node => node.tagName === 'OX-FLOATING-OVERLAY');
|
|
21
|
+
if (url.href !== location.href)
|
|
22
|
+
void writeAddress(url, { fromOverlay: overlay?.name });
|
|
23
|
+
};
|
|
24
|
+
document.body.addEventListener('click', click);
|
|
25
|
+
const stop = addressController().subscribe(entry => locationUpdatedCallback(entry.url, new Event('address-change')));
|
|
26
|
+
return () => {
|
|
27
|
+
document.body.removeEventListener('click', click);
|
|
28
|
+
stop();
|
|
29
|
+
};
|
|
27
30
|
};
|
|
28
31
|
//# sourceMappingURL=router.js.map
|
package/dist/src/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/router.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE3E,uFAAuF;AACvF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,uBAAqE,EAAE,EAAE;IACrG,MAAM,KAAK,GAAG,CAAC,KAAiB,EAAE,EAAE;QAClC,IACE,KAAK,CAAC,gBAAgB;YACtB,KAAK,CAAC,MAAM,KAAK,CAAC;YAClB,KAAK,CAAC,OAAO;YACb,KAAK,CAAC,OAAO;YACb,KAAK,CAAC,QAAQ;YACd,KAAK,CAAC,MAAM;YAEZ,OAAM;QACR,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,EAAE,CAAA;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAA6B,EAAE,CAAC,IAAI,YAAY,iBAAiB,CAAC,CAAA;QAChG,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,GAAG,KAAK,UAAU;YAAE,OAAM;QACpG,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QAClD,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;YAAE,OAAM;QAC1C,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAE,IAAoB,CAAC,OAAO,KAAK,qBAAqB,CAC3C,CAAA;QAC9C,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI;YAAE,KAAK,YAAY,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IACxF,CAAC,CAAA;IACD,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAC9C,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAA;IACpH,OAAO,GAAG,EAAE;QACV,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QACjD,IAAI,EAAE,CAAA;IACR,CAAC,CAAA;AACH,CAAC,CAAA","sourcesContent":["import { addressController, writeAddress } from '@operato/utils/address.js'\n\n/** Link requests and committed entry notifications share the same address contract. */\nexport const installRouter = (locationUpdatedCallback: (location: URL, event: Event | null) => void) => {\n const click = (event: MouseEvent) => {\n if (\n event.defaultPrevented ||\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n )\n return\n const path = event.composedPath()\n const anchor = path.find((node): node is HTMLAnchorElement => node instanceof HTMLAnchorElement)\n if (!anchor || anchor.target || anchor.hasAttribute('download') || anchor.rel === 'external') return\n const url = new URL(anchor.href, document.baseURI)\n if (url.origin !== location.origin) return\n event.preventDefault()\n const overlay = path.find(node => (node as HTMLElement).tagName === 'OX-FLOATING-OVERLAY') as\n (HTMLElement & { name: string }) | undefined\n if (url.href !== location.href) void writeAddress(url, { fromOverlay: overlay?.name })\n }\n document.body.addEventListener('click', click)\n const stop = addressController().subscribe(entry => locationUpdatedCallback(entry.url, new Event('address-change')))\n return () => {\n document.body.removeEventListener('click', click)\n stop()\n }\n}\n"]}
|
package/dist/test/drafts.test.js
CHANGED
|
@@ -36,7 +36,7 @@ describe('draft decision', () => {
|
|
|
36
36
|
it('★ older than seven days is dropped without asking', () => {
|
|
37
37
|
expect(draftDecision({ at: now - DRAFT_LIFETIME_MS - 1, base: 7 }, 7, now)).to.equal('drop');
|
|
38
38
|
});
|
|
39
|
-
it(
|
|
39
|
+
it("a key never lets one user read another's draft, and survives odd names", () => {
|
|
40
40
|
expect(draftKey(at('x', 'u1'))).to.not.equal(draftKey(at('x', 'u2')));
|
|
41
41
|
expect(draftKey({ user: 'a/b', domain: 'd', screen: 's', target: 't' })).to.not.equal(draftKey({ user: 'a', domain: 'b/d', screen: 's', target: 't' }));
|
|
42
42
|
});
|
|
@@ -49,7 +49,7 @@ describe('draft store', () => {
|
|
|
49
49
|
expect(read?.value.parts).to.equal(3);
|
|
50
50
|
expect(read?.base).to.equal(7);
|
|
51
51
|
});
|
|
52
|
-
it(
|
|
52
|
+
it("★ sign-out takes one user's drafts, and only theirs", async () => {
|
|
53
53
|
await writeDraft({ key: draftKey(at('a', 'leaving')), value: 1, base: null, at: 1 });
|
|
54
54
|
await writeDraft({ key: draftKey(at('b', 'leaving')), value: 2, base: null, at: 1 });
|
|
55
55
|
await writeDraft({ key: draftKey(at('a', 'staying')), value: 3, base: null, at: 1 });
|
|
@@ -61,9 +61,15 @@ describe('draft store', () => {
|
|
|
61
61
|
});
|
|
62
62
|
describe('UnsavedDraftController', () => {
|
|
63
63
|
const originalOpen = OxPrompt.open;
|
|
64
|
-
|
|
64
|
+
const hosts = [];
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
;
|
|
67
|
+
OxPrompt.open = originalOpen;
|
|
68
|
+
hosts.splice(0).forEach(host => host.disconnect());
|
|
69
|
+
});
|
|
65
70
|
function screen(target, state) {
|
|
66
71
|
const host = new Host();
|
|
72
|
+
hosts.push(host);
|
|
67
73
|
const restored = [];
|
|
68
74
|
const controller = new UnsavedDraftController(host, {
|
|
69
75
|
address: () => at(target),
|
|
@@ -108,8 +114,9 @@ describe('UnsavedDraftController', () => {
|
|
|
108
114
|
await b.controller.settled();
|
|
109
115
|
expect(await readDraft(draftKey(at('saved')))).to.equal(undefined);
|
|
110
116
|
});
|
|
111
|
-
it(
|
|
117
|
+
it("★ where drafts cannot be kept, the browser's own question comes back while something is unsaved", async () => {
|
|
112
118
|
const { controller } = screen('fallback', { value: 1, base: null, saved: null });
|
|
119
|
+
await controller.flush();
|
|
113
120
|
controller.cannotKeep = true;
|
|
114
121
|
const event = new Event('beforeunload', { cancelable: true });
|
|
115
122
|
window.dispatchEvent(event);
|
|
@@ -119,5 +126,82 @@ describe('UnsavedDraftController', () => {
|
|
|
119
126
|
window.dispatchEvent(kept);
|
|
120
127
|
expect(kept.defaultPrevented).to.equal(false);
|
|
121
128
|
});
|
|
129
|
+
it('a changed edit is not reported durable before its transaction has committed', async () => {
|
|
130
|
+
const state = { value: 1, base: 7, saved: 7 };
|
|
131
|
+
const { controller, host } = screen('durable', state);
|
|
132
|
+
expect(controller.keeps).to.equal(false);
|
|
133
|
+
await controller.flush();
|
|
134
|
+
expect(controller.keeps).to.equal(true);
|
|
135
|
+
state.value = 2;
|
|
136
|
+
expect(controller.keeps).to.equal(false);
|
|
137
|
+
await controller.flush();
|
|
138
|
+
expect(controller.keeps).to.equal(true);
|
|
139
|
+
host.disconnect();
|
|
140
|
+
});
|
|
141
|
+
it('an edit captured before changing queries keeps its original key and value', async () => {
|
|
142
|
+
const host = new Host();
|
|
143
|
+
hosts.push(host);
|
|
144
|
+
let target = 'october';
|
|
145
|
+
let value = 10;
|
|
146
|
+
const controller = new UnsavedDraftController(host, {
|
|
147
|
+
address: () => at(target),
|
|
148
|
+
snapshot: () => ({ value, base: 1 }),
|
|
149
|
+
savedRevision: () => 1,
|
|
150
|
+
restore() { }
|
|
151
|
+
});
|
|
152
|
+
host.connect();
|
|
153
|
+
const storing = controller.flush();
|
|
154
|
+
target = 'november';
|
|
155
|
+
value = 11;
|
|
156
|
+
await storing;
|
|
157
|
+
expect((await readDraft(draftKey(at('october'))))?.value).to.equal(10);
|
|
158
|
+
expect(await readDraft(draftKey(at('november')))).to.equal(undefined);
|
|
159
|
+
expect(controller.keeps).to.equal(false);
|
|
160
|
+
host.disconnect();
|
|
161
|
+
});
|
|
162
|
+
it('disconnecting an owner cancels restoration even if its old question later accepts', async () => {
|
|
163
|
+
const first = screen('stale-offer', { value: 42, base: 5, saved: 5 });
|
|
164
|
+
await first.controller.flush();
|
|
165
|
+
let answer;
|
|
166
|
+
OxPrompt.open = () => new Promise(resolve => {
|
|
167
|
+
answer = resolve;
|
|
168
|
+
});
|
|
169
|
+
const second = screen('stale-offer', { base: 5, saved: 5 });
|
|
170
|
+
const offered = second.controller.offer();
|
|
171
|
+
while (!answer)
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
173
|
+
second.host.disconnect();
|
|
174
|
+
answer(true);
|
|
175
|
+
expect(await offered).to.equal(false);
|
|
176
|
+
expect(second.restored).to.deep.equal([]);
|
|
177
|
+
expect(await readDraft(draftKey(at('stale-offer')))).not.to.equal(undefined);
|
|
178
|
+
first.host.disconnect();
|
|
179
|
+
});
|
|
180
|
+
it('a signed-out owner cannot restore or delete its old draft from a late answer', async () => {
|
|
181
|
+
const host = new Host();
|
|
182
|
+
hosts.push(host);
|
|
183
|
+
let signedIn = true;
|
|
184
|
+
const restored = [];
|
|
185
|
+
const controller = new UnsavedDraftController(host, {
|
|
186
|
+
address: () => (signedIn ? at('signed-out') : undefined),
|
|
187
|
+
snapshot: () => ({ value: 42, base: 5 }),
|
|
188
|
+
savedRevision: () => 5,
|
|
189
|
+
restore: value => restored.push(value)
|
|
190
|
+
});
|
|
191
|
+
host.connect();
|
|
192
|
+
await controller.flush();
|
|
193
|
+
let answer;
|
|
194
|
+
OxPrompt.open = () => new Promise(resolve => {
|
|
195
|
+
answer = resolve;
|
|
196
|
+
});
|
|
197
|
+
const offered = controller.offer();
|
|
198
|
+
while (!answer)
|
|
199
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
200
|
+
signedIn = false;
|
|
201
|
+
answer(true);
|
|
202
|
+
expect(await offered).to.equal(false);
|
|
203
|
+
expect(restored).to.deep.equal([]);
|
|
204
|
+
host.disconnect();
|
|
205
|
+
});
|
|
122
206
|
});
|
|
123
207
|
//# sourceMappingURL=drafts.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drafts.test.js","sourceRoot":"","sources":["../../test/drafts.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing'\nimport { OxPrompt } from '@operato/popup'\n\nimport {\n draftDecision,\n draftKey,\n readDraft,\n removeDraftsOf,\n writeDraft,\n DRAFT_LIFETIME_MS,\n UnsavedDraftController,\n type DraftAddress\n} from '../src/index.js'\n\n/**\n * Unsaved edits kept as drafts (design/ui/unsaved-drafts.md, architect ruling 2026-09-27): the edit is\n * written as it happens and offered back when the screen opens, so nothing has to ask at reload.\n */\n\nconst at = (target: string, user = 'u1'): DraftAddress => ({ user, domain: 'd', screen: 'test-modeller', target })\n\nclass Host {\n controllers: any[] = []\n addController(c: any) {\n this.controllers.push(c)\n }\n removeController() {}\n requestUpdate() {}\n updateComplete = Promise.resolve(true)\n connect() {\n this.controllers.forEach(c => c.hostConnected?.())\n }\n disconnect() {\n this.controllers.forEach(c => c.hostDisconnected?.())\n }\n}\n\ndescribe('draft decision', () => {\n const now = 1_000_000_000_000\n\n it('★ a draft whose base is the saved copy now is offered', () => {\n expect(draftDecision({ at: now - 60_000, base: 7 }, 7, now)).to.equal('offer')\n expect(draftDecision({ at: now - 60_000, base: null }, null, now)).to.equal('offer')\n })\n\n it('★ saved again since the draft began: offered, and the save will report the conflict', () => {\n expect(draftDecision({ at: now - 60_000, base: 7 }, 8, now)).to.equal('offer-behind')\n expect(draftDecision({ at: now - 60_000, base: null }, 1, now)).to.equal('offer-behind')\n })\n\n it('★ older than seven days is dropped without asking', () => {\n expect(draftDecision({ at: now - DRAFT_LIFETIME_MS - 1, base: 7 }, 7, now)).to.equal('drop')\n })\n\n it('a key never lets one user read another\\'s draft, and survives odd names', () => {\n expect(draftKey(at('x', 'u1'))).to.not.equal(draftKey(at('x', 'u2')))\n expect(draftKey({ user: 'a/b', domain: 'd', screen: 's', target: 't' })).to.not.equal(\n draftKey({ user: 'a', domain: 'b/d', screen: 's', target: 't' })\n )\n })\n})\n\ndescribe('draft store', () => {\n it('★ what is written is read back', async () => {\n const key = draftKey(at('round-trip'))\n await writeDraft({ key, value: { parts: 3 }, base: 7, at: 123 })\n const read = await readDraft<{ parts: number }>(key)\n expect(read?.value.parts).to.equal(3)\n expect(read?.base).to.equal(7)\n })\n\n it('★ sign-out takes one user\\'s drafts, and only theirs', async () => {\n await writeDraft({ key: draftKey(at('a', 'leaving')), value: 1, base: null, at: 1 })\n await writeDraft({ key: draftKey(at('b', 'leaving')), value: 2, base: null, at: 1 })\n await writeDraft({ key: draftKey(at('a', 'staying')), value: 3, base: null, at: 1 })\n\n await removeDraftsOf('leaving')\n\n expect(await readDraft(draftKey(at('a', 'leaving')))).to.equal(undefined)\n expect(await readDraft(draftKey(at('b', 'leaving')))).to.equal(undefined)\n expect((await readDraft(draftKey(at('a', 'staying'))))?.value).to.equal(3)\n })\n})\n\ndescribe('UnsavedDraftController', () => {\n const originalOpen = OxPrompt.open\n afterEach(() => ((OxPrompt as any).open = originalOpen))\n\n function screen(target: string, state: { value?: number; base: number | null; saved: number | null }) {\n const host = new Host()\n const restored: any[] = []\n const controller = new UnsavedDraftController<number>(host as any, {\n address: () => at(target),\n snapshot: () => (state.value == null ? undefined : { value: state.value, base: state.base }),\n savedRevision: () => state.saved,\n restore: (value, base) => restored.push({ value, base })\n })\n host.connect()\n return { host, controller, restored }\n }\n\n it('★ an edit is written; reopening offers it and puts it back with its base', async () => {\n const first = screen('reopen', { value: 42, base: 5, saved: 5 })\n first.controller.changed()\n await first.controller.flush()\n first.host.disconnect()\n\n let asked = ''\n ;(OxPrompt as any).open = async (options: any) => ((asked = options.text), true)\n const again = screen('reopen', { base: 5, saved: 5 })\n expect(await again.controller.offer()).to.equal(true)\n expect(again.restored).to.deep.equal([{ value: 42, base: 5 }])\n expect(asked).to.not.contain('conflict')\n })\n\n it('★ saved again elsewhere: the question says the save will report the conflict', async () => {\n const first = screen('behind', { value: 1, base: 5, saved: 5 })\n await first.controller.flush()\n\n let asked = ''\n ;(OxPrompt as any).open = async (options: any) => ((asked = options.text), true)\n const again = screen('behind', { base: 6, saved: 6 })\n await again.controller.offer()\n expect(asked).to.contain('conflict')\n expect(again.restored[0].base).to.equal(5)\n })\n\n it('★ discard removes it; saving removes it', async () => {\n ;(OxPrompt as any).open = async () => false\n const a = screen('discard', { value: 1, base: null, saved: null })\n await a.controller.flush()\n expect(await a.controller.offer()).to.equal(false)\n expect(await readDraft(draftKey(at('discard')))).to.equal(undefined)\n\n const b = screen('saved', { value: 1, base: null, saved: null })\n await b.controller.flush()\n await b.controller.settled()\n expect(await readDraft(draftKey(at('saved')))).to.equal(undefined)\n })\n\n it('★ where drafts cannot be kept, the browser\\'s own question comes back while something is unsaved', async () => {\n const { controller } = screen('fallback', { value: 1, base: null, saved: null })\n ;(controller as any).cannotKeep = true\n const event = new Event('beforeunload', { cancelable: true })\n window.dispatchEvent(event)\n expect(event.defaultPrevented).to.equal(true)\n\n ;(controller as any).cannotKeep = false\n const kept = new Event('beforeunload', { cancelable: true })\n window.dispatchEvent(kept)\n expect(kept.defaultPrevented).to.equal(false)\n })\n})\n"]}
|
|
1
|
+
{"version":3,"file":"drafts.test.js","sourceRoot":"","sources":["../../test/drafts.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing'\nimport { OxPrompt } from '@operato/popup'\n\nimport {\n draftDecision,\n draftKey,\n readDraft,\n removeDraftsOf,\n writeDraft,\n DRAFT_LIFETIME_MS,\n UnsavedDraftController,\n type DraftAddress\n} from '../src/index.js'\n\n/**\n * Unsaved edits kept as drafts (design/ui/unsaved-drafts.md, architect ruling 2026-09-27): the edit is\n * written as it happens and offered back when the screen opens, so nothing has to ask at reload.\n */\n\nconst at = (target: string, user = 'u1'): DraftAddress => ({ user, domain: 'd', screen: 'test-modeller', target })\n\nclass Host {\n controllers: any[] = []\n addController(c: any) {\n this.controllers.push(c)\n }\n removeController() {}\n requestUpdate() {}\n updateComplete = Promise.resolve(true)\n connect() {\n this.controllers.forEach(c => c.hostConnected?.())\n }\n disconnect() {\n this.controllers.forEach(c => c.hostDisconnected?.())\n }\n}\n\ndescribe('draft decision', () => {\n const now = 1_000_000_000_000\n\n it('★ a draft whose base is the saved copy now is offered', () => {\n expect(draftDecision({ at: now - 60_000, base: 7 }, 7, now)).to.equal('offer')\n expect(draftDecision({ at: now - 60_000, base: null }, null, now)).to.equal('offer')\n })\n\n it('★ saved again since the draft began: offered, and the save will report the conflict', () => {\n expect(draftDecision({ at: now - 60_000, base: 7 }, 8, now)).to.equal('offer-behind')\n expect(draftDecision({ at: now - 60_000, base: null }, 1, now)).to.equal('offer-behind')\n })\n\n it('★ older than seven days is dropped without asking', () => {\n expect(draftDecision({ at: now - DRAFT_LIFETIME_MS - 1, base: 7 }, 7, now)).to.equal('drop')\n })\n\n it(\"a key never lets one user read another's draft, and survives odd names\", () => {\n expect(draftKey(at('x', 'u1'))).to.not.equal(draftKey(at('x', 'u2')))\n expect(draftKey({ user: 'a/b', domain: 'd', screen: 's', target: 't' })).to.not.equal(\n draftKey({ user: 'a', domain: 'b/d', screen: 's', target: 't' })\n )\n })\n})\n\ndescribe('draft store', () => {\n it('★ what is written is read back', async () => {\n const key = draftKey(at('round-trip'))\n await writeDraft({ key, value: { parts: 3 }, base: 7, at: 123 })\n const read = await readDraft<{ parts: number }>(key)\n expect(read?.value.parts).to.equal(3)\n expect(read?.base).to.equal(7)\n })\n\n it(\"★ sign-out takes one user's drafts, and only theirs\", async () => {\n await writeDraft({ key: draftKey(at('a', 'leaving')), value: 1, base: null, at: 1 })\n await writeDraft({ key: draftKey(at('b', 'leaving')), value: 2, base: null, at: 1 })\n await writeDraft({ key: draftKey(at('a', 'staying')), value: 3, base: null, at: 1 })\n\n await removeDraftsOf('leaving')\n\n expect(await readDraft(draftKey(at('a', 'leaving')))).to.equal(undefined)\n expect(await readDraft(draftKey(at('b', 'leaving')))).to.equal(undefined)\n expect((await readDraft(draftKey(at('a', 'staying'))))?.value).to.equal(3)\n })\n})\n\ndescribe('UnsavedDraftController', () => {\n const originalOpen = OxPrompt.open\n const hosts: Host[] = []\n afterEach(() => {\n ;(OxPrompt as any).open = originalOpen\n hosts.splice(0).forEach(host => host.disconnect())\n })\n\n function screen(target: string, state: { value?: number; base: number | null; saved: number | null }) {\n const host = new Host()\n hosts.push(host)\n const restored: any[] = []\n const controller = new UnsavedDraftController<number>(host as any, {\n address: () => at(target),\n snapshot: () => (state.value == null ? undefined : { value: state.value, base: state.base }),\n savedRevision: () => state.saved,\n restore: (value, base) => restored.push({ value, base })\n })\n host.connect()\n return { host, controller, restored }\n }\n\n it('★ an edit is written; reopening offers it and puts it back with its base', async () => {\n const first = screen('reopen', { value: 42, base: 5, saved: 5 })\n first.controller.changed()\n await first.controller.flush()\n first.host.disconnect()\n\n let asked = ''\n ;(OxPrompt as any).open = async (options: any) => ((asked = options.text), true)\n const again = screen('reopen', { base: 5, saved: 5 })\n expect(await again.controller.offer()).to.equal(true)\n expect(again.restored).to.deep.equal([{ value: 42, base: 5 }])\n expect(asked).to.not.contain('conflict')\n })\n\n it('★ saved again elsewhere: the question says the save will report the conflict', async () => {\n const first = screen('behind', { value: 1, base: 5, saved: 5 })\n await first.controller.flush()\n\n let asked = ''\n ;(OxPrompt as any).open = async (options: any) => ((asked = options.text), true)\n const again = screen('behind', { base: 6, saved: 6 })\n await again.controller.offer()\n expect(asked).to.contain('conflict')\n expect(again.restored[0].base).to.equal(5)\n })\n\n it('★ discard removes it; saving removes it', async () => {\n ;(OxPrompt as any).open = async () => false\n const a = screen('discard', { value: 1, base: null, saved: null })\n await a.controller.flush()\n expect(await a.controller.offer()).to.equal(false)\n expect(await readDraft(draftKey(at('discard')))).to.equal(undefined)\n\n const b = screen('saved', { value: 1, base: null, saved: null })\n await b.controller.flush()\n await b.controller.settled()\n expect(await readDraft(draftKey(at('saved')))).to.equal(undefined)\n })\n\n it(\"★ where drafts cannot be kept, the browser's own question comes back while something is unsaved\", async () => {\n const { controller } = screen('fallback', { value: 1, base: null, saved: null })\n await controller.flush()\n ;(controller as any).cannotKeep = true\n const event = new Event('beforeunload', { cancelable: true })\n window.dispatchEvent(event)\n expect(event.defaultPrevented).to.equal(true)\n\n ;(controller as any).cannotKeep = false\n const kept = new Event('beforeunload', { cancelable: true })\n window.dispatchEvent(kept)\n expect(kept.defaultPrevented).to.equal(false)\n })\n it('a changed edit is not reported durable before its transaction has committed', async () => {\n const state = { value: 1, base: 7, saved: 7 }\n const { controller, host } = screen('durable', state)\n expect(controller.keeps).to.equal(false)\n await controller.flush()\n expect(controller.keeps).to.equal(true)\n state.value = 2\n expect(controller.keeps).to.equal(false)\n await controller.flush()\n expect(controller.keeps).to.equal(true)\n host.disconnect()\n })\n\n it('an edit captured before changing queries keeps its original key and value', async () => {\n const host = new Host()\n hosts.push(host)\n let target = 'october'\n let value = 10\n const controller = new UnsavedDraftController(host as any, {\n address: () => at(target),\n snapshot: () => ({ value, base: 1 }),\n savedRevision: () => 1,\n restore() {}\n })\n host.connect()\n const storing = controller.flush()\n target = 'november'\n value = 11\n await storing\n expect((await readDraft<number>(draftKey(at('october'))))?.value).to.equal(10)\n expect(await readDraft(draftKey(at('november')))).to.equal(undefined)\n expect(controller.keeps).to.equal(false)\n host.disconnect()\n })\n\n it('disconnecting an owner cancels restoration even if its old question later accepts', async () => {\n const first = screen('stale-offer', { value: 42, base: 5, saved: 5 })\n await first.controller.flush()\n let answer!: (accepted: boolean) => void\n ;(OxPrompt as any).open = () =>\n new Promise(resolve => {\n answer = resolve\n })\n const second = screen('stale-offer', { base: 5, saved: 5 })\n const offered = second.controller.offer()\n while (!answer) await new Promise(resolve => setTimeout(resolve, 0))\n second.host.disconnect()\n answer(true)\n expect(await offered).to.equal(false)\n expect(second.restored).to.deep.equal([])\n expect(await readDraft(draftKey(at('stale-offer')))).not.to.equal(undefined)\n first.host.disconnect()\n })\n\n it('a signed-out owner cannot restore or delete its old draft from a late answer', async () => {\n const host = new Host()\n hosts.push(host)\n let signedIn = true\n const restored: number[] = []\n const controller = new UnsavedDraftController(host as any, {\n address: () => (signedIn ? at('signed-out') : undefined),\n snapshot: () => ({ value: 42, base: 5 }),\n savedRevision: () => 5,\n restore: value => restored.push(value)\n })\n host.connect()\n await controller.flush()\n let answer!: (accepted: boolean) => void\n ;(OxPrompt as any).open = () =>\n new Promise(resolve => {\n answer = resolve\n })\n const offered = controller.offer()\n while (!answer) await new Promise(resolve => setTimeout(resolve, 0))\n signedIn = false\n answer(true)\n expect(await offered).to.equal(false)\n expect(restored).to.deep.equal([])\n host.disconnect()\n })\n})\n"]}
|