@ankhorage/contracts 1.6.0 → 1.7.0
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 +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/state.d.ts +41 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +2 -0
- package/dist/state.js.map +1 -0
- package/package.json +5 -1
- package/src/index.ts +1 -0
- package/src/state.test.ts +188 -0
- package/src/state.ts +63 -0
package/CHANGELOG.md
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,MAAM,CAAC;AACrB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,MAAM,CAAC;AACrB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,MAAM,CAAC;AACrB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC","sourcesContent":["export * from './auth';\nexport * from './db';\nexport * from './storage';\nexport * from './types';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,MAAM,CAAC;AACrB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC","sourcesContent":["export * from './auth';\nexport * from './db';\nexport * from './state';\nexport * from './storage';\nexport * from './types';\n"]}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type StatePrimitive = string | number | boolean | null;
|
|
2
|
+
export type StateValue = StatePrimitive | readonly StateValue[] | {
|
|
3
|
+
readonly [key: string]: StateValue;
|
|
4
|
+
};
|
|
5
|
+
export type StatePath = string | readonly string[];
|
|
6
|
+
export interface StateAdapterCapabilities {
|
|
7
|
+
readonly subscriptions: boolean;
|
|
8
|
+
readonly computed: boolean;
|
|
9
|
+
readonly persistence: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface StateAdapterError {
|
|
12
|
+
readonly code: string;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly cause?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export type StateSuccess<TValue = void> = [TValue] extends [void] ? {
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
} : {
|
|
19
|
+
readonly ok: true;
|
|
20
|
+
readonly data: TValue;
|
|
21
|
+
};
|
|
22
|
+
export type StateResult<TValue = void> = StateSuccess<TValue> | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly error: StateAdapterError;
|
|
25
|
+
};
|
|
26
|
+
export interface StateSnapshot<TValue extends StateValue = StateValue> {
|
|
27
|
+
readonly path: StatePath;
|
|
28
|
+
readonly value: TValue | undefined;
|
|
29
|
+
}
|
|
30
|
+
export type StateListener<TValue extends StateValue = StateValue> = (snapshot: StateSnapshot<TValue>) => void;
|
|
31
|
+
export interface StateSubscription {
|
|
32
|
+
unsubscribe(): Promise<void> | void;
|
|
33
|
+
}
|
|
34
|
+
export interface StateAdapter {
|
|
35
|
+
readonly capabilities: StateAdapterCapabilities;
|
|
36
|
+
get<TValue extends StateValue = StateValue>(path: StatePath): StateResult<TValue | undefined>;
|
|
37
|
+
set<TValue extends StateValue = StateValue>(path: StatePath, value: TValue): StateResult;
|
|
38
|
+
subscribe<TValue extends StateValue = StateValue>(path: StatePath, listener: StateListener<TValue>): StateResult<StateSubscription>;
|
|
39
|
+
delete?(path: StatePath): StateResult;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAE9D,MAAM,MAAM,UAAU,GAClB,cAAc,GACd,SAAS,UAAU,EAAE,GACrB;IACE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;CACpC,CAAC;AAEN,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;AAEnD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,MAAM,YAAY,CAAC,MAAM,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,GAC7D;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;CACnB,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEN,MAAM,MAAM,WAAW,CAAC,MAAM,GAAG,IAAI,IACjC,YAAY,CAAC,MAAM,CAAC,GACpB;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;CACnC,CAAC;AAEN,MAAM,WAAW,aAAa,CAAC,MAAM,SAAS,UAAU,GAAG,UAAU;IACnE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAED,MAAM,MAAM,aAAa,CAAC,MAAM,SAAS,UAAU,GAAG,UAAU,IAAI,CAClE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,KAC5B,IAAI,CAAC;AAEV,MAAM,WAAW,iBAAiB;IAChC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,YAAY,EAAE,wBAAwB,CAAC;IAEhD,GAAG,CAAC,MAAM,SAAS,UAAU,GAAG,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,WAAW,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC9F,GAAG,CAAC,MAAM,SAAS,UAAU,GAAG,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC;IACzF,SAAS,CAAC,MAAM,SAAS,UAAU,GAAG,UAAU,EAC9C,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAC9B,WAAW,CAAC,iBAAiB,CAAC,CAAC;IAClC,MAAM,CAAC,CAAC,IAAI,EAAE,SAAS,GAAG,WAAW,CAAC;CACvC"}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"","sourcesContent":["export type StatePrimitive = string | number | boolean | null;\n\nexport type StateValue =\n | StatePrimitive\n | readonly StateValue[]\n | {\n readonly [key: string]: StateValue;\n };\n\nexport type StatePath = string | readonly string[];\n\nexport interface StateAdapterCapabilities {\n readonly subscriptions: boolean;\n readonly computed: boolean;\n readonly persistence: boolean;\n}\n\nexport interface StateAdapterError {\n readonly code: string;\n readonly message: string;\n readonly cause?: unknown;\n}\n\nexport type StateSuccess<TValue = void> = [TValue] extends [void]\n ? {\n readonly ok: true;\n }\n : {\n readonly ok: true;\n readonly data: TValue;\n };\n\nexport type StateResult<TValue = void> =\n | StateSuccess<TValue>\n | {\n readonly ok: false;\n readonly error: StateAdapterError;\n };\n\nexport interface StateSnapshot<TValue extends StateValue = StateValue> {\n readonly path: StatePath;\n readonly value: TValue | undefined;\n}\n\nexport type StateListener<TValue extends StateValue = StateValue> = (\n snapshot: StateSnapshot<TValue>,\n) => void;\n\nexport interface StateSubscription {\n unsubscribe(): Promise<void> | void;\n}\n\nexport interface StateAdapter {\n readonly capabilities: StateAdapterCapabilities;\n\n get<TValue extends StateValue = StateValue>(path: StatePath): StateResult<TValue | undefined>;\n set<TValue extends StateValue = StateValue>(path: StatePath, value: TValue): StateResult;\n subscribe<TValue extends StateValue = StateValue>(\n path: StatePath,\n listener: StateListener<TValue>,\n ): StateResult<StateSubscription>;\n delete?(path: StatePath): StateResult;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/contracts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@ankhorage/color-theory": "^0.0.7"
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"types": "./dist/db.d.ts",
|
|
26
26
|
"default": "./dist/db.js"
|
|
27
27
|
},
|
|
28
|
+
"./state": {
|
|
29
|
+
"types": "./dist/state.d.ts",
|
|
30
|
+
"default": "./dist/state.js"
|
|
31
|
+
},
|
|
28
32
|
"./storage": {
|
|
29
33
|
"types": "./dist/storage.d.ts",
|
|
30
34
|
"default": "./dist/storage.js"
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
StateAdapter,
|
|
5
|
+
StateListener,
|
|
6
|
+
StatePath,
|
|
7
|
+
StateResult,
|
|
8
|
+
StateSubscription,
|
|
9
|
+
StateValue,
|
|
10
|
+
} from './state';
|
|
11
|
+
|
|
12
|
+
function pathToKey(path: StatePath): string {
|
|
13
|
+
return Array.isArray(path) ? path.join('.') : path;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isStateValue(value: unknown): value is StateValue {
|
|
17
|
+
if (value === null) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return value.every(isStateValue);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof value !== 'object') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return Object.values(value).every(isStateValue);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createError(code: string, message: string): StateResult {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: { code, message },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createFakeStateAdapter(): StateAdapter {
|
|
44
|
+
const values = new Map<string, StateValue>();
|
|
45
|
+
const listeners = new Map<string, Set<StateListener>>();
|
|
46
|
+
|
|
47
|
+
const emit = (path: StatePath, value: StateValue | undefined) => {
|
|
48
|
+
const key = pathToKey(path);
|
|
49
|
+
const pathListeners = listeners.get(key);
|
|
50
|
+
|
|
51
|
+
if (!pathListeners) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const listener of pathListeners) {
|
|
56
|
+
listener({ path, value });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
capabilities: {
|
|
62
|
+
subscriptions: true,
|
|
63
|
+
computed: false,
|
|
64
|
+
persistence: false,
|
|
65
|
+
},
|
|
66
|
+
get(path) {
|
|
67
|
+
return { ok: true, data: values.get(pathToKey(path)) };
|
|
68
|
+
},
|
|
69
|
+
set(path, value) {
|
|
70
|
+
if (!isStateValue(value)) {
|
|
71
|
+
return createError('invalid_value', 'State value must be serializable.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
values.set(pathToKey(path), value);
|
|
75
|
+
emit(path, value);
|
|
76
|
+
return { ok: true };
|
|
77
|
+
},
|
|
78
|
+
subscribe(path, listener) {
|
|
79
|
+
const key = pathToKey(path);
|
|
80
|
+
const pathListeners = listeners.get(key) ?? new Set<StateListener>();
|
|
81
|
+
pathListeners.add(listener);
|
|
82
|
+
listeners.set(key, pathListeners);
|
|
83
|
+
|
|
84
|
+
const subscription: StateSubscription = {
|
|
85
|
+
unsubscribe() {
|
|
86
|
+
pathListeners.delete(listener);
|
|
87
|
+
if (pathListeners.size === 0) {
|
|
88
|
+
listeners.delete(key);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return { ok: true, data: subscription };
|
|
94
|
+
},
|
|
95
|
+
delete(path) {
|
|
96
|
+
values.delete(pathToKey(path));
|
|
97
|
+
emit(path, undefined);
|
|
98
|
+
return { ok: true };
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe('StateAdapter', () => {
|
|
104
|
+
it('supports path-based set and get', () => {
|
|
105
|
+
const adapter = createFakeStateAdapter();
|
|
106
|
+
|
|
107
|
+
const setResult = adapter.set(['forms', 'contact', 'values'], {
|
|
108
|
+
firstname: 'Fabio',
|
|
109
|
+
newsletter: true,
|
|
110
|
+
});
|
|
111
|
+
const getResult = adapter.get(['forms', 'contact', 'values']);
|
|
112
|
+
|
|
113
|
+
expect(setResult).toEqual({ ok: true });
|
|
114
|
+
expect(getResult).toEqual({
|
|
115
|
+
ok: true,
|
|
116
|
+
data: {
|
|
117
|
+
firstname: 'Fabio',
|
|
118
|
+
newsletter: true,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('supports string paths and serializable nested values', () => {
|
|
124
|
+
const adapter = createFakeStateAdapter();
|
|
125
|
+
const value: StateValue = {
|
|
126
|
+
selectedIds: ['post-1', 'post-2'],
|
|
127
|
+
filters: {
|
|
128
|
+
search: 'hello',
|
|
129
|
+
onlyPublished: true,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
adapter.set('collections.posts', value);
|
|
134
|
+
|
|
135
|
+
expect(adapter.get('collections.posts')).toEqual({ ok: true, data: value });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('notifies subscribers when a value changes', () => {
|
|
139
|
+
const adapter = createFakeStateAdapter();
|
|
140
|
+
const snapshots: StateValue[] = [];
|
|
141
|
+
const subscriptionResult = adapter.subscribe('counter.value', ({ value }) => {
|
|
142
|
+
if (value !== undefined) {
|
|
143
|
+
snapshots.push(value);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
adapter.set('counter.value', 1);
|
|
148
|
+
adapter.set('counter.value', 2);
|
|
149
|
+
|
|
150
|
+
expect(subscriptionResult.ok).toBe(true);
|
|
151
|
+
expect(snapshots).toEqual([1, 2]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('stops notifying after unsubscribe', async () => {
|
|
155
|
+
const adapter = createFakeStateAdapter();
|
|
156
|
+
const snapshots: StateValue[] = [];
|
|
157
|
+
const subscriptionResult = adapter.subscribe('counter.value', ({ value }) => {
|
|
158
|
+
if (value !== undefined) {
|
|
159
|
+
snapshots.push(value);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!subscriptionResult.ok) {
|
|
164
|
+
throw new Error('Expected subscription to succeed.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
adapter.set('counter.value', 1);
|
|
168
|
+
await subscriptionResult.data.unsubscribe();
|
|
169
|
+
adapter.set('counter.value', 2);
|
|
170
|
+
|
|
171
|
+
expect(snapshots).toEqual([1]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('supports deleting values when the adapter implements delete', () => {
|
|
175
|
+
const adapter = createFakeStateAdapter();
|
|
176
|
+
const snapshots: (StateValue | undefined)[] = [];
|
|
177
|
+
adapter.subscribe('session.user', ({ value }) => {
|
|
178
|
+
snapshots.push(value);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
adapter.set('session.user', { id: 'user-1' });
|
|
182
|
+
const deleteResult = adapter.delete?.('session.user');
|
|
183
|
+
|
|
184
|
+
expect(deleteResult).toEqual({ ok: true });
|
|
185
|
+
expect(adapter.get('session.user')).toEqual({ ok: true, data: undefined });
|
|
186
|
+
expect(snapshots).toEqual([{ id: 'user-1' }, undefined]);
|
|
187
|
+
});
|
|
188
|
+
});
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type StatePrimitive = string | number | boolean | null;
|
|
2
|
+
|
|
3
|
+
export type StateValue =
|
|
4
|
+
| StatePrimitive
|
|
5
|
+
| readonly StateValue[]
|
|
6
|
+
| {
|
|
7
|
+
readonly [key: string]: StateValue;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type StatePath = string | readonly string[];
|
|
11
|
+
|
|
12
|
+
export interface StateAdapterCapabilities {
|
|
13
|
+
readonly subscriptions: boolean;
|
|
14
|
+
readonly computed: boolean;
|
|
15
|
+
readonly persistence: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StateAdapterError {
|
|
19
|
+
readonly code: string;
|
|
20
|
+
readonly message: string;
|
|
21
|
+
readonly cause?: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type StateSuccess<TValue = void> = [TValue] extends [void]
|
|
25
|
+
? {
|
|
26
|
+
readonly ok: true;
|
|
27
|
+
}
|
|
28
|
+
: {
|
|
29
|
+
readonly ok: true;
|
|
30
|
+
readonly data: TValue;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type StateResult<TValue = void> =
|
|
34
|
+
| StateSuccess<TValue>
|
|
35
|
+
| {
|
|
36
|
+
readonly ok: false;
|
|
37
|
+
readonly error: StateAdapterError;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface StateSnapshot<TValue extends StateValue = StateValue> {
|
|
41
|
+
readonly path: StatePath;
|
|
42
|
+
readonly value: TValue | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type StateListener<TValue extends StateValue = StateValue> = (
|
|
46
|
+
snapshot: StateSnapshot<TValue>,
|
|
47
|
+
) => void;
|
|
48
|
+
|
|
49
|
+
export interface StateSubscription {
|
|
50
|
+
unsubscribe(): Promise<void> | void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface StateAdapter {
|
|
54
|
+
readonly capabilities: StateAdapterCapabilities;
|
|
55
|
+
|
|
56
|
+
get<TValue extends StateValue = StateValue>(path: StatePath): StateResult<TValue | undefined>;
|
|
57
|
+
set<TValue extends StateValue = StateValue>(path: StatePath, value: TValue): StateResult;
|
|
58
|
+
subscribe<TValue extends StateValue = StateValue>(
|
|
59
|
+
path: StatePath,
|
|
60
|
+
listener: StateListener<TValue>,
|
|
61
|
+
): StateResult<StateSubscription>;
|
|
62
|
+
delete?(path: StatePath): StateResult;
|
|
63
|
+
}
|