ar_sync 1.1.0 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -3
- data/.gitignore +2 -0
- data/Gemfile +1 -0
- data/ar_sync.gemspec +1 -1
- data/bin/console +2 -2
- data/core/ArSyncModel.d.ts +10 -12
- data/core/ArSyncModel.js +7 -5
- data/core/ArSyncStore.d.ts +11 -3
- data/core/ArSyncStore.js +112 -112
- data/core/hooks.d.ts +1 -0
- data/core/hooks.js +5 -4
- data/gemfiles/Gemfile-rails-6 +4 -3
- data/gemfiles/{Gemfile-rails-7 → Gemfile-rails-7-0} +4 -3
- data/gemfiles/Gemfile-rails-7-1 +10 -0
- data/lib/ar_sync/class_methods.rb +1 -5
- data/lib/ar_sync/core.rb +2 -6
- data/lib/ar_sync/instance_methods.rb +16 -1
- data/lib/ar_sync/rails.rb +14 -17
- data/lib/ar_sync/version.rb +1 -1
- data/src/core/ArSyncModel.ts +9 -8
- data/src/core/ArSyncStore.ts +128 -121
- data/src/core/hooks.ts +6 -5
- metadata +5 -19
- data/Gemfile.lock +0 -54
data/src/core/ArSyncStore.ts
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
import ArSyncApi from './ArSyncApi'
|
2
|
+
export type Request = { api: string; query: any; params?: any; id?: IDType }
|
3
|
+
|
4
|
+
type IDType = number | string
|
2
5
|
|
3
6
|
class ModelBatchRequest {
|
4
7
|
timer: number | null = null
|
@@ -6,8 +9,8 @@ class ModelBatchRequest {
|
|
6
9
|
Map<string,
|
7
10
|
{
|
8
11
|
query,
|
9
|
-
requests: Map<
|
10
|
-
id:
|
12
|
+
requests: Map<IDType, {
|
13
|
+
id: IDType
|
11
14
|
model?
|
12
15
|
callbacks: {
|
13
16
|
resolve: (model: any) => void
|
@@ -17,7 +20,7 @@ class ModelBatchRequest {
|
|
17
20
|
}
|
18
21
|
>
|
19
22
|
>()
|
20
|
-
fetch(api: string, query, id:
|
23
|
+
fetch(api: string, query, id: IDType) {
|
21
24
|
this.setTimer()
|
22
25
|
return new Promise((resolve, reject) => {
|
23
26
|
const queryJSON = JSON.stringify(query)
|
@@ -36,7 +39,7 @@ class ModelBatchRequest {
|
|
36
39
|
const ids = Array.from(requests.keys())
|
37
40
|
ArSyncApi.syncFetch({ api, query, params: { ids } }).then((models: any[]) => {
|
38
41
|
for (const model of models) {
|
39
|
-
const req = requests.get(model.id)
|
42
|
+
const req = requests.get(model._sync.id)
|
40
43
|
if (req) req.model = model
|
41
44
|
}
|
42
45
|
requests.forEach(({ model, callbacks }) => {
|
@@ -67,18 +70,18 @@ type ParsedQuery = {
|
|
67
70
|
params: any
|
68
71
|
} | {}
|
69
72
|
|
73
|
+
type SyncField = { id: IDType; keys: string[] }
|
70
74
|
type Unsubscribable = { unsubscribe: () => void }
|
71
75
|
|
72
76
|
class ArSyncContainerBase {
|
73
77
|
data
|
74
78
|
listeners: Unsubscribable[] = []
|
75
79
|
networkSubscriber?: Unsubscribable
|
76
|
-
parentModel
|
80
|
+
parentModel: ArSyncRecord | ArSyncCollection | null = null
|
77
81
|
parentKey
|
78
82
|
children: ArSyncContainerBase[] | { [key: string]: ArSyncContainerBase | null }
|
79
|
-
sync_keys: string[]
|
80
83
|
onConnectionChange?: (status: boolean) => void
|
81
|
-
replaceData(_data,
|
84
|
+
replaceData(_data, _parentSyncKeys?) {}
|
82
85
|
initForReload(request) {
|
83
86
|
this.networkSubscriber = ArSyncStore.connectionManager.subscribeNetwork((state) => {
|
84
87
|
if (!state) {
|
@@ -124,7 +127,7 @@ class ArSyncContainerBase {
|
|
124
127
|
for (const l of this.listeners) l.unsubscribe()
|
125
128
|
this.listeners = []
|
126
129
|
}
|
127
|
-
static
|
130
|
+
static compactQueryAttributes(query: ParsedQuery) {
|
128
131
|
function compactAttributes(attributes: Record<string, ParsedQuery>): [ParsedQuery, boolean] {
|
129
132
|
const attrs = {}
|
130
133
|
const keys: string[] = []
|
@@ -160,6 +163,7 @@ class ArSyncContainerBase {
|
|
160
163
|
return result
|
161
164
|
}
|
162
165
|
const result = compactQuery(query)
|
166
|
+
if (typeof result === 'object' && 'attributes' in result) return result.attributes
|
163
167
|
return result === true ? {} : result
|
164
168
|
}
|
165
169
|
static parseQuery(query, attrsonly: true): Record<string, ParsedQuery>
|
@@ -196,63 +200,52 @@ class ArSyncContainerBase {
|
|
196
200
|
if (attrsonly) return attributes
|
197
201
|
return { attributes, as: column, params }
|
198
202
|
}
|
199
|
-
static
|
203
|
+
static load({ api, id, params, query }: Request, root: ArSyncStore) {
|
200
204
|
const parsedQuery = ArSyncRecord.parseQuery(query)
|
201
|
-
const
|
205
|
+
const compactQueryAttributes = ArSyncRecord.compactQueryAttributes(parsedQuery)
|
202
206
|
if (id != null) {
|
203
|
-
return modelBatchRequest.fetch(api,
|
207
|
+
return modelBatchRequest.fetch(api, compactQueryAttributes, id).then(data => {
|
204
208
|
if (!data) throw { retry: false }
|
205
|
-
const request = { api, id, query:
|
206
|
-
return new ArSyncRecord(parsedQuery, data, request, root)
|
209
|
+
const request = { api, id, query: compactQueryAttributes }
|
210
|
+
return new ArSyncRecord(parsedQuery, data, request, root, null, null)
|
207
211
|
})
|
208
212
|
} else {
|
209
|
-
const request = { api, query:
|
213
|
+
const request = { api, query: compactQueryAttributes, params }
|
210
214
|
return ArSyncApi.syncFetch(request).then((response: any) => {
|
211
215
|
if (!response) {
|
212
216
|
throw { retry: false }
|
213
|
-
} else if (response.collection && response.order) {
|
214
|
-
return new ArSyncCollection(response.
|
217
|
+
} else if (response.collection && response.order && response._sync) {
|
218
|
+
return new ArSyncCollection(response._sync.keys, 'collection', parsedQuery, response, request, root, null, null)
|
215
219
|
} else if (response instanceof Array) {
|
216
|
-
return new ArSyncCollection([], '', parsedQuery, response, request, root)
|
220
|
+
return new ArSyncCollection([], '', parsedQuery, response, request, root, null, null)
|
217
221
|
} else {
|
218
|
-
return new ArSyncRecord(parsedQuery, response, request, root)
|
222
|
+
return new ArSyncRecord(parsedQuery, response, request, root, null, null)
|
219
223
|
}
|
220
224
|
})
|
221
225
|
}
|
222
226
|
}
|
223
|
-
static load(apiParams, root) {
|
224
|
-
if (!(apiParams instanceof Array)) return this._load(apiParams, root)
|
225
|
-
return new Promise((resolve, _reject) => {
|
226
|
-
const resultModels: any[] = []
|
227
|
-
let countdown = apiParams.length
|
228
|
-
apiParams.forEach((param, i) => {
|
229
|
-
this._load(param, root).then(model => {
|
230
|
-
resultModels[i] = model
|
231
|
-
countdown --
|
232
|
-
if (countdown === 0) resolve(resultModels)
|
233
|
-
})
|
234
|
-
})
|
235
|
-
})
|
236
|
-
}
|
237
227
|
}
|
238
228
|
|
239
229
|
type NotifyData = {
|
240
230
|
action: 'add' | 'remove' | 'update'
|
241
|
-
|
242
|
-
id:
|
231
|
+
class: string
|
232
|
+
id: IDType
|
243
233
|
field?: string
|
244
234
|
}
|
245
235
|
|
246
236
|
class ArSyncRecord extends ArSyncContainerBase {
|
247
|
-
id:
|
248
|
-
root
|
237
|
+
id: IDType
|
238
|
+
root: ArSyncStore
|
249
239
|
query
|
250
240
|
queryAttributes
|
251
241
|
data
|
242
|
+
syncKeys: string[]
|
252
243
|
children: { [key: string]: ArSyncContainerBase | null }
|
253
244
|
paths: string[]
|
254
245
|
reloadQueryCache
|
255
|
-
|
246
|
+
rootRecord: boolean
|
247
|
+
fetching = new Set<string>()
|
248
|
+
constructor(query, data, request, root: ArSyncStore, parentModel: ArSyncRecord | ArSyncCollection | null, parentKey: string | number | null) {
|
256
249
|
super()
|
257
250
|
this.root = root
|
258
251
|
if (request) this.initForReload(request)
|
@@ -260,48 +253,43 @@ class ArSyncRecord extends ArSyncContainerBase {
|
|
260
253
|
this.queryAttributes = query.attributes || {}
|
261
254
|
this.data = {}
|
262
255
|
this.children = {}
|
256
|
+
this.rootRecord = !parentModel
|
257
|
+
this.id = data._sync.id
|
258
|
+
this.syncKeys = data._sync.keys
|
263
259
|
this.replaceData(data)
|
260
|
+
this.parentModel = parentModel
|
261
|
+
this.parentKey = parentKey
|
264
262
|
}
|
265
|
-
|
266
|
-
this.
|
267
|
-
|
268
|
-
replaceData(data) {
|
269
|
-
this.setSyncKeys(data.sync_keys)
|
263
|
+
replaceData(data: { _sync: SyncField }) {
|
264
|
+
this.id = data._sync.id
|
265
|
+
this.syncKeys = data._sync.keys
|
270
266
|
this.unsubscribeAll()
|
271
|
-
if (this.data.id !== data.id) {
|
272
|
-
this.mark()
|
273
|
-
this.data.id = data.id
|
274
|
-
}
|
275
267
|
this.paths = []
|
276
268
|
for (const key in this.queryAttributes) {
|
277
269
|
const subQuery = this.queryAttributes[key]
|
278
270
|
const aliasName = subQuery.as || key
|
279
271
|
const subData = data[aliasName]
|
280
272
|
const child = this.children[aliasName]
|
281
|
-
if (key === '
|
282
|
-
if (subData instanceof Array || (subData && subData.collection && subData.order)) {
|
273
|
+
if (key === '_sync') continue
|
274
|
+
if (subData instanceof Array || (subData && subData.collection && subData.order && subData._sync)) {
|
283
275
|
if (child) {
|
284
|
-
child.replaceData(subData, this.
|
276
|
+
child.replaceData(subData, this.syncKeys)
|
285
277
|
} else {
|
286
|
-
const collection = new ArSyncCollection(this.
|
278
|
+
const collection = new ArSyncCollection(this.syncKeys, key, subQuery, subData, null, this.root, this, aliasName)
|
287
279
|
this.mark()
|
288
280
|
this.children[aliasName] = collection
|
289
281
|
this.data[aliasName] = collection.data
|
290
|
-
collection.parentModel = this
|
291
|
-
collection.parentKey = aliasName
|
292
282
|
}
|
293
283
|
} else {
|
294
284
|
if (subQuery.attributes && Object.keys(subQuery.attributes).length > 0) this.paths.push(key)
|
295
|
-
if (subData && subData.
|
285
|
+
if (subData && subData._sync) {
|
296
286
|
if (child) {
|
297
287
|
child.replaceData(subData)
|
298
288
|
} else {
|
299
|
-
const model = new ArSyncRecord(subQuery, subData, null, this.root)
|
289
|
+
const model = new ArSyncRecord(subQuery, subData, null, this.root, this, aliasName)
|
300
290
|
this.mark()
|
301
291
|
this.children[aliasName] = model
|
302
292
|
this.data[aliasName] = model.data
|
303
|
-
model.parentModel = this
|
304
|
-
model.parentKey = aliasName
|
305
293
|
}
|
306
294
|
} else {
|
307
295
|
if(child) {
|
@@ -317,6 +305,7 @@ class ArSyncRecord extends ArSyncContainerBase {
|
|
317
305
|
}
|
318
306
|
if (this.queryAttributes['*']) {
|
319
307
|
for (const key in data) {
|
308
|
+
if (key === '_sync') continue
|
320
309
|
if (!this.queryAttributes[key] && this.data[key] !== data[key]) {
|
321
310
|
this.mark()
|
322
311
|
this.data[key] = data[key]
|
@@ -326,28 +315,34 @@ class ArSyncRecord extends ArSyncContainerBase {
|
|
326
315
|
this.subscribeAll()
|
327
316
|
}
|
328
317
|
onNotify(notifyData: NotifyData, path?: string) {
|
329
|
-
const { action,
|
318
|
+
const { action, class: className, id } = notifyData
|
330
319
|
const query = path && this.queryAttributes[path]
|
331
320
|
const aliasName = (query && query.as) || path;
|
332
321
|
if (action === 'remove') {
|
333
322
|
const child = this.children[aliasName]
|
323
|
+
this.fetching.delete(`${aliasName}:${id}`) // To cancel consumeAdd
|
334
324
|
if (child) child.release()
|
335
325
|
this.children[aliasName] = null
|
336
326
|
this.mark()
|
337
327
|
this.data[aliasName] = null
|
338
328
|
this.onChange([aliasName], null)
|
339
329
|
} else if (action === 'add') {
|
340
|
-
|
341
|
-
|
330
|
+
const child = this.children[aliasName]
|
331
|
+
if (child instanceof ArSyncRecord && child.id === id) return
|
332
|
+
const fetchKey = `${aliasName}:${id}`
|
333
|
+
this.fetching.add(fetchKey)
|
334
|
+
modelBatchRequest.fetch(className, ArSyncRecord.compactQueryAttributes(query), id).then(data => {
|
335
|
+
// Record already removed
|
336
|
+
if (!this.fetching.has(fetchKey)) return
|
337
|
+
|
338
|
+
this.fetching.delete(fetchKey)
|
342
339
|
if (!data || !this.data) return
|
343
|
-
const model = new ArSyncRecord(query, data, null, this.root)
|
340
|
+
const model = new ArSyncRecord(query, data, null, this.root, this, aliasName)
|
344
341
|
const child = this.children[aliasName]
|
345
342
|
if (child) child.release()
|
346
343
|
this.children[aliasName] = model
|
347
344
|
this.mark()
|
348
345
|
this.data[aliasName] = model.data
|
349
|
-
model.parentModel = this
|
350
|
-
model.parentKey = aliasName
|
351
346
|
this.onChange([aliasName], model.data)
|
352
347
|
}).catch(e => {
|
353
348
|
console.error(`failed to load ${className}:${id} ${e}`)
|
@@ -365,42 +360,42 @@ class ArSyncRecord extends ArSyncContainerBase {
|
|
365
360
|
}
|
366
361
|
subscribeAll() {
|
367
362
|
const callback = data => this.onNotify(data)
|
368
|
-
for (const key of this.
|
363
|
+
for (const key of this.syncKeys) {
|
369
364
|
this.subscribe(key, callback)
|
370
365
|
}
|
371
366
|
for (const path of this.paths) {
|
372
367
|
const pathCallback = data => this.onNotify(data, path)
|
373
|
-
for (const key of this.
|
368
|
+
for (const key of this.syncKeys) this.subscribe(key + path, pathCallback)
|
369
|
+
}
|
370
|
+
if (this.rootRecord) {
|
371
|
+
const key = this.syncKeys[0]
|
372
|
+
if (key) this.subscribe(key + '_destroy', () => this.root.handleDestroy())
|
374
373
|
}
|
375
374
|
}
|
376
375
|
patchQuery(key: string) {
|
377
|
-
const
|
378
|
-
if (
|
379
|
-
let { attributes, as, params } = val
|
380
|
-
if (attributes && Object.keys(val.attributes).length === 0) attributes = null
|
381
|
-
if (!attributes && !as && !params) return key
|
382
|
-
const result: { attributes?; as?; params? } = {}
|
383
|
-
if (attributes) result.attributes = attributes
|
384
|
-
if (as) result.as = as
|
385
|
-
if (params) result.params = params
|
386
|
-
return result
|
376
|
+
const subQuery = this.queryAttributes[key]
|
377
|
+
if (subQuery) return { [key]: subQuery }
|
387
378
|
}
|
388
379
|
reloadQuery() {
|
389
380
|
if (this.reloadQueryCache) return this.reloadQueryCache
|
390
|
-
|
381
|
+
let arrayQuery = [] as string[] | null
|
382
|
+
const hashQuery = {}
|
391
383
|
for (const key in this.queryAttributes) {
|
392
|
-
if (key === '
|
384
|
+
if (key === '_sync') continue
|
393
385
|
const val = this.queryAttributes[key]
|
394
386
|
if (!val || !val.attributes) {
|
395
|
-
|
387
|
+
arrayQuery?.push(key)
|
388
|
+
hashQuery[key] = true
|
396
389
|
} else if (!val.params && Object.keys(val.attributes).length === 0) {
|
397
|
-
|
390
|
+
arrayQuery = null
|
391
|
+
hashQuery[key] = val
|
398
392
|
}
|
399
393
|
}
|
400
|
-
return
|
394
|
+
return this.reloadQueryCache = arrayQuery || hashQuery
|
401
395
|
}
|
402
396
|
update(data) {
|
403
397
|
for (const key in data) {
|
398
|
+
if (key === '_sync') continue
|
404
399
|
const subQuery = this.queryAttributes[key]
|
405
400
|
if (subQuery && subQuery.attributes && Object.keys(subQuery.attributes).length > 0) continue
|
406
401
|
if (this.data[key] === data[key]) continue
|
@@ -417,35 +412,39 @@ class ArSyncRecord extends ArSyncContainerBase {
|
|
417
412
|
if (!this.root || !this.root.immutable || !Object.isFrozen(this.data)) return
|
418
413
|
this.data = { ...this.data }
|
419
414
|
this.root.mark(this.data)
|
420
|
-
if (this.parentModel) this.parentModel.markAndSet(this.parentKey, this.data)
|
415
|
+
if (this.parentModel && this.parentKey) (this.parentModel as { markAndSet(key: string | number, data: any): any }).markAndSet(this.parentKey, this.data)
|
421
416
|
}
|
422
417
|
}
|
423
418
|
|
424
419
|
type Ordering = { first?: number; last?: number; orderBy: string; direction: 'asc' | 'desc' }
|
425
420
|
class ArSyncCollection extends ArSyncContainerBase {
|
426
|
-
root
|
421
|
+
root: ArSyncStore
|
427
422
|
path: string
|
428
423
|
ordering: Ordering = { orderBy: 'id', direction: 'asc' }
|
429
424
|
query
|
430
425
|
queryAttributes
|
431
|
-
|
426
|
+
compactQueryAttributes
|
427
|
+
syncKeys: string[]
|
432
428
|
data: any[]
|
433
429
|
children: ArSyncRecord[]
|
434
430
|
aliasOrderKey = 'id'
|
435
|
-
|
431
|
+
fetching = new Set<IDType>()
|
432
|
+
constructor(parentSyncKeys: string[], path: string, query, data: any[], request, root: ArSyncStore, parentModel: ArSyncRecord | null, parentKey: string | null){
|
436
433
|
super()
|
437
434
|
this.root = root
|
438
435
|
this.path = path
|
439
436
|
this.query = query
|
440
437
|
this.queryAttributes = query.attributes || {}
|
441
|
-
this.
|
438
|
+
this.compactQueryAttributes = ArSyncRecord.compactQueryAttributes(query)
|
442
439
|
if (request) this.initForReload(request)
|
443
440
|
if (query.params) {
|
444
441
|
this.setOrdering(query.params)
|
445
442
|
}
|
446
443
|
this.data = []
|
447
444
|
this.children = []
|
448
|
-
this.replaceData(data,
|
445
|
+
this.replaceData(data, parentSyncKeys)
|
446
|
+
this.parentModel = parentModel
|
447
|
+
this.parentKey = parentKey
|
449
448
|
}
|
450
449
|
setOrdering(ordering: { first?: unknown; last?: unknown; orderBy?: unknown; direction?: unknown }) {
|
451
450
|
let direction: 'asc' | 'desc' = 'asc'
|
@@ -460,17 +459,17 @@ class ArSyncCollection extends ArSyncContainerBase {
|
|
460
459
|
this.aliasOrderKey = (subQuery && subQuery.as) || orderBy
|
461
460
|
this.ordering = { first, last, direction, orderBy }
|
462
461
|
}
|
463
|
-
setSyncKeys(
|
464
|
-
if (
|
465
|
-
this.
|
462
|
+
setSyncKeys(parentSyncKeys: string[] | undefined) {
|
463
|
+
if (parentSyncKeys) {
|
464
|
+
this.syncKeys = parentSyncKeys.map(key => key + this.path)
|
466
465
|
} else {
|
467
|
-
this.
|
466
|
+
this.syncKeys = []
|
468
467
|
}
|
469
468
|
}
|
470
|
-
replaceData(data: any[] | { collection: any[]; ordering: Ordering },
|
471
|
-
this.setSyncKeys(
|
472
|
-
const existings = new Map<
|
473
|
-
for (const child of this.children) existings.set(child.
|
469
|
+
replaceData(data: any[] | { collection: any[]; ordering: Ordering }, parentSyncKeys: string[]) {
|
470
|
+
this.setSyncKeys(parentSyncKeys)
|
471
|
+
const existings = new Map<IDType, ArSyncRecord>()
|
472
|
+
for (const child of this.children) existings.set(child.id, child)
|
474
473
|
let collection: any[]
|
475
474
|
if (Array.isArray(data)) {
|
476
475
|
collection = data
|
@@ -482,14 +481,12 @@ class ArSyncCollection extends ArSyncContainerBase {
|
|
482
481
|
const newData: any[] = []
|
483
482
|
for (const subData of collection) {
|
484
483
|
let model: ArSyncRecord | undefined = undefined
|
485
|
-
if (typeof(subData) === 'object' && subData && '
|
484
|
+
if (typeof(subData) === 'object' && subData && '_sync' in subData) model = existings.get(subData._sync.id)
|
486
485
|
let data = subData
|
487
486
|
if (model) {
|
488
487
|
model.replaceData(subData)
|
489
|
-
} else if (subData.
|
490
|
-
model = new ArSyncRecord(this.query, subData, null, this.root)
|
491
|
-
model.parentModel = this
|
492
|
-
model.parentKey = subData.id
|
488
|
+
} else if (subData._sync) {
|
489
|
+
model = new ArSyncRecord(this.query, subData, null, this.root, this, subData._sync.id)
|
493
490
|
}
|
494
491
|
if (model) {
|
495
492
|
newChildren.push(model)
|
@@ -499,7 +496,7 @@ class ArSyncCollection extends ArSyncContainerBase {
|
|
499
496
|
}
|
500
497
|
while (this.children.length) {
|
501
498
|
const child = this.children.pop()!
|
502
|
-
if (!existings.has(child.
|
499
|
+
if (!existings.has(child.id)) child.release()
|
503
500
|
}
|
504
501
|
if (this.data.length || newChildren.length) this.mark()
|
505
502
|
while (this.data.length) this.data.pop()
|
@@ -507,13 +504,13 @@ class ArSyncCollection extends ArSyncContainerBase {
|
|
507
504
|
for (const el of newData) this.data.push(el)
|
508
505
|
this.subscribeAll()
|
509
506
|
}
|
510
|
-
consumeAdd(className: string, id:
|
507
|
+
consumeAdd(className: string, id: IDType) {
|
511
508
|
const { first, last, direction } = this.ordering
|
512
509
|
const limit = first || last
|
513
|
-
if (this.
|
514
|
-
if (limit && limit <= this.
|
515
|
-
const lastItem = this.
|
516
|
-
const firstItem = this.
|
510
|
+
if (this.children.find(a => a.id === id)) return
|
511
|
+
if (limit && limit <= this.children.length) {
|
512
|
+
const lastItem = this.children[this.children.length - 1]
|
513
|
+
const firstItem = this.children[0]
|
517
514
|
if (direction === 'asc') {
|
518
515
|
if (first) {
|
519
516
|
if (lastItem && lastItem.id < id) return
|
@@ -528,11 +525,14 @@ class ArSyncCollection extends ArSyncContainerBase {
|
|
528
525
|
}
|
529
526
|
}
|
530
527
|
}
|
531
|
-
|
528
|
+
this.fetching.add(id)
|
529
|
+
modelBatchRequest.fetch(className, this.compactQueryAttributes, id).then((data: any) => {
|
530
|
+
// Record already removed
|
531
|
+
if (!this.fetching.has(id)) return
|
532
|
+
|
533
|
+
this.fetching.delete(id)
|
532
534
|
if (!data || !this.data) return
|
533
|
-
const model = new ArSyncRecord(this.query, data, null, this.root)
|
534
|
-
model.parentModel = this
|
535
|
-
model.parentKey = id
|
535
|
+
const model = new ArSyncRecord(this.query, data, null, this.root, this, id)
|
536
536
|
const overflow = limit && limit <= this.data.length
|
537
537
|
let rmodel: ArSyncRecord | undefined
|
538
538
|
this.mark()
|
@@ -587,8 +587,9 @@ class ArSyncCollection extends ArSyncContainerBase {
|
|
587
587
|
this.data.sort((a, b) => a[orderKey] > b[orderKey] ? -1 : +1)
|
588
588
|
}
|
589
589
|
}
|
590
|
-
consumeRemove(id:
|
591
|
-
const idx = this.
|
590
|
+
consumeRemove(id: IDType) {
|
591
|
+
const idx = this.children.findIndex(a => a.id === id)
|
592
|
+
this.fetching.delete(id) // To cancel consumeAdd
|
592
593
|
if (idx < 0) return
|
593
594
|
this.mark()
|
594
595
|
this.children[idx].release()
|
@@ -596,58 +597,64 @@ class ArSyncCollection extends ArSyncContainerBase {
|
|
596
597
|
this.data.splice(idx, 1)
|
597
598
|
this.onChange([id], null)
|
598
599
|
}
|
599
|
-
onNotify(notifyData) {
|
600
|
+
onNotify(notifyData: NotifyData) {
|
600
601
|
if (notifyData.action === 'add') {
|
601
|
-
this.consumeAdd(notifyData.
|
602
|
+
this.consumeAdd(notifyData.class, notifyData.id)
|
602
603
|
} else if (notifyData.action === 'remove') {
|
603
604
|
this.consumeRemove(notifyData.id)
|
604
605
|
}
|
605
606
|
}
|
606
607
|
subscribeAll() {
|
607
608
|
const callback = data => this.onNotify(data)
|
608
|
-
for (const key of this.
|
609
|
+
for (const key of this.syncKeys) this.subscribe(key, callback)
|
609
610
|
}
|
610
611
|
onChange(path: (string | number)[], data) {
|
611
612
|
super.onChange(path, data)
|
612
613
|
if (path[1] === this.aliasOrderKey) this.markAndSort()
|
613
614
|
}
|
614
|
-
markAndSet(id:
|
615
|
+
markAndSet(id: IDType, data) {
|
615
616
|
this.mark()
|
616
|
-
const idx = this.
|
617
|
+
const idx = this.children.findIndex(a => a.id === id)
|
617
618
|
if (idx >= 0) this.data[idx] = data
|
618
619
|
}
|
619
620
|
mark() {
|
620
621
|
if (!this.root || !this.root.immutable || !Object.isFrozen(this.data)) return
|
621
622
|
this.data = [...this.data]
|
622
623
|
this.root.mark(this.data)
|
623
|
-
if (this.parentModel) this.parentModel.markAndSet(this.parentKey, this.data)
|
624
|
+
if (this.parentModel && this.parentKey) (this.parentModel as { markAndSet(key: string | number, data: any): any }).markAndSet(this.parentKey, this.data)
|
624
625
|
}
|
625
626
|
}
|
626
627
|
|
627
|
-
export
|
628
|
+
export class ArSyncStore {
|
628
629
|
immutable: boolean
|
629
630
|
markedForFreezeObjects: any[]
|
630
631
|
changes
|
631
632
|
eventListeners
|
632
633
|
markForRelease: true | undefined
|
633
634
|
container
|
634
|
-
request
|
635
|
-
complete
|
635
|
+
request: Request
|
636
|
+
complete = false
|
636
637
|
notfound?: boolean
|
638
|
+
destroyed = false
|
637
639
|
data
|
638
640
|
changesBufferTimer: number | undefined | null
|
639
641
|
retryLoadTimer: number | undefined | null
|
640
642
|
static connectionManager
|
641
|
-
constructor(request, { immutable } = {} as { immutable?: boolean }) {
|
643
|
+
constructor(request: Request, { immutable } = {} as { immutable?: boolean }) {
|
642
644
|
this.immutable = !!immutable
|
643
645
|
this.markedForFreezeObjects = []
|
644
646
|
this.changes = []
|
645
647
|
this.eventListeners = { events: {}, serial: 0 }
|
646
648
|
this.request = request
|
647
|
-
this.complete = false
|
648
649
|
this.data = null
|
649
650
|
this.load(0)
|
650
651
|
}
|
652
|
+
handleDestroy() {
|
653
|
+
this.release()
|
654
|
+
this.data = null
|
655
|
+
this.destroyed = true
|
656
|
+
this.trigger('destroy')
|
657
|
+
}
|
651
658
|
load(retryCount: number) {
|
652
659
|
ArSyncContainerBase.load(this.request, this).then((container: ArSyncContainerBase) => {
|
653
660
|
if (this.markForRelease) {
|
data/src/core/hooks.ts
CHANGED
@@ -21,11 +21,11 @@ function checkHooks() {
|
|
21
21
|
if (!useState) throw 'uninitialized. needs `initializeHooks({ useState, useEffect, useMemo, useRef })`'
|
22
22
|
}
|
23
23
|
|
24
|
-
interface ModelStatus { complete: boolean; notfound?: boolean; connected: boolean }
|
24
|
+
interface ModelStatus { complete: boolean; notfound?: boolean; connected: boolean; destroyed: boolean }
|
25
25
|
export type DataAndStatus<T> = [T | null, ModelStatus]
|
26
26
|
export interface Request { api: string; params?: any; id?: number; query: any }
|
27
27
|
|
28
|
-
const initialResult: DataAndStatus<any> = [null, { complete: false, notfound: undefined, connected: true }]
|
28
|
+
const initialResult: DataAndStatus<any> = [null, { complete: false, notfound: undefined, connected: true, destroyed: false }]
|
29
29
|
export function useArSyncModel<T>(request: Request | null): DataAndStatus<T> {
|
30
30
|
checkHooks()
|
31
31
|
const [result, setResult] = useState<DataAndStatus<T>>(initialResult)
|
@@ -39,12 +39,12 @@ export function useArSyncModel<T>(request: Request | null): DataAndStatus<T> {
|
|
39
39
|
}
|
40
40
|
const model = new ArSyncModel<T>(request, { immutable: true })
|
41
41
|
function update() {
|
42
|
-
const { complete, notfound, connected, data } = model
|
42
|
+
const { complete, notfound, connected, destroyed, data } = model
|
43
43
|
setResult(resultWas => {
|
44
44
|
const [dataWas, statusWas] = resultWas
|
45
|
-
const statusPersisted = statusWas.complete === complete && statusWas.notfound === notfound && statusWas.connected === connected
|
45
|
+
const statusPersisted = statusWas.complete === complete && statusWas.notfound === notfound && statusWas.connected === connected && statusWas.destroyed === destroyed
|
46
46
|
if (dataWas === data && statusPersisted) return resultWas
|
47
|
-
const status = statusPersisted ? statusWas : { complete, notfound, connected }
|
47
|
+
const status = statusPersisted ? statusWas : { complete, notfound, connected, destroyed }
|
48
48
|
return [data, status]
|
49
49
|
})
|
50
50
|
}
|
@@ -55,6 +55,7 @@ export function useArSyncModel<T>(request: Request | null): DataAndStatus<T> {
|
|
55
55
|
}
|
56
56
|
model.subscribe('load', update)
|
57
57
|
model.subscribe('change', update)
|
58
|
+
model.subscribe('destroy', update)
|
58
59
|
model.subscribe('connection', update)
|
59
60
|
return () => model.release()
|
60
61
|
}, [requestString])
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ar_sync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- tompng
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-07-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -52,20 +52,6 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: pry
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
55
|
- !ruby/object:Gem::Dependency
|
70
56
|
name: sqlite3
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -105,7 +91,6 @@ files:
|
|
105
91
|
- ".gitignore"
|
106
92
|
- ".travis.yml"
|
107
93
|
- Gemfile
|
108
|
-
- Gemfile.lock
|
109
94
|
- LICENSE.txt
|
110
95
|
- README.md
|
111
96
|
- Rakefile
|
@@ -129,7 +114,8 @@ files:
|
|
129
114
|
- core/hooks.d.ts
|
130
115
|
- core/hooks.js
|
131
116
|
- gemfiles/Gemfile-rails-6
|
132
|
-
- gemfiles/Gemfile-rails-7
|
117
|
+
- gemfiles/Gemfile-rails-7-0
|
118
|
+
- gemfiles/Gemfile-rails-7-1
|
133
119
|
- index.d.ts
|
134
120
|
- index.js
|
135
121
|
- lib/ar_sync.rb
|
@@ -176,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
176
162
|
- !ruby/object:Gem::Version
|
177
163
|
version: '0'
|
178
164
|
requirements: []
|
179
|
-
rubygems_version: 3.
|
165
|
+
rubygems_version: 3.5.9
|
180
166
|
signing_key:
|
181
167
|
specification_version: 4
|
182
168
|
summary: ActiveRecord - JavaScript Sync
|