Skip to content

Commit 8721fda

Browse files
committed
overhaul of error handling
1 parent c0df5cc commit 8721fda

4 files changed

Lines changed: 244 additions & 31 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@
33
coverage/
44
cov/
55
lcov/
6-
cov_profile/
6+
cov_profile/
7+
cov_profile.lcov
8+
9+
.cov_profile/
10+
.cov_profile.lcov

deno.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"tasks": {
3+
"test": "rm -r .cov_profile && rm .cov_profile.lcov && rm -r cov && deno test --coverage=.cov_profile && deno coverage .cov_profile --lcov --output=.cov_profile.lcov && genhtml -o cov/html .cov_profile.lcov && open cov/html/index.html"
4+
}
5+
}

mod.ts

Lines changed: 136 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/// <reference path="./global.d.ts" />
22

3+
// Mark: Polyfill
34
// Polyfill for browsers that don't support the native `structuredClone`.
45
(async () => {
56
if ('structuredClone' in window === false) {
67
await import('https://deno.land/x/corejs@v3.24.1/index.js');
78
}
89
})();
910

11+
// Mark: Definition Interfaces
1012
export interface Observer<T> {
1113
update(subject: Store<T>): void;
1214
}
@@ -20,6 +22,65 @@ export interface Subject<T> {
2022
set(options: (prevState: T) => T): void;
2123
}
2224

