ar_sync 1.0.5 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "actioncable": "^5.2.0",
12
12
  "@types/actioncable": "^5.2.0",
13
13
  "eslint": "^5.16.0",
14
- "typescript": "3.7.2",
14
+ "typescript": "3.9.5",
15
15
  "@typescript-eslint/eslint-plugin": "^1.6.0",
16
16
  "@typescript-eslint/parser": "^1.6.0"
17
17
  }
@@ -10,12 +10,17 @@ async function apiBatchFetch(endpoint: string, requests: object[]) {
10
10
  if (res.status === 200) return res.json()
11
11
  throw new Error(res.statusText)
12
12
  }
13
-
13
+ type FetchError = {
14
+ type: string
15
+ message: string
16
+ retry: boolean
17
+ }
14
18
  interface PromiseCallback {
15
- resolve: (data: object) => void
16
- reject: (error: object) => void
19
+ resolve: (data: any) => void
20
+ reject: (error: FetchError) => void
17
21
  }
18
22
 
23
+ type Request = { api: string; params?: any; query: any; id?: number }
19
24
  class ApiFetcher {
20
25
  endpoint: string
21
26
  batches: [object, PromiseCallback][] = []
@@ -23,7 +28,15 @@ class ApiFetcher {
23
28
  constructor(endpoint: string) {
24
29
  this.endpoint = endpoint
25
30
  }
26
- fetch(request: object) {
31
+ fetch(request: Request) {
32
+ if (request.id != null) {
33
+ return new Promise((resolve, reject) => {
34
+ this.fetch({ api: request.api, params: { ids: [request.id] }, query: request.query }).then((result: any[]) => {
35
+ if (result[0]) resolve(result[0])
36
+ else reject({ type: 'Not Found', retry: false })
37
+ }).catch(reject)
38
+ })
39
+ }
27
40
  return new Promise((resolve, reject) => {
28
41
  this.batches.push([request, { resolve, reject }])
29
42
  if (this.batchFetchTimer) return
@@ -49,7 +62,7 @@ class ApiFetcher {
49
62
  const result = results[i]
50
63
  const callbacks = callbacksList[i]
51
64
  for (const callback of callbacks) {
52
- if (result.data) {
65
+ if (result.data !== undefined) {
53
66
  callback.resolve(result.data)
54
67
  } else {
55
68
  const error = result.error || { type: 'Unknown Error' }
@@ -73,7 +86,7 @@ const syncFetcher = new ApiFetcher('/sync_api')
73
86
  const ArSyncApi = {
74
87
  domain: null as string | null,
75
88
  _batchFetch: apiBatchFetch,
76
- fetch: (request: object) => staticFetcher.fetch(request),
77
- syncFetch: (request: object) => syncFetcher.fetch(request),
89
+ fetch: (request: Request) => staticFetcher.fetch(request),
90
+ syncFetch: (request: Request) => syncFetcher.fetch(request),
78
91
  }
79
92
  export default ArSyncApi
@@ -1,47 +1,56 @@
1
- import ArSyncAPI from './ArSyncApi'
1
+ import ArSyncApi from './ArSyncApi'
2
2
 
3
- const ModelBatchRequest = {
4
- timer: null,
5
- apiRequests: {} as {
6
- [api: string]: {
7
- [queryJSON: string]: {
8
- query
9
- requests: {
10
- [id: number]: {
11
- id: number
12
- model?
13
- callbacks: ((model) => void)[]
14
- }
15
- }
3
+ class ModelBatchRequest {
4
+ timer: number | null = null
5
+ apiRequests = new Map<string,
6
+ Map<string,
7
+ {
8
+ query,
9
+ requests: Map<number, {
10
+ id: number
11
+ model?
12
+ callbacks: {
13
+ resolve: (model: any) => void
14
+ reject: (error?: any) => void
15
+ }[]
16
+ }>
16
17
  }
17
- }
18
- },
18
+ >
19
+ >()
19
20
  fetch(api: string, query, id: number) {
20
21
  this.setTimer()
21
- return new Promise(resolve => {
22
+ return new Promise((resolve, reject) => {
22
23
  const queryJSON = JSON.stringify(query)
23
- const apiRequest = this.apiRequests[api] = this.apiRequests[api] || {}
24
- const queryRequests = apiRequest[queryJSON] = apiRequest[queryJSON] || { query, requests: {} }
25
- const request = queryRequests.requests[id] = queryRequests.requests[id] || { id, callbacks: [] }
26
- request.callbacks.push(resolve)
24
+ let apiRequest = this.apiRequests.get(api)
25
+ if (!apiRequest) this.apiRequests.set(api, apiRequest = new Map())
26
+ let queryRequests = apiRequest.get(queryJSON)
27
+ if (!queryRequests) apiRequest.set(queryJSON, queryRequests = { query, requests: new Map() })
28
+ let request = queryRequests.requests.get(id)
29
+ if (!request) queryRequests.requests.set(id, request = { id, callbacks: [] })
30
+ request.callbacks.push({ resolve, reject })
27
31
  })
28
- },
32
+ }
29
33
  batchFetch() {
30
- const { apiRequests } = this as typeof ModelBatchRequest
31
- for (const api in apiRequests) {
32
- const apiRequest = apiRequests[api]
33
- for (const { query, requests } of Object.values(apiRequest)) {
34
- const ids = Object.values(requests).map(({ id }) => id)
35
- ArSyncAPI.syncFetch({ api, query, params: { ids } }).then((models: any[]) => {
36
- for (const model of models) requests[model.id].model = model
37
- for (const { model, callbacks } of Object.values(requests)) {
38
- for (const callback of callbacks) callback(model)
34
+ this.apiRequests.forEach((apiRequest, api) => {
35
+ apiRequest.forEach(({ query, requests }) => {
36
+ const ids = Array.from(requests.keys())
37
+ ArSyncApi.syncFetch({ api, query, params: { ids } }).then((models: any[]) => {
38
+ for (const model of models) {
39
+ const req = requests.get(model.id)
40
+ if (req) req.model = model
39
41
  }
42
+ requests.forEach(({ model, callbacks }) => {
43
+ callbacks.forEach(cb => cb.resolve(model))
44
+ })
45
+ }).catch(e => {
46
+ requests.forEach(({ callbacks }) => {
47
+ callbacks.forEach(cb => cb.reject(e))
48
+ })
40
49
  })
41
- }
42
- }
43
- this.apiRequests = {}
44
- },
50
+ })
51
+ })
52
+ this.apiRequests.clear()
53
+ }
45
54
  setTimer() {
46
55
  if (this.timer) return
47
56
  this.timer = setTimeout(() => {
@@ -50,6 +59,7 @@ const ModelBatchRequest = {
50
59
  }, 20)
51
60
  }
52
61
  }
62
+ const modelBatchRequest = new ModelBatchRequest
53
63
 
54
64
  type ParsedQuery = {
55
65
  attributes: Record<string, ParsedQuery>
@@ -57,31 +67,42 @@ type ParsedQuery = {
57
67
  params: any
58
68
  } | {}
59
69
 
70
+ type Unsubscribable = { unsubscribe: () => void }
71
+
60
72
  class ArSyncContainerBase {
61
73
  data
62
- listeners
63
- networkSubscriber
74
+ listeners: Unsubscribable[] = []
75
+ networkSubscriber?: Unsubscribable
64
76
  parentModel
65
77
  parentKey
66
78
  children: ArSyncContainerBase[] | { [key: string]: ArSyncContainerBase | null }
67
79
  sync_keys: string[]
68
- onConnectionChange
69
- constructor() {
70
- this.listeners = []
71
- }
80
+ onConnectionChange?: (status: boolean) => void
72
81
  replaceData(_data, _sync_keys?) {}
73
82
  initForReload(request) {
74
83
  this.networkSubscriber = ArSyncStore.connectionManager.subscribeNetwork((state) => {
75
- if (state) {
76
- ArSyncAPI.syncFetch(request).then(data => {
77
- if (this.data) {
84
+ if (!state) {
85
+ if (this.onConnectionChange) this.onConnectionChange(false)
86
+ return
87
+ }
88
+ if (request.id != null) {
89
+ modelBatchRequest.fetch(request.api, request.query, request.id).then(data => {
90
+ if (this.data && data) {
78
91
  this.replaceData(data)
79
92
  if (this.onConnectionChange) this.onConnectionChange(true)
80
93
  if (this.onChange) this.onChange([], this.data)
81
94
  }
82
95
  })
83
96
  } else {
84
- if (this.onConnectionChange) this.onConnectionChange(false)
97
+ ArSyncApi.syncFetch(request).then(data => {
98
+ if (this.data && data) {
99
+ this.replaceData(data)
100
+ if (this.onConnectionChange) this.onConnectionChange(true)
101
+ if (this.onChange) this.onChange([], this.data)
102
+ }
103
+ }).catch(e => {
104
+ console.error(`failed to reload. ${e}`)
105
+ })
85
106
  }
86
107
  })
87
108
  }
@@ -103,8 +124,8 @@ class ArSyncContainerBase {
103
124
  for (const l of this.listeners) l.unsubscribe()
104
125
  this.listeners = []
105
126
  }
106
- static compactQuery(query: ParsedQuery) {
107
- function compactAttributes(attributes: Record<string, ParsedQuery>) {
127
+ static compactQueryAttributes(query: ParsedQuery) {
128
+ function compactAttributes(attributes: Record<string, ParsedQuery>): [ParsedQuery, boolean] {
108
129
  const attrs = {}
109
130
  const keys: string[] = []
110
131
  for (const key in attributes) {
@@ -118,13 +139,13 @@ class ArSyncContainerBase {
118
139
  if (Object.keys(attrs).length === 0) {
119
140
  if (keys.length === 0) return [true, false]
120
141
  if (keys.length === 1) return [keys[0], false]
121
- return [keys]
142
+ return [keys, false]
122
143
  }
123
144
  const needsEscape = attrs['attributes'] || attrs['params'] || attrs['as']
124
145
  if (keys.length === 0) return [attrs, needsEscape]
125
146
  return [[...keys, attrs], needsEscape]
126
147
  }
127
- function compactQuery(query: ParsedQuery) {
148
+ function compactQuery(query: ParsedQuery): ParsedQuery {
128
149
  if (!('attributes' in query)) return true
129
150
  const { as, params } = query
130
151
  const [attributes, needsEscape] = compactAttributes(query.attributes)
@@ -138,10 +159,9 @@ class ArSyncContainerBase {
138
159
  if (attributes !== true) result.attributes = attributes
139
160
  return result
140
161
  }
141
- try{
142
162
  const result = compactQuery(query)
163
+ if (typeof result === 'object' && 'attributes' in result) return result.attributes
143
164
  return result === true ? {} : result
144
- }catch(e){throw JSON.stringify(query)+e.stack}
145
165
  }
146
166
  static parseQuery(query, attrsonly: true): Record<string, ParsedQuery>
147
167
  static parseQuery(query): ParsedQuery
@@ -179,13 +199,19 @@ class ArSyncContainerBase {
179
199
  }
180
200
  static _load({ api, id, params, query }, root) {
181
201
  const parsedQuery = ArSyncRecord.parseQuery(query)
182
- const compactQuery = ArSyncRecord.compactQuery(parsedQuery)
183
- if (id) {
184
- return ModelBatchRequest.fetch(api, compactQuery, id).then(data => new ArSyncRecord(parsedQuery, data, null, root))
202
+ const compactQueryAttributes = ArSyncRecord.compactQueryAttributes(parsedQuery)
203
+ if (id != null) {
204
+ return modelBatchRequest.fetch(api, compactQueryAttributes, id).then(data => {
205
+ if (!data) throw { retry: false }
206
+ const request = { api, id, query: compactQueryAttributes }
207
+ return new ArSyncRecord(parsedQuery, data, request, root)
208
+ })
185
209
  } else {
186
- const request = { api, query: compactQuery, params }
187
- return ArSyncAPI.syncFetch(request).then((response: any) => {
188
- if (response.collection && response.order) {
210
+ const request = { api, query: compactQueryAttributes, params }
211
+ return ArSyncApi.syncFetch(request).then((response: any) => {
212
+ if (!response) {
213
+ throw { retry: false }
214
+ } else if (response.collection && response.order) {
189
215
  return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, response, request, root)
190
216
  } else if (response instanceof Array) {
191
217
  return new ArSyncCollection([], '', parsedQuery, response, request, root)
@@ -222,6 +248,7 @@ class ArSyncRecord extends ArSyncContainerBase {
222
248
  id: number
223
249
  root
224
250
  query
251
+ queryAttributes
225
252
  data
226
253
  children: { [key: string]: ArSyncContainerBase | null }
227
254
  paths: string[]
@@ -231,15 +258,13 @@ class ArSyncRecord extends ArSyncContainerBase {
231
258
  this.root = root
232
259
  if (request) this.initForReload(request)
233
260
  this.query = query
261
+ this.queryAttributes = query.attributes || {}
234
262
  this.data = {}
235
263
  this.children = {}
236
264
  this.replaceData(data)
237
265
  }
238
- setSyncKeys(sync_keys: string[]) {
239
- this.sync_keys = sync_keys
240
- if (!this.sync_keys) {
241
- this.sync_keys = []
242
- }
266
+ setSyncKeys(sync_keys: string[] | undefined) {
267
+ this.sync_keys = sync_keys ?? []
243
268
  }
244
269
  replaceData(data) {
245
270
  this.setSyncKeys(data.sync_keys)
@@ -249,8 +274,8 @@ class ArSyncRecord extends ArSyncContainerBase {
249
274
  this.data.id = data.id
250
275
  }
251
276
  this.paths = []
252
- for (const key in this.query.attributes) {
253
- const subQuery = this.query.attributes[key]
277
+ for (const key in this.queryAttributes) {
278
+ const subQuery = this.queryAttributes[key]
254
279
  const aliasName = subQuery.as || key
255
280
  const subData = data[aliasName]
256
281
  const child = this.children[aliasName]
@@ -291,9 +316,9 @@ class ArSyncRecord extends ArSyncContainerBase {
291
316
  }
292
317
  }
293
318
  }
294
- if (this.query.attributes['*']) {
319
+ if (this.queryAttributes['*']) {
295
320
  for (const key in data) {
296
- if (!this.query.attributes[key] && this.data[key] !== data[key]) {
321
+ if (!this.queryAttributes[key] && this.data[key] !== data[key]) {
297
322
  this.mark()
298
323
  this.data[key] = data[key]
299
324
  }
@@ -302,8 +327,8 @@ class ArSyncRecord extends ArSyncContainerBase {
302
327
  this.subscribeAll()
303
328
  }
304
329
  onNotify(notifyData: NotifyData, path?: string) {
305
- const { action, class_name, id } = notifyData
306
- const query = path && this.query.attributes[path]
330
+ const { action, class_name: className, id } = notifyData
331
+ const query = path && this.queryAttributes[path]
307
332
  const aliasName = (query && query.as) || path;
308
333
  if (action === 'remove') {
309
334
  const child = this.children[aliasName]
@@ -314,7 +339,7 @@ class ArSyncRecord extends ArSyncContainerBase {
314
339
  this.onChange([aliasName], null)
315
340
  } else if (action === 'add') {
316
341
  if (this.data[aliasName] && this.data[aliasName].id === id) return
317
- ModelBatchRequest.fetch(class_name, ArSyncRecord.compactQuery(query), id).then(data => {
342
+ modelBatchRequest.fetch(className, ArSyncRecord.compactQueryAttributes(query), id).then(data => {
318
343
  if (!data || !this.data) return
319
344
  const model = new ArSyncRecord(query, data, null, this.root)
320
345
  const child = this.children[aliasName]
@@ -325,12 +350,17 @@ class ArSyncRecord extends ArSyncContainerBase {
325
350
  model.parentModel = this
326
351
  model.parentKey = aliasName
327
352
  this.onChange([aliasName], model.data)
353
+ }).catch(e => {
354
+ console.error(`failed to load ${className}:${id} ${e}`)
328
355
  })
329
356
  } else {
330
357
  const { field } = notifyData
331
358
  const query = field ? this.patchQuery(field) : this.reloadQuery()
332
- if (query) ModelBatchRequest.fetch(class_name, query, id).then(data => {
359
+ if (!query) return
360
+ modelBatchRequest.fetch(className, query, id).then(data => {
333
361
  if (this.data) this.update(data)
362
+ }).catch(e => {
363
+ console.error(`failed to load patch ${className}:${id} ${e}`)
334
364
  })
335
365
  }
336
366
  }
@@ -345,34 +375,29 @@ class ArSyncRecord extends ArSyncContainerBase {
345
375
  }
346
376
  }
347
377
  patchQuery(key: string) {
348
- const val = this.query.attributes[key]
349
- if (!val) return
350
- let { attributes, as, params } = val
351
- if (attributes && Object.keys(val.attributes).length === 0) attributes = null
352
- if (!attributes && !as && !params) return key
353
- const result: { attributes?; as?; params? } = {}
354
- if (attributes) result.attributes = attributes
355
- if (as) result.as = as
356
- if (params) result.params = params
357
- return result
378
+ const subQuery = this.queryAttributes[key]
379
+ if (subQuery) return { [key]: subQuery }
358
380
  }
359
381
  reloadQuery() {
360
382
  if (this.reloadQueryCache) return this.reloadQueryCache
361
- const reloadQuery = this.reloadQueryCache = { attributes: [] as any[] }
362
- for (const key in this.query.attributes) {
383
+ let arrayQuery = [] as string[] | null
384
+ const hashQuery = {}
385
+ for (const key in this.queryAttributes) {
363
386
  if (key === 'sync_keys') continue
364
- const val = this.query.attributes[key]
387
+ const val = this.queryAttributes[key]
365
388
  if (!val || !val.attributes) {
366
- reloadQuery.attributes.push(key)
389
+ arrayQuery?.push(key)
390
+ hashQuery[key] = true
367
391
  } else if (!val.params && Object.keys(val.attributes).length === 0) {
368
- reloadQuery.attributes.push({ [key]: val })
392
+ arrayQuery = null
393
+ hashQuery[key] = val
369
394
  }
370
395
  }
371
- return reloadQuery
396
+ return this.reloadQueryCache = arrayQuery || hashQuery
372
397
  }
373
398
  update(data) {
374
399
  for (const key in data) {
375
- const subQuery = this.query.attributes[key]
400
+ const subQuery = this.queryAttributes[key]
376
401
  if (subQuery && subQuery.attributes && Object.keys(subQuery.attributes).length > 0) continue
377
402
  if (this.data[key] === data[key]) continue
378
403
  this.mark()
@@ -392,12 +417,14 @@ class ArSyncRecord extends ArSyncContainerBase {
392
417
  }
393
418
  }
394
419
 
420
+ type Ordering = { first?: number; last?: number; orderBy: string; direction: 'asc' | 'desc' }
395
421
  class ArSyncCollection extends ArSyncContainerBase {
396
422
  root
397
423
  path: string
398
- order: { limit: number | null; key: string; mode: 'asc' | 'desc' } = { limit: null, mode: 'asc', key: 'id' }
424
+ ordering: Ordering = { orderBy: 'id', direction: 'asc' }
399
425
  query
400
- compactQuery
426
+ queryAttributes
427
+ compactQueryAttributes
401
428
  data: any[]
402
429
  children: ArSyncRecord[]
403
430
  aliasOrderKey = 'id'
@@ -406,31 +433,28 @@ class ArSyncCollection extends ArSyncContainerBase {
406
433
  this.root = root
407
434
  this.path = path
408
435
  this.query = query
409
- this.compactQuery = ArSyncRecord.compactQuery(query)
436
+ this.queryAttributes = query.attributes || {}
437
+ this.compactQueryAttributes = ArSyncRecord.compactQueryAttributes(query)
410
438
  if (request) this.initForReload(request)
411
- if (query.params && (query.params.order || query.params.limit)) {
412
- this.setOrdering(query.params.limit, query.params.order)
439
+ if (query.params) {
440
+ this.setOrdering(query.params)
413
441
  }
414
442
  this.data = []
415
443
  this.children = []
416
444
  this.replaceData(data, sync_keys)
417
445
  }
418
- setOrdering(limit: unknown, order: unknown) {
419
- let mode: 'asc' | 'desc' = 'asc'
420
- let key: string = 'id'
421
- if (order === 'asc' || order === 'desc') {
422
- mode = order
423
- } else if (typeof order === 'object' && order) {
424
- const keys = Object.keys(order)
425
- if (keys.length > 1) throw 'multiple order keys are not supported'
426
- if (keys.length === 1) key = keys[0]
427
- mode = order[key] === 'asc' ? 'asc' : 'desc'
428
- }
429
- const limitNumber = (typeof limit === 'number') ? limit : null
430
- if (limitNumber !== null && key !== 'id') throw 'limit with custom order key is not supported'
431
- const subQuery = this.query.attributes[key]
432
- this.aliasOrderKey = (subQuery && subQuery.as) || key
433
- this.order = { limit: limitNumber, mode, key }
446
+ setOrdering(ordering: { first?: unknown; last?: unknown; orderBy?: unknown; direction?: unknown }) {
447
+ let direction: 'asc' | 'desc' = 'asc'
448
+ let orderBy: string = 'id'
449
+ let first: number | undefined = undefined
450
+ let last: number | undefined = undefined
451
+ if (ordering.direction === 'desc') direction = ordering.direction
452
+ if (typeof ordering.orderBy === 'string') orderBy = ordering.orderBy
453
+ if (typeof ordering.first === 'number') first = ordering.first
454
+ if (typeof ordering.last === 'number') last = ordering.last
455
+ const subQuery = this.queryAttributes[orderBy]
456
+ this.aliasOrderKey = (subQuery && subQuery.as) || orderBy
457
+ this.ordering = { first, last, direction, orderBy }
434
458
  }
435
459
  setSyncKeys(sync_keys: string[]) {
436
460
  if (sync_keys) {
@@ -439,26 +463,26 @@ class ArSyncCollection extends ArSyncContainerBase {
439
463
  this.sync_keys = []
440
464
  }
441
465
  }
442
- replaceData(data: any[] | { collection: any[]; order: any }, sync_keys: string[]) {
466
+ replaceData(data: any[] | { collection: any[]; ordering: Ordering }, sync_keys: string[]) {
443
467
  this.setSyncKeys(sync_keys)
444
- const existings: { [key: string]: ArSyncRecord } = {}
445
- for (const child of this.children) existings[child.data.id] = child
468
+ const existings = new Map<number, ArSyncRecord>()
469
+ for (const child of this.children) existings.set(child.data.id, child)
446
470
  let collection: any[]
447
- if ('collection' in data && 'order' in data) {
448
- collection = data.collection
449
- this.setOrdering(data.order.limit, data.order.mode)
450
- } else {
471
+ if (Array.isArray(data)) {
451
472
  collection = data
473
+ } else {
474
+ collection = data.collection
475
+ this.setOrdering(data.ordering)
452
476
  }
453
477
  const newChildren: any[] = []
454
478
  const newData: any[] = []
455
479
  for (const subData of collection) {
456
- let model: ArSyncRecord | null = null
457
- if (typeof(subData) === 'object' && subData && 'id' in subData) model = existings[subData.id]
480
+ let model: ArSyncRecord | undefined = undefined
481
+ if (typeof(subData) === 'object' && subData && 'sync_keys' in subData) model = existings.get(subData.id)
458
482
  let data = subData
459
483
  if (model) {
460
484
  model.replaceData(subData)
461
- } else if (subData.id) {
485
+ } else if (subData.sync_keys) {
462
486
  model = new ArSyncRecord(this.query, subData, null, this.root)
463
487
  model.parentModel = this
464
488
  model.parentKey = subData.id
@@ -471,7 +495,7 @@ class ArSyncCollection extends ArSyncContainerBase {
471
495
  }
472
496
  while (this.children.length) {
473
497
  const child = this.children.pop()!
474
- if (!existings[child.data.id]) child.release()
498
+ if (!existings.has(child.data.id)) child.release()
475
499
  }
476
500
  if (this.data.length || newChildren.length) this.mark()
477
501
  while (this.data.length) this.data.pop()
@@ -480,54 +504,78 @@ class ArSyncCollection extends ArSyncContainerBase {
480
504
  this.subscribeAll()
481
505
  }
482
506
  consumeAdd(className: string, id: number) {
507
+ const { first, last, direction } = this.ordering
508
+ const limit = first || last
483
509
  if (this.data.findIndex(a => a.id === id) >= 0) return
484
- if (this.order.limit === this.data.length) {
485
- if (this.order.mode === 'asc') {
486
- const last = this.data[this.data.length - 1]
487
- if (last && last.id < id) return
510
+ if (limit && limit <= this.data.length) {
511
+ const lastItem = this.data[this.data.length - 1]
512
+ const firstItem = this.data[0]
513
+ if (direction === 'asc') {
514
+ if (first) {
515
+ if (lastItem && lastItem.id < id) return
516
+ } else {
517
+ if (firstItem && id < firstItem.id) return
518
+ }
488
519
  } else {
489
- const last = this.data[this.data.length - 1]
490
- if (last && last.id > id) return
520
+ if (first) {
521
+ if (lastItem && id < lastItem.id) return
522
+ } else {
523
+ if (firstItem && firstItem.id < id) return
524
+ }
491
525
  }
492
526
  }
493
- ModelBatchRequest.fetch(className, this.compactQuery, id).then((data: any) => {
527
+ modelBatchRequest.fetch(className, this.compactQueryAttributes, id).then((data: any) => {
494
528
  if (!data || !this.data) return
495
529
  const model = new ArSyncRecord(this.query, data, null, this.root)
496
530
  model.parentModel = this
497
531
  model.parentKey = id
498
- const overflow = this.order.limit && this.order.limit === this.data.length
532
+ const overflow = limit && limit <= this.data.length
499
533
  let rmodel: ArSyncRecord | undefined
500
534
  this.mark()
501
535
  const orderKey = this.aliasOrderKey
502
- if (this.order.mode === 'asc') {
503
- const last = this.data[this.data.length - 1]
504
- this.children.push(model)
505
- this.data.push(model.data)
506
- if (last && last[orderKey] > data[orderKey]) this.markAndSort()
507
- if (overflow) {
508
- rmodel = this.children.shift()!
509
- rmodel.release()
510
- this.data.shift()
536
+ const firstItem = this.data[0]
537
+ const lastItem = this.data[this.data.length - 1]
538
+ if (direction === 'asc') {
539
+ if (firstItem && data[orderKey] < firstItem[orderKey]) {
540
+ this.children.unshift(model)
541
+ this.data.unshift(model.data)
542
+ } else {
543
+ const skipSort = lastItem && lastItem[orderKey] < data[orderKey]
544
+ this.children.push(model)
545
+ this.data.push(model.data)
546
+ if (!skipSort) this.markAndSort()
511
547
  }
512
548
  } else {
513
- const first = this.data[0]
514
- this.children.unshift(model)
515
- this.data.unshift(model.data)
516
- if (first && first[orderKey] > data[orderKey]) this.markAndSort()
517
- if (overflow) {
549
+ if (firstItem && data[orderKey] > firstItem[orderKey]) {
550
+ this.children.unshift(model)
551
+ this.data.unshift(model.data)
552
+ } else {
553
+ const skipSort = lastItem && lastItem[orderKey] > data[orderKey]
554
+ this.children.push(model)
555
+ this.data.push(model.data)
556
+ if (!skipSort) this.markAndSort()
557
+ }
558
+ }
559
+ if (overflow) {
560
+ if (first) {
518
561
  rmodel = this.children.pop()!
519
- rmodel.release()
520
562
  this.data.pop()
563
+ } else {
564
+ rmodel = this.children.shift()!
565
+ this.data.shift()
521
566
  }
567
+ rmodel.release()
522
568
  }
523
569
  this.onChange([model.id], model.data)
524
570
  if (rmodel) this.onChange([rmodel.id], null)
571
+ }).catch(e => {
572
+ console.error(`failed to load ${className}:${id} ${e}`)
525
573
  })
526
574
  }
527
575
  markAndSort() {
528
576
  this.mark()
529
577
  const orderKey = this.aliasOrderKey
530
- if (this.order.mode === 'asc') {
578
+ if (this.ordering.direction === 'asc') {
531
579
  this.children.sort((a, b) => a.data[orderKey] < b.data[orderKey] ? -1 : +1)
532
580
  this.data.sort((a, b) => a[orderKey] < b[orderKey] ? -1 : +1)
533
581
  } else {
@@ -38,6 +38,7 @@ export default class ConnectionManager {
38
38
  return { unsubscribe }
39
39
  }
40
40
  subscribe(key, func) {
41
+ if (!this.networkStatus) return { unsubscribe(){} }
41
42
  const subscription = this.connect(key)
42
43
  const id = subscription.serial++
43
44
  subscription.ref++