ar_sync 1.0.5 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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++