ar_sync 1.0.1 → 1.0.2

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +5 -2
  3. data/README.md +6 -6
  4. data/ar_sync.gemspec +2 -2
  5. data/bin/console +1 -2
  6. data/core/ActioncableAdapter.d.ts +26 -2
  7. data/core/ActioncableAdapter.js +3 -3
  8. data/core/ArSyncApi.d.ts +7 -4
  9. data/core/ArSyncApi.js +9 -4
  10. data/core/{ArSyncModelBase.d.ts → ArSyncModel.d.ts} +13 -18
  11. data/core/{ArSyncModelBase.js → ArSyncModel.js} +33 -10
  12. data/{graph → core}/ArSyncStore.d.ts +3 -3
  13. data/{graph → core}/ArSyncStore.js +188 -57
  14. data/core/DataType.d.ts +17 -13
  15. data/core/hooks.d.ts +28 -0
  16. data/core/hooks.js +105 -0
  17. data/index.d.ts +2 -0
  18. data/index.js +6 -0
  19. data/lib/ar_sync.rb +1 -18
  20. data/lib/ar_sync/class_methods.rb +31 -89
  21. data/lib/ar_sync/collection.rb +4 -29
  22. data/lib/ar_sync/core.rb +35 -67
  23. data/lib/ar_sync/instance_methods.rb +40 -86
  24. data/lib/ar_sync/rails.rb +18 -27
  25. data/lib/ar_sync/type_script.rb +39 -18
  26. data/lib/ar_sync/version.rb +1 -1
  27. data/lib/generators/ar_sync/install/install_generator.rb +33 -32
  28. data/lib/generators/ar_sync/types/types_generator.rb +6 -3
  29. data/package-lock.json +21 -10
  30. data/package.json +1 -1
  31. data/src/core/ActioncableAdapter.ts +28 -3
  32. data/src/core/ArSyncApi.ts +8 -4
  33. data/src/core/{ArSyncModelBase.ts → ArSyncModel.ts} +51 -20
  34. data/src/{graph → core}/ArSyncStore.ts +199 -84
  35. data/src/core/DataType.ts +33 -20
  36. data/src/core/hooks.ts +108 -0
  37. data/src/index.ts +2 -0
  38. data/vendor/assets/javascripts/{ar_sync_tree.js.erb → ar_sync.js.erb} +6 -7
  39. metadata +33 -38
  40. data/core/hooksBase.d.ts +0 -29
  41. data/core/hooksBase.js +0 -80
  42. data/graph/ArSyncModel.d.ts +0 -10
  43. data/graph/ArSyncModel.js +0 -22
  44. data/graph/hooks.d.ts +0 -3
  45. data/graph/hooks.js +0 -10
  46. data/graph/index.d.ts +0 -2
  47. data/graph/index.js +0 -4
  48. data/src/core/hooksBase.ts +0 -86
  49. data/src/graph/ArSyncModel.ts +0 -21
  50. data/src/graph/hooks.ts +0 -7
  51. data/src/graph/index.ts +0 -2
  52. data/src/tree/ArSyncModel.ts +0 -145
  53. data/src/tree/ArSyncStore.ts +0 -323
  54. data/src/tree/hooks.ts +0 -7
  55. data/src/tree/index.ts +0 -2
  56. data/tree/ArSyncModel.d.ts +0 -39
  57. data/tree/ArSyncModel.js +0 -143
  58. data/tree/ArSyncStore.d.ts +0 -21
  59. data/tree/ArSyncStore.js +0 -365
  60. data/tree/hooks.d.ts +0 -3
  61. data/tree/hooks.js +0 -10
  62. data/tree/index.d.ts +0 -2
  63. data/tree/index.js +0 -4
  64. data/vendor/assets/javascripts/ar_sync_graph.js.erb +0 -17
@@ -1,3 +1,3 @@
1
1
  module ArSync
2
- VERSION = '1.0.1'
2
+ VERSION = '1.0.2'
3
3
  end
@@ -1,46 +1,47 @@
1
1
  module ArSync
2
2
  class InstallGenerator < ::Rails::Generators::Base
3
- class_option :mode, enum: %w[tree graph], desc: 'sync mode', default: 'graph'
3
+ def create_schema_class
4
+ create_file 'app/models/sync_schema.rb', <<~CODE
5
+ class SyncSchema < ArSync::SyncSchemaBase
6
+ # serializer_field :profile, type: User do |current_user|
7
+ # current_user
8
+ # end
4
9
 
5
- def create_api_controller
6
- base_code = <<~CODE
7
- include ArSync::ApiControllerConcern
10
+ # serializer_field :post, type: Post do |current_user, id:|
11
+ # Post.where(current_user_can_access).find_by id: id
12
+ # end
8
13
 
9
- # serializer_field :profile, type: User do |_user|
10
- # current_user
11
- # end
14
+ # Reload API for all types should be defined here.
12
15
 
13
- # serializer_field :post, type: Post do |_user, id:|
14
- # Post.where(current_user_can_access).find id
15
- # end
16
- CODE
17
- graph_additional_code = <<~CODE
18
- # Reload API for all types should be defined here.
16
+ # serializer_field :User do |current_user, ids:|
17
+ # User.where(current_user_can_access).where id: ids
18
+ # end
19
19
 
