@strata-sync/mobx 0.1.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/AGENTS.md +23 -0
- package/README.md +42 -0
- package/package.json +35 -0
- package/src/adapter.ts +212 -0
- package/src/index.ts +21 -0
- package/tests/adapter.test.ts +119 -0
- package/tests/observability.test.ts +104 -0
- package/tsconfig.json +29 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @strata-sync/mobx
|
|
2
|
+
|
|
3
|
+
MobX reactivity adapter for the sync system.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `npm run build` — compile TypeScript (`tsc`)
|
|
8
|
+
- `npm run dev` — watch mode (`tsc --watch`)
|
|
9
|
+
- `npm run test` — run tests (`vitest`)
|
|
10
|
+
- `npm run lint` — lint with Biome
|
|
11
|
+
- `npm run check-types` — type check without emitting
|
|
12
|
+
|
|
13
|
+
## Gotchas
|
|
14
|
+
|
|
15
|
+
- `mobx` ^6.0.0 is a peer dependency — the consuming app must install it
|
|
16
|
+
- This package implements the reactivity adapter interface defined in `@strata-sync/core` — changes to the interface require updates here
|
|
17
|
+
- MobX observable properties are set up during model hydration — accessing properties before hydration will not trigger reactions
|
|
18
|
+
|
|
19
|
+
## Conventions
|
|
20
|
+
|
|
21
|
+
- Implement the `ReactivityAdapter` interface from sync-core — do not create a custom interface
|
|
22
|
+
- Use MobX `makeObservable` / `makeAutoObservable` patterns, not legacy decorators
|
|
23
|
+
- Computed values should derive from observable model properties only
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @strata-sync/mobx
|
|
2
|
+
|
|
3
|
+
MobX reactivity adapter for the Strata Sync.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
sync-mobx implements the reactivity adapter interface defined in `@strata-sync/core` using MobX observables:
|
|
8
|
+
|
|
9
|
+
- **Observable model instances** — model properties become MobX observables
|
|
10
|
+
- **Computed values** — derived state from observable model properties
|
|
11
|
+
- **Reaction-based updates** — automatic re-rendering when synced data changes
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @strata-sync/mobx
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Peer dependency: `mobx` ^6.0.0
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Register the MobX adapter when initializing the sync client:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createMobxAdapter } from "@strata-sync/mobx";
|
|
27
|
+
|
|
28
|
+
const adapter = createMobxAdapter();
|
|
29
|
+
|
|
30
|
+
// Pass to SyncClient configuration
|
|
31
|
+
const client = new SyncClient({
|
|
32
|
+
reactivityAdapter: adapter,
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The adapter makes model instances observable, so MobX `observer()` components and `autorun` / `reaction` will automatically track and respond to sync updates.
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
1. When models are hydrated from sync deltas, the adapter wraps properties with MobX observables
|
|
41
|
+
2. React components wrapped with `observer()` automatically re-render when observed properties change
|
|
42
|
+
3. Computed values can derive state from multiple observable model properties
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@strata-sync/mobx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"dev": "tsc --watch",
|
|
19
|
+
"lint": "biome lint .",
|
|
20
|
+
"check-types": "tsc --noEmit",
|
|
21
|
+
"test": "vitest"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@strata-sync/core": "*",
|
|
25
|
+
"mobx": "^6.13.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@biomejs/biome": "^2.3.12",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"vitest": "^4.0.18"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"mobx": "^6.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DisposeFn,
|
|
3
|
+
ObservableArray,
|
|
4
|
+
ObservableBox,
|
|
5
|
+
ObservableMap,
|
|
6
|
+
ObservableOptions,
|
|
7
|
+
ReactionOptions,
|
|
8
|
+
ReactivityAdapter,
|
|
9
|
+
} from "@strata-sync/core";
|
|
10
|
+
import {
|
|
11
|
+
computed,
|
|
12
|
+
makeObservable,
|
|
13
|
+
observable,
|
|
14
|
+
reaction,
|
|
15
|
+
runInAction,
|
|
16
|
+
} from "mobx";
|
|
17
|
+
|
|
18
|
+
class MobXBox<T> implements ObservableBox<T> {
|
|
19
|
+
value: T;
|
|
20
|
+
|
|
21
|
+
constructor(initialValue: T) {
|
|
22
|
+
this.value = initialValue;
|
|
23
|
+
makeObservable(this, {
|
|
24
|
+
value: observable,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get(): T {
|
|
29
|
+
return this.value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set(value: T): void {
|
|
33
|
+
runInAction(() => {
|
|
34
|
+
this.value = value;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class MobXMap<K, V> implements ObservableMap<K, V> {
|
|
40
|
+
private readonly map: Map<K, V>;
|
|
41
|
+
|
|
42
|
+
constructor(entries?: Iterable<[K, V]>) {
|
|
43
|
+
this.map = observable.map<K, V>(entries ? new Map(entries) : undefined);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get(key: K): V | undefined {
|
|
47
|
+
return this.map.get(key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set(key: K, value: V): void {
|
|
51
|
+
runInAction(() => {
|
|
52
|
+
this.map.set(key, value);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
has(key: K): boolean {
|
|
57
|
+
return this.map.has(key);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
delete(key: K): boolean {
|
|
61
|
+
return runInAction(() => this.map.delete(key));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clear(): void {
|
|
65
|
+
runInAction(() => {
|
|
66
|
+
this.map.clear();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
keys(): IterableIterator<K> {
|
|
71
|
+
return this.map.keys();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
values(): IterableIterator<V> {
|
|
75
|
+
return this.map.values();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
entries(): IterableIterator<[K, V]> {
|
|
79
|
+
return this.map.entries();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get size(): number {
|
|
83
|
+
return this.map.size;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
forEach(callback: (value: V, key: K) => void): void {
|
|
87
|
+
this.map.forEach(callback);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class MobXArray<T> implements ObservableArray<T> {
|
|
92
|
+
private readonly array: T[];
|
|
93
|
+
|
|
94
|
+
constructor(items?: T[]) {
|
|
95
|
+
this.array = observable.array<T>(items ?? []);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get(index: number): T | undefined {
|
|
99
|
+
return this.array[index];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
toArray(): T[] {
|
|
103
|
+
return [...this.array];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
push(...items: T[]): number {
|
|
107
|
+
return runInAction(() => this.array.push(...items));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pop(): T | undefined {
|
|
111
|
+
return runInAction(() => this.array.pop());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
remove(predicate: (item: T) => boolean): T[] {
|
|
115
|
+
return runInAction(() => {
|
|
116
|
+
const removed: T[] = [];
|
|
117
|
+
for (let i = this.array.length - 1; i >= 0; i--) {
|
|
118
|
+
const item = this.array[i];
|
|
119
|
+
if (item !== undefined && predicate(item)) {
|
|
120
|
+
removed.push(item);
|
|
121
|
+
this.array.splice(i, 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return removed;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
replace(items: T[]): void {
|
|
129
|
+
runInAction(() => {
|
|
130
|
+
this.array.length = 0;
|
|
131
|
+
this.array.push(...items);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
clear(): void {
|
|
136
|
+
runInAction(() => {
|
|
137
|
+
this.array.length = 0;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
find(predicate: (item: T) => boolean): T | undefined {
|
|
142
|
+
return this.array.find(predicate);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
filter(predicate: (item: T) => boolean): T[] {
|
|
146
|
+
return this.array.filter(predicate);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
map<U>(mapper: (item: T) => U): U[] {
|
|
150
|
+
return this.array.map(mapper);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get length(): number {
|
|
154
|
+
return this.array.length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[Symbol.iterator](): Iterator<T> {
|
|
158
|
+
return this.array[Symbol.iterator]();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const mobxReactivityAdapter: ReactivityAdapter = {
|
|
163
|
+
createBox<T>(
|
|
164
|
+
initialValue: T,
|
|
165
|
+
_options?: ObservableOptions
|
|
166
|
+
): ObservableBox<T> {
|
|
167
|
+
return new MobXBox(initialValue);
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
createMap<K, V>(
|
|
171
|
+
entries?: Iterable<[K, V]>,
|
|
172
|
+
_options?: ObservableOptions
|
|
173
|
+
): ObservableMap<K, V> {
|
|
174
|
+
return new MobXMap(entries);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
createArray<T>(
|
|
178
|
+
items?: T[],
|
|
179
|
+
_options?: ObservableOptions
|
|
180
|
+
): ObservableArray<T> {
|
|
181
|
+
return new MobXArray(items);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
makeObservable<T extends object>(target: T, _options?: ObservableOptions): T {
|
|
185
|
+
return observable(target);
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
batch(fn) {
|
|
189
|
+
runInAction(fn);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
runInAction<T>(fn: () => T): T {
|
|
193
|
+
return runInAction(fn);
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
reaction<T>(
|
|
197
|
+
expression: () => T,
|
|
198
|
+
effect: (value: T) => void,
|
|
199
|
+
options?: ReactionOptions
|
|
200
|
+
): DisposeFn {
|
|
201
|
+
return reaction(expression, effect, options);
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
computed<T>(getter: () => T, _options?: ObservableOptions): { get(): T } {
|
|
205
|
+
const c = computed(getter);
|
|
206
|
+
return { get: () => c.get() };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export function createMobXReactivity(): ReactivityAdapter {
|
|
211
|
+
return mobxReactivityAdapter;
|
|
212
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type { Hydrated, LazyReference } from "@strata-sync/core";
|
|
2
|
+
// biome-ignore lint/performance/noBarrelFile: package entry point
|
|
3
|
+
export {
|
|
4
|
+
BackReference,
|
|
5
|
+
CachedPromise,
|
|
6
|
+
ClientModel,
|
|
7
|
+
Collection,
|
|
8
|
+
EphemeralProperty,
|
|
9
|
+
LazyCollection,
|
|
10
|
+
ManyToOne,
|
|
11
|
+
Model,
|
|
12
|
+
makeObservableProperty,
|
|
13
|
+
makeReferenceModelProperty,
|
|
14
|
+
OneToMany,
|
|
15
|
+
Property,
|
|
16
|
+
Reference,
|
|
17
|
+
ReferenceArray,
|
|
18
|
+
ReferenceCollection,
|
|
19
|
+
resolvePromise,
|
|
20
|
+
} from "@strata-sync/core";
|
|
21
|
+
export { createMobXReactivity, mobxReactivityAdapter } from "./adapter";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMobXReactivity, mobxReactivityAdapter } from "../src/index";
|
|
3
|
+
|
|
4
|
+
describe("mobx reactivity adapter", () => {
|
|
5
|
+
it("returns the shared adapter instance", () => {
|
|
6
|
+
expect(createMobXReactivity()).toBe(mobxReactivityAdapter);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("creates observable boxes that notify reactions", () => {
|
|
10
|
+
const adapter = createMobXReactivity();
|
|
11
|
+
const box = adapter.createBox(1);
|
|
12
|
+
const values: number[] = [];
|
|
13
|
+
|
|
14
|
+
const dispose = adapter.reaction(
|
|
15
|
+
() => box.get(),
|
|
16
|
+
(value) => values.push(value),
|
|
17
|
+
{ fireImmediately: true }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
box.set(2);
|
|
21
|
+
box.set(3);
|
|
22
|
+
|
|
23
|
+
expect(values).toEqual([1, 2, 3]);
|
|
24
|
+
dispose();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("batches updates with action", () => {
|
|
28
|
+
const adapter = createMobXReactivity();
|
|
29
|
+
const box = adapter.createBox(0);
|
|
30
|
+
const values: number[] = [];
|
|
31
|
+
|
|
32
|
+
const dispose = adapter.reaction(
|
|
33
|
+
() => box.get(),
|
|
34
|
+
(value) => values.push(value)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
adapter.batch(() => {
|
|
38
|
+
box.set(1);
|
|
39
|
+
box.set(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(values).toEqual([2]);
|
|
43
|
+
dispose();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("creates observable maps that react to key changes", () => {
|
|
47
|
+
const adapter = createMobXReactivity();
|
|
48
|
+
const map = adapter.createMap([["a", 1]]);
|
|
49
|
+
const values: Array<number | undefined> = [];
|
|
50
|
+
|
|
51
|
+
const dispose = adapter.reaction(
|
|
52
|
+
() => map.get("a"),
|
|
53
|
+
(value) => values.push(value),
|
|
54
|
+
{ fireImmediately: true }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
map.set("a", 2);
|
|
58
|
+
|
|
59
|
+
expect(values).toEqual([1, 2]);
|
|
60
|
+
dispose();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("creates observable arrays that track length changes", () => {
|
|
64
|
+
const adapter = createMobXReactivity();
|
|
65
|
+
const array = adapter.createArray([1, 2, 3]);
|
|
66
|
+
const lengths: number[] = [];
|
|
67
|
+
|
|
68
|
+
const dispose = adapter.reaction(
|
|
69
|
+
() => array.length,
|
|
70
|
+
(value) => lengths.push(value),
|
|
71
|
+
{ fireImmediately: true }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
array.push(4);
|
|
75
|
+
array.remove((value) => value % 2 === 0);
|
|
76
|
+
|
|
77
|
+
expect(array.toArray()).toEqual([1, 3]);
|
|
78
|
+
expect(lengths).toEqual([3, 4, 2]);
|
|
79
|
+
dispose();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("supports computed values derived from observables", () => {
|
|
83
|
+
const adapter = createMobXReactivity();
|
|
84
|
+
const box = adapter.createBox(2);
|
|
85
|
+
const doubled = adapter.computed(() => box.get() * 2);
|
|
86
|
+
const values: number[] = [];
|
|
87
|
+
|
|
88
|
+
const dispose = adapter.reaction(
|
|
89
|
+
() => doubled.get(),
|
|
90
|
+
(value) => values.push(value),
|
|
91
|
+
{ fireImmediately: true }
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
box.set(3);
|
|
95
|
+
|
|
96
|
+
expect(values).toEqual([4, 6]);
|
|
97
|
+
dispose();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("makes objects observable", () => {
|
|
101
|
+
const adapter = createMobXReactivity();
|
|
102
|
+
const target = { count: 0 };
|
|
103
|
+
const observableTarget = adapter.makeObservable(target);
|
|
104
|
+
const values: number[] = [];
|
|
105
|
+
|
|
106
|
+
const dispose = adapter.reaction(
|
|
107
|
+
() => observableTarget.count,
|
|
108
|
+
(value) => values.push(value),
|
|
109
|
+
{ fireImmediately: true }
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
adapter.runInAction(() => {
|
|
113
|
+
observableTarget.count = 1;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(values).toEqual([0, 1]);
|
|
117
|
+
dispose();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
makeObservableProperty,
|
|
4
|
+
makeReferenceModelProperty,
|
|
5
|
+
} from "../src/index";
|
|
6
|
+
|
|
7
|
+
interface PropertyChange {
|
|
8
|
+
name: string;
|
|
9
|
+
oldValue: unknown;
|
|
10
|
+
newValue: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class TestModel {
|
|
14
|
+
_mobx: Record<string, { get(): unknown; set(value: unknown): void }> = {};
|
|
15
|
+
__data: Record<string, unknown> = { title: "Initial" };
|
|
16
|
+
declare title: string | undefined;
|
|
17
|
+
changes: PropertyChange[] = [];
|
|
18
|
+
|
|
19
|
+
propertyChanged(name: string, oldValue: unknown, newValue: unknown): void {
|
|
20
|
+
this.changes.push({ name, oldValue, newValue });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
makeObservableProperty(TestModel.prototype, "title");
|
|
25
|
+
|
|
26
|
+
describe("makeObservableProperty", () => {
|
|
27
|
+
it("proxies through MobX boxes and tracks changes", () => {
|
|
28
|
+
const model = new TestModel();
|
|
29
|
+
|
|
30
|
+
expect(model.title).toBe("Initial");
|
|
31
|
+
|
|
32
|
+
model.title = "Updated";
|
|
33
|
+
|
|
34
|
+
expect(model.__data.title).toBe("Updated");
|
|
35
|
+
expect(model._mobx.title?.get()).toBe("Updated");
|
|
36
|
+
expect(model.changes).toEqual([
|
|
37
|
+
{ name: "title", oldValue: "Initial", newValue: "Updated" },
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
model.title = "Again";
|
|
41
|
+
|
|
42
|
+
expect(model.changes).toEqual([
|
|
43
|
+
{ name: "title", oldValue: "Initial", newValue: "Updated" },
|
|
44
|
+
{ name: "title", oldValue: "Updated", newValue: "Again" },
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("falls back to __data when no MobX storage is present", () => {
|
|
49
|
+
const model = {
|
|
50
|
+
__data: { status: "open" },
|
|
51
|
+
changes: [] as PropertyChange[],
|
|
52
|
+
propertyChanged(name: string, oldValue: unknown, newValue: unknown) {
|
|
53
|
+
this.changes.push({ name, oldValue, newValue });
|
|
54
|
+
},
|
|
55
|
+
} as {
|
|
56
|
+
__data: Record<string, unknown>;
|
|
57
|
+
status?: string;
|
|
58
|
+
changes: PropertyChange[];
|
|
59
|
+
propertyChanged: (
|
|
60
|
+
name: string,
|
|
61
|
+
oldValue: unknown,
|
|
62
|
+
newValue: unknown
|
|
63
|
+
) => void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
makeObservableProperty(model, "status");
|
|
67
|
+
|
|
68
|
+
expect(model.status).toBe("open");
|
|
69
|
+
|
|
70
|
+
model.status = "closed";
|
|
71
|
+
|
|
72
|
+
expect(model.__data.status).toBe("closed");
|
|
73
|
+
expect((model as { _mobx?: unknown })._mobx).toBeUndefined();
|
|
74
|
+
expect(model.changes).toEqual([
|
|
75
|
+
{ name: "status", oldValue: "open", newValue: "closed" },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("makeReferenceModelProperty", () => {
|
|
81
|
+
it("gets and sets reference ids via the store", () => {
|
|
82
|
+
const store = {
|
|
83
|
+
get: (modelName: string, id: string) => ({ modelName, id }),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const model = {
|
|
87
|
+
store,
|
|
88
|
+
userId: "user-1" as string | null,
|
|
89
|
+
user: null as { id?: string } | null,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
makeReferenceModelProperty(model, "user", "userId", "User");
|
|
93
|
+
|
|
94
|
+
expect(model.user).toEqual({ modelName: "User", id: "user-1" });
|
|
95
|
+
|
|
96
|
+
model.user = { id: "user-2" };
|
|
97
|
+
|
|
98
|
+
expect(model.userId).toBe("user-2");
|
|
99
|
+
|
|
100
|
+
model.user = null;
|
|
101
|
+
|
|
102
|
+
expect(model.userId).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./src",
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUnusedLocals": true,
|
|
17
|
+
"noUnusedParameters": true,
|
|
18
|
+
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"forceConsistentCasingInFileNames": true,
|
|
21
|
+
"esModuleInterop": true,
|
|
22
|
+
"resolveJsonModule": true,
|
|
23
|
+
"isolatedModules": true,
|
|
24
|
+
"useDefineForClassFields": true,
|
|
25
|
+
"experimentalDecorators": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src/**/*"],
|
|
28
|
+
"exclude": ["node_modules", "dist"]
|
|
29
|
+
}
|