ar_sync 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +27 -0
  3. data/Gemfile +1 -0
  4. data/Gemfile.lock +29 -31
  5. data/ar_sync.gemspec +1 -1
  6. data/core/{ActioncableAdapter.d.ts → ActionCableAdapter.d.ts} +0 -0
  7. data/core/ActionCableAdapter.js +31 -0
  8. data/core/ArSyncApi.d.ts +8 -2
  9. data/core/ArSyncApi.js +123 -49
  10. data/core/ArSyncModel.js +69 -60
  11. data/core/ArSyncStore.js +522 -381
  12. data/core/ConnectionManager.d.ts +1 -1
  13. data/core/ConnectionManager.js +45 -38
  14. data/core/DataType.d.ts +14 -9
  15. data/core/hooks.d.ts +5 -0
  16. data/core/hooks.js +64 -36
  17. data/gemfiles/Gemfile-rails-6 +9 -0
  18. data/gemfiles/Gemfile-rails-7 +9 -0
  19. data/index.js +2 -2
  20. data/lib/ar_sync/class_methods.rb +71 -36
  21. data/lib/ar_sync/collection.rb +23 -19
  22. data/lib/ar_sync/core.rb +3 -3
  23. data/lib/ar_sync/instance_methods.rb +7 -4
  24. data/lib/ar_sync/rails.rb +1 -1
  25. data/lib/ar_sync/type_script.rb +50 -14
  26. data/lib/ar_sync/version.rb +1 -1
  27. data/lib/generators/ar_sync/install/install_generator.rb +1 -1
  28. data/package-lock.json +1706 -227
  29. data/package.json +1 -1
  30. data/src/core/{ActioncableAdapter.ts → ActionCableAdapter.ts} +0 -0
  31. data/src/core/ArSyncApi.ts +20 -7
  32. data/src/core/ArSyncStore.ts +177 -125
  33. data/src/core/ConnectionManager.ts +1 -0
  34. data/src/core/DataType.ts +15 -16
  35. data/src/core/hooks.ts +31 -7
  36. data/tsconfig.json +2 -2
  37. data/vendor/assets/javascripts/{ar_sync_actioncable_adapter.js.erb → ar_sync_action_cable_adapter.js.erb} +1 -1
  38. metadata +17 -16
  39. data/core/ActioncableAdapter.js +0 -29
  40. data/lib/ar_sync/field.rb +0 -96
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
  }
@@ -104,7 +125,7 @@ class ArSyncContainerBase {
104
125
  this.listeners = []
105
126
  }
