ar_sync 1.0.0

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 (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 {};