Skip to content

Commit cc397da

Browse files
author
Lee Richmond
committed
Avoid syncing dirty props on store update
This is probably a better default, following principle of least surprise, unless we hear otherwise. If you load instanceA, and 1m later load instanceB with new data from the server, instanceA will change. But if instanceA has already updated the relevant attribute (it is dirty), avoid making that change. Relatedly, if instanceA syncs with a new store update, it would see that property as dirty, as it didn't match the attributes it was instantiated with. This commit also ensures synced properties are not seen as dirty.
1 parent 829ee0c commit cc397da

File tree

3 files changed

+68
-24
lines changed

3 files changed

+68
-24
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ build/
99
test-old/
1010
dist/
1111
coverage/
12+
lib/
13+
lib-esm/

src/model.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,14 +439,20 @@ export class JSORMBase {
439439
if (this._onStoreChange) return this._onStoreChange
440440
this._onStoreChange = (event, attrs) => {
441441
let diff = {} as any
442+
// Update all non-dirty attributes
442443
Object.keys(attrs).forEach(k => {
443444
let self = this as any
444-
if (self[k] !== attrs[k]) {
445+
let changes = this.changes() as any
446+
if (self[k] !== attrs[k] && !changes[k]) {
445447
diff[k] = [self[k], attrs[k]]
446448
self[k] = attrs[k]
449+
450+
// ensure this property is not marked as dirty
451+
self._originalAttributes[k] = attrs[k]
447452
}
448453
})
449454

455+
// fire afterSync hook if applicable
450456
let hasDiff = Object.keys(diff).length > 0
451457
let hasAfterSync = typeof this.afterSync !== "undefined"
452458
if (hasDiff && hasAfterSync) this.afterSync(diff)

test/integration/id-map.test.ts

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@ afterEach(() => {
66
ApplicationRecord.store.data = {}
77
})
88

9+
let responsePayload = (firstName: string) => {
10+
return {
11+
data: {
12+
id: "1",
13+
type: "authors",
14+
attributes: { firstName }
15+
}
16+
}
17+
}
18+
919
describe("ID Map", () => {
1020
beforeEach(() => {
1121
ApplicationRecord.sync = true
12-
let responsePayload = (firstName: string) => {
13-
return {
14-
data: {
15-
id: "1",
16-
type: "authors",
17-
attributes: { firstName }
18-
}
19-
}
20-
}
2122

2223
fetchMock.get(
2324
"http://example.com/api/v1/authors/1",
@@ -38,7 +39,7 @@ describe("ID Map", () => {
3839
})
3940

4041
afterEach(() => {
41-
fetchMock.reset()
42+
fetchMock.restore()
4243
})
4344

4445
describe("when fetching from server", () => {
@@ -61,36 +62,71 @@ describe("ID Map", () => {
6162
expect(stored["authors-1"]).to.deep.eq(data.attributes)
6263
})
6364

64-
it("syncs with id map", async () => {
65+
it("syncs non-dirty attributes with id map", async () => {
6566
let author1 = (await Author.find(1)).data
66-
author1.firstName = "updated"
67+
expect(author1.firstName).to.eq("John")
68+
69+
fetchMock.restore()
70+
fetchMock.get(
71+
"http://example.com/api/v1/authors/1",
72+
responsePayload("Jane")
73+
)
74+
6775
let author2 = (await Author.find(1)).data
76+
expect(author2.firstName).to.eq("Jane")
77+
expect(author1.firstName).to.eq("Jane")
78+
})
79+
80+
it("does not see mark newly-synced attributes as dirty", async () => {
81+
let author1 = (await Author.find(1)).data
6882
expect(author1.firstName).to.eq("John")
83+
84+
fetchMock.restore()
85+
fetchMock.get(
86+
"http://example.com/api/v1/authors/1",
87+
responsePayload("Jane")
88+
)
89+
90+
await Author.find(1)
91+
expect(author1.firstName).to.eq("Jane")
92+
expect(author1.isDirty()).to.eq(false)
6993
})
7094

71-
describe("when implementing afterSync hook", () => {
72-
let originalAfterSync: any
95+
it("does not sync dirty attributes with id map", async () => {
96+
let author1 = (await Author.find(1)).data
97+
author1.firstName = "updated"
7398

74-
beforeEach(() => {
75-
originalAfterSync = Author.prototype.afterSync
76-
Author.prototype.afterSync = sinon.spy()
77-
})
99+
fetchMock.restore()
100+
fetchMock.get(
101+
"http://example.com/api/v1/authors/1",
102+
responsePayload("Jane")
103+
)
78104

79-
afterEach(() => {
80-
Author.prototype.afterSync = originalAfterSync
81-
})
105+
let author2 = (await Author.find(1)).data
106+
expect(author2.firstName).to.eq("Jane")
107+
expect(author1.firstName).to.eq("updated")
108+
})
82109

110+
describe("when implementing afterSync hook", () => {
83111
it("fires after sync, passing changes as an argument", async() => {
84112
let author1 = (await Author.find(1)).data
85-
author1.firstName = 'updated'
113+
author1.afterSync = sinon.spy()
114+
115+
fetchMock.restore()
116+
fetchMock.get(
117+
"http://example.com/api/v1/authors/1",
118+
responsePayload("Jane")
119+
)
120+
86121
let author2 = (await Author.find(1)).data
87122
expect(author1.afterSync).to
88-
.have.been.calledWith({ firstName: ["updated", "John"] })
123+
.have.been.calledWith({ firstName: ["John", "Jane"] })
89124
})
90125

91126
describe("when store updates, but no changes", () => {
92127
it("does not fire the hook", async() => {
93128
let author1 = (await Author.find(1)).data
129+
author1.afterSync = sinon.spy()
94130
let author2 = (await Author.find(1)).data
95131
let called = (<any>author1.afterSync).called
96132
expect(called).to.eq(false)

0 commit comments

Comments
 (0)