25+
// Mark: Custom Types
26+
/**
27+
* Convenience type alias for a pointer.
28+
*/
29+
export type Pointer = string;
30+
31+
/**
32+
* Convenience type for `Store` of any type.
33+
*/
34+
// deno-lint-ignore no-explicit-any
35+
export type AnyStore = Store<any>;
36+
37+
// Mark: Custom Errors
38+
/**
39+
* Error that is triggered when a duplicated observer is being attached to a store.
40+
*
41+
* @final
42+
*/
43+
export class DuplicateObserverError extends Error {
44+
constructor() {
45+
super("This observer is already attached to the store.");
46+
}
47+
}
48+
49+
/**
50+
* Error that is triggered when a non-existing/non-attached observer is being detached from a store.
51+
*
52+
* @final
53+
*/
54+
export class UnknownObserverError extends Error {
55+
constructor() {
56+
super("Attempted removal of unattached observer.");
57+
}
58+
}
59+
60+
/**
61+
* Error that is triggered when attempting to insert a store at an address that was already allocated,
62+
* without explicit override.
63+
*
64+
* @final
65+
*/
66+
export class MemoryAllocationError extends Error {
67+
constructor() {
68+
super("Attempted to insert store at already allocated memory address without explicit override.");
69+
}
70+
}
71+
72+
/**
73+
* Error that is triggered when attempting to access a memory address that is unallocated.
74+
*
75+
* @final
76+
*/
77+
export class NullPointerError extends Error {
78+
constructor() {
79+
super("Attempted to access unallocated memory address.");
80+
}
81+
}
82+
83+
// Mark: Store
2384
/**
2485
* A simple store that follows the Observer pattern.
2586
*/
@@ -55,11 +116,12 @@ export class Store<T> implements Subject<T> {
55116
* Attaches/subscribes a new {@link Observer} to the store.
56117
*
57118
* @param observer Subscribes a new {@link Observer} to the store.
119+
* @throws {DuplicateObserverError} If the observer has already been attached.
58120
*/
59121
public attach(observer: Observer<T>): void {
60122
const isExist = this.#observers.includes(observer);
61123
if (isExist) {
62-
return console.warn('Subject: Observer has been attached already.');
124+
throw new DuplicateObserverError();
63125
}
64126

65127
this.#observers.push(observer);
@@ -69,11 +131,12 @@ export class Store<T> implements Subject<T> {
69131
* Detaches/unsubscribes an {@link Observer} from the store.
70132
*
71133
* @param observer The `Observer` to remove.
134+
* @throws {UnknownObserverError} If the observer was non-existent in the store.
72135
*/
73136
public detach(observer: Observer<T>): void {
74137
const observerIndex = this.#observers.indexOf(observer);
75138
if (observerIndex === -1) {
76-
return console.error('Subject: Nonexistent observer.', observer);
139+
throw new UnknownObserverError();
77140
}
78141

79142
this.#observers.splice(observerIndex, 1);
@@ -106,10 +169,7 @@ export class Store<T> implements Subject<T> {
106169
}
107170
}
108171

109-
/**
110-
* Convenience type alias for a pointer.
111-
*/
112-
export type Pointer = string;
172+
// Mark: useStore Options
113173

114174
/**
115175
* The options to create a store.
@@ -148,8 +208,33 @@ export interface StoreOptions<T> {
148208
* @optional
149209
*/
150210
override?: boolean;
211+
212+
/**
213+
* See {@link StoreOptionsErrorHandling}
214+
*/
215+
errorHandling?: StoreOptionsErrorHandling;
151216
}
152217

218+
/**
219+
* Defines how {@link useStore} should handle errors.
220+
*/
221+
export interface StoreOptionsErrorHandling {
222+
/**
223+
* Set to `true` if error messages should be outputed to the console.
224+
*
225+
* `verbose` is set to `false` by default.
226+
*/
227+
verbose?: boolean;
228+
229+
/**
230+
* By default {@link useStore} will silently ignore errors to allow the program to
231+
* continue its execution. If `stopOnError` is set to `true`, the function will stop
232+
* if an error is thrown and will rethrow it.
233+
*/
234+
stopOnError?: boolean;
235+
}
236+
237+
// Mark: useStore
153238
/**
154239
* Convenience function to create a store. Multiple options can be provided, see {@link StoreOptions}.
155240
*
@@ -173,6 +258,8 @@ export interface StoreOptions<T> {
173258
* Stores.get<number>(store2Ptr).set((prevState) => prevState + 1); // Ouput: 1
174259
* ```
175260
*
261+
* @throws {DuplicateObserverError} If provided observer(s) is/are already attached to the store or duplicates.
262+
*
176263
* @param state The initial state. If the store already exists, it will not overwrite the state unless the `override` option is set to `true`.
177264
* @param options See {@link StoreOptions}.
178265
* @returns The {@link Pointer} to the location of the store.
@@ -200,14 +287,34 @@ export function useStore<T>(state: T, options?: StoreOptions<T>): Pointer {
200287
const store = new Store(state);
201288

202289
for (const observer of observers) {
203-
store.attach(observer);
290+
try {
291+
store.attach(observer);
292+
} catch (error) {
293+
if (options.errorHandling?.verbose === true) {
294+
console.error(error);
295+
}
296+
297+
if (options.errorHandling?.stopOnError === true) {
298+
throw error;
299+
}
300+
}
204301
}
205302

206303
Stores.addStoreAtPointer(store, pointer, { override: true });
207304
} else {
208305
// If a store exists at address, it will NOT be overrided, but additional observers will be attached to the store.
209306

210-
Stores.upsert(state, pointer, ...observers);
307+
try {
308+
Stores.upsert(state, pointer, ...observers);
309+
} catch (error) {
310+
if (options.errorHandling?.verbose === true) {
311+
console.error(error);
312+
}
313+
314+
if (options.errorHandling?.stopOnError === true) {
315+
throw error;
316+
}
317+
}
211318
}
212319

213320
return pointer;
@@ -218,12 +325,7 @@ export function useStore<T>(state: T, options?: StoreOptions<T>): Pointer {
218325
}
219326
}
220327

221-
/**
222-
* Convenience type for `Store` of any type.
223-
*/
224-
// deno-lint-ignore no-explicit-any
225-
export type AnyStore = Store<any>;
226-
328+
// Mark: StoreStack
227329
/**
228330
* @internal
229331
* Structure for the `Store` memory stack.
@@ -272,16 +374,18 @@ export class StoreStack {
272374
* @param newItem The {@link Store} to be added to the stack.
273375
* @param pointer The {@link Pointer} to the memory address where the store should be inserted.
274376
* @param options Defines if a store should be overrided if it exists at the `Pointer` and if a verbose error should be returned if override is set to false.
377+
*
378+
* @throws {MemoryAllocationError} If the address is already allocated and `override` isn't set to `true`.
275379
*/
276380
public addStoreAtPointer(newItem: AnyStore, pointer: Pointer, options: { override?: boolean, verbose?: boolean }): void {
277381
const { override, verbose } = options;
278382

279383
if (typeof this.#stores[pointer] !== "undefined" && !override) {
280384
if (verbose) {
281-
return console.warn('Error: Cannot add store at pointer, address is already allocated.');
282-
} else {
283-
return;
385+
console.error('Error: Cannot add store at pointer, address is already allocated.');
284386
}
387+
388+
throw new MemoryAllocationError();
285389
}
286390

287391
this.#stores[pointer] = newItem;
@@ -292,6 +396,8 @@ export class StoreStack {
292396
* If no store is instantiated, it will create one holding the `defaultValue` provided.
293397
* Otherwise, it will only attach {@link Observer Observers} if they are provided.
294398
*
399+
* @throws {DuplicateObserverError} If observer is duplicate.
400+
*
295401
* @param defaultValue A default state value to insert if a new store is created.
296402
* @param pointer The {@link Pointer} to the memory address.
297403
* @param observers {@link Observer Observers} to attach to the store.
@@ -302,7 +408,11 @@ export class StoreStack {
302408
}
303409

304410
for (const observer of observers) {
305-
this.#stores[pointer].attach(observer);
411+
try {
412+
this.#stores[pointer].attach(observer);
413+
} catch (error) {
414+
throw error;
415+
}
306416
}
307417
}
308418

@@ -312,21 +422,24 @@ export class StoreStack {
312422
*
313423
* @param ptr The {@link Pointer}'s address of the store.
314424
* @param options If the removal fails, should the function verbose it to the console? Defaults to false.
425+
*
426+
* @throws {NullPointerError} If attempting to delete a store at unallocated memory address.
315427
*/
316428
public removeStore(ptr: Pointer, options: { verbose?: boolean }): void {
317429
if (typeof this.#stores[ptr] === "undefined") {
318430
if (options.verbose) {
319-
return console.error('Error: The pointer address points to unallocated memory.');
320-
} else {
321-
return;
431+
console.error('Error: The pointer address points to unallocated memory.');
322432
}
433+
434+
throw new NullPointerError();
323435
}
324436

325437
delete this.#stores[ptr];
326438
}
327439

328440
/**
329-
* Returns a reference to the store at the address. A type can be passed in order to make the return typed.
441+
* Returns a reference to the store at the address if it exists, otherwise undefined. A type can be
442+
* passed in order to make the return typed.
330443
*
331444
* ## Example
332445
* ```typescript
@@ -347,6 +460,7 @@ export class StoreStack {
347460
}
348461
}
349462

463+
// Mark: Stores
350464
/**
351465
* Convenience constant, provide access to the global `StoreStack` and creates it if it doesn't exist.
352466
*/

0 commit comments

Comments
 (0)