dsh-offwork 1.1.1 → 1.2.2
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 +39 -0
- package/README.md +48 -26
- package/ROADMAP.md +70 -0
- package/client.js +5066 -1099
- package/client.source.js +1988 -541
- package/companion-app.js +1431 -0
- package/companion-image.js +59 -0
- package/companion-state.js +424 -0
- package/companion.js +6 -9
- package/countdown.js +83 -0
- package/docs/RELEASE-1.2.2.md +20 -0
- package/hydration-store.js +55 -16
- package/hydration.js +231 -250
- package/icon.svg +28 -1
- package/index.js +59 -6
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/media-manifest.js +29 -0
- package/media-server.js +123 -0
- package/package.json +13 -3
- package/pet-interactions.js +121 -17
- package/pet-motion.js +2 -1
- package/pet-reminder.js +20 -0
- package/pet-surfaces.js +130 -6
- package/pet-video.js +221 -72
- package/pet-view.js +71 -4
- package/settings-api.js +126 -0
- package/tests/classic-overview.test.mjs +77 -0
- package/tests/companion-state.test.mjs +267 -0
- package/tests/companion.test.mjs +12 -12
- package/tests/countdown.test.mjs +115 -0
- package/tests/host.test.mjs +26 -13
- package/tests/hydration-extras.test.mjs +168 -204
- package/tests/hydration-store.test.mjs +199 -213
- package/tests/hydration.test.mjs +116 -186
- package/tests/media-server.test.mjs +179 -0
- package/tests/pet-interactions.test.mjs +19 -0
- package/tests/pet-reminder.test.mjs +71 -0
- package/tests/pet-video.test.mjs +171 -5
- package/tests/settings-api.test.mjs +130 -0
- package/tests/ui.test.mjs +4 -1
- package/tools/build.mjs +45 -16
- package/tools/check-package.mjs +12 -0
- package/ui-utils.js +44 -0
|
@@ -2,286 +2,272 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { IDBFactory } from 'fake-indexeddb';
|
|
4
4
|
import { createHydrationStore } from '../hydration-store.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
5
|
+
import { defaultHydration, hydrationView, transitionHydration } from '../hydration.js';
|
|
6
|
+
const name = 'dsh.offwork-clock.hydration.v1',
|
|
7
|
+
begin = Date.parse('2026-11-06T20:00:00+08:00'),
|
|
8
|
+
due = begin + 3600000;
|
|
9
|
+
const read = (store) => store.update((s) => s);
|
|
10
|
+
const raw = (idb, storeName, value, version) =>
|
|
11
|
+
new Promise((resolve, reject) => {
|
|
12
|
+
const r = version ? idb.open(name, version) : idb.open(name);
|
|
13
|
+
r.onupgradeneeded = () => {
|
|
14
|
+
if (!r.result.objectStoreNames.contains(storeName)) r.result.createObjectStore(storeName);
|
|
15
|
+
};
|
|
16
|
+
r.onerror = () => reject(r.error);
|
|
17
|
+
r.onsuccess = () => {
|
|
18
|
+
const db = r.result,
|
|
19
|
+
tx = db.transaction(storeName, value === undefined ? 'readonly' : 'readwrite'),
|
|
20
|
+
s = tx.objectStore(storeName);
|
|
21
|
+
let result;
|
|
22
|
+
if (value !== undefined) s.put(value, 'current');
|
|
23
|
+
else {
|
|
24
|
+
const q = s.get('current');
|
|
25
|
+
q.onsuccess = () => (result = q.result);
|
|
26
|
+
}
|
|
27
|
+
tx.oncomplete = () => {
|
|
28
|
+
db.close();
|
|
29
|
+
resolve(result);
|
|
30
|
+
};
|
|
31
|
+
tx.onabort = () => {
|
|
32
|
+
db.close();
|
|
33
|
+
reject(tx.error);
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
const legacy = (schema = 2) => ({
|
|
38
|
+
schema,
|
|
39
|
+
revision: 40,
|
|
40
|
+
enabled: true,
|
|
41
|
+
interval: 5,
|
|
42
|
+
shift: {
|
|
43
|
+
id: 'retained-shift',
|
|
44
|
+
schedule: 'retained-schedule',
|
|
45
|
+
config: 'retained-config',
|
|
46
|
+
count: 9,
|
|
47
|
+
paused: true,
|
|
48
|
+
due: 300000,
|
|
49
|
+
sequence: 4,
|
|
50
|
+
claim: null,
|
|
51
|
+
...(schema === 2 ? { quietUntil: begin + 1800000, recentActions: ['old-cup'] } : {}),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
31
54
|
const fixture = (t) => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
const idb = new IDBFactory(),
|
|
56
|
+
first = createHydrationStore(idb, name, () => begin),
|
|
57
|
+
second = createHydrationStore(idb, name, () => begin);
|
|
35
58
|
t.after(() => {
|
|
36
59
|
first.close();
|
|
37
60
|
second.close();
|
|
38
61
|
});
|
|
39
|
-
return {
|
|
62
|
+
return { idb, first, second };
|
|
40
63
|
};
|
|
41
64
|
const seed = (store) =>
|
|
42
|
-
store.update((
|
|
43
|
-
transitionHydration(
|
|
44
|
-
state,
|
|
45
|
-
context(begin),
|
|
46
|
-
{ type: 'preferences', enabled: true, interval: 60 },
|
|
47
|
-
begin,
|
|
48
|
-
'a',
|
|
49
|
-
),
|
|
65
|
+
store.update((s) =>
|
|
66
|
+
transitionHydration(s, { type: 'preferences', enabled: true, interval: 60 }, begin, 'a'),
|
|
50
67
|
);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const request = indexedDB.open('dsh.offwork-clock.hydration.v1', 1);
|
|
54
|
-
request.onsuccess = () => {
|
|
55
|
-
const db = request.result;
|
|
56
|
-
const transaction = db.transaction('state', 'readwrite');
|
|
57
|
-
transaction.objectStore('state').put(state, 'current');
|
|
58
|
-
transaction.oncomplete = () => {
|
|
59
|
-
db.close();
|
|
60
|
-
resolve();
|
|
61
|
-
};
|
|
62
|
-
transaction.onabort = () => {
|
|
63
|
-
db.close();
|
|
64
|
-
reject(transaction.error);
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
request.onerror = () => reject(request.error);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('read-only first load does not create a default record; committed state survives reopening', async (t) => {
|
|
71
|
-
const { indexedDB, first } = fixture(t);
|
|
68
|
+
test('fresh reads do not write a default record and committed deadlines survive reopening', async (t) => {
|
|
69
|
+
const { idb, first } = fixture(t);
|
|
72
70
|
assert.deepEqual(await read(first), defaultHydration());
|
|
71
|
+
assert.equal(await raw(idb, 'wall-clock'), undefined);
|
|
73
72
|
const state = await seed(first);
|
|
74
73
|
first.close();
|
|
75
|
-
const reopened = createHydrationStore(
|
|
74
|
+
const reopened = createHydrationStore(idb);
|
|
76
75
|
t.after(() => reopened.close());
|
|
77
76
|
assert.deepEqual(await read(reopened), state);
|
|
78
|
-
assert.ok(state.revision > 0);
|
|
79
77
|
});
|
|
80
|
-
|
|
81
|
-
test('transactions serialize competing claims and duplicate processing across connections', async (t) => {
|
|
78
|
+
test('competing leases and repeated reminder acknowledgments serialize across connections', async (t) => {
|
|
82
79
|
const { first, second } = fixture(t);
|
|
83
80
|
await seed(first);
|
|
84
|
-
await Promise.all([
|
|
85
|
-
first.update((s) => transitionHydration(s, context(due), { type: 'claim' }, due, 'a')),
|
|
86
|
-
second.update((s) => transitionHydration(s, context(due), { type: 'claim' }, due, 'b')),
|
|
87
|
-
]);
|
|
88
|
-
const claimed = await read(first);
|
|
89
|
-
const owner = claimed.shift.claim.owner;
|
|
90
|
-
const eventId = hydrationView(claimed, context(due)).eventId;
|
|
91
81
|
await Promise.all(
|
|
92
|
-
[first, second].map((
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
82
|
+
[first, second].map((s, i) =>
|
|
83
|
+
s.update((v) => transitionHydration(v, { type: 'claim' }, due, i ? 'b' : 'a')),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
const claimed = await read(first),
|
|
87
|
+
eventId = hydrationView(claimed, due).eventId;
|
|
88
|
+
await Promise.all(
|
|
89
|
+
[first, second].map((s) =>
|
|
90
|
+
s.update((v) => transitionHydration(v, { type: 'drink', eventId }, due, claimed.claim.owner)),
|
|
96
91
|
),
|
|
97
92
|
);
|
|
98
|
-
assert.equal((await read(first)).
|
|
93
|
+
assert.equal((await read(first)).count, 1);
|
|
99
94
|
assert.deepEqual(await read(first), await read(second));
|
|
100
95
|
});
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
test('concurrent manual records deduplicate the same ID and retain independent IDs without Host settings', async (t) => {
|
|
97
|
+
const { first, second } = fixture(t);
|
|
98
|
+
const record = (store, id) =>
|
|
99
|
+
store.update((s) => transitionHydration(s, { type: 'manual-drink', actionId: id }, begin, 'a'));
|
|
100
|
+
await Promise.all([record(first, 'same'), record(second, 'same')]);
|
|
101
|
+
assert.equal((await read(first)).count, 1);
|
|
102
|
+
await Promise.all([record(first, 'one'), record(second, 'two')]);
|
|
103
|
+
assert.equal((await read(first)).count, 3);
|
|
104
|
+
});
|
|
105
|
+
test('foreign release does not erase ownership, while the owner can release without a schedule', async (t) => {
|
|
103
106
|
const { first, second } = fixture(t);
|
|
104
107
|
await seed(first);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{ type: 'tick', baseSchedule: initial.shift.schedule },
|
|
111
|
-
due,
|
|
112
|
-
'a',
|
|
113
|
-
),
|
|
114
|
-
);
|
|
115
|
-
assert.notEqual(current.shift.schedule, initial.shift.schedule);
|
|
116
|
-
for (const type of ['tick', 'claim', 'release', 'pause', 'resume', 'clear-count']) {
|
|
117
|
-
await second.update((s) => transitionHydration(s, context(due), { type }, due, 'b'));
|
|
118
|
-
assert.deepEqual(await read(first), current, type);
|
|
119
|
-
}
|
|
120
|
-
// Even a newly opened stale tab cannot revert a timezone change that changes the shift id.
|
|
121
|
-
const timezone = await first.update((s) =>
|
|
122
|
-
transitionHydration(
|
|
123
|
-
s,
|
|
124
|
-
context(due, { timezone: 'UTC', end: '19:00' }),
|
|
125
|
-
{ type: 'tick', baseSchedule: current.shift.schedule },
|
|
126
|
-
due,
|
|
127
|
-
'a',
|
|
128
|
-
),
|
|
129
|
-
);
|
|
130
|
-
await second.update((s) => transitionHydration(s, context(due), { type: 'tick' }, due, 'b'));
|
|
131
|
-
assert.deepEqual(await read(first), timezone);
|
|
108
|
+
const claimed = await first.update((s) => transitionHydration(s, { type: 'claim' }, due, 'a'));
|
|
109
|
+
await second.update((s) => transitionHydration(s, { type: 'release' }, due, 'b'));
|
|
110
|
+
assert.deepEqual(await read(first), claimed);
|
|
111
|
+
await first.update((s) => transitionHydration(s, { type: 'release' }, due, 'a'));
|
|
112
|
+
assert.equal((await read(first)).claim, null);
|
|
132
113
|
});
|
|
133
|
-
|
|
134
|
-
test('a tab without Host settings cannot erase another tab claim', async (t) => {
|
|
114
|
+
test('partial preference updates preserve another tab interval change', async (t) => {
|
|
135
115
|
const { first, second } = fixture(t);
|
|
136
116
|
await seed(first);
|
|
137
|
-
|
|
138
|
-
transitionHydration(s,
|
|
117
|
+
await first.update((s) =>
|
|
118
|
+
transitionHydration(s, { type: 'preferences', interval: 30 }, begin, 'a'),
|
|
139
119
|
);
|
|
140
|
-
await second.update((s) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
120
|
+
await second.update((s) =>
|
|
121
|
+
transitionHydration(s, { type: 'preferences', enabled: false }, begin, 'b'),
|
|
122
|
+
);
|
|
123
|
+
const state = await read(first);
|
|
124
|
+
assert.equal(state.interval, 30);
|
|
125
|
+
assert.equal(state.enabled, false);
|
|
144
126
|
});
|
|
145
|
-
|
|
146
|
-
test(
|
|
147
|
-
|
|
127
|
+
for (const schema of [1, 2])
|
|
128
|
+
test(`legacy schema ${schema} migrates once in a shared transaction and keeps the original snapshot`, async (t) => {
|
|
129
|
+
const { idb, first, second } = fixture(t),
|
|
130
|
+
old = legacy(schema);
|
|
131
|
+
await raw(idb, 'state', old, 1);
|
|
132
|
+
const [a, b] = await Promise.all([read(first), read(second)]);
|
|
133
|
+
assert.equal(a.schema, 3);
|
|
134
|
+
assert.equal(a.revision, 41);
|
|
135
|
+
assert.deepEqual(a, b);
|
|
136
|
+
assert.equal(a.count, 9);
|
|
137
|
+
assert.equal(a.interval, 5);
|
|
138
|
+
assert.equal(a.paused, true);
|
|
139
|
+
assert.equal(a.claim, null);
|
|
140
|
+
assert.deepEqual(a.recentActions, schema === 2 ? ['old-cup'] : []);
|
|
141
|
+
assert.equal(a.quietUntil, schema === 2 ? begin + 1800000 : 0);
|
|
142
|
+
assert.deepEqual(await raw(idb, 'state'), old);
|
|
143
|
+
assert.deepEqual(await read(second), a);
|
|
144
|
+
const oldClient = new Promise((resolve, reject) => {
|
|
145
|
+
const r = idb.open(name, 1);
|
|
146
|
+
r.onerror = () => reject(r.error);
|
|
147
|
+
r.onsuccess = () => {
|
|
148
|
+
r.result.close();
|
|
149
|
+
resolve();
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
await assert.rejects(oldClient, { name: 'VersionError' });
|
|
153
|
+
await first.update(() => defaultHydration(), { reset: true });
|
|
154
|
+
assert.equal((await read(second)).count, 0);
|
|
155
|
+
assert.equal((await read(second)).enabled, false);
|
|
156
|
+
assert.deepEqual(await raw(idb, 'state'), old);
|
|
157
|
+
});
|
|
158
|
+
test('invalid legacy records abort migration without overwriting either record; explicit reset does not reimport', async (t) => {
|
|
159
|
+
const { idb, first } = fixture(t),
|
|
160
|
+
bad = { schema: 99, revision: 40 };
|
|
161
|
+
await raw(idb, 'state', bad, 1);
|
|
162
|
+
await assert.rejects(read(first), /Invalid legacy/);
|
|
163
|
+
assert.equal(await raw(idb, 'wall-clock'), undefined);
|
|
164
|
+
assert.deepEqual(await raw(idb, 'state'), bad);
|
|
165
|
+
const reset = await first.update(() => defaultHydration(), { reset: true });
|
|
166
|
+
assert.equal(reset.count, 0);
|
|
167
|
+
assert.deepEqual(await read(first), reset);
|
|
168
|
+
});
|
|
169
|
+
test('invalid new records fail closed and explicit reset increments their stored revision', async (t) => {
|
|
170
|
+
const { idb, first, second } = fixture(t);
|
|
148
171
|
await seed(first);
|
|
149
|
-
await
|
|
172
|
+
await raw(idb, 'wall-clock', { schema: 99, revision: 40 });
|
|
150
173
|
await assert.rejects(read(second), /Invalid hydration data/);
|
|
151
174
|
const reset = await first.update(() => defaultHydration(), { reset: true });
|
|
152
175
|
assert.equal(reset.revision, 41);
|
|
153
|
-
assert.equal(reset.
|
|
154
|
-
assert.equal(reset.shift, null);
|
|
176
|
+
assert.equal(reset.count, 0);
|
|
155
177
|
assert.deepEqual(await read(second), reset);
|
|
156
178
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const { first } = fixture(t);
|
|
179
|
+
test('mutating and aborting reducers are atomic, including revision exhaustion', async (t) => {
|
|
180
|
+
const { idb, first } = fixture(t);
|
|
160
181
|
await seed(first);
|
|
161
182
|
const state = await first.update((s) => {
|
|
162
183
|
s.interval = 30;
|
|
163
184
|
return s;
|
|
164
185
|
});
|
|
165
|
-
assert.equal((await read(first)).interval, 30);
|
|
166
186
|
await assert.rejects(
|
|
167
187
|
first.update((s) => {
|
|
168
|
-
s.interval =
|
|
188
|
+
s.interval = 5;
|
|
169
189
|
throw new Error('abort');
|
|
170
190
|
}),
|
|
171
191
|
/abort/,
|
|
172
192
|
);
|
|
173
193
|
assert.deepEqual(await read(first), state);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
await first.update((s) =>
|
|
180
|
-
transitionHydration(s, context(begin), { type: 'preferences', interval: 30 }, begin, 'a'),
|
|
181
|
-
);
|
|
182
|
-
await second.update((s) =>
|
|
183
|
-
transitionHydration(s, context(begin), { type: 'preferences', enabled: false }, begin, 'b'),
|
|
194
|
+
const exhausted = { ...state, revision: Number.MAX_SAFE_INTEGER };
|
|
195
|
+
await raw(idb, 'wall-clock', exhausted);
|
|
196
|
+
await assert.rejects(
|
|
197
|
+
first.update((s) => ({ ...s, interval: 5 })),
|
|
198
|
+
/revision exhausted/,
|
|
184
199
|
);
|
|
185
|
-
|
|
186
|
-
assert.equal(state.interval, 30);
|
|
187
|
-
assert.equal(state.enabled, false);
|
|
200
|
+
assert.deepEqual(await read(first), exhausted);
|
|
188
201
|
});
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
test('transient opening errors recover while missing storage rejects explicitly', async () => {
|
|
203
|
+
const idb = new IDBFactory();
|
|
191
204
|
let calls = 0;
|
|
192
|
-
const indexedDB = new IDBFactory();
|
|
193
205
|
const store = createHydrationStore({
|
|
194
206
|
open(...args) {
|
|
195
207
|
if (++calls === 1) throw new Error('temporary');
|
|
196
|
-
return
|
|
208
|
+
return idb.open(...args);
|
|
197
209
|
},
|
|
198
210
|
});
|
|
199
211
|
await assert.rejects(read(store), /temporary/);
|
|
200
212
|
assert.deepEqual(await read(store), defaultHydration());
|
|
201
213
|
assert.equal(calls, 2);
|
|
202
214
|
store.close();
|
|
203
|
-
const
|
|
204
|
-
await assert.rejects(read(
|
|
205
|
-
|
|
215
|
+
const missing = createHydrationStore(null);
|
|
216
|
+
await assert.rejects(read(missing), /unavailable/);
|
|
217
|
+
missing.close();
|
|
206
218
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
assert.equal(a.schema, 2);
|
|
221
|
-
assert.equal(a.revision, 41);
|
|
222
|
-
assert.deepEqual(a, b);
|
|
223
|
-
assert.equal(a.interval, 5);
|
|
224
|
-
assert.equal(a.shift.count, 9);
|
|
225
|
-
assert.equal(a.shift.paused, true);
|
|
226
|
-
assert.equal(a.shift.due, legacy.shift.due);
|
|
227
|
-
assert.deepEqual(await read(second), a);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('concurrent check-ins deduplicate the same ID but retain independent IDs', async (t) => {
|
|
231
|
-
const { first, second } = fixture(t);
|
|
232
|
-
await seed(first);
|
|
233
|
-
const record = (store, id) =>
|
|
234
|
-
store.update((s) =>
|
|
235
|
-
transitionHydration(
|
|
236
|
-
s,
|
|
237
|
-
context(begin),
|
|
238
|
-
{
|
|
239
|
-
type: 'manual-drink',
|
|
240
|
-
actionId: id,
|
|
241
|
-
shiftId: context(begin).id,
|
|
242
|
-
},
|
|
243
|
-
begin,
|
|
244
|
-
'a',
|
|
245
|
-
),
|
|
246
|
-
);
|
|
247
|
-
await Promise.all([record(first, 'same'), record(second, 'same')]);
|
|
248
|
-
assert.equal((await read(first)).shift.count, 1);
|
|
249
|
-
await Promise.all([record(first, 'one'), record(second, 'two')]);
|
|
250
|
-
assert.equal((await read(first)).shift.count, 3);
|
|
219
|
+
test('a blocked upgrade reports failure and can retry after the old connection closes', async (t) => {
|
|
220
|
+
const { idb, first } = fixture(t);
|
|
221
|
+
await raw(idb, 'state', legacy(), 1);
|
|
222
|
+
const old = await new Promise((resolve, reject) => {
|
|
223
|
+
const r = idb.open(name, 1);
|
|
224
|
+
r.onsuccess = () => resolve(r.result);
|
|
225
|
+
r.onerror = () => reject(r.error);
|
|
226
|
+
});
|
|
227
|
+
await assert.rejects(read(first), /blocked/);
|
|
228
|
+
old.close();
|
|
229
|
+
const migrated = await read(first);
|
|
230
|
+
assert.equal(migrated.count, 9);
|
|
231
|
+
assert.equal(migrated.schema, 3);
|
|
251
232
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
233
|
+
test('deleting a migrated current record never resurrects the retained legacy count', async (t) => {
|
|
234
|
+
const { idb, first } = fixture(t);
|
|
235
|
+
await raw(idb, 'state', legacy(), 1);
|
|
236
|
+
await read(first);
|
|
237
|
+
await new Promise((resolve, reject) => {
|
|
238
|
+
const request = idb.open(name);
|
|
239
|
+
request.onerror = () => reject(request.error);
|
|
240
|
+
request.onsuccess = () => {
|
|
241
|
+
const db = request.result,
|
|
242
|
+
tx = db.transaction('wall-clock', 'readwrite');
|
|
243
|
+
tx.objectStore('wall-clock').delete('current');
|
|
244
|
+
tx.oncomplete = () => {
|
|
245
|
+
db.close();
|
|
246
|
+
resolve();
|
|
247
|
+
};
|
|
248
|
+
tx.onabort = () => {
|
|
249
|
+
db.close();
|
|
250
|
+
reject(tx.error);
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
const next = await read(first);
|
|
255
|
+
assert.equal(next.count, 0);
|
|
256
|
+
assert.equal(next.enabled, false);
|
|
257
|
+
assert.equal((await raw(idb, 'state')).shift.count, 9);
|
|
263
258
|
});
|
|
264
259
|
|
|
265
|
-
test('quiet and manual
|
|
260
|
+
test('quiet time and manual drinking preserve each other across connections', async (t) => {
|
|
266
261
|
const { first, second } = fixture(t);
|
|
267
262
|
await seed(first);
|
|
268
263
|
await Promise.all([
|
|
269
|
-
first.update((s) =>
|
|
270
|
-
transitionHydration(s, context(begin), { type: 'quiet', minutes: 30 }, begin, 'a'),
|
|
271
|
-
),
|
|
264
|
+
first.update((s) => transitionHydration(s, { type: 'quiet', minutes: 60 }, begin, 'a')),
|
|
272
265
|
second.update((s) =>
|
|
273
|
-
transitionHydration(
|
|
274
|
-
s,
|
|
275
|
-
context(begin),
|
|
276
|
-
{ type: 'manual-drink', actionId: 'cup', shiftId: context(begin).id },
|
|
277
|
-
begin,
|
|
278
|
-
'b',
|
|
279
|
-
),
|
|
266
|
+
transitionHydration(s, { type: 'manual-drink', actionId: 'cup' }, begin, 'b'),
|
|
280
267
|
),
|
|
281
268
|
]);
|
|
282
269
|
const state = await read(first);
|
|
283
|
-
assert.equal(state.
|
|
284
|
-
assert.equal(state.
|
|
285
|
-
assert.equal(
|
|
286
|
-
assert.deepEqual(await read(second), state);
|
|
270
|
+
assert.equal(state.count, 1);
|
|
271
|
+
assert.equal(state.quietUntil, begin + 3600000);
|
|
272
|
+
assert.equal(state.dueAt, begin + 3600000);
|
|
287
273
|
});
|