20
- # serializer_field :User do |_user, ids:|
21
- # User.where(current_user_can_access).where id: ids
22
- # end
20
+ # serializer_field :Post do |current_user, ids:|
21
+ # Post.where(current_user_can_access).where id: ids
22
+ # end
23
23
 
24
- # serializer_field :Post do |_user, ids:|
25
- # Post.where(current_user_can_access).where id: ids
26
- # end
24
+ # serializer_field :Comment do |current_user, ids:|
25
+ # Comment.where(current_user_can_access).where id: ids
26
+ # end
27
+ end
28
+ CODE
29
+ end
27
30
 
28
- # serializer_field :Comment do |_user, ids:|
29
- # Comment.where(current_user_can_access).where id: ids
30
- # end
31
+ def create_api_controller
32
+ create_file 'app/controllers/sync_api_controller.rb', <<~CODE
33
+ class SyncApiController < ApplicationController
34
+ include ArSync::ApiControllerConcern
35
+ def schema
36
+ SyncSchema.new
37
+ end
38
+ end
31
39
  CODE
32
- controller_body = options['mode'] == 'tree' ? base_code : base_code + "\n" + graph_additional_code
33
- code = [
34
- "class SyncApiController < ApplicationController\n",
35
- controller_body.lines.map { |l| l.blank? ? l : ' ' + l },
36
- "end\n"
37
- ].join
38
- create_file 'app/controllers/sync_api_controller.rb', code
39
40
  end
40
41
 
41
42
  def create_config
42
43
  create_file 'config/initializers/ar_sync.rb', <<~CODE
43
- ArSync.use :#{options['mode']}
44
+ ActiveRecord::Base.include ArSync::ModelBase
44
45
  ArSync.configure do |config|
45
46
  config.current_user_method = :current_user
46
47
  config.key_prefix = 'ar_sync_'
@@ -75,10 +76,10 @@ module ArSync
75
76
  inject_into_file(
76
77
  'app/assets/javascripts/application.js',
77
78
  [
78
- "//= require ar_sync_#{options['mode']}",
79
+ '//= require ar_sync',
79
80
  '//= require action_cable',
80
81
  '//= require ar_sync_actioncable_adapter',
81
- 'ArSyncModel.setConnectionAdapter(new ArSyncActionCableAdapter())'
82
+ 'ArSyncModel.setConnectionAdapter(new ArSyncActionCableAdapter(ActionCable))'
82
83
  ].join("\n") + "\n",
83
84
  before: '//= require_tree .'
84
85
  )
@@ -1,11 +1,14 @@
1
1
  require 'shellwords'
2
2
  module ArSync
3
3
  class TypesGenerator < ::Rails::Generators::Base
4
- argument :output_dir, type: :string, desc: "output directory"
4
+ argument :output_dir, type: :string, required: true
5
+ argument :schema_class_name, type: :string, required: false
5
6
  def generate_typescript_files
6
7
  dir = ::Rails.root.join output_dir
7
- comment = "// generated by: rails generate ar_sync:types #{Shellwords.escape output_dir}\n\n"
8
- ArSync::TypeScript.generate_typed_files SyncApiController, dir: dir, comment: comment
8
+ schema_class = (schema_class_name || 'SyncSchema').constantize
9
+ args = [output_dir, *schema_class_name].map { |a| Shellwords.escape a }
10
+ comment = "// generated by: rails generate ar_sync:types #{args.join(' ')}\n\n"
11
+ ArSync::TypeScript.generate_typed_files schema_class, dir: dir, comment: comment
9
12
  end
10
13
  end
11
14
  end
@@ -338,10 +338,21 @@
338
338
  }
339
339
  },
340
340
  "eslint-utils": {
341
- "version": "1.3.1",
342
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
343
- "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
344
- "dev": true
341
+ "version": "1.4.3",
342
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
343
+ "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
344
+ "dev": true,
345
+ "requires": {
346
+ "eslint-visitor-keys": "^1.1.0"
347
+ },
348
+ "dependencies": {
349
+ "eslint-visitor-keys": {
350
+ "version": "1.1.0",
351
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
352
+ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
353
+ "dev": true
354
+ }
355
+ }
345
356
  },
346
357
  "eslint-visitor-keys": {
347
358
  "version": "1.0.0",
@@ -640,9 +651,9 @@
640
651
  }
641
652
  },
642
653
  "lodash": {
643
- "version": "4.17.11",
644
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
645
- "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
654
+ "version": "4.17.15",
655
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
656
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
646
657
  "dev": true
647
658
  },
648
659
  "lodash.unescape": {
@@ -1067,9 +1078,9 @@
1067
1078
  }
1068
1079
  },
1069
1080
  "typescript": {
1070
- "version": "3.4.5",
1071
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz",
1072
- "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==",
1081
+ "version": "3.7.2",
1082
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz",
1083
+ "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==",
1073
1084
  "dev": true
1074
1085
  },
1075
1086
  "uri-js": {
@@ -12,7 +12,7 @@
12
12
  "actioncable": "^5.2.0",
13
13
  "@types/actioncable": "^5.2.0",
14
14
  "eslint": "^5.16.0",
15
- "typescript": "^3.4.5",
15
+ "typescript": "3.7.2",
16
16
  "@typescript-eslint/eslint-plugin": "^1.6.0",
17
17
  "@typescript-eslint/parser": "^1.6.0"
18
18
  }
