Skip to content

Commit 68cd2a3

Browse files
committed
test: update the mock to handle exclusive locks
1 parent c22780c commit 68cd2a3

File tree

2 files changed

+149
-14
lines changed

2 files changed

+149
-14
lines changed

tests/setup.ts

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createSerializer } from 'enzyme-to-json';
44
import { configure as mobxConfigure } from 'mobx';
55

66
import { ElectronFiddleMock } from './mocks/mocks';
7+
import { getOrCreateMapValue } from './utils';
78

89
enzymeConfigure({ adapter: new Adapter() });
910

@@ -75,47 +76,159 @@ delete (window as any).localStorage;
7576
window.navigator = window.navigator ?? {};
7677
(window.navigator.clipboard as any) = {};
7778

79+
type MockLock = Lock & {
80+
abortController: AbortController;
81+
};
82+
7883
class FakeNavigatorLocks implements LockManager {
7984
locks = {
80-
held: new Set<Lock>(),
81-
pending: new Set<Lock>(),
85+
held: new Map<string, Map<LockMode, Set<MockLock>>>(),
8286
};
8387

8488
query = async () => {
8589
const result = {
86-
held: [...this.locks.held],
87-
pending: [...this.locks.pending],
90+
held: [...this.locks.held.values()].reduce((acc, item) => {
91+
acc.push(...[...item.get('exclusive')!.values()]);
92+
acc.push(...[...item.get('shared')!.values()]);
93+
94+
return acc;
95+
}, [] as MockLock[]),
8896
};
8997

9098
return result as LockManagerSnapshot;
9199
};
92100

93-
/**
94-
* WIP. Right now, this is a **very** naive mock that will just happily grant a shared lock when one is requested,
95-
* but I'll add some bookkeeping and expand it to cover the exclusive lock case as well.
96-
*
97-
* @TODO remove this comment
98-
*/
99101
request = (async (...args: Parameters<LockManager['request']>) => {
100102
const [
101103
name,
102104
options = {
105+
ifAvailable: false,
103106
mode: 'exclusive',
107+
steal: false,
104108
},
105109
cb,
106110
] = args;
107111

108-
const { mode } = options;
112+
const { ifAvailable, mode, steal } = options;
113+
114+
const lock = {
115+
name,
116+
mode,
117+
abortController: new AbortController(),
118+
} as MockLock;
119+
120+
const heldLocksWithSameName = getOrCreateMapValue(
121+
this.locks.held,
122+
name,
123+
new Map<LockMode, Set<MockLock>>(),
124+
);
125+
126+
const exclusiveLocksWithSameName = getOrCreateMapValue(
127+
heldLocksWithSameName,
128+
'exclusive',
129+
new Set<MockLock>(),
130+
);
109131

110-
const lock = { name, mode, cb } as Lock;
132+
const sharedLocksWithSameName = getOrCreateMapValue(
133+
heldLocksWithSameName,
134+
'shared',
135+
new Set<MockLock>(),
136+
);
111137

112138
if (mode === 'shared') {
113-
this.locks.held.add(lock);
139+
sharedLocksWithSameName.add(lock);
114140

115-
await cb(lock);
141+
try {
142+
await cb(lock);
143+
} finally {
144+
sharedLocksWithSameName.delete(lock);
145+
}
146+
147+
return;
148+
}
149+
150+
// exclusive lock
151+
152+
// no locks with this name -> grant an exclusive lock
153+
if (
154+
exclusiveLocksWithSameName.size === 0 &&
155+
sharedLocksWithSameName.size === 0
156+
) {
157+
exclusiveLocksWithSameName.add(lock);
158+
159+
try {
160+
await cb(lock);
161+
} finally {
162+
exclusiveLocksWithSameName.delete(lock);
163+
}
116164

117165
return;
118166
}
167+
168+
// steal any currently held locks
169+
if (steal) {
170+
for (const lock of sharedLocksWithSameName) {
171+
lock.abortController.abort();
172+
}
173+
174+
for (const lock of exclusiveLocksWithSameName) {
175+
lock.abortController.abort();
176+
}
177+
178+
sharedLocksWithSameName.clear();
179+
exclusiveLocksWithSameName.clear();
180+
181+
exclusiveLocksWithSameName.add(lock);
182+
183+
try {
184+
await cb(lock);
185+
} finally {
186+
exclusiveLocksWithSameName.delete(lock);
187+
}
188+
189+
return;
190+
}
191+
192+
// run the callback without waiting for the lock to be released
193+
if (ifAvailable) {
194+
// just run the callback without waiting for it
195+
cb(null);
196+
197+
return;
198+
}
199+
200+
// @TODO add the lock to the list of pending locks?
201+
202+
// it's an exclusive lock, so there's only one value
203+
const currentLock = exclusiveLocksWithSameName.values().next()
204+
.value as MockLock;
205+
206+
const { abortController: currentLockAbortController } = currentLock;
207+
208+
// wait for the current lock to be released
209+
await new Promise<void>((resolve, reject) => {
210+
currentLockAbortController.signal.onabort = () => resolve();
211+
212+
const { abortController: pendingLockAbortController } = lock;
213+
214+
// this allows the locking mechanism to release this lock
215+
pendingLockAbortController.signal.onabort = () => reject();
216+
});
217+
218+
// clear the exclusive locks
219+
exclusiveLocksWithSameName.clear();
220+
221+
// grant our lock
222+
exclusiveLocksWithSameName.add(lock);
223+
224+
try {
225+
// run the callback
226+
await cb(lock);
227+
} finally {
228+
exclusiveLocksWithSameName.delete(lock);
229+
}
230+
231+
return;
119232
}) as LockManager['request'];
120233
}
121234

tests/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,25 @@ export function emitEvent(type: FiddleEvent, ...args: any[]) {
146146
},
147147
);
148148
}
149+
150+
export function getOrCreateMapValue<T extends Map<unknown, unknown>>(
151+
map: T,
152+
key: MapKey<T>,
153+
fallbackValue: MapValue<T>,
154+
): MapValue<T> {
155+
if (!map.has(key)) {
156+
map.set(key, fallbackValue);
157+
158+
return fallbackValue;
159+
}
160+
161+
return map.get(key) as MapValue<T>;
162+
}
163+
164+
type MapKey<T extends Map<unknown, unknown>> = T extends Map<infer I, unknown>
165+
? I
166+
: never;
167+
168+
type MapValue<T extends Map<unknown, unknown>> = T extends Map<unknown, infer I>
169+
? I
170+
: never;

0 commit comments

Comments
 (0)