Line data Source code
1 : part of flutter_data;
2 :
3 : const _offlineAdapterKey = '_offline:keys';
4 :
5 : /// Represents an offline request that is pending to be retried.
6 : class OfflineOperation<T extends DataModel<T>> with EquatableMixin {
7 : final DataRequestLabel label;
8 : final String httpRequest;
9 : final Map<String, String>? headers;
10 : final String? body;
11 : late final String? key;
12 : final _OnSuccessGeneric<T>? onSuccess;
13 : final _OnErrorGeneric<T>? onError;
14 : final RemoteAdapter<T> adapter;
15 :
16 1 : OfflineOperation({
17 : required this.label,
18 : required this.httpRequest,
19 : this.headers,
20 : this.body,
21 : String? key,
22 : this.onSuccess,
23 : this.onError,
24 : required this.adapter,
25 : }) {
26 4 : this.key = key ?? label.model?._key;
27 : }
28 :
29 : /// Metadata format:
30 : /// _offline:findOne/users#3@d7bcc9
31 12 : static String metadataFor(DataRequestLabel label) => '_offline:$label';
32 :
33 1 : Map<String, dynamic> toJson() {
34 1 : return <String, dynamic>{
35 1 : 'r': httpRequest,
36 1 : 'k': key,
37 1 : 'b': body,
38 1 : 'h': headers,
39 : };
40 : }
41 :
42 1 : factory OfflineOperation.fromJson(
43 : DataRequestLabel label,
44 : Map<String, dynamic> json,
45 : RemoteAdapter<T> adapter,
46 : ) {
47 1 : final operation = OfflineOperation(
48 : label: label,
49 1 : httpRequest: json['r'] as String,
50 1 : key: json['k'] as String?,
51 1 : body: json['b'] as String?,
52 : headers:
53 3 : json['h'] == null ? null : Map<String, String>.from(json['h'] as Map),
54 : adapter: adapter,
55 : );
56 :
57 1 : if (operation.key != null) {
58 3 : final model = adapter.localAdapter.findOne(operation.key!);
59 : if (model != null) {
60 : // adapter.initializeModel(model, key: operation.key);
61 2 : operation.label.model = model;
62 : }
63 : }
64 : return operation;
65 : }
66 :
67 1 : Uri get uri {
68 4 : return httpRequest.split(' ').last.asUri;
69 : }
70 :
71 1 : DataRequestMethod get method {
72 : return DataRequestMethod.values
73 7 : .singleWhere((m) => m.toShortString() == httpRequest.split(' ').first);
74 : }
75 :
76 : /// Adds an edge from the `_offlineAdapterKey` to the `key` for save/delete
77 : /// and stores header/param metadata. Also stores callbacks.
78 1 : void add() {
79 : // DO NOT proceed if operation is in queue
80 3 : if (!adapter.offlineOperations.contains(this)) {
81 2 : final node = json.encode(toJson());
82 2 : final metadata = metadataFor(label);
83 :
84 4 : adapter.log(label, 'offline/add $metadata');
85 3 : adapter.graph._addEdge(_offlineAdapterKey, node, metadata: metadata);
86 :
87 : // keep callbacks in memory
88 6 : adapter.read(_offlineCallbackProvider)[metadata] ??= [];
89 1 : adapter
90 4 : .read(_offlineCallbackProvider)[metadata]!
91 4 : .add([onSuccess, onError]);
92 : } else {
93 : // trick
94 2 : adapter.graph
95 2 : ._notify([_offlineAdapterKey, ''], type: DataGraphEventType.addEdge);
96 : }
97 : }
98 :
99 : /// Removes all edges from the `_offlineAdapterKey` for
100 : /// current metadata, as well as callbacks from memory.
101 6 : static void remove(DataRequestLabel label, RemoteAdapter adapter) {
102 6 : final metadata = metadataFor(label);
103 12 : if (adapter.graph._hasEdge(_offlineAdapterKey, metadata: metadata)) {
104 2 : adapter.graph._removeEdges(_offlineAdapterKey, metadata: metadata);
105 2 : adapter.log(label, 'offline/remove $metadata');
106 4 : adapter.read(_offlineCallbackProvider).remove(metadata);
107 : }
108 : }
109 :
110 1 : Future<void> retry() async {
111 2 : final metadata = metadataFor(label);
112 : // look up callbacks (or provide defaults)
113 5 : final fns = adapter.read(_offlineCallbackProvider)[metadata] ??
114 0 : [
115 0 : [null, null]
116 : ];
117 :
118 2 : for (final pair in fns) {
119 3 : await adapter.sendRequest<T>(
120 1 : uri,
121 1 : method: method,
122 1 : headers: headers,
123 1 : label: label,
124 1 : body: body,
125 1 : onSuccess: pair.first as _OnSuccessGeneric<T>?,
126 1 : onError: pair.last as _OnErrorGeneric<T>?,
127 : );
128 : }
129 : }
130 :
131 3 : T? get model => label.model as T?;
132 :
133 1 : @override
134 5 : List<Object?> get props => [label, httpRequest, body, headers];
135 :
136 0 : @override
137 : bool get stringify => true;
138 : }
139 :
140 : extension OfflineOperationsX on Set<OfflineOperation<DataModel>> {
141 : /// Retries all offline operations for current type.
142 1 : FutureOr<void> retry() async {
143 1 : if (isNotEmpty) {
144 4 : await Future.wait(map((operation) {
145 1 : return operation.retry();
146 : }));
147 : }
148 : }
149 :
150 : /// Removes all offline operations.
151 1 : void reset() {
152 1 : if (isEmpty) {
153 : return;
154 : }
155 2 : final adapter = first.adapter;
156 : // removes node and severs edges
157 2 : if (adapter.graph._hasNode(_offlineAdapterKey)) {
158 2 : final node = adapter.graph._getNode(_offlineAdapterKey);
159 3 : for (final metadata in (node ?? {}).keys.toImmutableList()) {
160 2 : adapter.graph._removeEdges(_offlineAdapterKey, metadata: metadata);
161 : }
162 : }
163 4 : adapter.read(_offlineCallbackProvider).clear();
164 : }
165 :
166 : /// Filter by [label] kind.
167 1 : List<OfflineOperation> only(DataRequestLabel label) {
168 7 : return where((_) => _.label.kind == label.kind).toImmutableList();
169 : }
170 : }
171 :
172 : // stores onSuccess/onError function combos
173 2 : final _offlineCallbackProvider =
174 3 : StateProvider<Map<String, List<List<Function?>>>>((_) => {});
175 :
176 : /// Every time there is an offline operation added to/
177 : /// removed from the queue, this will notify clients
178 : /// with all pending types (could be none) such that
179 : /// they can implement their own retry strategy.
180 0 : final pendingOfflineTypesProvider =
181 0 : StateNotifierProvider<DelayedStateNotifier<Set<String>>, Set<String>?>(
182 0 : (ref) {
183 0 : final graph = ref.watch(graphNotifierProvider);
184 :
185 0 : Set<String> _pendingTypes() {
186 0 : final node = graph._getNode(_offlineAdapterKey, orAdd: true)!;
187 : // obtain types from metadata e.g. _offline:users#4:findOne
188 0 : return node.keys.map((m) => m.split(':')[1].split('#')[0]).toSet();
189 : }
190 :
191 0 : final notifier = DelayedStateNotifier<Set<String>>();
192 : // emit initial value
193 0 : Timer.run(() {
194 0 : if (notifier.mounted) {
195 0 : notifier.state = _pendingTypes();
196 : }
197 : });
198 :
199 0 : final dispose = graph.where((event) {
200 : // filter the right events
201 0 : return [DataGraphEventType.addEdge, DataGraphEventType.removeEdge]
202 0 : .contains(event.type) &&
203 0 : event.keys.length == 2 &&
204 0 : event.keys.containsFirst(_offlineAdapterKey);
205 0 : }).addListener((_) {
206 0 : if (notifier.mounted) {
207 : // recalculate all pending types
208 0 : notifier.state = _pendingTypes();
209 : }
210 : });
211 :
212 0 : notifier.onDispose = dispose;
213 :
214 : return notifier;
215 : });
|