@@ -1,11 +1,36 @@
1
- import * as ActionCable from 'actioncable'
2
1
  import ConnectionAdapter from './ConnectionAdapter'
3
2
 
3
+ declare module ActionCable {
4
+ function createConsumer(): Cable
5
+ interface Cable {
6
+ subscriptions: Subscriptions
7
+ }
8
+ interface CreateMixin {
9
+ connected: () => void
10
+ disconnected: () => void
11
+ received: (obj: any) => void
12
+ }
13
+ interface ChannelNameWithParams {
14
+ channel: string
15
+ [key: string]: any
16
+ }
17
+ interface Subscriptions {
18
+ create(channel: ChannelNameWithParams, obj: CreateMixin): Channel
19
+ }
20
+ interface Channel {
21
+ unsubscribe(): void;
22
+ perform(action: string, data: {}): void;
23
+ send(data: any): boolean;
24
+ }
25
+ }
26
+
4
27
  export default class ActionCableAdapter implements ConnectionAdapter {
5
28
  connected: boolean
6
29
  _cable: ActionCable.Cable
7
- constructor() {
30
+ actionCableClass: typeof ActionCable
31
+ constructor(actionCableClass: typeof ActionCable) {
8
32
  this.connected = true
33
+ this.actionCableClass = actionCableClass
9
34
  this.subscribe(Math.random().toString(), () => {})
10
35
  }
11
36
  subscribe(key: string, received: (data: any) => void) {
@@ -19,7 +44,7 @@ export default class ActionCableAdapter implements ConnectionAdapter {
19
44
  this.connected = true
20
45
  this.onreconnect()
21
46
  }
22
- if (!this._cable) this._cable = ActionCable.createConsumer()
47
+ if (!this._cable) this._cable = this.actionCableClass.createConsumer()
23
48
  return this._cable.subscriptions.create(
24
49
  { channel: 'SyncChannel', key },
25
50
  { received, disconnected, connected }
@@ -5,6 +5,7 @@ async function apiBatchFetch(endpoint: string, requests: object[]) {
5
5
  }
6
6
  const body = JSON.stringify({ requests })
7
7
  const option = { credentials: 'include', method: 'POST', headers, body } as const
8
+ if (ArSyncApi.domain) endpoint = ArSyncApi.domain + endpoint
8
9
  const res = await fetch(endpoint, option)
9
10
  if (res.status === 200) return res.json()
10
11
  throw new Error(res.statusText)
@@ -43,7 +44,7 @@ class ApiFetcher {
43
44
  }
44
45
  }
45
46
  this.batches = []
46
- apiBatchFetch(this.endpoint, requests).then((results) => {
47
+ ArSyncApi._batchFetch(this.endpoint, requests).then((results) => {
47
48
  for (const i in callbacksList) {
48
49
  const result = results[i]
49
50
  const callbacks = callbacksList[i]
@@ -52,11 +53,11 @@ class ApiFetcher {
52
53
  callback.resolve(result.data)
53
54
  } else {
54
55
  const error = result.error || { type: 'Unknown Error' }
55
- callback.reject(error)
56
+ callback.reject({ ...error, retry: false })
56
57
  }
57
58
  }
58
59
  }
59
- }).catch((e) => {
60
+ }).catch(e => {
60
61
  const error = { type: e.name, message: e.message, retry: true }
61
62
  for (const callbacks of callbacksList) {
62
63
  for (const callback of callbacks) callback.reject(error)
@@ -69,7 +70,10 @@ class ApiFetcher {
69
70
 
70
71
  const staticFetcher = new ApiFetcher('/static_api')
71
72
  const syncFetcher = new ApiFetcher('/sync_api')
72
- export default {
73
+ const ArSyncApi = {
74
+ domain: null as string | null,
75
+ _batchFetch: apiBatchFetch,
73
76
  fetch: (request: object) => staticFetcher.fetch(request),
74
77
  syncFetch: (request: object) => syncFetcher.fetch(request),
75
78
  }
79
+ export default ArSyncApi
@@ -1,18 +1,35 @@
1
+ import ArSyncStore from './ArSyncStore'
2
+ import ArSyncConnectionManager from './ConnectionManager'
3
+ import ConnectionAdapter from './ConnectionAdapter'
4
+
1
5
  interface Request { api: string; query: any; params?: any }
2
- type Path = (string | number)[]
6
+ type Path = Readonly<(string | number)[]>
3
7
  interface Change { path: Path; value: any }
4
8
  type ChangeCallback = (change: Change) => void
5
9
  type LoadCallback = () => void
6
10
  type ConnectionCallback = (status: boolean) => void
7
11
  type SubscriptionType = 'load' | 'change' | 'connection'
8
12
  type SubscriptionCallback = ChangeCallback | LoadCallback | ConnectionCallback
9
- interface Adapter {
10
- subscribe: (key: string, received: (data: any) => void) => { unsubscribe: () => void }
11
- ondisconnect: () => void
12
- onreconnect: () => void
13
- }
14
13
 
15
- export default abstract class ArSyncModelBase<T> {
14
+ type PathFirst<P extends Readonly<any[]>> = ((...args: P) => void) extends (first: infer First, ...other: any) => void ? First : never
15
+
16
+ type PathRest<U> = U extends Readonly<any[]> ?
17
+ ((...args: U) => any) extends (head: any, ...args: infer T) => any
18
+ ? U extends Readonly<[any, any, ...any[]]> ? T : never
19
+ : never
20
+ : never;
21
+
22
+ type DigResult<Data, P extends Readonly<any[]>> =
23
+ Data extends null | undefined ? Data :
24
+ PathFirst<P> extends never ? Data :
25
+ PathFirst<P> extends keyof Data ?
26
+ (Data extends Readonly<any[]> ? undefined : never) | {
27
+ 0: Data[PathFirst<P>];
28
+ 1: DigResult<Data[PathFirst<P>], PathRest<P>>
29
+ }[PathRest<P> extends never ? 0 : 1]
30
+ : undefined
31
+
32
+ export default class ArSyncModel<T> {
16
33
  private _ref
17
34
  private _listenerSerial: number
18
35
  private _listeners
@@ -20,16 +37,14 @@ export default abstract class ArSyncModelBase<T> {
20
37
  notfound?: boolean
21
38
  connected: boolean
22
39
  data: T | null
23
- static _cache: { [key: string]: { key: string; count: number; timer: number | null; model } }
24
- static cacheTimeout: number
25
- abstract refManagerClass(): any
26
- abstract connectionManager(): { networkStatus: boolean }
40
+ static _cache: { [key: string]: { key: string; count: number; timer: number | null; model } } = {}
41
+ static cacheTimeout: number = 10 * 1000
27
42
  constructor(request: Request, option?: { immutable: boolean }) {
28
- this._ref = this.refManagerClass().retrieveRef(request, option)
43
+ this._ref = ArSyncModel.retrieveRef(request, option)
29
44
  this._listenerSerial = 0
30
45
  this._listeners = {}
31
46
  this.complete = false
32
- this.connected = this.connectionManager().networkStatus
47
+ this.connected = ArSyncStore.connectionManager.networkStatus
33
48
  const setData = () => {
34
49
  this.data = this._ref.model.data
35
50
  this.complete = this._ref.model.complete
@@ -52,6 +67,23 @@ export default abstract class ArSyncModelBase<T> {
52
67
  })
53
68
  return subscription
54
69
  }
70
+ dig<P extends Path>(path: P) {
71
+ return ArSyncModel.digData(this.data, path)
72
+ }
73
+ static digData<Data, P extends Path>(data: Data, path: P): DigResult<Data, P> {
74
+ function dig(data: Data, path: Path) {
75
+ if (path.length === 0) return data as any
76
+ if (data == null) return data
77
+ const key = path[0]
78
+ const other = path.slice(1)
79
+ if (Array.isArray(data)) {
80
+ return this.digData(data.find(el => el.id === key), other)
81
+ } else {
82
+ return this.digData(data[key], other)
83
+ }
84
+ }
85
+ return dig(data, path)
86
+ }
55
87
  subscribe(event: SubscriptionType, callback: SubscriptionCallback): { unsubscribe: () => void } {
56
88
  const id = this._listenerSerial++
57
89
  const subscription = this._ref.model.subscribe(event, callback)
@@ -74,7 +106,7 @@ export default abstract class ArSyncModelBase<T> {
74
106
  release() {
75
107
  for (const id in this._listeners) this._listeners[id].unsubscribe()
76
108
  this._listeners = {}
77
- this.refManagerClass()._detach(this._ref)
109
+ ArSyncModel._detach(this._ref)
78
110
  this._ref = null
79
111
  }
80
112
  static retrieveRef(
@@ -84,15 +116,12 @@ export default abstract class ArSyncModelBase<T> {
84
116
  const key = JSON.stringify([request, option])
85
117
  let ref = this._cache[key]
86
118
  if (!ref) {
87
- const model = this.createRefModel(request, option)
119
+ const model = new ArSyncStore(request, option)
88
120
  ref = this._cache[key] = { key, count: 0, timer: null, model }
89
121
  }
90
122
  this._attach(ref)
91
123
  return ref
92
124
  }
93
- static createRefModel(_request: Request, _option?: { immutable: boolean }) {
94
- throw 'abstract method'
95
- }
96
125
  static _detach(ref) {
97
126
  ref.count--
98
127
  const timeout = this.cacheTimeout
@@ -111,8 +140,10 @@ export default abstract class ArSyncModelBase<T> {
111
140
  ref.count++
112
141
  if (ref.timer) clearTimeout(ref.timer)
113
142
  }
114
- static setConnectionAdapter(_adapter: Adapter) {}
115
- static waitForLoad(...models: ArSyncModelBase<{}>[]) {
143
+ static setConnectionAdapter(adapter: ConnectionAdapter) {
144
+ ArSyncStore.connectionManager = new ArSyncConnectionManager(adapter)
145
+ }
146
+ static waitForLoad(...models: ArSyncModel<{}>[]) {
116
147
  return new Promise((resolve) => {
117
148
  let count = 0
118
149
  for (const model of models) {
@@ -1,4 +1,4 @@
1
- import ArSyncAPI from '../core/ArSyncApi'
1
+ import ArSyncAPI from './ArSyncApi'
2
2
 
3
3
  const ModelBatchRequest = {
4
4
  timer: null,
@@ -16,7 +16,7 @@ const ModelBatchRequest = {
16
16
  }
17
17
  }
18
18
  },
19
- fetch(api, query, id) {
19
+ fetch(api: string, query, id: number) {
20
20
  this.setTimer()
21
21
  return new Promise(resolve => {
22
22
  const queryJSON = JSON.stringify(query)
@@ -51,13 +51,20 @@ const ModelBatchRequest = {
51
51
  }
52
52
  }
53
53
 
54
+ type ParsedQuery = {
55
+ attributes: Record<string, ParsedQuery>
56
+ as?: string
57
+ params: any
58
+ } | {}
59
+
54
60
  class ArSyncContainerBase {
55
61
  data
56
62
  listeners
57
63
  networkSubscriber
58
64
  parentModel
59
65
  parentKey
60
- children: ArSyncContainerBase[]
66
+ children: ArSyncContainerBase[] | { [key: string]: ArSyncContainerBase | null }
67
+ sync_keys: string[]
61
68
  onConnectionChange
62
69
  constructor() {
63
70
  this.listeners = []
@@ -89,15 +96,57 @@ class ArSyncContainerBase {
89
96
  onChange(path, data) {
90
97
  if (this.parentModel) this.parentModel.onChange([this.parentKey, ...path], data)
91
98
  }
92
- subscribe(key, listener) {
99
+ subscribe(key: string, listener) {
93
100
  this.listeners.push(ArSyncStore.connectionManager.subscribe(key, listener))
94
101
  }
95
102
  unsubscribeAll() {
96
103
  for (const l of this.listeners) l.unsubscribe()
97
104
  this.listeners = []
98
105
  }
99
- static parseQuery(query, attrsonly = false){
100
- const attributes = {}
106
+ static compactQuery(query: ParsedQuery) {
107
+ function compactAttributes(attributes: Record<string, ParsedQuery>) {
108
+ const attrs = {}
109
+ const keys: string[] = []
110
+ for (const key in attributes) {
111
+ const c = compactQuery(attributes[key])
112
+ if (c === true) {
113
+ keys.push(key)
114
+ } else {
115
+ attrs[key] = c
116
+ }
117
+ }
118
+ if (Object.keys(attrs).length === 0) {
119
+ if (keys.length === 0) return [true, false]
120
+ if (keys.length === 1) return [keys[0], false]
121
+ return [keys]
122
+ }
123
+ const needsEscape = attrs['attributes'] || attrs['params'] || attrs['as']
124
+ if (keys.length === 0) return [attrs, needsEscape]
125
+ return [[...keys, attrs], needsEscape]
126
+ }
127
+ function compactQuery(query: ParsedQuery) {
128
+ if (!('attributes' in query)) return true
129
+ const { as, params } = query
130
+ const [attributes, needsEscape] = compactAttributes(query.attributes)
131
+ if (as == null && params == null){
132
+ if (needsEscape) return { attributes }
133
+ return attributes
134
+ }
135
+ const result: { as?: string; params?: any; attributes?: any } = {}
136
+ if (as) result.as = as
137
+ if (params) result.params = params
138
+ if (attributes !== true) result.attributes = attributes
139
+ return result
140
+ }
141
+ try{
142
+ const result = compactQuery(query)
143
+ return result === true ? {} : result
144
+ }catch(e){throw JSON.stringify(query)+e.stack}
145
+ }
146
+ static parseQuery(query, attrsonly: true): Record<string, ParsedQuery>
147
+ static parseQuery(query): ParsedQuery
148
+ static parseQuery(query, attrsonly?: true) {
149
+ const attributes: Record<string, ParsedQuery> = {}
101
150
  let column = null
102
151
  let params = null
103
152
  if (!query) query = []
@@ -130,13 +179,16 @@ class ArSyncContainerBase {
130
179
  }
131
180
  static _load({ api, id, params, query }, root) {
132
181
  const parsedQuery = ArSyncRecord.parseQuery(query)
182
+ const compactQuery = ArSyncRecord.compactQuery(parsedQuery)
133
183
  if (id) {
134
- return ModelBatchRequest.fetch(api, query, id).then(data => new ArSyncRecord(parsedQuery, data[0], null, root))
184
+ return ModelBatchRequest.fetch(api, compactQuery, id).then(data => new ArSyncRecord(parsedQuery, data, null, root))
135
185
  } else {
136
- const request = { api, query, params }
186
+ const request = { api, query: compactQuery, params }
137
187
  return ArSyncAPI.syncFetch(request).then((response: any) => {
138
188
  if (response.collection && response.order) {
139
189
  return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, response, request, root)
190
+ } else if (response instanceof Array) {
191
+ return new ArSyncCollection([], '', parsedQuery, response, request, root)
140
192
  } else {
141
193
  return new ArSyncRecord(parsedQuery, response, request, root)
142
194
  }
@@ -159,14 +211,20 @@ class ArSyncContainerBase {
159
211
  }
160
212
  }
161
213
 
214
+ type NotifyData = {
215
+ action: 'add' | 'remove' | 'update'
216
+ class_name: string
217
+ id: number
218
+ field?: string
219
+ }
220
+
162
221
  class ArSyncRecord extends ArSyncContainerBase {
163
- id
222
+ id: number
164
223
  root
165
224
  query
166
225
  data
167
- children
168
- sync_keys
169
- paths
226
+ children: { [key: string]: ArSyncContainerBase | null }
227
+ paths: string[]
170
228
  reloadQueryCache
171
229
  constructor(query, data, request, root) {
172
230
  super()
@@ -177,11 +235,10 @@ class ArSyncRecord extends ArSyncContainerBase {
177
235
  this.children = {}
178
236
  this.replaceData(data)
179
237
  }
180
- setSyncKeys(sync_keys) {
238
+ setSyncKeys(sync_keys: string[]) {
181
239
  this.sync_keys = sync_keys
182
240
  if (!this.sync_keys) {
183
241
  this.sync_keys = []
184
- console.error('warning: no sync_keys')
185
242
  }
186
243
  }
187
244
  replaceData(data) {
@@ -196,10 +253,11 @@ class ArSyncRecord extends ArSyncContainerBase {
196
253
  const subQuery = this.query.attributes[key]
197
254
  const aliasName = subQuery.as || key
198
255
  const subData = data[aliasName]
256
+ const child = this.children[aliasName]
199
257
  if (key === 'sync_keys') continue
200
- if (subQuery.attributes && (subData instanceof Array || (subData && subData.collection && subData.order))) {
201
- if (this.children[aliasName]) {
202
- this.children[aliasName].replaceData(subData, this.sync_keys)
258
+ if (subData instanceof Array || (subData && subData.collection && subData.order)) {
259
+ if (child) {
260
+ child.replaceData(subData, this.sync_keys)
203
261
  } else {
204
262
  const collection = new ArSyncCollection(this.sync_keys, key, subQuery, subData, null, this.root)
205
263
  this.mark()
@@ -209,10 +267,10 @@ class ArSyncRecord extends ArSyncContainerBase {
209
267
  collection.parentKey = aliasName
210
268
  }
211
269
  } else {
212
- if (subQuery.attributes && Object.keys(subQuery.attributes).length > 0) this.paths.push(key);
270
+ if (subQuery.attributes && Object.keys(subQuery.attributes).length > 0) this.paths.push(key)
213
271
  if (subData && subData.sync_keys) {
214
- if (this.children[aliasName]) {
215
- this.children[aliasName].replaceData(subData)
272
+ if (child) {
273
+ child.replaceData(subData)
216
274
  } else {
217
275
  const model = new ArSyncRecord(subQuery, subData, null, this.root)
218
276
  this.mark()
@@ -222,8 +280,8 @@ class ArSyncRecord extends ArSyncContainerBase {
222
280
  model.parentKey = aliasName
223
281
  }
224
282
  } else {
225
- if(this.children[aliasName]) {
226
- this.children[aliasName].release()
283
+ if(child) {
284
+ child.release()
227
285
  delete this.children[aliasName]
228
286
  }
229
287
  if (this.data[aliasName] !== subData) {
@@ -233,33 +291,46 @@ class ArSyncRecord extends ArSyncContainerBase {
233
291
  }
234
292
  }
235
293
  }
294
+ if (this.query.attributes['*']) {
295
+ for (const key in data) {
296
+ if (!this.query.attributes[key] && this.data[key] !== data[key]) {
297
+ this.mark()
298
+ this.data[key] = data[key]
299
+ }
300
+ }
301
+ }
236
302
  this.subscribeAll()
237
303
  }
238
- onNotify(notifyData, path?) {
304
+ onNotify(notifyData: NotifyData, path?: string) {
239
305
  const { action, class_name, id } = notifyData
306
+ const query = path && this.query.attributes[path]
307
+ const aliasName = (query && query.as) || path;
240
308
  if (action === 'remove') {
241
- this.children[path].release()
242
- this.children[path] = null
309
+ const child = this.children[aliasName]
310
+ if (child) child.release()
311
+ this.children[aliasName] = null
243
312
  this.mark()
244
- this.data[path] = null
245
- this.onChange([path], null)
313
+ this.data[aliasName] = null
314
+ this.onChange([aliasName], null)
246
315
  } else if (action === 'add') {
247
- if (this.data.id === id) return
248
- const query = this.query.attributes[path]
249
- ModelBatchRequest.fetch(class_name, query, id).then(data => {
250
- if (!data) return
316
+ if (this.data[aliasName] && this.data[aliasName].id === id) return
317
+ ModelBatchRequest.fetch(class_name, ArSyncRecord.compactQuery(query), id).then(data => {
318
+ if (!data || !this.data) return
251
319
  const model = new ArSyncRecord(query, data, null, this.root)
252
- if (this.children[path]) this.children[path].release()
253
- this.children[path] = model
320
+ const child = this.children[aliasName]
321
+ if (child) child.release()
322
+ this.children[aliasName] = model
254
323
  this.mark()
255
- this.data[path] = model.data
324
+ this.data[aliasName] = model.data
256
325
  model.parentModel = this
257
- model.parentKey = path
258
- this.onChange([path], model.data)
326
+ model.parentKey = aliasName
327
+ this.onChange([aliasName], model.data)
259
328
  })
260
329
  } else {
261
- ModelBatchRequest.fetch(class_name, this.reloadQuery(), id).then(data => {
262
- this.update(data)
330
+ const { field } = notifyData
331
+ const query = field ? this.patchQuery(field) : this.reloadQuery()
332
+ if (query) ModelBatchRequest.fetch(class_name, query, id).then(data => {
333
+ if (this.data) this.update(data)
263
334
  })
264
335
  }
265
336
  }
@@ -273,6 +344,18 @@ class ArSyncRecord extends ArSyncContainerBase {
273
344
  for (const key of this.sync_keys) this.subscribe(key + path, pathCallback)
274
345
  }
275
346
  }
347
+ 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
358
+ }
276
359
  reloadQuery() {
277
360
  if (this.reloadQueryCache) return this.reloadQueryCache
278
361
  const reloadQuery = this.reloadQueryCache = { attributes: [] as any[] }
@@ -289,13 +372,15 @@ class ArSyncRecord extends ArSyncContainerBase {
289
372
  }
290
373
  update(data) {
291
374
  for (const key in data) {
375
+ const subQuery = this.query.attributes[key]
376
+ if (subQuery && subQuery.attributes && Object.keys(subQuery.attributes).length > 0) continue
292
377
  if (this.data[key] === data[key]) continue
293
378
  this.mark()
294
379
  this.data[key] = data[key]
295
380
  this.onChange([key], data[key])
296
381
  }
297
382
  }
298
- markAndSet(key, data) {
383
+ markAndSet(key: string, data: any) {
299
384
  this.mark()
300
385
  this.data[key] = data
301
386
  }
@@ -305,68 +390,87 @@ class ArSyncRecord extends ArSyncContainerBase {
305
390
  this.root.mark(this.data)
306
391
  if (this.parentModel) this.parentModel.markAndSet(this.parentKey, this.data)
307
392
  }
308
- onChange(path, data) {
309
- if (this.parentModel) this.parentModel.onChange([this.parentKey, ...path], data)
310
- }
311
393
  }
394
+
312
395
  class ArSyncCollection extends ArSyncContainerBase {
313
396
  root
314
- path
315
- order
397
+ path: string
398
+ order: { limit: number | null; key: string; mode: 'asc' | 'desc' } = { limit: null, mode: 'asc', key: 'id' }
316
399
  query
317
- data
318
- children
319
- sync_keys
320
- constructor(sync_keys, path, query, data, request, root){
400
+ compactQuery
401
+ data: any[]
402
+ children: ArSyncRecord[]
403
+ aliasOrderKey = 'id'
404
+ constructor(sync_keys: string[], path: string, query, data: any[], request, root){
321
405
  super()
322
406
  this.root = root
323
407
  this.path = path
408
+ this.query = query
409
+ this.compactQuery = ArSyncRecord.compactQuery(query)
324
410
  if (request) this.initForReload(request)
325
411
  if (query.params && (query.params.order || query.params.limit)) {
326
- this.order = { limit: query.params.limit, mode: query.params.order || 'asc' }
327
- } else {
328
- this.order = { limit: null, mode: 'asc' }
412
+ this.setOrdering(query.params.limit, query.params.order)
329
413
  }
330
- this.query = query
331
414
  this.data = []
332
415
  this.children = []
333
416
  this.replaceData(data, sync_keys)
334
417
  }
335
- setSyncKeys(sync_keys) {
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 }
434
+ }
435
+ setSyncKeys(sync_keys: string[]) {
336
436
  if (sync_keys) {
337
437
  this.sync_keys = sync_keys.map(key => key + this.path)
338
438
  } else {
339
- console.error('warning: no sync_keys')
340
439
  this.sync_keys = []
341
440
  }
342
441
  }
343
- replaceData(data, sync_keys) {
442
+ replaceData(data: any[] | { collection: any[]; order: any }, sync_keys: string[]) {
344
443
  this.setSyncKeys(sync_keys)
345
- const existings = {}
444
+ const existings: { [key: string]: ArSyncRecord } = {}
346
445
  for (const child of this.children) existings[child.data.id] = child
347
- let collection
348
- if (data.collection && data.order) {
446
+ let collection: any[]
447
+ if ('collection' in data && 'order' in data) {
349
448
  collection = data.collection
350
- this.order = data.order
449
+ this.setOrdering(data.order.limit, data.order.mode)
351
450
  } else {
352
451
  collection = data
353
452
  }
354
453
  const newChildren: any[] = []
355
454
  const newData: any[] = []
356
455
  for (const subData of collection) {
357
- let model = existings[subData.id]
456
+ let model: ArSyncRecord | null = null
457
+ if (typeof(subData) === 'object' && subData && 'id' in subData) model = existings[subData.id]
458
+ let data = subData
358
459
  if (model) {
359
460
  model.replaceData(subData)
360
- } else {
461
+ } else if (subData.id) {
361
462
  model = new ArSyncRecord(this.query, subData, null, this.root)
362
463
  model.parentModel = this
363
464
  model.parentKey = subData.id
364
465
  }
365
- newChildren.push(model)
366
- newData.push(model.data)
466
+ if (model) {
467
+ newChildren.push(model)
468
+ data = model.data
469
+ }
470
+ newData.push(data)
367
471
  }
368
472
  while (this.children.length) {
369
- const child = this.children.pop()
473
+ const child = this.children.pop()!
370
474
  if (!existings[child.data.id]) child.release()
371
475
  }
372
476
  if (this.data.length || newChildren.length) this.mark()
@@ -375,7 +479,7 @@ class ArSyncCollection extends ArSyncContainerBase {
375
479
  for (const el of newData) this.data.push(el)
376
480
  this.subscribeAll()
377
481
  }
378
- consumeAdd(className, id) {
482
+ consumeAdd(className: string, id: number) {
379
483
  if (this.data.findIndex(a => a.id === id) >= 0) return
380
484
  if (this.order.limit === this.data.length) {
381
485
  if (this.order.mode === 'asc') {
@@ -386,24 +490,22 @@ class ArSyncCollection extends ArSyncContainerBase {
386
490
  if (last && last.id > id) return
387
491
  }
388
492
  }
389
- ModelBatchRequest.fetch(className, this.query, id).then((data) => {
390
- if (!data) return
493
+ ModelBatchRequest.fetch(className, this.compactQuery, id).then((data: any) => {
494
+ if (!data || !this.data) return
391
495
  const model = new ArSyncRecord(this.query, data, null, this.root)
392
496
  model.parentModel = this
393
497
  model.parentKey = id
394
498
  const overflow = this.order.limit && this.order.limit === this.data.length
395
- let rmodel
499
+ let rmodel: ArSyncRecord | undefined
396
500
  this.mark()
501
+ const orderKey = this.aliasOrderKey
397
502
  if (this.order.mode === 'asc') {
398
503
  const last = this.data[this.data.length - 1]
399
504
  this.children.push(model)
400
505
  this.data.push(model.data)
401
- if (last && last.id > id) {
402
- this.children.sort((a, b) => a.data.id < b.data.id ? -1 : +1)
403
- this.data.sort((a, b) => a.id < b.id ? -1 : +1)
404
- }
506
+ if (last && last[orderKey] > data[orderKey]) this.markAndSort()
405
507
  if (overflow) {
406
- rmodel = this.children.shift()
508
+ rmodel = this.children.shift()!
407
509
  rmodel.release()
408
510
  this.data.shift()
409
511
  }
@@ -411,12 +513,9 @@ class ArSyncCollection extends ArSyncContainerBase {
411
513
  const first = this.data[0]
412
514
  this.children.unshift(model)
413
515
  this.data.unshift(model.data)
414
- if (first && first.id > id) {
415
- this.children.sort((a, b) => a.data.id > b.data.id ? -1 : +1)
416
- this.data.sort((a, b) => a.id > b.id ? -1 : +1)
417
- }
516
+ if (first && first[orderKey] > data[orderKey]) this.markAndSort()
418
517
  if (overflow) {
419
- rmodel = this.children.pop()
518
+ rmodel = this.children.pop()!
420
519
  rmodel.release()
421
520
  this.data.pop()
422
521
  }
@@ -425,7 +524,18 @@ class ArSyncCollection extends ArSyncContainerBase {
425
524
  if (rmodel) this.onChange([rmodel.id], null)
426
525
  })
427
526
  }
428
- consumeRemove(id) {
527
+ markAndSort() {
528
+ this.mark()
529
+ const orderKey = this.aliasOrderKey
530
+ if (this.order.mode === 'asc') {
531
+ this.children.sort((a, b) => a.data[orderKey] < b.data[orderKey] ? -1 : +1)
532
+ this.data.sort((a, b) => a[orderKey] < b[orderKey] ? -1 : +1)
533
+ } else {
534
+ this.children.sort((a, b) => a.data[orderKey] > b.data[orderKey] ? -1 : +1)
535
+ this.data.sort((a, b) => a[orderKey] > b[orderKey] ? -1 : +1)
536
+ }
537
+ }
538
+ consumeRemove(id: number) {
429
539
  const idx = this.data.findIndex(a => a.id === id)
430
540
  if (idx < 0) return
431
541
  this.mark()
@@ -445,7 +555,11 @@ class ArSyncCollection extends ArSyncContainerBase {
445
555
  const callback = data => this.onNotify(data)
446
556
  for (const key of this.sync_keys) this.subscribe(key, callback)
447
557
  }
448
- markAndSet(id, data) {
558
+ onChange(path: (string | number)[], data) {
559
+ super.onChange(path, data)
560
+ if (path[1] === this.aliasOrderKey) this.markAndSort()
561
+ }
562
+ markAndSet(id: number, data) {
449
563
  this.mark()
450
564
  const idx = this.data.findIndex(a => a.id === id)
451
565
  if (idx >= 0) this.data[idx] = data
@@ -459,11 +573,11 @@ class ArSyncCollection extends ArSyncContainerBase {
459
573
  }
460
574
 
461
575
  export default class ArSyncStore {
462
- immutable
463
- markedForFreezeObjects
576
+ immutable: boolean
577
+ markedForFreezeObjects: any[]
464
578
  changes
465
579
  eventListeners
466
- markForRelease
580
+ markForRelease: true | undefined
467
581
  container
468
582
  request
469
583
  complete: boolean
@@ -473,7 +587,7 @@ export default class ArSyncStore {
473
587
  retryLoadTimer: number | undefined | null
474
588
  static connectionManager
475
589
  constructor(request, { immutable } = {} as { immutable?: boolean }) {
476
- this.immutable = immutable
590
+ this.immutable = !!immutable
477
591
  this.markedForFreezeObjects = []
478
592
  this.changes = []
479
593
  this.eventListeners = { events: {}, serial: 0 }
@@ -503,6 +617,7 @@ export default class ArSyncStore {
503
617
  this.trigger('connection', state)
504
618
  }
505
619
  }).catch(e => {
620
+ if (!e || e.retry === undefined) throw e
506
621
  if (this.markForRelease) return
507
622
  if (!e.retry) {
508
623
  this.complete = true