ar_sync 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +53 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +128 -0
  8. data/Rakefile +10 -0
  9. data/ar_sync.gemspec +28 -0
  10. data/bin/console +12 -0
  11. data/bin/setup +8 -0
  12. data/core/ActioncableAdapter.d.ts +10 -0
  13. data/core/ActioncableAdapter.js +29 -0
  14. data/core/ArSyncApi.d.ts +5 -0
  15. data/core/ArSyncApi.js +74 -0
  16. data/core/ArSyncModelBase.d.ts +71 -0
  17. data/core/ArSyncModelBase.js +110 -0
  18. data/core/ConnectionAdapter.d.ts +7 -0
  19. data/core/ConnectionAdapter.js +2 -0
  20. data/core/ConnectionManager.d.ts +19 -0
  21. data/core/ConnectionManager.js +75 -0
  22. data/core/DataType.d.ts +60 -0
  23. data/core/DataType.js +2 -0
  24. data/core/hooksBase.d.ts +29 -0
  25. data/core/hooksBase.js +80 -0
  26. data/graph/ArSyncModel.d.ts +10 -0
  27. data/graph/ArSyncModel.js +22 -0
  28. data/graph/ArSyncStore.d.ts +28 -0
  29. data/graph/ArSyncStore.js +593 -0
  30. data/graph/hooks.d.ts +3 -0
  31. data/graph/hooks.js +10 -0
  32. data/graph/index.d.ts +2 -0
  33. data/graph/index.js +4 -0
  34. data/lib/ar_sync.rb +25 -0
  35. data/lib/ar_sync/class_methods.rb +215 -0
  36. data/lib/ar_sync/collection.rb +83 -0
  37. data/lib/ar_sync/config.rb +18 -0
  38. data/lib/ar_sync/core.rb +138 -0
  39. data/lib/ar_sync/field.rb +96 -0
  40. data/lib/ar_sync/instance_methods.rb +130 -0
  41. data/lib/ar_sync/rails.rb +155 -0
  42. data/lib/ar_sync/type_script.rb +80 -0
  43. data/lib/ar_sync/version.rb +3 -0
  44. data/lib/generators/ar_sync/install/install_generator.rb +87 -0
  45. data/lib/generators/ar_sync/types/types_generator.rb +11 -0
  46. data/package-lock.json +1115 -0
  47. data/package.json +19 -0
  48. data/src/core/ActioncableAdapter.ts +30 -0
  49. data/src/core/ArSyncApi.ts +75 -0
  50. data/src/core/ArSyncModelBase.ts +126 -0
  51. data/src/core/ConnectionAdapter.ts +5 -0
  52. data/src/core/ConnectionManager.ts +69 -0
  53. data/src/core/DataType.ts +73 -0
  54. data/src/core/hooksBase.ts +86 -0
  55. data/src/graph/ArSyncModel.ts +21 -0
  56. data/src/graph/ArSyncStore.ts +567 -0
  57. data/src/graph/hooks.ts +7 -0
  58. data/src/graph/index.ts +2 -0
  59. data/src/tree/ArSyncModel.ts +145 -0
  60. data/src/tree/ArSyncStore.ts +323 -0
  61. data/src/tree/hooks.ts +7 -0
  62. data/src/tree/index.ts +2 -0
  63. data/tree/ArSyncModel.d.ts +39 -0
  64. data/tree/ArSyncModel.js +143 -0
  65. data/tree/ArSyncStore.d.ts +21 -0
  66. data/tree/ArSyncStore.js +365 -0
  67. data/tree/hooks.d.ts +3 -0
  68. data/tree/hooks.js +10 -0
  69. data/tree/index.d.ts +2 -0
  70. data/tree/index.js +4 -0
  71. data/tsconfig.json +15 -0
  72. data/vendor/assets/javascripts/ar_sync_actioncable_adapter.js.erb +7 -0
  73. data/vendor/assets/javascripts/ar_sync_graph.js.erb +17 -0
  74. data/vendor/assets/javascripts/ar_sync_tree.js.erb +17 -0
  75. metadata +187 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '01944ab2f90f657d412a17a52cb72eb249b5a3a96ca0531f273c1045900aae92'