106
127
  static compactQuery(query: ParsedQuery) {
107
- function compactAttributes(attributes: Record<string, 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,8 @@ class ArSyncContainerBase {
138
159
  if (attributes !== true) result.attributes = attributes
139
160
  return result
140
161
  }
141
- try{
142
162
  const result = compactQuery(query)
143
163
  return result === true ? {} : result
144
- }catch(e){throw JSON.stringify(query)+e.stack}
145
164
  }
146
165
  static parseQuery(query, attrsonly: true): Record<string, ParsedQuery>
147
166
  static parseQuery(query): ParsedQuery
@@ -180,12 +199,18 @@ class ArSyncContainerBase {
180
199
  static _load({ api, id, params, query }, root) {
181
200
  const parsedQuery = ArSyncRecord.parseQuery(query)
182
201
  const compactQuery = ArSyncRecord.compactQuery(parsedQuery)
183
- if (id) {
184
- return ModelBatchRequest.fetch(api, compactQuery, id).then(data => new ArSyncRecord(parsedQuery, data, null, root))
202
+ if (id != null) {
203
+ return modelBatchRequest.fetch(api, compactQuery, id).then(data => {
204
+ if (!data) throw { retry: false }
205
+ const request = { api, id, query: compactQuery }
206
+ return new ArSyncRecord(parsedQuery, data, request, root)
207
+ })
185
208
  } else {
186
209
  const request = { api, query: compactQuery, params }
187
- return ArSyncAPI.syncFetch(request).then((response: any) => {
188
- if (response.collection && response.order) {
210
+ return ArSyncApi.syncFetch(request).then((response: any) => {
211
+ if (!response) {
212
+ throw { retry: false }
213
+ } else if (response.collection && response.order) {
189
214
  return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, response, request, root)
190
215
  } else if (response instanceof Array) {
191
216
  return new ArSyncCollection([], '', parsedQuery, response, request, root)
@@ -222,6 +247,7 @@ class ArSyncRecord extends ArSyncContainerBase {
222
247
  id: number
223
248
  root
224
249
  query
250
+ queryAttributes
225
251
  data
226
252
  children: { [key: string]: ArSyncContainerBase | null }
227
253
  paths: string[]
@@ -231,15 +257,13 @@ class ArSyncRecord extends ArSyncContainerBase {
231
257
  this.root = root
232
258
  if (request) this.initForReload(request)
233
259
  this.query = query
260
+ this.queryAttributes = query.attributes || {}
234
261
  this.data = {}
235
262
  this.children = {}
236
263
  this.replaceData(data)
237
264
  }
238
- setSyncKeys(sync_keys: string[]) {
239
- this.sync_keys = sync_keys
240
- if (!this.sync_keys) {
241
- this.sync_keys = []
242
- }
265
+ setSyncKeys(sync_keys: string[] | undefined) {
266
+ this.sync_keys = sync_keys ?? []
243
267
  }
244
268
  replaceData(data) {
245
269
  this.setSyncKeys(data.sync_keys)
@@ -249,8 +273,8 @@ class ArSyncRecord extends ArSyncContainerBase {
249
273
  this.data.id = data.id
250
274
  }
251
275
  this.paths = []
252
- for (const key in this.query.attributes) {
253
- const subQuery = this.query.attributes[key]
276
+ for (const key in this.queryAttributes) {
277
+ const subQuery = this.queryAttributes[key]
254
278
  const aliasName = subQuery.as || key
255
279
  const subData = data[aliasName]
256
280
  const child = this.children[aliasName]
@@ -291,9 +315,9 @@ class ArSyncRecord extends ArSyncContainerBase {
291
315
  }
292
316
  }
293
317
  }
294
- if (this.query.attributes['*']) {
318
+ if (this.queryAttributes['*']) {
295
319
  for (const key in data) {
296
- if (!this.query.attributes[key] && this.data[key] !== data[key]) {
320
+ if (!this.queryAttributes[key] && this.data[key] !== data[key]) {
297
321
  this.mark()
298
322
  this.data[key] = data[key]
299
323
  }
@@ -302,8 +326,8 @@ class ArSyncRecord extends ArSyncContainerBase {
302
326
  this.subscribeAll()
303
327
  }
304
328
  onNotify(notifyData: NotifyData, path?: string) {
305
- const { action, class_name, id } = notifyData
306
- const query = path && this.query.attributes[path]
329
+ const { action, class_name: className, id } = notifyData
330
+ const query = path && this.queryAttributes[path]
307
331
  const aliasName = (query && query.as) || path;
308
332
  if (action === 'remove') {
309
333
  const child = this.children[aliasName]
@@ -314,7 +338,7 @@ class ArSyncRecord extends ArSyncContainerBase {
314
338
  this.onChange([aliasName], null)
315
339
  } else if (action === 'add') {
316
340
  if (this.data[aliasName] && this.data[aliasName].id === id) return
317
- ModelBatchRequest.fetch(class_name, ArSyncRecord.compactQuery(query), id).then(data => {
341
+ modelBatchRequest.fetch(className, ArSyncRecord.compactQuery(query), id).then(data => {
318
342
  if (!data || !this.data) return
319
343
  const model = new ArSyncRecord(query, data, null, this.root)
320
344
  const child = this.children[aliasName]
@@ -325,12 +349,17 @@ class ArSyncRecord extends ArSyncContainerBase {
325
349
  model.parentModel = this
326
350
  model.parentKey = aliasName
327
351
  this.onChange([aliasName], model.data)
352
+ }).catch(e => {
353
+ console.error(`failed to load ${className}:${id} ${e}`)
328
354
  })
329
355
  } else {
330
356
  const { field } = notifyData
331
357
  const query = field ? this.patchQuery(field) : this.reloadQuery()
332
- if (query) ModelBatchRequest.fetch(class_name, query, id).then(data => {
358
+ if (!query) return
359
+ modelBatchRequest.fetch(className, query, id).then(data => {
333
360
  if (this.data) this.update(data)
361
+ }).catch(e => {
362
+ console.error(`failed to load patch ${className}:${id} ${e}`)
334
363
  })
335
364
  }
336
365
  }
@@ -345,7 +374,7 @@ class ArSyncRecord extends ArSyncContainerBase {
345
374
  }
346
375
  }
347
376
  patchQuery(key: string) {
348
- const val = this.query.attributes[key]
377
+ const val = this.queryAttributes[key]
349
378
  if (!val) return
350
379
  let { attributes, as, params } = val
351
380
  if (attributes && Object.keys(val.attributes).length === 0) attributes = null
@@ -359,9 +388,9 @@ class ArSyncRecord extends ArSyncContainerBase {
359
388
  reloadQuery() {
360
389
  if (this.reloadQueryCache) return this.reloadQueryCache
361
390
  const reloadQuery = this.reloadQueryCache = { attributes: [] as any[] }
362
- for (const key in this.query.attributes) {
391
+ for (const key in this.queryAttributes) {
363
392
  if (key === 'sync_keys') continue
364
- const val = this.query.attributes[key]
393
+ const val = this.queryAttributes[key]
365
394
  if (!val || !val.attributes) {
366
395
  reloadQuery.attributes.push(key)
367
396
  } else if (!val.params && Object.keys(val.attributes).length === 0) {
@@ -372,7 +401,7 @@ class ArSyncRecord extends ArSyncContainerBase {
372
401
  }
373
402
  update(data) {
374
403
  for (const key in data) {
375
- const subQuery = this.query.attributes[key]
404
+ const subQuery = this.queryAttributes[key]
376
405
  if (subQuery && subQuery.attributes && Object.keys(subQuery.attributes).length > 0) continue
377
406
  if (this.data[key] === data[key]) continue
378
407
  this.mark()
@@ -392,11 +421,13 @@ class ArSyncRecord extends ArSyncContainerBase {
392
421
  }
393
422
  }
394
423
 
424
+ type Ordering = { first?: number; last?: number; orderBy: string; direction: 'asc' | 'desc' }
395
425
  class ArSyncCollection extends ArSyncContainerBase {
396
426
  root
397
427
  path: string
398
- order: { limit: number | null; key: string; mode: 'asc' | 'desc' } = { limit: null, mode: 'asc', key: 'id' }
428
+ ordering: Ordering = { orderBy: 'id', direction: 'asc' }
399
429
  query
430
+ queryAttributes
400
431
  compactQuery
401
432
  data: any[]
402
433
  children: ArSyncRecord[]
@@ -406,31 +437,28 @@ class ArSyncCollection extends ArSyncContainerBase {
406
437
  this.root = root
407
438
  this.path = path
408
439
  this.query = query
440
+ this.queryAttributes = query.attributes || {}
409
441
  this.compactQuery = ArSyncRecord.compactQuery(query)
410
442
  if (request) this.initForReload(request)
411
- if (query.params && (query.params.order || query.params.limit)) {
412
- this.setOrdering(query.params.limit, query.params.order)
443
+ if (query.params) {
444
+ this.setOrdering(query.params)
413
445
  }
414
446
  this.data = []
415
447
  this.children = []
416
448
  this.replaceData(data, sync_keys)
417
449
  }
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 }
450
+ setOrdering(ordering: { first?: unknown; last?: unknown; orderBy?: unknown; direction?: unknown }) {
451
+ let direction: 'asc' | 'desc' = 'asc'
452
+ let orderBy: string = 'id'
453
+ let first: number | undefined = undefined
454
+ let last: number | undefined = undefined
455
+ if (ordering.direction === 'desc') direction = ordering.direction
456
+ if (typeof ordering.orderBy === 'string') orderBy = ordering.orderBy
457
+ if (typeof ordering.first === 'number') first = ordering.first
458
+ if (typeof ordering.last === 'number') last = ordering.last
459
+ const subQuery = this.queryAttributes[orderBy]
460
+ this.aliasOrderKey = (subQuery && subQuery.as) || orderBy
461
+ this.ordering = { first, last, direction, orderBy }
434
462
  }
435
463
  setSyncKeys(sync_keys: string[]) {
436
464
  if (sync_keys) {
@@ -439,26 +467,26 @@ class ArSyncCollection extends ArSyncContainerBase {
439
467
  this.sync_keys = []
440
468
  }
441
469
  }
442
- replaceData(data: any[] | { collection: any[]; order: any }, sync_keys: string[]) {
470
+ replaceData(data: any[] | { collection: any[]; ordering: Ordering }, sync_keys: string[]) {
443
471
  this.setSyncKeys(sync_keys)
444
- const existings: { [key: string]: ArSyncRecord } = {}
445
- for (const child of this.children) existings[child.data.id] = child
472
+ const existings = new Map<number, ArSyncRecord>()
473
+ for (const child of this.children) existings.set(child.data.id, child)
446
474
  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 {
475
+ if (Array.isArray(data)) {
451
476
  collection = data
477
+ } else {
478
+ collection = data.collection
479
+ this.setOrdering(data.ordering)
452
480
  }
453
481
  const newChildren: any[] = []
454
482
  const newData: any[] = []
455
483
  for (const subData of collection) {
456
- let model: ArSyncRecord | null = null
457
- if (typeof(subData) === 'object' && subData && 'id' in subData) model = existings[subData.id]
484
+ let model: ArSyncRecord | undefined = undefined
485
+ if (typeof(subData) === 'object' && subData && 'sync_keys' in subData) model = existings.get(subData.id)
458
486
  let data = subData
459
487
  if (model) {
460
488
  model.replaceData(subData)
461
- } else if (subData.id) {
489
+ } else if (subData.sync_keys) {
462
490
  model = new ArSyncRecord(this.query, subData, null, this.root)
463
491
  model.parentModel = this
464
492
  model.parentKey = subData.id
@@ -471,7 +499,7 @@ class ArSyncCollection extends ArSyncContainerBase {
471
499
  }
472
500
  while (this.children.length) {
473
501
  const child = this.children.pop()!
474
- if (!existings[child.data.id]) child.release()
502
+ if (!existings.has(child.data.id)) child.release()
475
503
  }
476
504
  if (this.data.length || newChildren.length) this.mark()
477
505
  while (this.data.length) this.data.pop()
@@ -480,54 +508,78 @@ class ArSyncCollection extends ArSyncContainerBase {
480
508
  this.subscribeAll()
481
509
  }
482
510
  consumeAdd(className: string, id: number) {
511
+ const { first, last, direction } = this.ordering
512
+ const limit = first || last
483
513
  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
514
+ if (limit && limit <= this.data.length) {
515
+ const lastItem = this.data[this.data.length - 1]
516
+ const firstItem = this.data[0]
517
+ if (direction === 'asc') {
518
+ if (first) {
519
+ if (lastItem && lastItem.id < id) return
520
+ } else {
521
+ if (firstItem && id < firstItem.id) return
522
+ }
488
523
  } else {
489
- const last = this.data[this.data.length - 1]
490
- if (last && last.id > id) return
524
+ if (first) {
525
+ if (lastItem && id < lastItem.id) return
526
+ } else {
527
+ if (firstItem && firstItem.id < id) return
528
+ }
491
529
  }
492
530
  }
493
- ModelBatchRequest.fetch(className, this.compactQuery, id).then((data: any) => {
531
+ modelBatchRequest.fetch(className, this.compactQuery, id).then((data: any) => {
494
532
  if (!data || !this.data) return
495
533
  const model = new ArSyncRecord(this.query, data, null, this.root)
496
534
  model.parentModel = this
497
535
  model.parentKey = id
498
- const overflow = this.order.limit && this.order.limit === this.data.length
536
+ const overflow = limit && limit <= this.data.length
499
537
  let rmodel: ArSyncRecord | undefined
500
538
  this.mark()
501
539
  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()
540
+ const firstItem = this.data[0]
541
+ const lastItem = this.data[this.data.length - 1]
542
+ if (direction === 'asc') {
543
+ if (firstItem && data[orderKey] < firstItem[orderKey]) {
544
+ this.children.unshift(model)
545
+ this.data.unshift(model.data)
546
+ } else {
547
+ const skipSort = lastItem && lastItem[orderKey] < data[orderKey]
548
+ this.children.push(model)
549
+ this.data.push(model.data)
550
+ if (!skipSort) this.markAndSort()
511
551
  }
512
552
  } 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) {
553
+ if (firstItem && data[orderKey] > firstItem[orderKey]) {
554
+ this.children.unshift(model)
555
+ this.data.unshift(model.data)
556
+ } else {
557
+ const skipSort = lastItem && lastItem[orderKey] > data[orderKey]
558
+ this.children.push(model)
559
+ this.data.push(model.data)
560
+ if (!skipSort) this.markAndSort()
561
+ }
562
+ }
563
+ if (overflow) {
564
+ if (first) {
518
565
  rmodel = this.children.pop()!
519
- rmodel.release()
520
566
  this.data.pop()
567
+ } else {
568
+ rmodel = this.children.shift()!
569
+ this.data.shift()
521
570
  }
571
+ rmodel.release()
522
572
  }
523
573
  this.onChange([model.id], model.data)
524
574
  if (rmodel) this.onChange([rmodel.id], null)
575
+ }).catch(e => {
576
+ console.error(`failed to load ${className}:${id} ${e}`)
525
577
  })
526
578
  }
527
579
  markAndSort() {
528
580
  this.mark()
529
581
  const orderKey = this.aliasOrderKey
530
- if (this.order.mode === 'asc') {
582
+ if (this.ordering.direction === 'asc') {
531
583
  this.children.sort((a, b) => a.data[orderKey] < b.data[orderKey] ? -1 : +1)
532
584
  this.data.sort((a, b) => a[orderKey] < b[orderKey] ? -1 : +1)
533
585
  } 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++
data/src/core/DataType.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  type RecordType = { _meta?: { query: any } }
2
- type Values<T> = T extends { [K in keyof T]: infer U } ? U : never
2
+ type Values<T> = T[keyof T]
3
3
  type AddNullable<Test, Type> = null extends Test ? Type | null : Type
4
4
  type DataTypeExtractField<BaseType, Key extends keyof BaseType> = Exclude<BaseType[Key], null> extends RecordType
5
5
  ? AddNullable<BaseType[Key], {}>
@@ -51,27 +51,26 @@ type CheckAttributesField<P, Q> = Q extends { attributes: infer R }
51
51
 
52
52
  type IsAnyCompareLeftType = { __any: never }
53
53
 
54
- type CollectExtraFields<Type, Path> =
54
+ type CollectExtraFields<Type, Key> =
55
55
  IsAnyCompareLeftType extends Type
56
- ? null
56
+ ? never
57
57
  : Type extends ExtraFieldErrorType
58
- ? Path
58
+ ? Key
59
59
  : Type extends (infer R)[]
60
- ? _CollectExtraFields<R>
61
- : _CollectExtraFields<Type>
62
-
63
- type _CollectExtraFields<Type> = Type extends object
64
- ? (keyof (Type) extends never
65
- ? null
66
- : Values<{ [key in keyof Type]: CollectExtraFields<Type[key], key> }>
67
- )
68
- : null
60
+ ? {
61
+ 0: Values<{ [key in keyof R]: CollectExtraFields<R[key], key> }>
62
+ 1: never
63
+ }[R extends object ? 0 : 1]
64
+ : {
65
+ 0: Values<{ [key in keyof Type]: CollectExtraFields<Type[key], key> }>
66
+ 1: never
67
+ }[Type extends object ? 0 : 1]
69
68
 
70
69
  type SelectString<T> = T extends string ? T : never
71
70
  type _ValidateDataTypeExtraFileds<Extra, Type> = SelectString<Extra> extends never
72
71
  ? Type
73
- : { error: { extraFields: SelectString<Extra> } }
74
- type ValidateDataTypeExtraFileds<Type> = _ValidateDataTypeExtraFileds<CollectExtraFields<Type, []>, Type>
72
+ : { error: { extraFields: Extra } }
73
+ type ValidateDataTypeExtraFileds<Type> = _ValidateDataTypeExtraFileds<CollectExtraFields<Type, never>, Type>
75
74
 
76
75
  type RequestBase = { api: string; query: any; id?: number; params?: any; _meta?: { data: any } }
77
76
  type DataTypeBaseFromRequestType<R extends RequestBase, ID> = R extends { _meta?: { data: infer DataType } }
@@ -83,4 +82,4 @@ type DataTypeBaseFromRequestType<R extends RequestBase, ID> = R extends { _meta?
83
82
  : never
84
83
  export type DataTypeFromRequest<Req extends RequestBase, R extends RequestBase> = ValidateDataTypeExtraFileds<
85
84
  DataTypeFromQuery<DataTypeBaseFromRequestType<Req, R['id']>, R['query']>
86
- >
85
+ >