@warp-drive/ember 0.0.0-alpha.3

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.md ADDED
@@ -0,0 +1,11 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (C) 2017-2023 Ember.js contributors
4
+ Portions Copyright (C) 2011-2017 Tilde, Inc. and contributors.
5
+ Portions Copyright (C) 2011 LivingSocial Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,525 @@
1
+ <p align="center">
2
+ <img
3
+ class="project-logo"
4
+ src="./NCC-1701-a-blue.svg#gh-light-mode-only"
5
+ alt="WarpDrive"
6
+ width="120px"
7
+ title="WarpDrive" />
8
+ <img
9
+ class="project-logo"
10
+ src="./NCC-1701-a.svg#gh-dark-mode-only"
11
+ alt="WarpDrive"
12
+ width="120px"
13
+ title="WarpDrive" />
14
+ </p>
15
+
16
+ <h3 align="center">:electron: Data utilities for using <em style="color: lightgreen">Warp</em><strong style="color: magenta">Drive</strong> with 🐹 <em style="color: orange">Ember</em><em style="color: lightblue">.js</em></h3>
17
+ <h4 align="center">And of course, <em style="color: orange">Ember</em><strong style="color: lightblue">Data</strong> too! </h4>
18
+
19
+ ---
20
+
21
+ ```cli
22
+ pnpm install @warp-drive/ember
23
+ ```
24
+
25
+ ## About
26
+
27
+ This library provides reactive utilities for working with promises and requests, building over these primitives to provide functions and components that enable you to build robust performant apps with elegant control flow
28
+
29
+ Documentation
30
+
31
+ - [PromiseState](#promisestate)
32
+ - [getPromiseState](#getpromisestate)
33
+ - [\<Await />](#await)
34
+ - [RequestState](#requeststate)
35
+ - [getRequestState](#getrequeststate)
36
+ - [\<Request />](#request)
37
+
38
+ ---
39
+
40
+ ## Why?
41
+
42
+ ### DX
43
+
44
+ Crafting a performant application experience is a creative art.
45
+
46
+ The data loading patterns that make for good DX are often at odds with the patterns that reduce fetch-waterfalls and loading times.
47
+
48
+ Fetching data from components *feels* right to most of us as developers. Being able to see
49
+ what we've queried right from the spot in which we will consume and use the response of the
50
+ query keeps the mental model clear and helps us iterate quickly.
51
+
52
+ But it also means that we have to render in order to know what to fetch, in order to know what to render, in order to know what to fetch and so on until the cycle eventually completes.
53
+
54
+ Thus, while on the surface providing superior DX, component based data-fetching patterns
55
+ sacrifice the user's experience for the developer's by encouraging a difficult-to-impossible
56
+ to optimize loading architecture.
57
+
58
+ It can also be tricky to pull off elegantly. Async/Await? Proxies? Resources? Generators?
59
+ Each has its own pitfalls when it comes to asynchronous data patterns in components and
60
+ crafting an experience that works well for both JavaScript and Templates is tough. And what
61
+ about work lifetimes?
62
+
63
+ This library helps you to craft great experiences without sacrificing DX. We still believe
64
+ you should load data based on user interactions and route navigations, not from components,
65
+ but what if you didn't need to use prop-drilling or contexts to access the result of a
66
+ route based query?
67
+
68
+ EmberData's RequestManager already allows for fulfillment from cache and for request
69
+ de-duping, so what if we could just pick up where we left off and use the result of a
70
+ request right away if it already was fetched elsewhere?
71
+
72
+ That brings us to our second motivation: performance.
73
+
74
+ ### Performance
75
+
76
+ Performance is always at the heart of WarpDrive libraries.
77
+
78
+ `@warp-drive/ember` isn't just a library of utilities for working with reactive
79
+ asynchronous data in your Ember app. It's *also* a way to optimize your app for
80
+ faster, more correct renders.
81
+
82
+ It starts with `setPromiseResult` a simple core primitive provided by the library
83
+ `@ember-data/request` that allows the result of any promise to be safely cached
84
+ without leaking memory. Results stashed with `setPromiseResult` can then be retrieved
85
+ via `getPromiseResult`. As long as the promise is in memory, the result will be too.
86
+
87
+ Every request made with `@ember-data/request` stashes its result in this way, and
88
+ the requests resolved from cache by the CacheHandler have their entry populated
89
+ syncronously. Consider the following code:
90
+
91
+ ```ts
92
+ const A = store.request({ url: '/users/1' });
93
+ const result = await A;
94
+ result.content.data.id; // '1'
95
+ const B = store.request({ url: '/user/1' });
96
+ ```
97
+
98
+ The above scenario is relatively common when a route, provider or previous location
99
+ in an app has loaded request A, and later something else triggers request B.
100
+
101
+ While it is true that `A !== B`, the magic of the RequestManager is that it is able
102
+ to safely stash the result of B such that the following works:
103
+
104
+ ```ts
105
+ const B = store.request({ url: '/user/1' });
106
+ const state = getPromiseResult(B);
107
+ state.result.content.data.id; // '1' 🤯
108
+ ```
109
+
110
+ Note how we can access the result of B even before we've awaited it? This is useful
111
+ for component rendering where we want to fetch data asynchronously, but when it is
112
+ immediately available the best possible result is to continue to render with the available
113
+ data without delay.
114
+
115
+ These primitives (`getPromiseResult` and `setPromiseResult`) are useful, but not all
116
+ that ergonomic on their own. They are also intentionally not reactive because they
117
+ are intended for use with *any* framework.
118
+
119
+ That's where `@warp-drive/ember` comes in. This library provides reactive utilities
120
+ for working with promises, building over these primitives to provide helpers, functions
121
+ and components that enable you to build robust performant app with elegant control flows.
122
+
123
+ ---
124
+
125
+ ## Documentation
126
+
127
+ ### PromiseState
128
+
129
+ PromiseState provides a reactive wrapper for a promise which allows you write declarative
130
+ code around a promise's control flow. It is useful in both Template and JavaScript contexts,
131
+ allowing you to quickly derive behaviors and data from pending, error and success states.
132
+
133
+ ```ts
134
+ interface PromiseState<T = unknown, E = unknown> {
135
+ isPending: boolean;
136
+ isSuccess: boolean;
137
+ isError: boolean;
138
+ result: T | null;
139
+ error: E | null;
140
+ }
141
+ ```
142
+
143
+ To get the state of a promise, use `getPromiseState`.
144
+
145
+ ### getPromiseState
146
+
147
+ `getPromiseState` can be used in both JavaScript and Template contexts.
148
+
149
+ ```ts
150
+ import { getPromiseState } from '@warp-drive/ember';
151
+
152
+ const state = getPromiseState(promise);
153
+ ```
154
+
155
+ For instance, we could write a getter on a component that updates whenever
156
+ the promise state advances or the promise changes, by combining the function
157
+ with the use of `@cached`
158
+
159
+ ```ts
160
+ class Component {
161
+ @cached
162
+ get title() {
163
+ const state = getPromiseState(this.args.request);
164
+ if (state.isPending) {
165
+ return 'loading...';
166
+ }
167
+ if (state.isError) { return null; }
168
+ return state.result.title;
169
+ }
170
+ }
171
+ ```
172
+
173
+ Or in a template as a helper:
174
+
175
+ ```gjs
176
+ import { getPromiseState } from '@warp-drive/ember';
177
+
178
+ <template>
179
+ {{#let (getPromiseState @request) as |state|}}
180
+ {{#if state.isPending}} <Spinner />
181
+ {{else if state.isError}} <ErrorForm @error={{state.error}} />
182
+ {{else}}
183
+ <h1>{{state.result.title}}</h1>
184
+ {{/if}}
185
+ {{/let}}
186
+ </template>
187
+ ```
188
+
189
+ #### \<Await />
190
+
191
+ Alternatively, use the `<Await>` component
192
+
193
+ ```gjs
194
+ import { Await } from '@warp-drive/ember';
195
+
196
+ <template>
197
+ <Await @promise={{@request}}>
198
+ <:pending>
199
+ <Spinner />
200
+ </:pending>
201
+
202
+ <:error as |error|>
203
+ <ErrorForm @error={{error}} />
204
+ </:error>
205
+
206
+ <:success as |result|>
207
+ <h1>{{result.title}}</h1>
208
+ </:success>
209
+ </Await>
210
+ </template>
211
+ ```
212
+
213
+ ### RequestState
214
+
215
+ RequestState extends PromiseState to provide a reactive wrapper for a request `Future` which
216
+ allows you write declarative code around a Future's control flow. It is useful in both Template
217
+ and JavaScript contexts, allowing you to quickly derive behaviors and data from pending, error
218
+ and success states.
219
+
220
+ The key difference between a Promise and a Future is that Futures provide access to a stream
221
+ of their content, as well as the ability to attempt to abort the request.
222
+
223
+ ```ts
224
+ interface Future<T> extends Promise<T>> {
225
+ getStream(): Promise<ReadableStream>;
226
+ abort(): void;
227
+ }
228
+ ```
229
+
230
+ These additional APIs allow us to craft even richer state experiences.
231
+
232
+
233
+ ```ts
234
+ interface RequestState<T = unknown, E = unknown> extends PromiseState<T, E> {
235
+ isCancelled: boolean;
236
+
237
+ // TODO detail out percentage props
238
+ }
239
+ ```
240
+
241
+ To get the state of a request, use `getRequestState`.
242
+
243
+ ### getRequestState
244
+
245
+ `getRequestState` can be used in both JavaScript and Template contexts.
246
+
247
+ ```ts
248
+ import { getRequestState } from '@warp-drive/ember';
249
+
250
+ const state = getRequestState(future);
251
+ ```
252
+
253
+ For instance, we could write a getter on a component that updates whenever
254
+ the request state advances or the future changes, by combining the function
255
+ with the use of `@cached`
256
+
257
+ ```ts
258
+ class Component {
259
+ @cached
260
+ get title() {
261
+ const state = getRequestState(this.args.request);
262
+ if (state.isPending) {
263
+ return 'loading...';
264
+ }
265
+ if (state.isError) { return null; }
266
+ return state.result.title;
267
+ }
268
+ }
269
+ ```
270
+
271
+ Or in a template as a helper:
272
+
273
+ ```gjs
274
+ import { getRequestState } from '@warp-drive/ember';
275
+
276
+ <template>
277
+ {{#let (getRequestState @request) as |state|}}
278
+ {{#if state.isPending}} <Spinner />
279
+ {{else if state.isError}} <ErrorForm @error={{state.error}} />
280
+ {{else}}
281
+ <h1>{{state.result.title}}</h1>
282
+ {{/if}}
283
+ {{/let}}
284
+ </template>
285
+ ```
286
+
287
+ #### \<Request />
288
+
289
+ Alternatively, use the `<Request>` component. Note: the request component
290
+ taps into additional capabilities *beyond* what `RequestState` offers.
291
+
292
+ - Completion states and an abort function are available as part of loading state
293
+
294
+ ```gjs
295
+ import { Request } from '@warp-drive/ember';
296
+
297
+ <template>
298
+ <Request @request={{@request}}>
299
+ <:loading as |state|>
300
+ <Spinner @percentDone={{state.completeRatio}} />
301
+ <button {{on "click" state.abort}}>Cancel</button>
302
+ </:loading>
303
+
304
+ <:error as |error|>
305
+ <ErrorForm @error={{error}} />
306
+ </:error>
307
+
308
+ <:content as |result|>
309
+ <h1>{{result.title}}</h1>
310
+ </:content>
311
+ </Request>
312
+ </template>
313
+ ```
314
+
315
+ - Streaming Data
316
+
317
+ The loading state exposes the download `ReadableStream` instance for consumption
318
+
319
+ ```gjs
320
+ import { Request } from '@warp-drive/ember';
321
+
322
+ <template>
323
+ <Request @request={{@request}}>
324
+ <:loading as |state|>
325
+ <Video @stream={{state.stream}} />
326
+ </:loading>
327
+
328
+ <:error as |error|>
329
+ <ErrorForm @error={{error}} />
330
+ </:error>
331
+ </Request>
332
+ </template>
333
+ ```
334
+
335
+ - Cancelled is an additional state.
336
+
337
+ ```gjs
338
+ import { Request } from '@warp-drive/ember';
339
+
340
+ <template>
341
+ <Request @request={{@request}}>
342
+ <:cancelled>
343
+ <h2>The Request Cancelled</h2>
344
+ </:cancelled>
345
+
346
+ <:error as |error|>
347
+ <ErrorForm @error={{error}} />
348
+ </:error>
349
+
350
+ <:content as |result|>
351
+ <h1>{{result.title}}</h1>
352
+ </:content>
353
+ </Request>
354
+ </template>
355
+ ```
356
+
357
+ If a request is aborted but no cancelled block is present, the error will be given
358
+ to the error block to handle.
359
+
360
+ If no error block is present, the error will be rethrown.
361
+
362
+ - Reloading states
363
+
364
+ Reload will reset the request state, and so reuse the error, cancelled, and loading
365
+ blocks as appropriate.
366
+
367
+ Background reload (refresh) is a special substate of `content` that can be entered while
368
+ existing content is still shown.
369
+
370
+ Both reload and background reload are available as methods that can be invoked from
371
+ within `content`. Background reload's can also be aborted.
372
+
373
+ ```gjs
374
+ import { Request } from '@warp-drive/ember';
375
+
376
+ <template>
377
+ <Request @request={{@request}}>
378
+ <:cancelled>
379
+ <h2>The Request Cancelled</h2>
380
+ </:cancelled>
381
+
382
+ <:error as |error|>
383
+ <ErrorForm @error={{error}} />
384
+ </:error>
385
+
386
+ <:content as |result state|>
387
+ {{#if state.isBackgroundReloading}}
388
+ <SmallSpinner />
389
+ <button {{on "click" state.abort}}>Cancel</button>
390
+ {{/if}}
391
+
392
+ <h1>{{result.title}}</h1>
393
+
394
+ <button {{on "click" state.refresh}}>Refresh</button>
395
+ <button {{on "click" state.reload}}>Reload</button>
396
+ </:content>
397
+ </Request>
398
+ </template>
399
+ ```
400
+
401
+ Usage of request can be nested for more advanced handling of background reload
402
+
403
+ ```gjs
404
+ import { Request } from '@warp-drive/ember';
405
+
406
+ <template>
407
+ <Request @request={{@request}}>
408
+ <:cancelled>
409
+ <h2>The Request Cancelled</h2>
410
+ </:cancelled>
411
+
412
+ <:error as |error|>
413
+ <ErrorForm @error={{error}} />
414
+ </:error>
415
+
416
+ <:content as |result state|>
417
+ <Request @request={{state.latestRequest}}>
418
+ <!-- Handle Background Request -->
419
+ </Request>
420
+
421
+ <h1>{{result.title}}</h1>
422
+
423
+ <button {{on "click" state.refresh}}>Refresh</button>
424
+ </:content>
425
+ </Request>
426
+ </template>
427
+ ```
428
+
429
+ - AutoRefresh behavior
430
+
431
+ Requests can be made to automatically refresh when a browser window or tab comes back to the
432
+ foreground after being backgrounded.
433
+
434
+ ```gjs
435
+ import { Request } from '@warp-drive/ember';
436
+
437
+ <template>
438
+ <Request @request={{@request}} @autoRefresh={{true}}>
439
+ <!-- ... -->
440
+ </Request>
441
+ </template>
442
+ ```
443
+
444
+ Similarly, refresh could be set up on a timer or on a websocket subscription by using the yielded
445
+ refresh function and passing it to another component.
446
+
447
+ ```gjs
448
+ import { Request } from '@warp-drive/ember';
449
+
450
+ <template>
451
+ <Request @request={{@request}} @autoRefresh={{true}}>
452
+ <:content as |result state|>
453
+ <h1>{{result.title}}</h1>
454
+
455
+ <Interval @period={{30_000}} @fn={{state.refresh}} />
456
+ <Subscribe @channel={{@someValue}} @fn={{state.refresh}} />
457
+ </:content>
458
+ </Request>
459
+ </template>
460
+ ```
461
+
462
+ If a matching request is refreshed or reloaded by any other component, the `Request` component will react accordingly.
463
+
464
+
465
+ ---
466
+
467
+ ### ♥️ Credits
468
+
469
+ <details>
470
+ <summary>Brought to you with ♥️ love by <a href="https://emberjs.com" title="EmberJS">🐹 Ember</a></summary>
471
+
472
+ <style type="text/css">
473
+ img.project-logo {
474
+ padding: 0 5em 1em 5em;
475
+ width: 100px;
476
+ border-bottom: 2px solid #0969da;
477
+ margin: 0 auto;
478
+ display: block;
479
+ }
480
+ details > summary {
481
+ font-size: 1.1rem;
482
+ line-height: 1rem;
483
+ margin-bottom: 1rem;
484
+ }
485
+ details {
486
+ font-size: 1rem;
487
+ }
488
+ details > summary strong {
489
+ display: inline-block;
490
+ padding: .2rem 0;
491
+ color: #000;
492
+ border-bottom: 3px solid #0969da;
493
+ }
494
+
495
+ details > details {
496
+ margin-left: 2rem;
497
+ }
498
+ details > details > summary {
499
+ font-size: 1rem;
500
+ line-height: 1rem;
501
+ margin-bottom: 1rem;
502
+ }
503
+ details > details > summary strong {
504
+ display: inline-block;
505
+ padding: .2rem 0;
506
+ color: #555;
507
+ border-bottom: 2px solid #555;
508
+ }
509
+ details > details {
510
+ font-size: .85rem;
511
+ }
512
+
513
+ @media (prefers-color-scheme: dark) {
514
+ details > summary strong {
515
+ color: #fff;
516
+ }
517
+ }
518
+ @media (prefers-color-scheme: dark) {
519
+ details > details > summary strong {
520
+ color: #afaba0;
521
+ border-bottom: 2px solid #afaba0;
522
+ }
523
+ }
524
+ </style>
525
+ </details>