4
+ data.tar.gz: 5f64eb0b5c66b24f886b693b9bee94a4f4ba95661768af4526fc3eda9be63d07
5
+ SHA512:
6
+ metadata.gz: 1305699ff98f51f6f8bb63bc8724c022c65475036b533b865fa48eabe4353c29805653c1204c81a1ee51a2e3d8f06de37b75eaabb3e7baa8b2f5dd0a9b04e08e
7
+ data.tar.gz: 7a25e40259ee1ff0baa93f783dddb1a9af36cab92ecd8686b2f22885022004aaf716e18a004cb6a2c4b44e15ae3bb9216e1f7c7def0f59920eed77ef4e062f5a
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /test/*.sqlite3
10
+ /test/generated_*
11
+ .ruby-version
12
+ /node_modules/
13
+ tsconfig.tsbuildinfo
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.2
5
+ before_install: gem install bundler -v 1.16.0
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in ar_sync.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,53 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ar_sync (1.0.0)
5
+ activerecord
6
+ ar_serializer
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (5.2.3)
12
+ activesupport (= 5.2.3)
13
+ activerecord (5.2.3)
14
+ activemodel (= 5.2.3)
15
+ activesupport (= 5.2.3)
16
+ arel (>= 9.0)
17
+ activesupport (5.2.3)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (>= 0.7, < 2)
20
+ minitest (~> 5.1)
21
+ tzinfo (~> 1.1)
22
+ ar_serializer (1.0.0)
23
+ activerecord
24
+ top_n_loader
25
+ arel (9.0.0)
26
+ coderay (1.1.2)
27
+ concurrent-ruby (1.1.5)
28
+ i18n (1.6.0)
29
+ concurrent-ruby (~> 1.0)
30
+ method_source (0.9.2)
31
+ minitest (5.11.3)
32
+ pry (0.12.2)
33
+ coderay (~> 1.1.0)
34
+ method_source (~> 0.9.0)
35
+ rake (12.3.2)
36
+ sqlite3 (1.3.13)
37
+ thread_safe (0.3.6)
38
+ top_n_loader (1.0.0)
39
+ activerecord
40
+ tzinfo (1.2.5)
41
+ thread_safe (~> 0.1)
42
+
43
+ PLATFORMS
44
+ ruby
45
+
46
+ DEPENDENCIES
47
+ ar_sync!
48
+ pry
49
+ rake
50
+ sqlite3
51
+
52
+ BUNDLED WITH
53
+ 1.16.1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 tompng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # ArSync - Reactive Programming with Ruby on Rails
2
+
3
+ Frontend JSON data will be synchronized with ActiveRecord.
4
+
5
+ - Provides an json api with query(shape of the json)
6
+ - Send a notificaiton with ActionCable and automaticaly updates the data
7
+
8
+ ## Installation
9
+
10
+ 1. Add this line to your application's Gemfile:
11
+ ```ruby
12
+ gem 'ar_sync'
13
+ ```
14
+
15
+ 2. Run generator
16
+ ```shell
17
+ rails g ar_sync:install
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ 1. Define parent, data, has_one, has_many to your models
23
+ ```ruby
24
+ class User < ApplicationRecord
25
+ has_many :posts
26
+ ...
27
+ sync_has_data :id, :name
28
+ sync_has_many :posts
29
+ end
30
+
31
+ class Post < ApplicationRecord
32
+ belongs_to :user
33
+ ...
34
+ sync_parent :user, inverse_of: :posts
35
+ sync_has_data :id, :title, :body, :createdAt, :updatedAt
36
+ sync_has_one :user, only: [:id, :name]
37
+ end
38
+ ```
39
+
40
+ 2. Define apis
41
+ ```ruby
42
+ # app/controllers/sync_api_controller.rb
43
+ class SyncApiController < ApplicationController
44
+ include ArSync::ApiControllerConcern
45
+ # User-defined api
46
+ serializer_field :my_simple_profile_api do |current_user|
47
+ current_user
48
+ end
49
+ serializer_field :my_simple_user_api do |current_user, id:|
50
+ User.where(condition).find id
51
+ end
52
+ # Reload api (field name = classname, params = `ids:`)
53
+ serializer_field :User do |current_user, ids:|
54
+ User.where(condition).where id: ids
55
+ end
56
+ serializer_field :Post do |current_user, ids:|
57
+ Post.where(condition).where id: ids
58
+ end
59
+ end
60
+ ```
61
+
62
+ 3. Write your view
63
+ ```html
64
+ <!-- if you're using vue -->
65
+ <script>
66
+ const userModel = new ArSyncModel({
67
+ api: 'my_simple_profile_api ',
68
+ query: { id: true, name: true, posts: ['title', 'createdAt'] }
69
+ })
70
+ userModel.onload(() => {
71
+ new Vue({ el: '#root', data: { user: userModel.data } })
72
+ })
73
+ </script>
74
+ <div id='root'>
75
+ <h1>{{user.name}}'s page</h1>
76
+ <ul>
77
+ <li v-for='post in user.posts'>
78
+ <a :href="'/posts/' + post.id">
79
+ {{post.title}}
80
+ </a>
81
+ <small>date: {{post.createdAt}}</small>
82
+ </li>
83
+ </ul>
84
+ <form action='/posts' data-remote=true method=post>
85
+ <input name='post[title]'>
86
+ <textarea name=post[body]></textarea>
87
+ <input type=submit>
88
+ </form>
89
+ </div>
90
+ ```
91
+ Now, your view and ActiveRecord are synchronized.
92
+
93
+
94
+ # With typescript
95
+ 1. Add `"ar_sync": "git://github.com/tompng/ar_sync.git"` to your package.json
96
+
97
+ 2. Generate types
98
+ ```shell
99
+ rails g ar_sync:types path_to_generated_code_dir/
100
+ ```
101
+
102
+ 3. Connection Setting
103
+ ```ts
104
+ import ArSyncModel from 'path_to_generated_code_dir/ArSyncModel'
105
+ import ActionCableAdapter from 'ar_sync/core/ActionCableAdapter'
106
+ ArSyncModel.setConnectionAdapter(new ActionCableAdapter)
107
+ // ArSyncModel.setConnectionAdapter(new MyCustomConnectionAdapter) // If you are using other transports
108
+ ```
109
+
110
+ 4. Write your components
111
+ ```ts
112
+ import { useArSyncModel } from 'path_to_generated_code_dir/hooks'
113
+ const HelloComponent: React.FC = () => {
114
+ const [user, status] = useArSyncModel({
115
+ api: 'my_simple_profile_api',
116
+ query: ['id', 'name']
117
+ })
118
+ // user // => { id: number; name: string } | null
119
+ if (!user) return <>loading...</>
120
+ // user.id // => number
121
+ // user.name // => string
122
+ // user.foobar // => compile error
123
+ return <h1>Hello, {user.name}!</h1>
124
+ }
125
+ ```
126
+
127
+ # Examples
128
+ https://github.com/tompng/ar_sync_sampleapp
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/ar_sync.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'ar_sync/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'ar_sync'
7
+ spec.version = ArSync::VERSION
8
+ spec.authors = ['tompng']
9
+ spec.email = ['tomoyapenguin@gmail.com']
10
+
11
+ spec.summary = %(ActiveRecord - JavaScript Sync)
12
+ spec.description = %(ActiveRecord data synchronized with frontend DataStore)
13
+ spec.homepage = "https://github.com/tompng/#{spec.name}"
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features|sampleapp)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'activerecord'
24
+ spec.add_dependency 'ar_serializer'
25
+ %w[rake pry sqlite3].each do |gem_name|
26
+ spec.add_development_dependency gem_name
27
+ end
28
+ end
data/bin/console ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'ar_sync'
5
+ require 'pry'
6
+ require_relative '../test/model_tree'
7
+ require_relative '../test/model_graph'
8
+ ArSync.on_notification do |events|
9
+ puts "\e[1m#{events.inspect}\e[m"
10
+ end
11
+
12
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,10 @@
1
+ import * as ActionCable from 'actioncable';
2
+ import ConnectionAdapter from './ConnectionAdapter';
3
+ export default class ActionCableAdapter implements ConnectionAdapter {
4
+ connected: boolean;
5
+ _cable: ActionCable.Cable;
6
+ constructor();
7
+ subscribe(key: string, received: (data: any) => void): ActionCable.Channel;
8
+ ondisconnect(): void;
9
+ onreconnect(): void;
10
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ActionCable = require("actioncable");
4
+ class ActionCableAdapter {
5
+ constructor() {
6
+ this.connected = true;
7
+ this.subscribe(Math.random().toString(), () => { });
8
+ }
9
+ subscribe(key, received) {
10
+ const disconnected = () => {
11
+ if (!this.connected)
12
+ return;
13
+ this.connected = false;
14
+ this.ondisconnect();
15
+ };
16
+ const connected = () => {
17
+ if (this.connected)
18
+ return;
19
+ this.connected = true;
20
+ this.onreconnect();
21
+ };
22
+ if (!this._cable)
23
+ this._cable = ActionCable.createConsumer();
24
+ return this._cable.subscriptions.create({ channel: 'SyncChannel', key }, { received, disconnected, connected });
25
+ }
26
+ ondisconnect() { }
27
+ onreconnect() { }
28
+ }
29
+ exports.default = ActionCableAdapter;
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ fetch: (request: object) => Promise<{}>;
3
+ syncFetch: (request: object) => Promise<{}>;
4
+ };
5
+ export default _default;
data/core/ArSyncApi.js ADDED
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ async function apiBatchFetch(endpoint, requests) {
4
+ const headers = {
5
+ 'Accept': 'application/json',
6
+ 'Content-Type': 'application/json'
7
+ };
8
+ const body = JSON.stringify({ requests });
9
+ const option = { credentials: 'include', method: 'POST', headers, body };
10
+ const res = await fetch(endpoint, option);
11
+ if (res.status === 200)
12
+ return res.json();
13
+ throw new Error(res.statusText);
14
+ }
15
+ class ApiFetcher {
16
+ constructor(endpoint) {
17
+ this.batches = [];
18
+ this.batchFetchTimer = null;
19
+ this.endpoint = endpoint;
20
+ }
21
+ fetch(request) {
22
+ return new Promise((resolve, reject) => {
23
+ this.batches.push([request, { resolve, reject }]);
24
+ if (this.batchFetchTimer)
25
+ return;
26
+ this.batchFetchTimer = setTimeout(() => {
27
+ this.batchFetchTimer = null;
28
+ const compacts = {};
29
+ const requests = [];
30
+ const callbacksList = [];
31
+ for (const batch of this.batches) {
32
+ const request = batch[0];
33
+ const callback = batch[1];
34
+ const key = JSON.stringify(request);
35
+ if (compacts[key]) {
36
+ compacts[key].push(callback);
37
+ }
38
+ else {
39
+ requests.push(request);
40
+ callbacksList.push(compacts[key] = [callback]);
41
+ }
42
+ }
43
+ this.batches = [];
44
+ apiBatchFetch(this.endpoint, requests).then((results) => {
45
+ for (const i in callbacksList) {
46
+ const result = results[i];
47
+ const callbacks = callbacksList[i];
48
+ for (const callback of callbacks) {
49
+ if (result.data) {
50
+ callback.resolve(result.data);
51
+ }
52
+ else {
53
+ const error = result.error || { type: 'Unknown Error' };
54
+ callback.reject(error);
55
+ }
56
+ }
57
+ }
58
+ }).catch((e) => {
59
+ const error = { type: e.name, message: e.message, retry: true };
60
+ for (const callbacks of callbacksList) {
61
+ for (const callback of callbacks)
62
+ callback.reject(error);
63
+ }
64
+ });
65
+ }, 16);
66
+ });
67
+ }
68
+ }
69
+ const staticFetcher = new ApiFetcher('/static_api');
70
+ const syncFetcher = new ApiFetcher('/sync_api');
71
+ exports.default = {
72
+ fetch: (request) => staticFetcher.fetch(request),
73
+ syncFetch: (request) => syncFetcher.fetch(request),
74
+ };
@@ -0,0 +1,71 @@
1
+ interface Request {
2
+ api: string;
3
+ query: any;
4
+ params?: any;
5
+ }
6
+ declare type Path = (string | number)[];
7
+ interface Change {
8
+ path: Path;
9
+ value: any;
10
+ }
11
+ declare type ChangeCallback = (change: Change) => void;
12
+ declare type LoadCallback = () => void;
13
+ declare type ConnectionCallback = (status: boolean) => void;
14
+ declare type SubscriptionType = 'load' | 'change' | 'connection';
15
+ declare type SubscriptionCallback = ChangeCallback | LoadCallback | ConnectionCallback;
16
+ interface Adapter {
17
+ subscribe: (key: string, received: (data: any) => void) => {
18
+ unsubscribe: () => void;
19
+ };
20
+ ondisconnect: () => void;
21
+ onreconnect: () => void;
22
+ }
23
+ export default abstract class ArSyncModelBase<T> {
24
+ private _ref;
25
+ private _listenerSerial;
26
+ private _listeners;
27
+ complete: boolean;
28
+ notfound?: boolean;
29
+ connected: boolean;
30
+ data: T | null;
31
+ static _cache: {
32
+ [key: string]: {
33
+ key: string;
34
+ count: number;
35
+ timer: number | null;
36
+ model: any;
37
+ };
38
+ };
39
+ static cacheTimeout: number;
40
+ abstract refManagerClass(): any;
41
+ abstract connectionManager(): {
42
+ networkStatus: boolean;
43
+ };
44
+ constructor(request: Request, option?: {
45
+ immutable: boolean;
46
+ });
47
+ onload(callback: LoadCallback): void;
48
+ subscribeOnce(event: SubscriptionType, callback: SubscriptionCallback): {
49
+ unsubscribe: () => void;
50
+ };
51
+ subscribe(event: SubscriptionType, callback: SubscriptionCallback): {
52
+ unsubscribe: () => void;
53
+ };
54
+ release(): void;
55
+ static retrieveRef(request: Request, option?: {
56
+ immutable: boolean;
57
+ }): {
58
+ key: string;
59
+ count: number;
60
+ timer: number | null;
61
+ model: any;
62
+ };
63
+ static createRefModel(_request: Request, _option?: {
64
+ immutable: boolean;
65
+ }): void;
66
+ static _detach(ref: any): void;
67
+ private static _attach;
68
+ static setConnectionAdapter(_adapter: Adapter): void;
69
+ static waitForLoad(...models: ArSyncModelBase<{}>[]): Promise<{}>;
70
+ }
71
+ export {};