Skip to content

Commit 4d506bd

Browse files
committed
feat: add once prop for single reads
1 parent fb38431 commit 4d506bd

File tree

7 files changed

+113
-39
lines changed

7 files changed

+113
-39
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,33 @@ You can bypass the loading state entirely by passing a `startWith` prop.
226226
</Doc>
227227
```
228228

229+
### Reactive
229230

231+
Components are reactive. When the input props change, they unsubscribe from the last stream and start a new one.
232+
233+
Example: Collections have a special slot props for pagination called `first` and `last`. Use them to create reactive pagination queries.
234+
235+
236+
```html
237+
<script>
238+
let query = (ref) => ref.orderBy('flavor').limit(3)
239+
240+
function nextPage(last) {
241+
query = (ref) => ref.orderBy('flavor').startAfter(last.flavor).limit(3);
242+
}
243+
</script>
244+
245+
<Collection path={'foods'} {query} let:data let:last>
246+
247+
{#each data as food}
248+
{food.name}
249+
{/each}
250+
251+
<button on:click={() => nextPage(last) }>Next</button>
252+
253+
254+
</Collection>
255+
```
230256

231257
### Events
232258

@@ -314,6 +340,7 @@ Props:
314340
- *path (required)* - Path to document as `string` OR a DocumentReference i.e `db.doc('path')`
315341
- *startWith* any value. Bypasses loading state.
316342
- *maxWait* `number` milliseconds to wait before showing fallback slot if nothing is returned. Default 10000.
343+
- *once* single read execution, no realtime updates. Default `false`.
317344
- *log* debugging info to the console. Default `false`.
318345
- *traceId* `string` name that runs a Firebase Performance trace for latency.
319346

@@ -361,6 +388,7 @@ Props:
361388
- *query* `function`, i.e (ref) => ref.where('age, '==', 23)
362389
- *startWith* any value. Bypasses loading state.
363390
- *maxWait* `number` milliseconds to wait before showing fallback slot if nothing is returned. Default 10000.
391+
- *once* single read execution, no realtime updates. Default `false`.
364392
- *log* debugging info to the console. Default `false`.
365393
- *traceId* `string` name that runs a Firebase Performance trace for latency.
366394

cypress/integration/main.spec.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('SvelteFire app', function() {
1818
cy.contains('Sign In').click();
1919
cy.contains('UID').should('exist');
2020
cy.contains('Sign Out').should('exist');
21-
cy.wait(15000); // Emulator takes long time to respond here...
21+
cy.wait(7000); // Emulator takes long time to respond here...
2222
})
2323

2424
it('fetches a document from Firestore', function() {
@@ -46,10 +46,17 @@ describe('SvelteFire app', function() {
4646
cy.get('#posts').children().should('be.have.length', 1);
4747
})
4848

49+
it('stops listening to updates when once prop is true', function() {
50+
cy.contains('Once-Doc').should('exist')
51+
cy.contains('Try to Delete').click();
52+
cy.contains('Once-Doc').should('exist');
53+
})
54+
4955
it('listens to events from a parent component', function() {
5056
cy.contains('Path: posts/event-post').should('exist')
5157
cy.contains('Event Data').should('exist');
5258
cy.contains('Update Event').click();
5359
cy.contains('My Data Changed').should('exist');
5460
})
61+
5562
})

example/src/App.svelte

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script>
22
export let name;
33
import { writable } from "svelte/store";
4+
import { onMount } from "svelte";
45
import { FirebaseApp, Doc, Collection, User } from "sveltefire";
56
67
import firebase from "firebase/app";
@@ -22,6 +23,7 @@
2223
2324
firebase.initializeApp(config);
2425
26+
2527
let appName;
2628
let eventData = {};
2729
let eventRef = {};
@@ -33,8 +35,8 @@
3335
eventRef = e.detail.ref;
3436
}
3537
36-
const useEmulator = async e => {
37-
const firebase = e.detail.firebase;
38+
async function useEmulator() {
39+
// const firebase = e.detail.firebase;
3840
3941
if (location.hostname === "localhost") {
4042
const db = firebase.firestore();
@@ -43,17 +45,20 @@
4345
4446
appName = firebase.app().name;
4547
46-
await db.doc("posts/slow-post").delete();
48+
db.doc("posts/slow-post").delete();
4749
48-
await db.doc("posts/event-post").set({ title: "Event Post" });
50+
db.doc("posts/event-post").set({ title: "Event Post" });
51+
db.doc("posts/once").set({ title: "Once-Doc" });
4952
5053
setTimeout(() => {
5154
console.log();
5255
db.doc("posts/slow").set({ title: "Slowness" });
53-
}, 7000);
56+
}, 5000);
5457
5558
}
5659
};
60+
61+
onMount(useEmulator)
5762
</script>
5863

5964
<style>
@@ -158,6 +163,20 @@
158163
<button on:click={() => slowRef.delete()}>X</button>
159164
</Doc>
160165

166+
<h3>One-Time Reads</h3>
167+
168+
<Doc
169+
path={`posts/once`}
170+
let:data={onceData}
171+
let:ref={onceRef}
172+
once>
173+
174+
{onceData.title}
175+
176+
<button on:click={() => onceRef.delete()}>Try to Delete</button>
177+
</Doc>
178+
179+
161180
<h2>Events</h2>
162181

163182
<p>Path: {eventRef.path}</p>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.0.5",
2+
"version": "0.0.6",
33
"name": "sveltefire",
44
"svelte": "src/index.js",
55
"main": "dist/index.js",

src/Collection.svelte

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@
55
export let log = false;
66
export let startWith = undefined;
77
export let maxWait = 10000;
8+
export let once = false;
89
9-
import { onDestroy, createEventDispatcher } from "svelte";
10+
import { onDestroy, onMount, createEventDispatcher } from "svelte";
1011
1112
import { collectionStore } from "./firestore";
1213
13-
let store = collectionStore(path, query, {
14+
const opts = {
1415
startWith,
1516
traceId,
1617
log,
17-
maxWait
18-
});
18+
maxWait,
19+
once
20+
}
21+
22+
let store = collectionStore(path, query, opts);
1923
2024
const dispatch = createEventDispatcher();
2125
@@ -25,28 +29,23 @@
2529
$: {
2630
if (unsub) {
2731
unsub();
28-
store = collectionStore(path, query, {
29-
startWith,
30-
traceId,
31-
log,
32-
maxWait
33-
});
32+
store = collectionStore(path, query, opts);
33+
dispatch("ref", { ref: store.ref });
3434
}
3535
36-
dispatch("ref", { ref: store.ref });
37-
3836
unsub = store.subscribe(data => {
3937
dispatch("data", {
4038
data
4139
});
4240
});
4341
}
4442
43+
onMount(() => dispatch("ref", { ref: store.ref }))
4544
onDestroy(() => unsub());
4645
</script>
4746

4847
{#if $store}
49-
<slot data={$store} ref={store.ref} error={store.error} />
48+
<slot data={$store} ref={store.ref} error={store.error} first={store.meta.first} last={store.meta.last} />
5049
{:else if store.loading}
5150
<slot name="loading" />
5251
{:else}

src/Doc.svelte

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44
export let traceId = "";
55
export let startWith = undefined; // Why? Firestore returns null for docs that don't exist, predictible loading state.
66
export let maxWait = 10000;
7+
export let once = false;
78
8-
import { onDestroy, createEventDispatcher } from "svelte";
9+
import { onDestroy, onMount, createEventDispatcher } from "svelte";
910
import { docStore } from "./firestore";
1011
11-
let store = docStore(path, {
12+
const opts = {
1213
startWith,
1314
traceId,
1415
log,
15-
maxWait
16-
});
16+
maxWait,
17+
once
18+
}
19+
20+
let store = docStore(path, opts);
1721
1822
const dispatch = createEventDispatcher();
1923
@@ -22,24 +26,20 @@
2226
// Props changed
2327
$: {
2428
if (unsub) {
29+
// Unsub and create new store
2530
unsub();
26-
store = docStore(path, {
27-
startWith,
28-
traceId,
29-
log,
30-
maxWait
31-
});
31+
store = docStore(path, opts);
32+
dispatch("ref", { ref: store.ref });
3233
}
3334
34-
dispatch("ref", { ref: store.ref });
35-
3635
unsub = store.subscribe(data => {
3736
dispatch("data", {
3837
data
3938
});
4039
});
4140
}
4241
42+
onMount(() => dispatch("ref", { ref: store.ref }))
4343
onDestroy(() => unsub());
4444
</script>
4545

src/firestore.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@ import { writable } from 'svelte/store';
22
import { assertApp } from './helpers';
33
import { startTrace, stopTrace } from './perf';
44

5-
6-
// Note. Doc without data flows from undefined -> null,
7-
85
// Svelte Store for Firestore Document
96
export function docStore(path, opts) {
107
const firestore = assertApp('firestore');
118

12-
const { startWith, log, traceId, maxWait } = { maxWait: 10000, ...opts };
13-
9+
const { startWith, log, traceId, maxWait, once } = { maxWait: 10000, ...opts };
1410

11+
// Create the Firestore Reference
1512
const ref = typeof path === 'string' ? firestore.doc(path) : path;
13+
14+
// Performance trace
1615
const trace = traceId && startTrace(traceId);
1716

1817
// Internal state
@@ -38,18 +37,27 @@ export function docStore(path, opts) {
3837
// Timout for fallback slot
3938
_waitForIt = maxWait && setTimeout(() => _loading && next(null, new Error(`Timeout at ${maxWait}. Using fallback slot.`) ), maxWait)
4039

40+
// Realtime firebase subscription
4141
_teardown = ref.onSnapshot(
4242
snapshot => {
4343
const data = snapshot.data() || startWith || null;
44+
45+
// Optional logging
4446
if (log) {
4547
console.groupCollapsed(`Doc ${snapshot.id}`);
4648
console.log(`Path: ${ref.path}`);
4749
console.log('Snapshot:', snapshot);
4850
console.groupEnd();
4951
}
5052

53+
// Emit next value
5154
next(data);
55+
56+
// Teardown after first emitted value if once
57+
once && _teardown();
5258
},
59+
60+
// Handle firebase thrown errors
5361
error => {
5462
console.error(error);
5563
next(null, error);
@@ -60,6 +68,7 @@ export function docStore(path, opts) {
6068
return () => _teardown();
6169
};
6270

71+
// Svelte store
6372
const store = writable(startWith, start);
6473

6574
const { subscribe, set } = store;
@@ -81,10 +90,10 @@ export function docStore(path, opts) {
8190
export function collectionStore(path, queryFn, opts) {
8291
const firestore = assertApp('firestore');
8392

84-
const { startWith, log, traceId, idField, refField, maxWait } = {
93+
const { startWith, log, traceId, maxWait, once, idField, refField } = {
8594
idField: 'id',
8695
refField: 'ref',
87-
maxWait: 10000,
96+
maxWait: 10000,
8897
...opts
8998
};
9099

@@ -94,13 +103,21 @@ export function collectionStore(path, queryFn, opts) {
94103

95104
let _loading = typeof startWith !== undefined;
96105
let _error = null;
106+
let _meta = {};
97107
let _teardown;
98108
let _waitForIt;
99109

110+
// Metadata for result
111+
const calcMeta = (val) => {
112+
return val && val.length ?
113+
{ first: val[0], last: val[val.length - 1] } : {}
114+
}
115+
100116
const next = (val, err) => {
101117
_loading = false;
102118
_waitForIt && clearTimeout(_waitForIt);
103119
_error = err || null;
120+
_meta = calcMeta(val);
104121
set(val);
105122
trace && stopTrace(trace);
106123
};
@@ -127,6 +144,7 @@ export function collectionStore(path, queryFn, opts) {
127144
console.groupEnd();
128145
}
129146
next(data);
147+
once && _teardown();
130148
},
131149

132150
error => {
@@ -150,6 +168,9 @@ export function collectionStore(path, queryFn, opts) {
150168
},
151169
get error() {
152170
return _error;
171+
},
172+
get meta() {
173+
return _meta;
153174
}
154175
};
155176
}

0 commit comments

Comments
 (0)