@iamjulianacosta/mobx-data 1.0.1
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/LICENSE +21 -0
- package/README.md +366 -0
- package/dist/CacheHandler-BTU_rYkv.js +208 -0
- package/dist/CacheHandler-BTU_rYkv.js.map +1 -0
- package/dist/CacheHandler-CXgY9IJo.cjs +2 -0
- package/dist/CacheHandler-CXgY9IJo.cjs.map +1 -0
- package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs +2 -0
- package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs.map +1 -0
- package/dist/EmbeddedRecordsMixin-VoHluHCT.js +261 -0
- package/dist/EmbeddedRecordsMixin-VoHluHCT.js.map +1 -0
- package/dist/JsonApiSerializer-CC5HXp4b.js +194 -0
- package/dist/JsonApiSerializer-CC5HXp4b.js.map +1 -0
- package/dist/JsonApiSerializer-CKB02AgP.cjs +2 -0
- package/dist/JsonApiSerializer-CKB02AgP.cjs.map +1 -0
- package/dist/MemoryAdapter-Bx1e7ndV.js +123 -0
- package/dist/MemoryAdapter-Bx1e7ndV.js.map +1 -0
- package/dist/MemoryAdapter-D1cTyydm.cjs +2 -0
- package/dist/MemoryAdapter-D1cTyydm.cjs.map +1 -0
- package/dist/ODataAdapter-C4IHK4BK.js +157 -0
- package/dist/ODataAdapter-C4IHK4BK.js.map +1 -0
- package/dist/ODataAdapter-DyyF1sdA.cjs +2 -0
- package/dist/ODataAdapter-DyyF1sdA.cjs.map +1 -0
- package/dist/RestAdapter-B4aRvs4m.js +355 -0
- package/dist/RestAdapter-B4aRvs4m.js.map +1 -0
- package/dist/RestAdapter-CJOwTsKK.cjs +2 -0
- package/dist/RestAdapter-CJOwTsKK.cjs.map +1 -0
- package/dist/SchemaService-DZwkFgZu.js +102 -0
- package/dist/SchemaService-DZwkFgZu.js.map +1 -0
- package/dist/SchemaService-Di_yjVzU.cjs +2 -0
- package/dist/SchemaService-Di_yjVzU.cjs.map +1 -0
- package/dist/Serializer-95gi5edy.cjs +2 -0
- package/dist/Serializer-95gi5edy.cjs.map +1 -0
- package/dist/Serializer-FxJbsZ50.js +139 -0
- package/dist/Serializer-FxJbsZ50.js.map +1 -0
- package/dist/Store-BdwMrbDi.cjs +2 -0
- package/dist/Store-BdwMrbDi.cjs.map +1 -0
- package/dist/Store-CZ7Z-Nme.js +912 -0
- package/dist/Store-CZ7Z-Nme.js.map +1 -0
- package/dist/adapter/Adapter.d.ts +146 -0
- package/dist/adapter/Adapter.d.ts.map +1 -0
- package/dist/adapter/MemoryAdapter.d.ts +44 -0
- package/dist/adapter/MemoryAdapter.d.ts.map +1 -0
- package/dist/adapter/RestAdapter.d.ts +57 -0
- package/dist/adapter/RestAdapter.d.ts.map +1 -0
- package/dist/adapter/index.cjs +2 -0
- package/dist/adapter/index.cjs.map +1 -0
- package/dist/adapter/index.d.ts +4 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +8 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/date-Bj4O2W1F.js +107 -0
- package/dist/date-Bj4O2W1F.js.map +1 -0
- package/dist/date-CRCe-9gf.cjs +2 -0
- package/dist/date-CRCe-9gf.cjs.map +1 -0
- package/dist/decorators-HQ1KnRdh.cjs +2 -0
- package/dist/decorators-HQ1KnRdh.cjs.map +1 -0
- package/dist/decorators-Zr35qr6A.js +50 -0
- package/dist/decorators-Zr35qr6A.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/json-api/JsonApiAdapter.d.ts +38 -0
- package/dist/json-api/JsonApiAdapter.d.ts.map +1 -0
- package/dist/json-api/JsonApiSerializer.d.ts +73 -0
- package/dist/json-api/JsonApiSerializer.d.ts.map +1 -0
- package/dist/json-api/index.cjs +2 -0
- package/dist/json-api/index.cjs.map +1 -0
- package/dist/json-api/index.d.ts +3 -0
- package/dist/json-api/index.d.ts.map +1 -0
- package/dist/json-api/index.js +6 -0
- package/dist/json-api/index.js.map +1 -0
- package/dist/model/Errors.d.ts +46 -0
- package/dist/model/Errors.d.ts.map +1 -0
- package/dist/model/Model.d.ts +226 -0
- package/dist/model/Model.d.ts.map +1 -0
- package/dist/model/Snapshot.d.ts +72 -0
- package/dist/model/Snapshot.d.ts.map +1 -0
- package/dist/model/StateMachine.d.ts +45 -0
- package/dist/model/StateMachine.d.ts.map +1 -0
- package/dist/model/index.cjs +2 -0
- package/dist/model/index.cjs.map +1 -0
- package/dist/model/index.d.ts +6 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +11 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/relationships.d.ts +182 -0
- package/dist/model/relationships.d.ts.map +1 -0
- package/dist/odata/ODataAdapter.d.ts +67 -0
- package/dist/odata/ODataAdapter.d.ts.map +1 -0
- package/dist/odata/index.cjs +2 -0
- package/dist/odata/index.cjs.map +1 -0
- package/dist/odata/index.d.ts +2 -0
- package/dist/odata/index.d.ts.map +1 -0
- package/dist/odata/index.js +5 -0
- package/dist/odata/index.js.map +1 -0
- package/dist/relationships-B55LBaCW.cjs +2 -0
- package/dist/relationships-B55LBaCW.cjs.map +1 -0
- package/dist/relationships-BEXANmWg.js +821 -0
- package/dist/relationships-BEXANmWg.js.map +1 -0
- package/dist/request/CacheHandler.d.ts +50 -0
- package/dist/request/CacheHandler.d.ts.map +1 -0
- package/dist/request/FetchHandler.d.ts +41 -0
- package/dist/request/FetchHandler.d.ts.map +1 -0
- package/dist/request/RequestManager.d.ts +52 -0
- package/dist/request/RequestManager.d.ts.map +1 -0
- package/dist/request/index.cjs +2 -0
- package/dist/request/index.cjs.map +1 -0
- package/dist/request/index.d.ts +5 -0
- package/dist/request/index.d.ts.map +1 -0
- package/dist/request/index.js +7 -0
- package/dist/request/index.js.map +1 -0
- package/dist/request/types.d.ts +111 -0
- package/dist/request/types.d.ts.map +1 -0
- package/dist/schema/SchemaService.d.ts +58 -0
- package/dist/schema/SchemaService.d.ts.map +1 -0
- package/dist/schema/decorators.d.ts +50 -0
- package/dist/schema/decorators.d.ts.map +1 -0
- package/dist/schema/index.cjs +2 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +13 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/types.d.ts +61 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/serializer/EmbeddedRecordsMixin.d.ts +80 -0
- package/dist/serializer/EmbeddedRecordsMixin.d.ts.map +1 -0
- package/dist/serializer/JsonSerializer.d.ts +52 -0
- package/dist/serializer/JsonSerializer.d.ts.map +1 -0
- package/dist/serializer/RestSerializer.d.ts +43 -0
- package/dist/serializer/RestSerializer.d.ts.map +1 -0
- package/dist/serializer/Serializer.d.ts +202 -0
- package/dist/serializer/Serializer.d.ts.map +1 -0
- package/dist/serializer/index.cjs +2 -0
- package/dist/serializer/index.cjs.map +1 -0
- package/dist/serializer/index.d.ts +5 -0
- package/dist/serializer/index.d.ts.map +1 -0
- package/dist/serializer/index.js +9 -0
- package/dist/serializer/index.js.map +1 -0
- package/dist/store/IdentityMap.d.ts +53 -0
- package/dist/store/IdentityMap.d.ts.map +1 -0
- package/dist/store/RecordArray.d.ts +114 -0
- package/dist/store/RecordArray.d.ts.map +1 -0
- package/dist/store/Store.d.ts +395 -0
- package/dist/store/Store.d.ts.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +8 -0
- package/dist/store/index.js.map +1 -0
- package/dist/transforms/Transform.d.ts +49 -0
- package/dist/transforms/Transform.d.ts.map +1 -0
- package/dist/transforms/boolean.d.ts +26 -0
- package/dist/transforms/boolean.d.ts.map +1 -0
- package/dist/transforms/date.d.ts +22 -0
- package/dist/transforms/date.d.ts.map +1 -0
- package/dist/transforms/index.cjs +2 -0
- package/dist/transforms/index.cjs.map +1 -0
- package/dist/transforms/index.d.ts +6 -0
- package/dist/transforms/index.d.ts.map +1 -0
- package/dist/transforms/index.js +9 -0
- package/dist/transforms/index.js.map +1 -0
- package/dist/transforms/number.d.ts +17 -0
- package/dist/transforms/number.d.ts.map +1 -0
- package/dist/transforms/string.d.ts +18 -0
- package/dist/transforms/string.d.ts.map +1 -0
- package/dist/types-C9NB2gRj.js +7 -0
- package/dist/types-C9NB2gRj.js.map +1 -0
- package/dist/types-uWOXMPWW.cjs +2 -0
- package/dist/types-uWOXMPWW.cjs.map +1 -0
- package/package.json +140 -0
- package/src/adapter/Adapter.ts +320 -0
- package/src/adapter/MemoryAdapter.ts +216 -0
- package/src/adapter/RestAdapter.ts +248 -0
- package/src/adapter/index.ts +7 -0
- package/src/index.ts +17 -0
- package/src/json-api/JsonApiAdapter.ts +93 -0
- package/src/json-api/JsonApiSerializer.ts +245 -0
- package/src/json-api/index.ts +2 -0
- package/src/model/Errors.ts +100 -0
- package/src/model/Model.ts +683 -0
- package/src/model/Snapshot.ts +162 -0
- package/src/model/StateMachine.ts +149 -0
- package/src/model/index.ts +20 -0
- package/src/model/relationships.ts +484 -0
- package/src/odata/ODataAdapter.ts +245 -0
- package/src/odata/index.ts +1 -0
- package/src/request/CacheHandler.ts +125 -0
- package/src/request/FetchHandler.ts +119 -0
- package/src/request/RequestManager.ts +112 -0
- package/src/request/index.ts +4 -0
- package/src/request/types.ts +139 -0
- package/src/schema/SchemaService.ts +161 -0
- package/src/schema/decorators.ts +162 -0
- package/src/schema/index.ts +3 -0
- package/src/schema/types.ts +66 -0
- package/src/serializer/EmbeddedRecordsMixin.ts +257 -0
- package/src/serializer/JsonSerializer.ts +173 -0
- package/src/serializer/RestSerializer.ts +138 -0
- package/src/serializer/Serializer.ts +397 -0
- package/src/serializer/index.ts +15 -0
- package/src/store/IdentityMap.ts +110 -0
- package/src/store/RecordArray.ts +210 -0
- package/src/store/Store.ts +1391 -0
- package/src/store/index.ts +11 -0
- package/src/transforms/Transform.ts +52 -0
- package/src/transforms/boolean.ts +57 -0
- package/src/transforms/date.ts +48 -0
- package/src/transforms/index.ts +5 -0
- package/src/transforms/number.ts +42 -0
- package/src/transforms/string.ts +35 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relationship proxy classes returned by relationship getters on Model instances.
|
|
3
|
+
*
|
|
4
|
+
* Three classes are exported:
|
|
5
|
+
*
|
|
6
|
+
* - `ManyArray<T>` — a synchronous, MobX-observable live view of a `hasMany`
|
|
7
|
+
* relationship. Its contents are resolved directly from the store's identity
|
|
8
|
+
* map on each access, so it stays in sync automatically as records are
|
|
9
|
+
* pushed, unloaded, or added/removed via `push` / `removeObject`.
|
|
10
|
+
*
|
|
11
|
+
* - `AsyncBelongsTo<T>` — a `PromiseLike` wrapper for a `belongsTo` that
|
|
12
|
+
* may need to be fetched from the server. Supports `await`, `then`, and
|
|
13
|
+
* MobX-observable state flags (`isLoading`, `isLoaded`, `isFulfilled`,
|
|
14
|
+
* `isRejected`, `value`).
|
|
15
|
+
*
|
|
16
|
+
* - `AsyncHasMany<T>` — same semantics as `AsyncBelongsTo` but resolves to a
|
|
17
|
+
* `ManyArray<T>` instead of a single record.
|
|
18
|
+
*
|
|
19
|
+
* Both async wrappers eagerly check the store cache on construction; if all
|
|
20
|
+
* referenced records are already present they transition to `fulfilled`
|
|
21
|
+
* without issuing any network requests.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
makeObservable, observable, computed, action, runInAction,
|
|
26
|
+
} from 'mobx';
|
|
27
|
+
import type { RelationshipDef } from '@mobx-data/schema';
|
|
28
|
+
import type { Model, ModelStoreLike, RelationshipRef } from './Model.js';
|
|
29
|
+
|
|
30
|
+
/** Context object shared by all relationship proxy classes. */
|
|
31
|
+
export interface RelationshipHost {
|
|
32
|
+
/** The record that owns the relationship. */
|
|
33
|
+
parent: Model;
|
|
34
|
+
/** Name of the relationship property on the owner. */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Relationship definition from the schema. */
|
|
37
|
+
meta: RelationshipDef;
|
|
38
|
+
/** Store instance used to peek / find related records. */
|
|
39
|
+
store: RelationshipCapableStore;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extended store interface required by the relationship proxy classes.
|
|
44
|
+
* `Store` satisfies this interface.
|
|
45
|
+
*/
|
|
46
|
+
export interface RelationshipCapableStore extends ModelStoreLike {
|
|
47
|
+
peekRecord<T extends Model = Model>(type: string, id: string): T | null;
|
|
48
|
+
findRecord<T extends Model = Model>(
|
|
49
|
+
type: string,
|
|
50
|
+
id: string,
|
|
51
|
+
options?: unknown,
|
|
52
|
+
): Promise<T>;
|
|
53
|
+
_getRelationshipRefFor(record: Model, name: string): RelationshipRef | null;
|
|
54
|
+
_getPendingMembers(record: Model, name: string): Iterable<Model>;
|
|
55
|
+
_hasManyAppend(record: Model, name: string, meta: RelationshipDef, value: Model): void;
|
|
56
|
+
_hasManyRemove(record: Model, name: string, meta: RelationshipDef, value: Model): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Synchronous, live-updating array proxy for a `hasMany` relationship.
|
|
61
|
+
*
|
|
62
|
+
* Each access to `length`, iteration, or mutation goes through the store so
|
|
63
|
+
* the array always reflects the current identity map state. Records that
|
|
64
|
+
* have been `push`ed to the relationship but not yet assigned a server id
|
|
65
|
+
* are tracked separately as "pending members" and are included in the
|
|
66
|
+
* resolved list.
|
|
67
|
+
*/
|
|
68
|
+
export class ManyArray<T extends Model = Model> implements Iterable<T> {
|
|
69
|
+
static refData(
|
|
70
|
+
ref: RelationshipRef | null,
|
|
71
|
+
): Array<{ type: string; id: string }> {
|
|
72
|
+
if (!ref || !ref.data) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return Array.isArray(ref.data) ? ref.data : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private host: RelationshipHost;
|
|
79
|
+
|
|
80
|
+
constructor(host: RelationshipHost) {
|
|
81
|
+
this.host = host;
|
|
82
|
+
makeObservable<this, 'resolved'>(this, {
|
|
83
|
+
resolved: computed,
|
|
84
|
+
length: computed,
|
|
85
|
+
push: action,
|
|
86
|
+
removeObject: action,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolves the current set of related records from the store identity map.
|
|
92
|
+
* Pending (unsaved) members appended via `push()` are appended at the end.
|
|
93
|
+
*/
|
|
94
|
+
private get resolved(): T[] {
|
|
95
|
+
const ref = this.host.store._getRelationshipRefFor(this.host.parent, this.host.name);
|
|
96
|
+
const resolved: T[] = [];
|
|
97
|
+
const seen = new Set<T>();
|
|
98
|
+
for (const reference of ManyArray.refData(ref)) {
|
|
99
|
+
const record = this.host.store.peekRecord<T>(reference.type, reference.id);
|
|
100
|
+
if (record) {
|
|
101
|
+
resolved.push(record);
|
|
102
|
+
seen.add(record);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const pending = this.host.store._getPendingMembers(
|
|
106
|
+
this.host.parent,
|
|
107
|
+
this.host.name,
|
|
108
|
+
);
|
|
109
|
+
for (const pendingRecord of pending as Iterable<T>) {
|
|
110
|
+
if (!seen.has(pendingRecord)) {
|
|
111
|
+
resolved.push(pendingRecord);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return resolved;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Number of related records currently in the array. */
|
|
118
|
+
get length(): number {
|
|
119
|
+
return this.resolved.length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Returns the record at `index`, or `undefined`. */
|
|
123
|
+
at(index: number): T | undefined {
|
|
124
|
+
return this.resolved[index];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Adds one or more records to the relationship.
|
|
129
|
+
* Delegates to `store._hasManyAppend` which also handles inverse tracking.
|
|
130
|
+
*/
|
|
131
|
+
push(...records: T[]): number {
|
|
132
|
+
for (const record of records) {
|
|
133
|
+
this.host.store._hasManyAppend(
|
|
134
|
+
this.host.parent,
|
|
135
|
+
this.host.name,
|
|
136
|
+
this.host.meta,
|
|
137
|
+
record,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return this.length;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Removes a record from the relationship.
|
|
145
|
+
* Delegates to `store._hasManyRemove` which also handles inverse tracking.
|
|
146
|
+
*/
|
|
147
|
+
removeObject(record: T): void {
|
|
148
|
+
this.host.store._hasManyRemove(
|
|
149
|
+
this.host.parent,
|
|
150
|
+
this.host.name,
|
|
151
|
+
this.host.meta,
|
|
152
|
+
record,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Returns `true` when `record` is currently in the relationship. */
|
|
157
|
+
includes(record: T): boolean {
|
|
158
|
+
return this.resolved.includes(record);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Returns a plain array snapshot of all related records. */
|
|
162
|
+
toArray(): T[] {
|
|
163
|
+
return [...this.resolved];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Maps over the related records. */
|
|
167
|
+
map<R>(callback: (record: T, i: number) => R): R[] {
|
|
168
|
+
return this.resolved.map(callback);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Filters the related records. */
|
|
172
|
+
filter(predicate: (record: T, i: number) => boolean): T[] {
|
|
173
|
+
return this.resolved.filter(predicate);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Iterates over the related records. */
|
|
177
|
+
forEach(callback: (record: T, i: number) => void): void {
|
|
178
|
+
this.resolved.forEach(callback);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
[Symbol.iterator](): Iterator<T> {
|
|
182
|
+
return this.resolved[Symbol.iterator]();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Async wrapper for a `belongsTo` relationship.
|
|
188
|
+
*
|
|
189
|
+
* Implements `PromiseLike<T | null>` so it can be `await`ed. Also exposes
|
|
190
|
+
* MobX-observable state flags:
|
|
191
|
+
* - `isPending` / `isFulfilled` / `isRejected` — promise lifecycle
|
|
192
|
+
* - `isLoading` — a network request is currently in flight
|
|
193
|
+
* - `isLoaded` — the relationship has been resolved (even to `null`)
|
|
194
|
+
* - `value` — the resolved record, or `null`
|
|
195
|
+
* - `reason` — the rejection error, if any
|
|
196
|
+
*
|
|
197
|
+
* Eagerly checks the store cache at construction; if the referenced record is
|
|
198
|
+
* already present it transitions straight to `fulfilled`.
|
|
199
|
+
*/
|
|
200
|
+
export class AsyncBelongsTo<T extends Model = Model>
|
|
201
|
+
implements PromiseLike<T | null> {
|
|
202
|
+
private host: RelationshipHost;
|
|
203
|
+
|
|
204
|
+
private loadedState: 'pending' | 'fulfilled' | 'rejected' = 'pending';
|
|
205
|
+
|
|
206
|
+
private currentValue: T | null = null;
|
|
207
|
+
|
|
208
|
+
private error: unknown = null;
|
|
209
|
+
|
|
210
|
+
private inflight: Promise<T | null> | null = null;
|
|
211
|
+
|
|
212
|
+
constructor(host: RelationshipHost) {
|
|
213
|
+
this.host = host;
|
|
214
|
+
makeObservable<this, 'loadedState' | 'currentValue' | 'error' | 'syncFromCache'>(this, {
|
|
215
|
+
loadedState: observable,
|
|
216
|
+
currentValue: observable.ref,
|
|
217
|
+
error: observable.ref,
|
|
218
|
+
isPending: computed,
|
|
219
|
+
isFulfilled: computed,
|
|
220
|
+
isRejected: computed,
|
|
221
|
+
isLoaded: computed,
|
|
222
|
+
isLoading: computed,
|
|
223
|
+
value: computed,
|
|
224
|
+
reason: computed,
|
|
225
|
+
syncFromCache: action,
|
|
226
|
+
});
|
|
227
|
+
this.syncFromCache();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Checks the store cache and transitions to `fulfilled` if the record is already loaded. */
|
|
231
|
+
private syncFromCache(): void {
|
|
232
|
+
const ref = this.host.store._getRelationshipRefFor(this.host.parent, this.host.name);
|
|
233
|
+
if (!ref || !ref.data || Array.isArray(ref.data)) {
|
|
234
|
+
this.currentValue = null;
|
|
235
|
+
this.loadedState = 'fulfilled';
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const cached = this.host.store.peekRecord<T>(ref.data.type, ref.data.id);
|
|
239
|
+
if (cached) {
|
|
240
|
+
this.currentValue = cached;
|
|
241
|
+
this.loadedState = 'fulfilled';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** `true` while the relationship has not yet been resolved. */
|
|
246
|
+
get isPending(): boolean {
|
|
247
|
+
return this.loadedState === 'pending';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** `true` once the relationship has been resolved (including to `null`). */
|
|
251
|
+
get isFulfilled(): boolean {
|
|
252
|
+
return this.loadedState === 'fulfilled';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** `true` if the network request failed. */
|
|
256
|
+
get isRejected(): boolean {
|
|
257
|
+
return this.loadedState === 'rejected';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** `true` while a network request is in flight. */
|
|
261
|
+
get isLoading(): boolean {
|
|
262
|
+
return this.inflight !== null && this.isPending;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** `true` once the relationship is resolved. */
|
|
266
|
+
get isLoaded(): boolean {
|
|
267
|
+
return this.loadedState === 'fulfilled';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The resolved record, or `null` when the relationship is empty or not yet loaded. */
|
|
271
|
+
get value(): T | null {
|
|
272
|
+
return this.currentValue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The rejection reason if `isRejected`. */
|
|
276
|
+
get reason(): unknown {
|
|
277
|
+
return this.error;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Ensures the related record is loaded, returning a `Promise<T | null>`.
|
|
282
|
+
* If the record is already in the store it resolves immediately.
|
|
283
|
+
* Concurrent calls share the same in-flight promise.
|
|
284
|
+
*/
|
|
285
|
+
load(): Promise<T | null> {
|
|
286
|
+
// Re-check cache each time in case it was populated externally.
|
|
287
|
+
this.syncFromCache();
|
|
288
|
+
if (this.loadedState === 'fulfilled' && this.currentValue) {
|
|
289
|
+
return Promise.resolve(this.currentValue);
|
|
290
|
+
}
|
|
291
|
+
if (this.inflight) {
|
|
292
|
+
return this.inflight;
|
|
293
|
+
}
|
|
294
|
+
const ref = this.host.store._getRelationshipRefFor(this.host.parent, this.host.name);
|
|
295
|
+
if (!ref || !ref.data || Array.isArray(ref.data)) {
|
|
296
|
+
return Promise.resolve(null);
|
|
297
|
+
}
|
|
298
|
+
const { type, id } = ref.data;
|
|
299
|
+
this.inflight = this.host.store.findRecord<T>(type, id).then(
|
|
300
|
+
(record) => {
|
|
301
|
+
runInAction(() => {
|
|
302
|
+
this.currentValue = record;
|
|
303
|
+
this.loadedState = 'fulfilled';
|
|
304
|
+
this.inflight = null;
|
|
305
|
+
});
|
|
306
|
+
return record;
|
|
307
|
+
},
|
|
308
|
+
(error) => {
|
|
309
|
+
runInAction(() => {
|
|
310
|
+
this.error = error;
|
|
311
|
+
this.loadedState = 'rejected';
|
|
312
|
+
this.inflight = null;
|
|
313
|
+
});
|
|
314
|
+
throw error;
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
return this.inflight;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Forces a fresh fetch, ignoring any cached value. */
|
|
321
|
+
reload(): Promise<T | null> {
|
|
322
|
+
this.inflight = null;
|
|
323
|
+
this.loadedState = 'pending';
|
|
324
|
+
return this.load();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
then<TResult1 = T | null, TResult2 = never>(
|
|
328
|
+
onfulfilled?:
|
|
329
|
+
| ((value: T | null) => TResult1 | PromiseLike<TResult1>)
|
|
330
|
+
| null
|
|
331
|
+
| undefined,
|
|
332
|
+
onrejected?:
|
|
333
|
+
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
|
334
|
+
| null
|
|
335
|
+
| undefined,
|
|
336
|
+
): PromiseLike<TResult1 | TResult2> {
|
|
337
|
+
return this.load().then(onfulfilled, onrejected);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Async wrapper for a `hasMany` relationship.
|
|
343
|
+
*
|
|
344
|
+
* Mirrors the `AsyncBelongsTo` API but resolves to a `ManyArray<T>` instead
|
|
345
|
+
* of a single record. Missing referenced records are fetched in parallel via
|
|
346
|
+
* `store.findRecord`.
|
|
347
|
+
*/
|
|
348
|
+
export class AsyncHasMany<T extends Model = Model>
|
|
349
|
+
implements PromiseLike<ManyArray<T>> {
|
|
350
|
+
private host: RelationshipHost;
|
|
351
|
+
|
|
352
|
+
private manyArray: ManyArray<T>;
|
|
353
|
+
|
|
354
|
+
private loadedState: 'pending' | 'fulfilled' | 'rejected' = 'pending';
|
|
355
|
+
|
|
356
|
+
private error: unknown = null;
|
|
357
|
+
|
|
358
|
+
private inflight: Promise<ManyArray<T>> | null = null;
|
|
359
|
+
|
|
360
|
+
constructor(host: RelationshipHost) {
|
|
361
|
+
this.host = host;
|
|
362
|
+
this.manyArray = new ManyArray<T>(host);
|
|
363
|
+
makeObservable<this, 'loadedState' | 'error' | 'syncFromCache'>(this, {
|
|
364
|
+
loadedState: observable,
|
|
365
|
+
error: observable.ref,
|
|
366
|
+
isPending: computed,
|
|
367
|
+
isFulfilled: computed,
|
|
368
|
+
isRejected: computed,
|
|
369
|
+
isLoaded: computed,
|
|
370
|
+
isLoading: computed,
|
|
371
|
+
length: computed,
|
|
372
|
+
syncFromCache: action,
|
|
373
|
+
});
|
|
374
|
+
this.syncFromCache();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Transitions to `fulfilled` if all referenced records are already in the cache. */
|
|
378
|
+
private syncFromCache(): void {
|
|
379
|
+
const ref = this.host.store._getRelationshipRefFor(this.host.parent, this.host.name);
|
|
380
|
+
const items = ManyArray.refData(ref);
|
|
381
|
+
const allCached = items.every(
|
|
382
|
+
(reference) => this.host.store.peekRecord(reference.type, reference.id) !== null,
|
|
383
|
+
);
|
|
384
|
+
if (allCached) {
|
|
385
|
+
this.loadedState = 'fulfilled';
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** `true` while the relationship has not yet been resolved. */
|
|
390
|
+
get isPending(): boolean {
|
|
391
|
+
return this.loadedState === 'pending';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** `true` once all referenced records have been resolved. */
|
|
395
|
+
get isFulfilled(): boolean {
|
|
396
|
+
return this.loadedState === 'fulfilled';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** `true` if any fetch failed. */
|
|
400
|
+
get isRejected(): boolean {
|
|
401
|
+
return this.loadedState === 'rejected';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** `true` while a network request is in flight. */
|
|
405
|
+
get isLoading(): boolean {
|
|
406
|
+
return this.inflight !== null && this.isPending;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** `true` once the relationship is resolved. */
|
|
410
|
+
get isLoaded(): boolean {
|
|
411
|
+
return this.loadedState === 'fulfilled';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** The underlying `ManyArray` (always available, even before `load()`). */
|
|
415
|
+
get value(): ManyArray<T> {
|
|
416
|
+
return this.manyArray;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Number of records currently in the resolved array. */
|
|
420
|
+
get length(): number {
|
|
421
|
+
return this.manyArray.length;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Ensures all referenced records are loaded.
|
|
426
|
+
* Records already in the cache are not re-fetched.
|
|
427
|
+
* Concurrent calls share the same in-flight promise.
|
|
428
|
+
*/
|
|
429
|
+
load(): Promise<ManyArray<T>> {
|
|
430
|
+
this.syncFromCache();
|
|
431
|
+
if (this.loadedState === 'fulfilled') {
|
|
432
|
+
return Promise.resolve(this.manyArray);
|
|
433
|
+
}
|
|
434
|
+
if (this.inflight) {
|
|
435
|
+
return this.inflight;
|
|
436
|
+
}
|
|
437
|
+
const ref = this.host.store._getRelationshipRefFor(this.host.parent, this.host.name);
|
|
438
|
+
const items = ManyArray.refData(ref);
|
|
439
|
+
const missing = items.filter(
|
|
440
|
+
(reference) => this.host.store.peekRecord(reference.type, reference.id) === null,
|
|
441
|
+
);
|
|
442
|
+
const loads = missing.map(
|
|
443
|
+
(reference) => this.host.store.findRecord(reference.type, reference.id),
|
|
444
|
+
);
|
|
445
|
+
this.inflight = Promise.all(loads).then(
|
|
446
|
+
() => {
|
|
447
|
+
runInAction(() => {
|
|
448
|
+
this.loadedState = 'fulfilled';
|
|
449
|
+
this.inflight = null;
|
|
450
|
+
});
|
|
451
|
+
return this.manyArray;
|
|
452
|
+
},
|
|
453
|
+
(error) => {
|
|
454
|
+
runInAction(() => {
|
|
455
|
+
this.error = error;
|
|
456
|
+
this.loadedState = 'rejected';
|
|
457
|
+
this.inflight = null;
|
|
458
|
+
});
|
|
459
|
+
throw error;
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
return this.inflight;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Forces a fresh fetch, ignoring any cached state. */
|
|
466
|
+
reload(): Promise<ManyArray<T>> {
|
|
467
|
+
this.inflight = null;
|
|
468
|
+
this.loadedState = 'pending';
|
|
469
|
+
return this.load();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
then<TResult1 = ManyArray<T>, TResult2 = never>(
|
|
473
|
+
onfulfilled?:
|
|
474
|
+
| ((value: ManyArray<T>) => TResult1 | PromiseLike<TResult1>)
|
|
475
|
+
| null
|
|
476
|
+
| undefined,
|
|
477
|
+
onrejected?:
|
|
478
|
+
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
|
479
|
+
| null
|
|
480
|
+
| undefined,
|
|
481
|
+
): PromiseLike<TResult1 | TResult2> {
|
|
482
|
+
return this.load().then(onfulfilled, onrejected);
|
|
483
|
+
}
|
|
484
|
+
}
|