Skip to content

Commit 1a819bf

Browse files
committed
fix the temp record workflow
When you call new Todo(), a temp record is created. The temp record contains a temporary `__id` (note the double underscore) and a `__isTemp` attribute. (These attributes can be discarded in a before hook so they don’t get sent to the server.) To assist in Vue reactivity, this temp record is updated with the API response. The record will keep the local `__id` for building components that recognize the change from temp to permanent record. Records will no longer have the `__isTemp` attribute. To keep this from being a breaking change, a few new mutation were created. They can be found in the `addOrUpdate` service action: - `remove__isTemp` does just what it says. - `replaceItemWithTemp` Adds or updates an item. If a matching temp record is found in the store, the temp record will completely replace the existingItem. This is to work around the common scenario where the realtime `created` event arrives before the `create` response returns to create the record. The reference to the original temporary record must be maintained in order to preserve reactivity.
1 parent 290a067 commit 1a819bf

File tree

6 files changed

+140
-12
lines changed

6 files changed

+140
-12
lines changed

src/service-module/service-module.actions.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,14 @@ export default function makeServiceActions(service) {
106106
})
107107
} else {
108108
const id = getId(response, idField)
109+
const tempId = tempIds[0]
109110

110-
await dispatch('addOrUpdate', response)
111+
if (id != null && tempId != null) {
112+
commit('updateTemp', { id, tempId })
113+
}
114+
response = await dispatch('addOrUpdate', response)
111115

112-
response = state.keyedById[id]
116+
// response = state.keyedById[id]
113117
}
114118
commit('unsetPending', 'create')
115119
commit('removeTemps', tempIds)
@@ -204,7 +208,10 @@ export default function makeServiceActions(service) {
204208
* Feathers client. The client modifies the params object.
205209
* @param response
206210
*/
207-
async handleFindResponse({ state, commit, dispatch }, { params, response }) {
211+
async handleFindResponse(
212+
{ state, commit, dispatch },
213+
{ params, response }
214+
) {
208215
const { qid = 'default', query } = params
209216
const { idField } = state
210217

@@ -289,24 +296,40 @@ export default function makeServiceActions(service) {
289296
return response
290297
},
291298

299+
/**
300+
* Adds or updates an item. If a matching temp record is found in the store,
301+
* the temp record will completely replace the existingItem. This is to work
302+
* around the common scenario where the realtime `created` event arrives before
303+
* the `create` response returns to create the record. The reference to the
304+
* original temporary record must be maintained in order to preserve reactivity.
305+
*/
292306
async addOrUpdate({ state, commit }, item) {
293307
const { idField } = state
294308
let id = getId(item, idField)
295309
let existingItem = state.keyedById[id]
296310

297311
const isIdOk = id !== null && id !== undefined
298312

299-
if (
300-
service.FeathersVuexModel &&
301-
!item.isFeathersVuexInstance
302-
) {
313+
if (service.FeathersVuexModel && !item.isFeathersVuexInstance) {
303314
item = new service.FeathersVuexModel(item)
304315
}
305316

317+
// If the item has a matching temp, update the temp and provide it as the new item.
318+
const temp = state.tempsByNewId[id]
319+
if (temp) {
320+
commit('merge', { dest: temp, source: item })
321+
commit('remove__isTemp', temp)
322+
}
306323
if (isIdOk) {
307-
existingItem ? commit('updateItem', item) : commit('addItem', item)
324+
if (existingItem && temp) {
325+
commit('replaceItemWithTemp', { item, temp })
326+
} else {
327+
existingItem
328+
? commit('updateItem', temp || item)
329+
: commit('addItem', temp || item)
330+
}
308331
}
309-
return item
332+
return temp || item
310333
}
311334
}
312335
/**

src/service-module/service-module.mutations.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,32 @@ export default function makeServiceMutations() {
143143
updateItems(state, items)
144144
},
145145

146+
// Adds an _id to a temp record so that that the addOrUpdate action
147+
// can migrate the temp to the keyedById state.
148+
updateTemp(state, { id, tempId }) {
149+
const temp = state.tempsById[tempId]
150+
if (state.tempsById) {
151+
temp[state.idField] = id
152+
state.tempsByNewId[id] = temp
153+
}
154+
},
155+
156+
/**
157+
* Overwrites the item with matching id with the temp record.
158+
* This is to preserve reactivity for temp records.
159+
*/
160+
replaceItemWithTemp(state, { item, temp }) {
161+
const id = item[state.idField]
162+
if (state.keyedById[id]) {
163+
state.keyedById[id] = temp
164+
Vue.delete(state.keyedById[id], '__isTemp')
165+
}
166+
},
167+
168+
remove__isTemp(state, temp) {
169+
Vue.delete(temp, '__isTemp')
170+
},
171+
146172
removeItem(state, item) {
147173
const { idField } = state
148174
const idToBeRemoved = _isObject(item) ? getId(item, idField) : item
@@ -155,8 +181,18 @@ export default function makeServiceMutations() {
155181
}
156182
},
157183

158-
// Removes temp records
184+
// Removes temp records. Also cleans up tempsByNewId
159185
removeTemps(state, tempIds) {
186+
const ids = tempIds.reduce((ids, id) => {
187+
const temp = state.tempsById[id]
188+
if (temp && temp[state.idField]) {
189+
delete temp.__isTemp
190+
Vue.delete(temp, '__isTemp')
191+
ids.push(temp[state.idField])
192+
}
193+
return ids
194+
}, [])
195+
state.tempsByNewId = _omit(state.tempsByNewId, ids)
160196
state.tempsById = _omit(state.tempsById, tempIds)
161197
},
162198

src/service-module/service-module.state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export default function makeDefaultState(servicePath, options) {
2323
ids: [],
2424
keyedById: {},
2525
copiesById: {},
26-
tempsById: {},
26+
tempsById: {}, // Really should be called tempsByTempId
27+
tempsByNewId: {}, // temporary storage for temps while getting transferred from tempsById to keyedById
2728
pagination: {
2829
defaultLimit: null,
2930
defaultSkip: null

test/service-module/make-service-plugin.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ describe('makeServicePlugin', function() {
8787
servicePath: 'todos',
8888
skipRequestIfExists: false,
8989
tempsById: {},
90+
tempsByNewId: {},
9091
whitelist: []
9192
}
9293

test/service-module/model-temp-ids.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ObjectID from 'bson-objectid'
1515

1616
interface RootState {
1717
transactions: ServiceState
18+
things: ServiceState
1819
}
1920

2021
class ComicService extends MemoryService {
@@ -50,6 +51,7 @@ function makeContext() {
5051
}
5152
}
5253
const store = new Vuex.Store({
54+
strict: true,
5355
plugins: [
5456
makeServicePlugin({
5557
Model: Comic,
@@ -147,7 +149,71 @@ describe('Models - Temp Ids', function() {
147149
assert(store.state.transactions.tempsById[txn.__id], 'it is in the store')
148150
})
149151

150-
it('clones into Model.copiesById', function () {
152+
it('maintains reference to temp item after save', function() {
153+
const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, {
154+
idField: '_id',
155+
serverAlias: 'temp-ids'
156+
})
157+
class Thing extends BaseModel {
158+
public static modelName = 'Thing'
159+
public constructor(data?, options?) {
160+
super(data, options)
161+
}
162+
}
163+
const store = new Vuex.Store<RootState>({
164+
plugins: [
165+
makeServicePlugin({
166+
Model: Thing,
167+
service: feathersClient.service('things')
168+
})
169+
]
170+
})
171+
172+
// Manually set the result in a hook to simulate the server request.
173+
feathersClient.service('things').hooks({
174+
before: {
175+
create: [
176+
// Testing removing the __id and __isTemp so they're not sent to the server.
177+
context => {
178+
delete context.data.__id
179+
delete context.data.__isTemp
180+
},
181+
context => {
182+
assert(!context.data.__id, '__id was not sent to API server')
183+
assert(!context.data.__id, '__isTemp was not sent to API server')
184+
context.result = {
185+
id: 1,
186+
description: 'Robb Wolf - the Paleo Solution',
187+
website:
188+
'https://robbwolf.com/shop-old/products/the-paleo-solution-the-original-human-diet/',
189+
amount: 1.99
190+
}
191+
return context
192+
}
193+
]
194+
}
195+
})
196+
197+
const thing = new Thing({
198+
description: 'Robb Wolf - the Paleo Solution',
199+
website:
200+
'https://robbwolf.com/shop-old/products/the-paleo-solution-the-original-human-diet/',
201+
amount: 1.99
202+
})
203+
204+
assert(store.state.things.tempsById[thing.__id], 'item is in the tempsById')
205+
206+
return thing.save().then(response => {
207+
assert(response._id === 1)
208+
assert(response.__id, 'the temp id is still intact')
209+
assert(!store.state.things.tempsById[response.__id])
210+
//@ts-ignore
211+
assert(!Object.keys(store.state.things.tempsByNewId).length)
212+
assert(response === thing, 'maintained the reference')
213+
})
214+
})
215+
216+
it('clones into Model.copiesById', function() {
151217
const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, {
152218
idField: '_id',
153219
serverAlias: 'temp-ids'

test/service-module/service-module.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,7 @@ describe('Service Module', function() {
637637
servicePath: 'service-todos',
638638
tempIdField: '__id',
639639
tempsById: {},
640+
tempsByNewId: {},
640641
pagination: {
641642
defaultLimit: null,
642643
defaultSkip: null

0 commit comments

Comments
 (0)