ar_sync 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -2
- data/README.md +6 -6
- data/ar_sync.gemspec +2 -2
- data/bin/console +1 -2
- data/core/ActioncableAdapter.d.ts +26 -2
- data/core/ActioncableAdapter.js +3 -3
- data/core/ArSyncApi.d.ts +7 -4
- data/core/ArSyncApi.js +9 -4
- data/core/{ArSyncModelBase.d.ts → ArSyncModel.d.ts} +13 -18
- data/core/{ArSyncModelBase.js → ArSyncModel.js} +33 -10
- data/{graph → core}/ArSyncStore.d.ts +3 -3
- data/{graph → core}/ArSyncStore.js +188 -57
- data/core/DataType.d.ts +17 -13
- data/core/hooks.d.ts +28 -0
- data/core/hooks.js +105 -0
- data/index.d.ts +2 -0
- data/index.js +6 -0
- data/lib/ar_sync.rb +1 -18
- data/lib/ar_sync/class_methods.rb +31 -89
- data/lib/ar_sync/collection.rb +4 -29
- data/lib/ar_sync/core.rb +35 -67
- data/lib/ar_sync/instance_methods.rb +40 -86
- data/lib/ar_sync/rails.rb +18 -27
- data/lib/ar_sync/type_script.rb +39 -18
- data/lib/ar_sync/version.rb +1 -1
- data/lib/generators/ar_sync/install/install_generator.rb +33 -32
- data/lib/generators/ar_sync/types/types_generator.rb +6 -3
- data/package-lock.json +21 -10
- data/package.json +1 -1
- data/src/core/ActioncableAdapter.ts +28 -3
- data/src/core/ArSyncApi.ts +8 -4
- data/src/core/{ArSyncModelBase.ts → ArSyncModel.ts} +51 -20
- data/src/{graph → core}/ArSyncStore.ts +199 -84
- data/src/core/DataType.ts +33 -20
- data/src/core/hooks.ts +108 -0
- data/src/index.ts +2 -0
- data/vendor/assets/javascripts/{ar_sync_tree.js.erb → ar_sync.js.erb} +6 -7
- metadata +33 -38
- data/core/hooksBase.d.ts +0 -29
- data/core/hooksBase.js +0 -80
- data/graph/ArSyncModel.d.ts +0 -10
- data/graph/ArSyncModel.js +0 -22
- data/graph/hooks.d.ts +0 -3
- data/graph/hooks.js +0 -10
- data/graph/index.d.ts +0 -2
- data/graph/index.js +0 -4
- data/src/core/hooksBase.ts +0 -86
- data/src/graph/ArSyncModel.ts +0 -21
- data/src/graph/hooks.ts +0 -7
- data/src/graph/index.ts +0 -2
- data/src/tree/ArSyncModel.ts +0 -145
- data/src/tree/ArSyncStore.ts +0 -323
- data/src/tree/hooks.ts +0 -7
- data/src/tree/index.ts +0 -2
- data/tree/ArSyncModel.d.ts +0 -39
- data/tree/ArSyncModel.js +0 -143
- data/tree/ArSyncStore.d.ts +0 -21
- data/tree/ArSyncStore.js +0 -365
- data/tree/hooks.d.ts +0 -3
- data/tree/hooks.js +0 -10
- data/tree/index.d.ts +0 -2
- data/tree/index.js +0 -4
- data/vendor/assets/javascripts/ar_sync_graph.js.erb +0 -17
data/lib/ar_sync/version.rb
CHANGED
@@ -1,46 +1,47 @@
|
|
1
1
|
module ArSync
|
2
2
|
class InstallGenerator < ::Rails::Generators::Base
|
3
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
10
|
-
# current_user
|
11
|
-
# end
|
14
|
+
# Reload API for all types should be defined here.
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
20
|
+
# serializer_field :Post do |current_user, ids:|
|
21
|
+
# Post.where(current_user_can_access).where id: ids
|
22
|
+
# end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
8
|
-
|
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
|
data/package-lock.json
CHANGED
@@ -338,10 +338,21 @@
|
|
338
338
|
}
|
339
339
|
},
|
340
340
|
"eslint-utils": {
|
341
|
-
"version": "1.3
|
342
|
-
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.
|
343
|
-
"integrity": "sha512-
|
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.
|
644
|
-
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.
|
645
|
-
"integrity": "sha512-
|
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.
|
1071
|
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.
|
1072
|
-
"integrity": "sha512-
|
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": {
|
data/package.json
CHANGED
@@ -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
|
-
|
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 =
|
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 }
|
data/src/core/ArSyncApi.ts
CHANGED
@@ -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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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 =
|
43
|
+
this._ref = ArSyncModel.retrieveRef(request, option)
|
29
44
|
this._listenerSerial = 0
|
30
45
|
this._listeners = {}
|
31
46
|
this.complete = false
|
32
|
-
this.connected =
|
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
|
-
|
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 =
|
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(
|
115
|
-
|
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 '
|
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
|
100
|
-
|
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,
|
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
|
-
|
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 (
|
201
|
-
if (
|
202
|
-
|
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 (
|
215
|
-
|
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(
|
226
|
-
|
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[
|
242
|
-
|
309
|
+
const child = this.children[aliasName]
|
310
|
+
if (child) child.release()
|
311
|
+
this.children[aliasName] = null
|
243
312
|
this.mark()
|
244
|
-
this.data[
|
245
|
-
this.onChange([
|
313
|
+
this.data[aliasName] = null
|
314
|
+
this.onChange([aliasName], null)
|
246
315
|
} else if (action === 'add') {
|
247
|
-
if (this.data.id === id) return
|
248
|
-
|
249
|
-
|
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
|
-
|
253
|
-
|
320
|
+
const child = this.children[aliasName]
|
321
|
+
if (child) child.release()
|
322
|
+
this.children[aliasName] = model
|
254
323
|
this.mark()
|
255
|
-
this.data[
|
324
|
+
this.data[aliasName] = model.data
|
256
325
|
model.parentModel = this
|
257
|
-
model.parentKey =
|
258
|
-
this.onChange([
|
326
|
+
model.parentKey = aliasName
|
327
|
+
this.onChange([aliasName], model.data)
|
259
328
|
})
|
260
329
|
} else {
|
261
|
-
|
262
|
-
|
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
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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.
|
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
|
-
|
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 (
|
446
|
+
let collection: any[]
|
447
|
+
if ('collection' in data && 'order' in data) {
|
349
448
|
collection = data.collection
|
350
|
-
this.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 =
|
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
|
-
|
366
|
-
|
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.
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|